From 5272deceef420fa41a98e42ddfea58fd512ea701 Mon Sep 17 00:00:00 2001 From: Simon Holthausen Date: Thu, 12 Dec 2024 23:56:45 +0100 Subject: [PATCH 01/10] feat: add page-state migration A simple, string-based migration for https://github.com/sveltejs/kit/pull/13140 (tested on that repo, zero false-positives, less than five false negatives) --- .changeset/polite-eyes-invent.md | 5 + packages/migrate/README.md | 1 + .../migrate/migrations/page-state/index.js | 119 ++++++++++++++++++ .../migrate/migrations/page-state/migrate.js | 74 +++++++++++ .../migrations/page-state/migrate.spec.js | 91 ++++++++++++++ 5 files changed, 290 insertions(+) create mode 100644 .changeset/polite-eyes-invent.md create mode 100644 packages/migrate/migrations/page-state/index.js create mode 100644 packages/migrate/migrations/page-state/migrate.js create mode 100644 packages/migrate/migrations/page-state/migrate.spec.js diff --git a/.changeset/polite-eyes-invent.md b/.changeset/polite-eyes-invent.md new file mode 100644 index 00000000..e9d7fa25 --- /dev/null +++ b/.changeset/polite-eyes-invent.md @@ -0,0 +1,5 @@ +--- +'svelte-migrate': minor +--- + +feat: add page-state migration diff --git a/packages/migrate/README.md b/packages/migrate/README.md index d6b7b677..794adf95 100644 --- a/packages/migrate/README.md +++ b/packages/migrate/README.md @@ -21,6 +21,7 @@ npx sv migrate [migration] | `sveltekit-2` | SvelteKit 1.0 | SvelteKit 2.0 | [Website](https://svelte.dev/docs/kit/migrating-to-sveltekit-2) | | `svelte-4` | Svelte 3 | Svelte 4 | [Website](https://svelte.dev/docs/svelte/v4-migration-guide) | | `svelte-5` | Svelte 4 | Svelte 5 | | +| `page-state` | `$app/stores` | `$app/state` | | | `package` | `@sveltejs/package@1` | `@sveltejs/package@2` | [#8922](https://github.com/sveltejs/kit/pull/8922) | | `routes` | SvelteKit pre-1.0 | SvelteKit 1.0 | [#5774](https://github.com/sveltejs/kit/discussions/5774) | diff --git a/packages/migrate/migrations/page-state/index.js b/packages/migrate/migrations/page-state/index.js new file mode 100644 index 00000000..f496f5f0 --- /dev/null +++ b/packages/migrate/migrations/page-state/index.js @@ -0,0 +1,119 @@ +import colors from 'kleur'; +import fs from 'node:fs'; +import process from 'node:process'; +import prompts from 'prompts'; +import semver from 'semver'; +import glob from 'tiny-glob/sync.js'; +import { bail, check_git, update_svelte_file } from '../../utils.js'; +import { transform_svelte_code, update_pkg_json } from './migrate.js'; + +export async function migrate() { + if (!fs.existsSync('package.json')) { + bail('Please re-run this script in a directory with a package.json'); + } + + const pkg = JSON.parse(fs.readFileSync('package.json', 'utf8')); + + const svelte_dep = pkg.devDependencies?.svelte ?? pkg.dependencies?.svelte; + if (svelte_dep && semver.validRange(svelte_dep) && semver.gtr('5.0.0', svelte_dep)) { + console.log( + colors + .bold() + .red('\nYou need to upgrade to Svelte version 5 first (`npx sv migrate svelte-5`).\n') + ); + process.exit(1); + } + + const kit_dep = pkg.devDependencies?.['@sveltejs/kit'] ?? pkg.dependencies?.['@sveltejs/kit']; + if (kit_dep && semver.validRange(kit_dep) && semver.gtr('2.0.0', kit_dep)) { + console.log( + colors + .bold() + .red('\nYou need to upgrade to SvelteKit version 2 first (`npx sv migrate sveltekit-2`).\n') + ); + process.exit(1); + } + + console.log( + colors + .bold() + .yellow( + '\nThis will update files in the current directory\n' + + "If you're inside a monorepo, don't run this in the root directory, rather run it in all projects independently.\n" + ) + ); + + const use_git = check_git(); + + const response = await prompts({ + type: 'confirm', + name: 'value', + message: 'Continue?', + initial: false + }); + + if (!response.value) { + process.exit(1); + } + + const folders = await prompts({ + type: 'multiselect', + name: 'value', + message: 'Which folders should be migrated?', + choices: fs + .readdirSync('.') + .filter( + (dir) => fs.statSync(dir).isDirectory() && dir !== 'node_modules' && !dir.startsWith('.') + ) + .map((dir) => ({ title: dir, value: dir, selected: true })) + }); + + if (!folders.value?.length) { + process.exit(1); + } + + update_pkg_json(); + + // For some reason {folders.value.join(',')} as part of the glob doesn't work and returns less files + const files = folders.value.flatMap( + /** @param {string} folder */ (folder) => + glob(`${folder}/**`, { filesOnly: true, dot: true }) + .map((file) => file.replace(/\\/g, '/')) + .filter( + (file) => + !file.includes('/node_modules/') && + // We're not transforming usage inside .ts/.js files since you can't use the $store syntax there, + // and therefore have to either subscribe or pass it along, which we can't auto-migrate + file.endsWith('.svelte') + ) + ); + + for (const file of files) { + update_svelte_file( + file, + (code) => code, + (code) => transform_svelte_code(code, { filename: file }) + ); + } + + console.log(colors.bold().green('✔ Your project has been migrated')); + + console.log('\nRecommended next steps:\n'); + + const cyan = colors.bold().cyan; + + const tasks = [ + "install the updated dependencies ('npm i' / 'pnpm i' / etc) " + use_git && + cyan('git commit -m "migration to $app/state"') + ].filter(Boolean); + + tasks.forEach((task, i) => { + console.log(` ${i + 1}: ${task}`); + }); + + console.log(''); + + if (use_git) { + console.log(`Run ${cyan('git diff')} to review changes.\n`); + } +} diff --git a/packages/migrate/migrations/page-state/migrate.js b/packages/migrate/migrations/page-state/migrate.js new file mode 100644 index 00000000..198a6807 --- /dev/null +++ b/packages/migrate/migrations/page-state/migrate.js @@ -0,0 +1,74 @@ +import fs from 'node:fs'; +import { update_pkg } from '../../utils.js'; + +export function update_pkg_json() { + fs.writeFileSync( + 'package.json', + update_pkg_json_content(fs.readFileSync('package.json', 'utf8')) + ); +} + +/** + * @param {string} content + */ +export function update_pkg_json_content(content) { + return update_pkg(content, [['@sveltejs/kit', '^2.12.0']]); +} + +/** + * @param {string} code + * @param {{ filename?: string }} options + */ +export function transform_svelte_code(code, options) { + // Quick check if nothing to do + if (!code.includes('$app/stores')) return code; + + // Check if file is using legacy APIs - if so, we can't migrate since reactive statements would break + const lines = code.split('\n'); + if (lines.some((line) => /^\s*(export let|\$:) /.test(line))) { + return code; + } + + const import_match = code.match(/import\s*{([^}]+)}\s*from\s*("|')\$app\/stores\2/); + if (!import_match) return code; // nothing to do + + const stores = import_match[1].split(',').map((i) => i.trim()); + let modified = code.replace('$app/stores', '$app/state'); + + for (const store of stores) { + // if someone uses that they're deep into stores and we better not touch this file + if (store === 'getStores') return code; + + const regex = new RegExp(`\\b${store}\\b`, 'g'); + let match; + let count_removed = 0; + + while ((match = regex.exec(modified)) !== null) { + const before = modified.slice(0, match.index); + const after = modified.slice(match.index + store.length); + + if (before.slice(-1) !== '$') { + if (/[_'"]/.test(before.slice(-1))) continue; // false positive + + if (store === 'updated' && after.startsWith('.check()')) { + continue; // this stays as is + } + + if ( + match.index - count_removed > /** @type {number} */ (import_match.index) && + match.index - count_removed < + /** @type {number} */ (import_match.index) + import_match[0].length + ) { + continue; // this is the import statement + } + + return code; + } + + modified = before.slice(0, -1) + store + (store === 'page' ? '' : '.current') + after; + count_removed++; + } + } + + return modified; +} diff --git a/packages/migrate/migrations/page-state/migrate.spec.js b/packages/migrate/migrations/page-state/migrate.spec.js new file mode 100644 index 00000000..589b9453 --- /dev/null +++ b/packages/migrate/migrations/page-state/migrate.spec.js @@ -0,0 +1,91 @@ +import { assert, test } from 'vitest'; +import { transform_svelte_code } from './migrate.js'; + +test('Updates $app/store #1', () => { + const result = transform_svelte_code( + ` + +
{$page.url}
+ +`, + {} + ); + assert.equal( + result, + ` + +
{page.url}
+ +` + ); +}); + +test('Updates $app/store #2', () => { + const result = transform_svelte_code( + ` + +is_navigating: {$navigating} +`, + {} + ); + assert.equal( + result, + ` + +is_navigating: {navigating.current} +` + ); +}); + +test('Does not update $app/store #1', () => { + const input = ` + +{x} +`; + const result = transform_svelte_code(input, {}); + assert.equal(result, input); +}); + +test('Does not update $app/store #2', () => { + const input = ` + +{$url} +`; + const result = transform_svelte_code(input, {}); + assert.equal(result, input); +}); + +test('Does not update $app/store #3', () => { + const input = ` + +{$page.url} +`; + const result = transform_svelte_code(input, {}); + assert.equal(result, input); +}); From e892d77f6a548c81f7c4f121cb58432761bae559 Mon Sep 17 00:00:00 2001 From: Simon Holthausen Date: Fri, 13 Dec 2024 16:49:36 +0100 Subject: [PATCH 02/10] handle aliases --- .../migrate/migrations/page-state/migrate.js | 14 ++++++++----- .../migrations/page-state/migrate.spec.js | 21 +++++++++++++++++++ 2 files changed, 30 insertions(+), 5 deletions(-) diff --git a/packages/migrate/migrations/page-state/migrate.js b/packages/migrate/migrations/page-state/migrate.js index 198a6807..af524b42 100644 --- a/packages/migrate/migrations/page-state/migrate.js +++ b/packages/migrate/migrations/page-state/migrate.js @@ -32,20 +32,24 @@ export function transform_svelte_code(code, options) { const import_match = code.match(/import\s*{([^}]+)}\s*from\s*("|')\$app\/stores\2/); if (!import_match) return code; // nothing to do - const stores = import_match[1].split(',').map((i) => i.trim()); + const stores = import_match[1].split(',').map((i) => { + const str = i.trim(); + const [name, alias] = str.split(' as ').map((s) => s.trim()); + return [name, alias || name]; + }); let modified = code.replace('$app/stores', '$app/state'); - for (const store of stores) { + for (let [store, alias] of stores) { // if someone uses that they're deep into stores and we better not touch this file if (store === 'getStores') return code; - const regex = new RegExp(`\\b${store}\\b`, 'g'); + const regex = new RegExp(`\\b${alias}\\b`, 'g'); let match; let count_removed = 0; while ((match = regex.exec(modified)) !== null) { const before = modified.slice(0, match.index); - const after = modified.slice(match.index + store.length); + const after = modified.slice(match.index + alias.length); if (before.slice(-1) !== '$') { if (/[_'"]/.test(before.slice(-1))) continue; // false positive @@ -65,7 +69,7 @@ export function transform_svelte_code(code, options) { return code; } - modified = before.slice(0, -1) + store + (store === 'page' ? '' : '.current') + after; + modified = before.slice(0, -1) + alias + (store === 'page' ? '' : '.current') + after; count_removed++; } } diff --git a/packages/migrate/migrations/page-state/migrate.spec.js b/packages/migrate/migrations/page-state/migrate.spec.js index 589b9453..b085725e 100644 --- a/packages/migrate/migrations/page-state/migrate.spec.js +++ b/packages/migrate/migrations/page-state/migrate.spec.js @@ -53,6 +53,27 @@ is_navigating: {navigating.current} ); }); +test('Updates $app/store #3', () => { + const result = transform_svelte_code( + ` + +{$_page.data} +`, + {} + ); + assert.equal( + result, + ` + +{_page.data} +` + ); +}); + test('Does not update $app/store #1', () => { const input = ` -is_navigating: {$navigating} +{$navigating?.to?.url.pathname} `, {} ); @@ -48,7 +48,7 @@ is_navigating: {$navigating} updated.check(); -is_navigating: {navigating.current} +{navigating?.to?.url.pathname} ` ); }); From 4d2e5dd419ab8543b8ffb10103eb692540725d93 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Mon, 16 Dec 2024 15:44:26 -0500 Subject: [PATCH 09/10] hopefully this will get it to shut up --- .../migrate/migrations/app-state/migrate.spec.js | 15 ++++++--------- 1 file changed, 6 insertions(+), 9 deletions(-) diff --git a/packages/migrate/migrations/app-state/migrate.spec.js b/packages/migrate/migrations/app-state/migrate.spec.js index b65bfc6f..8c30416e 100644 --- a/packages/migrate/migrations/app-state/migrate.spec.js +++ b/packages/migrate/migrations/app-state/migrate.spec.js @@ -11,8 +11,7 @@ test('Updates $app/store #1', () => { -`, - {} +` ); assert.equal( result, @@ -37,8 +36,7 @@ test('Updates $app/store #2', () => { {$navigating?.to?.url.pathname} -`, - {} +` ); assert.equal( result, @@ -60,8 +58,7 @@ test('Updates $app/store #3', () => { {$_page.data} -`, - {} +` ); assert.equal( result, @@ -82,7 +79,7 @@ test('Does not update $app/store #1', () => { {x} `; - const result = transform_svelte_code(input, {}); + const result = transform_svelte_code(input); assert.equal(result, input); }); @@ -95,7 +92,7 @@ test('Does not update $app/store #2', () => { {$url} `; - const result = transform_svelte_code(input, {}); + const result = transform_svelte_code(input); assert.equal(result, input); }); @@ -107,6 +104,6 @@ test('Does not update $app/store #3', () => { {$page.url} `; - const result = transform_svelte_code(input, {}); + const result = transform_svelte_code(input); assert.equal(result, input); }); From 25a948b6a6336aba44e06bd28a9cb9c432af56c8 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Mon, 16 Dec 2024 16:12:48 -0500 Subject: [PATCH 10/10] add migration task --- packages/migrate/migrations/app-state/migrate.js | 10 ++++++++++ packages/migrate/migrations/app-state/migrate.spec.js | 2 +- 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/packages/migrate/migrations/app-state/migrate.js b/packages/migrate/migrations/app-state/migrate.js index d48b4975..43613663 100644 --- a/packages/migrate/migrations/app-state/migrate.js +++ b/packages/migrate/migrations/app-state/migrate.js @@ -38,6 +38,8 @@ export function transform_svelte_code(code) { }); let modified = code.replace('$app/stores', '$app/state'); + let needs_navigating_migration_task = false; + for (const [store, alias] of stores) { // if someone uses that they're deep into stores and we better not touch this file if (store === 'getStores') return code; @@ -68,10 +70,18 @@ export function transform_svelte_code(code) { return code; } + if (store === 'navigating' && after[0] !== '.') { + needs_navigating_migration_task = true; + } + modified = before.slice(0, -1) + alias + (store === 'updated' ? '.current' : '') + after; count_removed++; } } + if (needs_navigating_migration_task) { + modified = `\n${modified}`; + } + return modified; } diff --git a/packages/migrate/migrations/app-state/migrate.spec.js b/packages/migrate/migrations/app-state/migrate.spec.js index 8c30416e..4162bddd 100644 --- a/packages/migrate/migrations/app-state/migrate.spec.js +++ b/packages/migrate/migrations/app-state/migrate.spec.js @@ -40,7 +40,7 @@ test('Updates $app/store #2', () => { ); assert.equal( result, - `