Skip to content

Commit 4d6da2a

Browse files
committed
Add automatic migrations for userConfig.
1 parent 898b46d commit 4d6da2a

8 files changed

Lines changed: 146 additions & 83 deletions

File tree

package.json

Lines changed: 64 additions & 64 deletions
Original file line numberDiff line numberDiff line change
@@ -1,66 +1,66 @@
11
{
2-
"name": "getstarted",
3-
"description": "Minimal & fast startpage for you browser.",
4-
"author": {
5-
"name": "Muhammad Faizan",
6-
"url": "https://mfaizan.com"
7-
},
8-
"license": "MIT",
9-
"private": true,
10-
"version": "0.3.0",
11-
"type": "module",
12-
"scripts": {
13-
"dev": "vite",
14-
"build": "vite build",
15-
"manifest": "tsx scripts/manifest.ts",
16-
"bundle": "tsx scripts/bundle.ts",
17-
"release": "tsx scripts/release.ts",
18-
"preview": "vite preview",
19-
"check": "svelte-check --tsconfig ./tsconfig.app.json && tsc -p tsconfig.node.json",
20-
"lint": "eslint . && prettier --check .",
21-
"format": "prettier --write ."
22-
},
23-
"devDependencies": {
24-
"@eslint/compat": "^1.2.5",
25-
"@eslint/js": "^9.18.0",
26-
"@internationalized/date": "^3.8.2",
27-
"@lucide/svelte": "^0.515.0",
28-
"@sveltejs/vite-plugin-svelte": "^6.0.0",
29-
"@tailwindcss/vite": "^4.1.11",
30-
"@tsconfig/svelte": "^5.0.4",
31-
"@types/chrome": "^0.1.0",
32-
"@types/firefox-webext-browser": "^120.0.4",
33-
"@types/node": "^24.0.14",
34-
"bits-ui": "^2.8.11",
35-
"clsx": "^2.1.1",
36-
"eslint": "^9.18.0",
37-
"eslint-config-prettier": "^10.0.1",
38-
"eslint-plugin-svelte": "^3.0.0",
39-
"globals": "^16.0.0",
40-
"prettier": "^3.4.2",
41-
"prettier-plugin-svelte": "^3.3.3",
42-
"prettier-plugin-tailwindcss": "^0.6.11",
43-
"semver": "^7.7.2",
44-
"svelte": "^5.35.5",
45-
"svelte-check": "^4.2.2",
46-
"svelte-dnd-action": "^0.9.63",
47-
"tailwind-merge": "^3.3.1",
48-
"tailwind-variants": "^1.0.0",
49-
"tailwindcss": "^4.1.11",
50-
"tsx": "^4.20.3",
51-
"tw-animate-css": "^1.3.5",
52-
"typescript": "~5.8.3",
53-
"typescript-eslint": "^8.20.0",
54-
"vaul-svelte": "1.0.0-next.7",
55-
"vite": "^7.0.4"
56-
},
57-
"packageManager": "pnpm@10.13.1+sha512.37ebf1a5c7a30d5fabe0c5df44ee8da4c965ca0c5af3dbab28c3a1681b70a256218d05c81c9c0dcf767ef6b8551eb5b960042b9ed4300c59242336377e01cfad",
58-
"dependencies": {
59-
"zod": "^4.0.5"
60-
},
61-
"pnpm": {
62-
"onlyBuiltDependencies": [
63-
"esbuild"
64-
]
65-
}
2+
"name": "getstarted",
3+
"description": "Minimal & fast startpage for you browser.",
4+
"author": {
5+
"name": "Muhammad Faizan",
6+
"url": "https://mfaizan.com"
7+
},
8+
"license": "MIT",
9+
"private": true,
10+
"version": "0.3.0",
11+
"type": "module",
12+
"scripts": {
13+
"dev": "vite",
14+
"build": "vite build",
15+
"manifest": "tsx scripts/manifest.ts",
16+
"bundle": "tsx scripts/bundle.ts",
17+
"release": "tsx scripts/release.ts",
18+
"preview": "vite preview",
19+
"check": "svelte-check --tsconfig ./tsconfig.app.json && tsc -p tsconfig.node.json",
20+
"lint": "eslint . && prettier --check .",
21+
"format": "prettier --write ."
22+
},
23+
"devDependencies": {
24+
"@eslint/compat": "^1.2.5",
25+
"@eslint/js": "^9.18.0",
26+
"@internationalized/date": "^3.8.2",
27+
"@lucide/svelte": "^0.515.0",
28+
"@sveltejs/vite-plugin-svelte": "^6.0.0",
29+
"@tailwindcss/vite": "^4.1.11",
30+
"@tsconfig/svelte": "^5.0.4",
31+
"@types/chrome": "^0.1.0",
32+
"@types/firefox-webext-browser": "^120.0.4",
33+
"@types/node": "^24.0.14",
34+
"bits-ui": "^2.8.11",
35+
"clsx": "^2.1.1",
36+
"eslint": "^9.18.0",
37+
"eslint-config-prettier": "^10.0.1",
38+
"eslint-plugin-svelte": "^3.0.0",
39+
"globals": "^16.0.0",
40+
"prettier": "^3.4.2",
41+
"prettier-plugin-svelte": "^3.3.3",
42+
"prettier-plugin-tailwindcss": "^0.6.11",
43+
"semver": "^7.7.2",
44+
"svelte": "^5.35.5",
45+
"svelte-check": "^4.2.2",
46+
"svelte-dnd-action": "^0.9.63",
47+
"tailwind-merge": "^3.3.1",
48+
"tailwind-variants": "^1.0.0",
49+
"tailwindcss": "^4.1.11",
50+
"tsx": "^4.20.3",
51+
"tw-animate-css": "^1.3.5",
52+
"typescript": "~5.8.3",
53+
"typescript-eslint": "^8.20.0",
54+
"vaul-svelte": "1.0.0-next.7",
55+
"vite": "^7.0.4"
56+
},
57+
"packageManager": "pnpm@10.13.1+sha512.37ebf1a5c7a30d5fabe0c5df44ee8da4c965ca0c5af3dbab28c3a1681b70a256218d05c81c9c0dcf767ef6b8551eb5b960042b9ed4300c59242336377e01cfad",
58+
"dependencies": {
59+
"zod": "^4.0.5"
60+
},
61+
"pnpm": {
62+
"onlyBuiltDependencies": [
63+
"esbuild"
64+
]
65+
}
6666
}

