From 3cd2a678a626ef41b20933c4bc06d81e69b844af Mon Sep 17 00:00:00 2001 From: Alexander Bocken Date: Wed, 29 Apr 2026 22:31:16 +0200 Subject: [PATCH] =?UTF-8?q?refactor:=20$app/stores=20=E2=86=92=20$app/stat?= =?UTF-8?q?e,=20legacy=20stores=20=E2=86=92=20runes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Codemod-driven migration of 55 .svelte files from the deprecated $app/stores module to the rune-based $app/state ($page.x → page.x, no auto-subscription wrapper). Two custom writable() stores converted to .svelte.ts factory functions matching the existing theme store pattern, with consumers updated to use .value getters and the explicit .set() method. UserHeader.svelte's login link now guards page.url.search behind the browser flag — search-param access throws during prerender, and this defensive change unblocks future prerender adoption on any page that includes the header. --- package.json | 2 +- scripts/codemod-app-stores-to-state.ts | 84 +++++++++++++++++++ src/lib/components/FavoriteButton.svelte | 4 +- src/lib/components/LanguageSelector.svelte | 18 ++-- src/lib/components/SectionError.svelte | 6 +- src/lib/components/UserHeader.svelte | 7 +- .../components/cospend/DebtBreakdown.svelte | 4 +- .../components/cospend/EnhancedBalance.svelte | 4 +- .../components/cospend/PaymentModal.svelte | 6 +- .../cospend/SplitMethodSelector.svelte | 4 +- .../components/fitness/ExerciseName.svelte | 4 +- .../components/fitness/ExercisePicker.svelte | 4 +- src/lib/components/fitness/FoodSearch.svelte | 4 +- .../components/fitness/MacroBreakdown.svelte | 4 +- .../components/fitness/MuscleHeatmap.svelte | 4 +- src/lib/components/fitness/SessionCard.svelte | 4 +- src/lib/components/fitness/SetTable.svelte | 4 +- .../components/fitness/TemplateCard.svelte | 4 +- src/lib/components/fitness/WorkoutFab.svelte | 4 +- src/lib/components/recipes/HefeSwapper.svelte | 4 +- .../components/recipes/IngredientsPage.svelte | 4 +- src/lib/stores/language.svelte.ts | 12 +++ src/lib/stores/language.ts | 27 ------ src/lib/stores/recipeTranslation.svelte.ts | 16 ++++ src/lib/stores/recipeTranslation.ts | 9 -- src/routes/+error.svelte | 6 +- .../[cospendRoot=cospendRoot]/+error.svelte | 4 +- .../[cospendRoot=cospendRoot]/+layout.svelte | 14 ++-- .../dash/+page.svelte | 4 +- .../list/+page.svelte | 6 +- .../payments/+page.svelte | 4 +- .../payments/add/+page.svelte | 4 +- .../payments/edit/[id]/+page.svelte | 4 +- .../payments/view/[id]/+page.svelte | 4 +- .../recurring/+page.svelte | 4 +- .../recurring/edit/[id]/+page.svelte | 4 +- .../settle/+page.svelte | 4 +- .../[faithLang=faithLang]/+error.svelte | 4 +- .../[faithLang=faithLang]/+layout.svelte | 4 +- .../katechese/zehn-gebote/+page.svelte | 6 +- .../[recipeLang=recipeLang]/+error.svelte | 4 +- .../[recipeLang=recipeLang]/+layout.svelte | 4 +- .../[name]/+error.svelte | 12 +-- .../[name]/+page.svelte | 2 +- .../offline-shell/+page.svelte | 4 +- src/routes/fitness/+error.svelte | 4 +- src/routes/fitness/+layout.svelte | 18 ++-- .../[checkin=fitnessCheckIn]/+page.svelte | 4 +- .../body-parts/+page.svelte | 4 +- .../edit/[id]/+page.svelte | 4 +- .../[exercises=fitnessExercises]/+page.svelte | 4 +- .../[id]/+page.svelte | 4 +- .../[[month=fitnessMonth]]/+page.svelte | 4 +- .../[id]/+page.svelte | 4 +- .../[[date=fitnessDate]]/+page.svelte | 6 +- .../food/[source]/[id]/+page.svelte | 4 +- .../meals/+page.svelte | 4 +- .../fitness/[stats=fitnessStats]/+page.svelte | 4 +- .../[part]/+page.svelte | 4 +- .../[workout=fitnessWorkout]/+page.svelte | 4 +- .../[active=fitnessActive]/+page.svelte | 4 +- src/routes/tasks/+layout.svelte | 4 +- 62 files changed, 255 insertions(+), 178 deletions(-) create mode 100644 scripts/codemod-app-stores-to-state.ts create mode 100644 src/lib/stores/language.svelte.ts delete mode 100644 src/lib/stores/language.ts create mode 100644 src/lib/stores/recipeTranslation.svelte.ts delete mode 100644 src/lib/stores/recipeTranslation.ts diff --git a/package.json b/package.json index e4beac44..adaab72a 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "homepage", - "version": "1.51.0", + "version": "1.52.0", "private": true, "type": "module", "scripts": { diff --git a/scripts/codemod-app-stores-to-state.ts b/scripts/codemod-app-stores-to-state.ts new file mode 100644 index 00000000..ee09e8f6 --- /dev/null +++ b/scripts/codemod-app-stores-to-state.ts @@ -0,0 +1,84 @@ +/** + * Migrate `$app/stores` (deprecated) to `$app/state` (rune-based). + * + * For each .svelte file: + * - Rewrite `from '$app/stores'` → `from '$app/state'` + * - For each named import, drop the `$` prefix from auto-subscriptions: + * `$page.url.pathname` → `page.url.pathname` + * `$navigating` → `navigating` + * `$updated` → `updated` + * Aliased imports (`page as appPage`) are tracked, so `$appPage` becomes `appPage`. + * + * Skips: + * - Non-.svelte files (server-only code uses getRequestEvent instead). + * - Files importing other things from $app/stores that don't have a state equivalent + * (none observed in this repo). + * + * Run: pnpm exec vite-node scripts/codemod-app-stores-to-state.ts [--dry] + */ + +import { readFileSync, writeFileSync, readdirSync, statSync } from 'node:fs'; +import { join, extname } from 'node:path'; + +const SRC = 'src'; +const DRY = process.argv.includes('--dry'); + +const STORES_IMPORT_RE = + /import\s*\{([^}]+)\}\s*from\s*['"]\$app\/stores['"]\s*;?/; + +function walk(dir: string, out: string[] = []): string[] { + for (const name of readdirSync(dir)) { + const p = join(dir, name); + const s = statSync(p); + if (s.isDirectory()) walk(p, out); + else if (extname(p) === '.svelte') out.push(p); + } + return out; +} + +function parseImports(inner: string): Array<{ orig: string; local: string }> { + return inner + .split(',') + .map((s) => s.trim()) + .filter(Boolean) + .map((spec) => { + const m = spec.match(/^(\w+)(?:\s+as\s+(\w+))?$/); + if (!m) return null; + return { orig: m[1], local: m[2] ?? m[1] }; + }) + .filter((x): x is { orig: string; local: string } => x !== null); +} + +function rewriteFile(src: string): { code: string; changed: boolean } { + const m = STORES_IMPORT_RE.exec(src); + if (!m) return { code: src, changed: false }; + + const imports = parseImports(m[1]); + if (imports.length === 0) return { code: src, changed: false }; + + // Replace the import path; preserve the same import shape. + let out = src.replace(STORES_IMPORT_RE, (full) => + full.replace(/['"]\$app\/stores['"]/, "'$app/state'") + ); + + // Drop `$` prefix from each local name where it appears as a store + // auto-subscription (i.e. $name followed by a non-word boundary). + for (const { local } of imports) { + const re = new RegExp(`\\$${local}\\b`, 'g'); + out = out.replace(re, local); + } + + return { code: out, changed: out !== src }; +} + +const files = walk(SRC); +let changed = 0; +for (const f of files) { + const orig = readFileSync(f, 'utf8'); + const { code, changed: didChange } = rewriteFile(orig); + if (!didChange) continue; + if (!DRY) writeFileSync(f, code); + changed++; + console.log(` ${f}`); +} +console.log(`\n${DRY ? '[dry] ' : ''}${changed} files migrated`); diff --git a/src/lib/components/FavoriteButton.svelte b/src/lib/components/FavoriteButton.svelte index 0f9eace2..bc2792a7 100644 --- a/src/lib/components/FavoriteButton.svelte +++ b/src/lib/components/FavoriteButton.svelte @@ -1,12 +1,12 @@ diff --git a/src/lib/components/fitness/ExercisePicker.svelte b/src/lib/components/fitness/ExercisePicker.svelte index b29a9bb4..628e7b6f 100644 --- a/src/lib/components/fitness/ExercisePicker.svelte +++ b/src/lib/components/fitness/ExercisePicker.svelte @@ -9,10 +9,10 @@ import PersonStanding from '@lucide/svelte/icons/person-standing'; import Shapes from '@lucide/svelte/icons/shapes'; import Weight from '@lucide/svelte/icons/weight'; - import { page } from '$app/stores'; + import { page } from '$app/state'; import { detectFitnessLang, t } from '$lib/js/fitnessI18n'; - const lang = $derived(detectFitnessLang($page.url.pathname)); + const lang = $derived(detectFitnessLang(page.url.pathname)); const isEn = $derived(lang === 'en'); /** diff --git a/src/lib/components/fitness/FoodSearch.svelte b/src/lib/components/fitness/FoodSearch.svelte index 55f2808e..52f0feb8 100644 --- a/src/lib/components/fitness/FoodSearch.svelte +++ b/src/lib/components/fitness/FoodSearch.svelte @@ -1,6 +1,6 @@ diff --git a/src/routes/[cospendRoot=cospendRoot]/+layout.svelte b/src/routes/[cospendRoot=cospendRoot]/+layout.svelte index 2059344a..f12bb87b 100644 --- a/src/routes/[cospendRoot=cospendRoot]/+layout.svelte +++ b/src/routes/[cospendRoot=cospendRoot]/+layout.svelte @@ -1,6 +1,6 @@ diff --git a/src/routes/[recipeLang=recipeLang]/+layout.svelte b/src/routes/[recipeLang=recipeLang]/+layout.svelte index b2d0625b..42eeaafa 100644 --- a/src/routes/[recipeLang=recipeLang]/+layout.svelte +++ b/src/routes/[recipeLang=recipeLang]/+layout.svelte @@ -1,7 +1,7 @@ diff --git a/src/routes/fitness/+layout.svelte b/src/routes/fitness/+layout.svelte index 5996dbf3..e4339d7b 100644 --- a/src/routes/fitness/+layout.svelte +++ b/src/routes/fitness/+layout.svelte @@ -1,6 +1,6 @@