src/lib/components/Settings.svelte

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -22,15 +22,20 @@
2222
2323
$effect(() => {
2424
if (save) {
25-
if (JSON.stringify($state.snapshot(config)) !== JSON.stringify(currentConfig)) {
26-
saveConfig($state.snapshot(config));
27-
currentConfig = $state.snapshot(config);
28-
}
25+
saveAndUpdateConfig(config);
2926
sheetOpen = false;
3027
}
3128
save = false;
3229
});
3330
31+
function saveAndUpdateConfig(configToSave: UserConfig) {
32+
if (JSON.stringify($state.snapshot(configToSave)) !== JSON.stringify(currentConfig)) {
33+
config = configToSave;
34+
saveConfig($state.snapshot(config));
35+
currentConfig = $state.snapshot(config);
36+
}
37+
}
38+
3439
function handleSubmitCallback(e: Event): boolean {
3540
e.preventDefault();
3641
errors = {};
@@ -82,7 +87,10 @@
8287
</Sheet.Header>
8388

8489
<div class="grid gap-2 px-4">
85-
<SettingsImportExport bind:config />
90+
<SettingsImportExport
91+
{config}
92+
onImport={(importedConfig) => saveAndUpdateConfig(importedConfig)}
93+
/>
8694
<Label>{#snippet child({ props })}<span {...props}>Theme</span>{/snippet}</Label>
8795
<ThemeToggle />
8896
</div>

src/lib/components/SettingsImportExport.svelte

Lines changed: 4 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,8 @@
77
import Download from '@lucide/svelte/icons/download';
88
import { tryImportUserConfig } from '$lib/utils/importUserConfig';
99
10-
let {
11-
config = $bindable(),
12-
onImport
13-
}: { config: UserConfig; onImport?: (config: UserConfig) => void } = $props();
10+
let { config, onImport }: { config: UserConfig; onImport: (config: UserConfig) => void } =
11+
$props();
1412
1513
const VERSION = import.meta.env.APP_VERSION || 'unknown';
1614
@@ -53,11 +51,8 @@
5351
showDialog(msg, 'Import Error');
5452
return;
5553
}
56-
if (result.config != null) {
57-
if (onImport) onImport(result.config);
58-
else config = result.config;
59-
}
60-
showDialog(`Settings imported successfully from ${file.name}.`, 'Import Successful');
54+
if (result.config != null && onImport) onImport(result.config);
55+
showDialog(`Settings imported successfully from ${file.name}.`, 'Import Successful');
6156
} catch {
6257
showDialog('Invalid settings file.');
6358
}

src/lib/config/default-config.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
import type { UserConfig } from '$lib/types/user-config';
22

33
const defaultConfig: UserConfig = {
4+
version: import.meta.env.APP_VERSION,
5+
migrationId: 30250820,
46
userName: 'Faizan',
57
searchEngine: 'google',
68
customEngines: undefined,

src/lib/types/user-config.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,8 @@ export interface Card {
2020
}
2121

2222
export interface UserConfig {
23+
version: string;
24+
migrationId: number;
2325
userName: string;
2426
searchEngine: string;
2527
customEngines?: SearchEngine[];

src/lib/utils/importUserConfig.ts

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
// src/lib/utils/importUserConfig.ts
22
import type { UserConfig } from '$lib/types/user-config';
3+
import { checkPendingMigrations, migrateUserConfig } from './migrateUserConfig';
34

45
/**
56
* Attempts to migrate or validate an imported config. Returns an object with:
@@ -45,9 +46,22 @@ export function tryImportUserConfig(
4546
}
4647
}
4748
// If we reach here, config is compatible
49+
let finalConfig: UserConfig = { ...importedConfig } as UserConfig;
50+
if (checkPendingMigrations(importedConfig as UserConfig)) {
51+
const migrated = migrateUserConfig(importedConfig as UserConfig);
52+
if (!migrated) {
53+
return {
54+
success: false,
55+
config: null,
56+
error: 'Failed to migrate settings to the current version.',
57+
helpUrl
58+
};
59+
}
60+
finalConfig = migrated;
61+
}
4862
return {
4963
success: true,
50-
config: importedConfig as UserConfig,
64+
config: finalConfig,
5165
error: null,
5266
helpUrl: null
5367
};

src/lib/utils/migrateUserConfig.ts

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
import type { UserConfig } from '$lib/types/user-config';
2+
import defaultConfig from '$lib/config/default-config';
3+
4+
// Migration functions indexed by migrationId (number)
5+
const migrations: Record<number, (currenConfig: UserConfig) => UserConfig> = {
6+
30250820: (currentConfig) => {
7+
const migrated = { ...currentConfig };
8+
migrated.version = defaultConfig.version;
9+
migrated.migrationId = defaultConfig.migrationId;
10+
return migrated;
11+
}
12+
// Add more migrations for future migrationIds here
13+
};
14+
15+
function getMigrationId(config: UserConfig): number {
16+
return config.migrationId || 0;
17+
}
18+
19+
export function migrateUserConfig(storedConfig: UserConfig): UserConfig {
20+
const currentMigrationId = getMigrationId(storedConfig);
21+
const migrationIds = Object.keys(migrations).map(Number).sort();
22+
23+
let migrated = { ...storedConfig };
24+
for (const id of migrationIds) {
25+
if (currentMigrationId < id) {
26+
migrated = migrations[id](migrated);
27+
}
28+
}
29+
return migrated;
30+
}
31+
32+
export function checkPendingMigrations(storedConfig: UserConfig): boolean {
33+
const defaultVersion = getMigrationId(defaultConfig);
34+
const storedVersion = getMigrationId(storedConfig);
35+
return defaultVersion !== storedVersion;
36+
}

src/lib/utils/user-config.ts

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,12 @@
11
import type { UserConfig } from '$lib/types/user-config';
22
import defaultConfig from '$lib/config/default-config';
33
import { getStorageAPI } from '$lib/utils/storage';
4+
import { checkPendingMigrations, migrateUserConfig } from './migrateUserConfig';
45

56
const STORAGE_KEY = 'userConfig';
67
const storage = getStorageAPI<UserConfig>(STORAGE_KEY);
78

8-
// Internal config object
9+
// Internal config object is a global variable, defaultConfig is only a fallback
910
export const config: UserConfig = structuredClone(defaultConfig);
1011

1112
// Subscriber pattern
@@ -24,11 +25,16 @@ export function subscribe(callback: Subscriber) {
2425
return () => subscribers.delete(callback);
2526
}
2627

27-
// Initialize config from storage
2828
await storage.get(STORAGE_KEY).then((stored) => {
2929
if (stored) {
30-
Object.assign(config, stored);
31-
notifySubscribers();
30+
// If stored version is different, then migrate
31+
if (checkPendingMigrations(stored)) {
32+
const migrated = migrateUserConfig(stored);
33+
saveConfig(migrated);
34+
} else {
35+
Object.assign(config, stored);
36+
notifySubscribers();
37+
}
3238
}
3339
});
3440

0 commit comments

Comments
 (0)