From ecebe54f85a09ad201a53d9bbdaf73967ed0a8d6 Mon Sep 17 00:00:00 2001
From: Ben McCann <322311+benmccann@users.noreply.github.com>
Date: Thu, 7 Nov 2024 11:34:12 -0800
Subject: [PATCH 1/6] chore: move migrate package to this repository
---
packages/migrate/CHANGELOG.md | 345 ++++++++++++++
packages/migrate/README.md | 31 ++
packages/migrate/bin.js | 27 ++
packages/migrate/migrations/package/index.js | 81 ++++
.../migrations/package/migrate_config.js | 81 ++++
.../migrations/package/migrate_config.spec.js | 78 ++++
.../migrate/migrations/package/migrate_pkg.js | 211 +++++++++
.../migrations/package/migrate_pkg.spec.js | 157 +++++++
packages/migrate/migrations/routes/index.js | 211 +++++++++
.../routes/migrate_page_js/index.js | 195 ++++++++
.../routes/migrate_page_js/index.spec.js | 10 +
.../routes/migrate_page_js/samples.md | 380 ++++++++++++++++
.../routes/migrate_page_server/index.js | 122 +++++
.../routes/migrate_page_server/index.spec.js | 10 +
.../routes/migrate_page_server/samples.md | 181 ++++++++
.../routes/migrate_scripts/index.js | 150 +++++++
.../routes/migrate_scripts/index.spec.js | 14 +
.../routes/migrate_scripts/samples.md | 247 ++++++++++
.../migrations/routes/migrate_server/index.js | 190 ++++++++
.../routes/migrate_server/index.spec.js | 10 +
.../routes/migrate_server/samples.md | 212 +++++++++
packages/migrate/migrations/routes/tasks.js | 5 +
packages/migrate/migrations/routes/utils.js | 374 ++++++++++++++++
.../migrations/self-closing-tags/index.js | 57 +++
.../migrations/self-closing-tags/migrate.js | 192 ++++++++
.../self-closing-tags/migrate.spec.js | 32 ++
packages/migrate/migrations/svelte-4/index.js | 112 +++++
.../migrate/migrations/svelte-4/migrate.js | 348 +++++++++++++++
.../migrations/svelte-4/migrate.spec.js | 382 ++++++++++++++++
packages/migrate/migrations/svelte-5/index.js | 216 +++++++++
.../migrate/migrations/svelte-5/migrate.js | 129 ++++++
.../migrations/svelte-5/migrate.spec.js | 135 ++++++
.../migrate/migrations/sveltekit-2/index.js | 167 +++++++
.../migrate/migrations/sveltekit-2/migrate.js | 318 +++++++++++++
.../migrations/sveltekit-2/migrate.spec.js | 32 ++
.../sveltekit-2/svelte-config-samples.md | 126 ++++++
.../sveltekit-2/tsconfig-samples.md | 40 ++
.../migrations/sveltekit-2/tsjs-samples.md | 170 +++++++
packages/migrate/package.json | 54 +++
packages/migrate/tsconfig.json | 12 +
packages/migrate/utils.js | 421 ++++++++++++++++++
packages/migrate/utils.spec.js | 93 ++++
pnpm-lock.yaml | 267 +++++++++++
43 files changed, 6625 insertions(+)
create mode 100644 packages/migrate/CHANGELOG.md
create mode 100644 packages/migrate/README.md
create mode 100755 packages/migrate/bin.js
create mode 100644 packages/migrate/migrations/package/index.js
create mode 100644 packages/migrate/migrations/package/migrate_config.js
create mode 100644 packages/migrate/migrations/package/migrate_config.spec.js
create mode 100644 packages/migrate/migrations/package/migrate_pkg.js
create mode 100644 packages/migrate/migrations/package/migrate_pkg.spec.js
create mode 100644 packages/migrate/migrations/routes/index.js
create mode 100644 packages/migrate/migrations/routes/migrate_page_js/index.js
create mode 100644 packages/migrate/migrations/routes/migrate_page_js/index.spec.js
create mode 100644 packages/migrate/migrations/routes/migrate_page_js/samples.md
create mode 100644 packages/migrate/migrations/routes/migrate_page_server/index.js
create mode 100644 packages/migrate/migrations/routes/migrate_page_server/index.spec.js
create mode 100644 packages/migrate/migrations/routes/migrate_page_server/samples.md
create mode 100644 packages/migrate/migrations/routes/migrate_scripts/index.js
create mode 100644 packages/migrate/migrations/routes/migrate_scripts/index.spec.js
create mode 100644 packages/migrate/migrations/routes/migrate_scripts/samples.md
create mode 100644 packages/migrate/migrations/routes/migrate_server/index.js
create mode 100644 packages/migrate/migrations/routes/migrate_server/index.spec.js
create mode 100644 packages/migrate/migrations/routes/migrate_server/samples.md
create mode 100644 packages/migrate/migrations/routes/tasks.js
create mode 100644 packages/migrate/migrations/routes/utils.js
create mode 100644 packages/migrate/migrations/self-closing-tags/index.js
create mode 100644 packages/migrate/migrations/self-closing-tags/migrate.js
create mode 100644 packages/migrate/migrations/self-closing-tags/migrate.spec.js
create mode 100644 packages/migrate/migrations/svelte-4/index.js
create mode 100644 packages/migrate/migrations/svelte-4/migrate.js
create mode 100644 packages/migrate/migrations/svelte-4/migrate.spec.js
create mode 100644 packages/migrate/migrations/svelte-5/index.js
create mode 100644 packages/migrate/migrations/svelte-5/migrate.js
create mode 100644 packages/migrate/migrations/svelte-5/migrate.spec.js
create mode 100644 packages/migrate/migrations/sveltekit-2/index.js
create mode 100644 packages/migrate/migrations/sveltekit-2/migrate.js
create mode 100644 packages/migrate/migrations/sveltekit-2/migrate.spec.js
create mode 100644 packages/migrate/migrations/sveltekit-2/svelte-config-samples.md
create mode 100644 packages/migrate/migrations/sveltekit-2/tsconfig-samples.md
create mode 100644 packages/migrate/migrations/sveltekit-2/tsjs-samples.md
create mode 100644 packages/migrate/package.json
create mode 100644 packages/migrate/tsconfig.json
create mode 100644 packages/migrate/utils.js
create mode 100644 packages/migrate/utils.spec.js
diff --git a/packages/migrate/CHANGELOG.md b/packages/migrate/CHANGELOG.md
new file mode 100644
index 00000000..96020c5b
--- /dev/null
+++ b/packages/migrate/CHANGELOG.md
@@ -0,0 +1,345 @@
+# svelte-migrate
+
+## 1.6.8
+### Patch Changes
+
+
+- fix: prevent duplicate imports ([#12931](https://github.com/sveltejs/kit/pull/12931))
+
+## 1.6.7
+### Patch Changes
+
+
+- fix: prefer TS in unclear migration situations if `tsconfig.json` found ([#12881](https://github.com/sveltejs/kit/pull/12881))
+
+## 1.6.6
+### Patch Changes
+
+
+- docs: update URLs for new svelte.dev site ([#12857](https://github.com/sveltejs/kit/pull/12857))
+
+## 1.6.5
+### Patch Changes
+
+
+- docs: demonstrate sv migrate over prior commands ([#12840](https://github.com/sveltejs/kit/pull/12840))
+
+
+- fix: bump enhanced-img version to avoid peer dep warning ([#12852](https://github.com/sveltejs/kit/pull/12852))
+
+## 1.6.4
+### Patch Changes
+
+
+- fix: migrate `svelte` and `vite-plugin-svelte` to latest ([#12838](https://github.com/sveltejs/kit/pull/12838))
+
+## 1.6.3
+### Patch Changes
+
+
+- chore: add `svelte-eslint-parser` to list of migratable dependencies ([#12828](https://github.com/sveltejs/kit/pull/12828))
+
+## 1.6.2
+### Patch Changes
+
+
+- chore: upgrade to ts-morph 24 ([#12781](https://github.com/sveltejs/kit/pull/12781))
+
+## 1.6.1
+### Patch Changes
+
+
+- chore: upgrade to ts-morph 23 ([#12607](https://github.com/sveltejs/kit/pull/12607))
+
+## 1.6.0
+### Minor Changes
+
+
+- feat: pass filename to `migrate` to allow for `svelte:self` migration ([#12749](https://github.com/sveltejs/kit/pull/12749))
+
+
+### Patch Changes
+
+
+- fix: prompt SvelteKit 2 migration during Svelte 5 migration if necessary ([#12748](https://github.com/sveltejs/kit/pull/12748))
+
+## 1.5.1
+### Patch Changes
+
+
+- fix: use `next` versions for `svelte` and `vite-plugin-svelte` ([#12729](https://github.com/sveltejs/kit/pull/12729))
+
+## 1.5.0
+### Minor Changes
+
+
+- feat: add Svelte 5 migration ([#12519](https://github.com/sveltejs/kit/pull/12519))
+
+## 1.4.5
+### Patch Changes
+
+
+- chore: configure provenance in a simpler manner ([#12570](https://github.com/sveltejs/kit/pull/12570))
+
+## 1.4.4
+### Patch Changes
+
+
+- chore: package provenance ([#12567](https://github.com/sveltejs/kit/pull/12567))
+
+## 1.4.3
+
+### Patch Changes
+
+- chore: add keywords for discovery in npm search ([#12330](https://github.com/sveltejs/kit/pull/12330))
+
+## 1.4.2
+
+### Patch Changes
+
+- fix: bump import-meta-resolve to remove deprecation warnings ([#12240](https://github.com/sveltejs/kit/pull/12240))
+
+## 1.4.1
+
+### Patch Changes
+
+- fix: continue traversing the children of non-self-closing elements ([#12175](https://github.com/sveltejs/kit/pull/12175))
+
+## 1.4.0
+
+### Minor Changes
+
+- feat: add self-closing-tags migration ([#12128](https://github.com/sveltejs/kit/pull/12128))
+
+## 1.3.8
+
+### Patch Changes
+
+- chore(deps): update dependency ts-morph to v22 ([`4447269e979f2b5be18e0fded0b5843a6258542d`](https://github.com/sveltejs/kit/commit/4447269e979f2b5be18e0fded0b5843a6258542d))
+
+## 1.3.7
+
+### Patch Changes
+
+- fix: don't downgrade versions when bumping dependencies ([#11716](https://github.com/sveltejs/kit/pull/11716))
+
+## 1.3.6
+
+### Patch Changes
+
+- fix: correct link to docs ([#11407](https://github.com/sveltejs/kit/pull/11407))
+
+## 1.3.5
+
+### Patch Changes
+
+- chore: update primary branch from master to main ([`47779436c5f6c4d50011d0ef8b2709a07c0fec5d`](https://github.com/sveltejs/kit/commit/47779436c5f6c4d50011d0ef8b2709a07c0fec5d))
+
+## 1.3.4
+
+### Patch Changes
+
+- suggest running migrate command with latest if migration does not exist ([#11362](https://github.com/sveltejs/kit/pull/11362))
+
+## 1.3.3
+
+### Patch Changes
+
+- chore: insert package at sorted position ([#11332](https://github.com/sveltejs/kit/pull/11332))
+
+- fix: adjust cookie migration logic, note installation ([#11331](https://github.com/sveltejs/kit/pull/11331))
+
+## 1.3.2
+
+### Patch Changes
+
+- fix: handle jsconfig.json ([#11325](https://github.com/sveltejs/kit/pull/11325))
+
+## 1.3.1
+
+### Patch Changes
+
+- chore: fix broken migration links ([#11320](https://github.com/sveltejs/kit/pull/11320))
+
+## 1.3.0
+
+### Minor Changes
+
+- feat: add sveltekit v2 migration ([#11294](https://github.com/sveltejs/kit/pull/11294))
+
+## 1.2.8
+
+### Patch Changes
+
+- chore(deps): update dependency ts-morph to v21 ([#11181](https://github.com/sveltejs/kit/pull/11181))
+
+## 1.2.7
+
+### Patch Changes
+
+- chore(deps): update dependency ts-morph to v20 ([#10766](https://github.com/sveltejs/kit/pull/10766))
+
+## 1.2.6
+
+### Patch Changes
+
+- fix: do not downgrade versions ([#10352](https://github.com/sveltejs/kit/pull/10352))
+
+## 1.2.5
+
+### Patch Changes
+
+- fix: note old eslint plugin deprecation ([#10319](https://github.com/sveltejs/kit/pull/10319))
+
+## 1.2.4
+
+### Patch Changes
+
+- fix: ensure glob finds all files in folders ([#10230](https://github.com/sveltejs/kit/pull/10230))
+
+## 1.2.3
+
+### Patch Changes
+
+- fix: handle missing fields in migrate script ([#10221](https://github.com/sveltejs/kit/pull/10221))
+
+## 1.2.2
+
+### Patch Changes
+
+- fix: finalize svelte-4 migration ([#10195](https://github.com/sveltejs/kit/pull/10195))
+
+- fix: changed `index` to `index.d.ts` in `typesVersions` ([#10180](https://github.com/sveltejs/kit/pull/10180))
+
+## 1.2.1
+
+### Patch Changes
+
+- docs: update readme ([#10066](https://github.com/sveltejs/kit/pull/10066))
+
+## 1.2.0
+
+### Minor Changes
+
+- feat: add Svelte 4 migration ([#9729](https://github.com/sveltejs/kit/pull/9729))
+
+## 1.1.3
+
+### Patch Changes
+
+- fix: include index in typesVersions because it's always matched ([#9147](https://github.com/sveltejs/kit/pull/9147))
+
+## 1.1.2
+
+### Patch Changes
+
+- fix: update existing exports with prepended outdir ([#9133](https://github.com/sveltejs/kit/pull/9133))
+
+- fix: use typesVersions to wire up deep imports ([#9133](https://github.com/sveltejs/kit/pull/9133))
+
+## 1.1.1
+
+### Patch Changes
+
+- fix: include utils in migrate's published files ([#9085](https://github.com/sveltejs/kit/pull/9085))
+
+## 1.1.0
+
+### Minor Changes
+
+- feat: add `@sveltejs/package` migration (v1->v2) ([#8922](https://github.com/sveltejs/kit/pull/8922))
+
+## 1.0.1
+
+### Patch Changes
+
+- fix: correctly check for old load props ([#8537](https://github.com/sveltejs/kit/pull/8537))
+
+## 1.0.0
+
+### Major Changes
+
+First major release, see below for the history of changes that lead up to this.
+Starting from now all releases follow semver and changes will be listed as Major/Minor/Patch
+
+## 1.0.0-next.13
+
+### Patch Changes
+
+- fix: more robust uppercase migration ([#7033](https://github.com/sveltejs/kit/pull/7033))
+
+## 1.0.0-next.12
+
+### Patch Changes
+
+- feat: do uppercase http verbs migration on the fly ([#6371](https://github.com/sveltejs/kit/pull/6371))
+
+## 1.0.0-next.11
+
+### Patch Changes
+
+- fix: git mv files correctly when they contain \$ characters ([#6129](https://github.com/sveltejs/kit/pull/6129))
+
+## 1.0.0-next.10
+
+### Patch Changes
+
+- Revert change to suggest props destructuring ([#6099](https://github.com/sveltejs/kit/pull/6099))
+
+## 1.0.0-next.9
+
+### Patch Changes
+
+- Handle Error without message, handle status 200, handle missing body ([#6096](https://github.com/sveltejs/kit/pull/6096))
+
+## 1.0.0-next.8
+
+### Patch Changes
+
+- Suggest props destructuring if possible ([#6069](https://github.com/sveltejs/kit/pull/6069))
+- Fix typo in migration task ([#6070](https://github.com/sveltejs/kit/pull/6070))
+
+## 1.0.0-next.7
+
+### Patch Changes
+
+- Migrate type comments on arrow functions ([#5933](https://github.com/sveltejs/kit/pull/5933))
+- Use LayoutLoad inside +layout.js files ([#5931](https://github.com/sveltejs/kit/pull/5931))
+
+## 1.0.0-next.6
+
+### Patch Changes
+
+- Create `.ts` files from `${whitespace}`;
+ }
+
+ if (/lang(?:uage)?=(['"])(ts|typescript)\1/.test(attrs)) {
+ ext = '.ts';
+ }
+
+ module = dedent(contents.replace(/^\n/, ''));
+
+ const declared = find_declarations(contents);
+ const delete_var = (/** @type {string } */ key) => {
+ const declaration = declared?.get(key);
+ if (declaration && !declaration.import) {
+ declared?.delete(key);
+ }
+ };
+ delete_var('load');
+ delete_var('router');
+ delete_var('hydrate');
+ delete_var('prerender');
+ const delete_kit_type = (/** @type {string } */ key) => {
+ const declaration = declared?.get(key);
+ if (
+ declaration &&
+ declaration.import?.type_only &&
+ declaration.import.from === '@sveltejs/kit' &&
+ !new RegExp(`\\W${key}\\W`).test(except_str(content, match))
+ ) {
+ declared?.delete(key);
+ }
+ };
+ delete_kit_type('Load');
+ delete_kit_type('LoadEvent');
+ delete_kit_type('LoadOutput');
+
+ if (!declared || declared.size > 0) {
+ const body = `\n${indent}${error(
+ 'Check code was safely removed',
+ TASKS.PAGE_MODULE_CTX
+ )}\n${comment(contents, indent)}`;
+
+ return `${whitespace}`;
+ }
+
+ // nothing was declared here, we can safely remove the script
+ return '';
+ }
+
+ if (!is_error && /export let [\w]+[^"`'\w\s]/.test(contents)) {
+ contents = `\n${indent}${error('Add data prop', TASKS.PAGE_DATA_PROP)}\n${contents}`;
+ // Possible TODO: migrate props to data.prop, or suggest $: ({propX, propY, ...} = data);
+ }
+
+ return `${whitespace}`;
+ }
+ );
+
+ return { module, main, ext };
+}
+
+/** @param {string} content */
+function find_declarations(content) {
+ const file = parse(content);
+ if (!file) return;
+
+ /** @type {Map} */
+ const declared = new Map();
+ /**
+ * @param {string} name
+ * @param {{from: string, type_only: boolean}} [import_def]
+ */
+ function add(name, import_def) {
+ declared.set(name, { name, import: import_def });
+ }
+
+ for (const statement of file.ast.statements) {
+ if (ts.isImportDeclaration(statement) && statement.importClause) {
+ const type_only = statement.importClause.isTypeOnly;
+ const from = ts.isStringLiteral(statement.moduleSpecifier)
+ ? statement.moduleSpecifier.text
+ : '';
+
+ if (statement.importClause.name) {
+ add(statement.importClause.name.text, { from, type_only });
+ }
+
+ const bindings = statement.importClause.namedBindings;
+
+ if (bindings) {
+ if (ts.isNamespaceImport(bindings)) {
+ add(bindings.name.text, { from, type_only });
+ } else {
+ for (const binding of bindings.elements) {
+ add(binding.name.text, { from, type_only: type_only || binding.isTypeOnly });
+ }
+ }
+ }
+ } else if (ts.isVariableStatement(statement)) {
+ for (const declaration of statement.declarationList.declarations) {
+ if (ts.isIdentifier(declaration.name)) {
+ add(declaration.name.text);
+ } else {
+ return; // bail out if it's not a simple variable
+ }
+ }
+ } else if (ts.isFunctionDeclaration(statement) || ts.isClassDeclaration(statement)) {
+ if (statement.name && ts.isIdentifier(statement.name)) {
+ add(statement.name.text);
+ }
+ } else if (ts.isExportDeclaration(statement) && !statement.exportClause) {
+ return; // export * from '..' -> bail
+ }
+ }
+
+ return declared;
+}
diff --git a/packages/migrate/migrations/routes/migrate_scripts/index.spec.js b/packages/migrate/migrations/routes/migrate_scripts/index.spec.js
new file mode 100644
index 00000000..a52fc712
--- /dev/null
+++ b/packages/migrate/migrations/routes/migrate_scripts/index.spec.js
@@ -0,0 +1,14 @@
+import { assert, test } from 'vitest';
+import { migrate_scripts } from './index.js';
+import { read_samples } from '../../../utils.js';
+
+for (const sample of read_samples(new URL('./samples.md', import.meta.url))) {
+ test(sample.description, () => {
+ const actual = migrate_scripts(
+ sample.before,
+ sample.description.includes('error'),
+ sample.description.includes('moved')
+ );
+ assert.equal(actual.main, sample.after);
+ });
+}
diff --git a/packages/migrate/migrations/routes/migrate_scripts/samples.md b/packages/migrate/migrations/routes/migrate_scripts/samples.md
new file mode 100644
index 00000000..6f57fb1f
--- /dev/null
+++ b/packages/migrate/migrations/routes/migrate_scripts/samples.md
@@ -0,0 +1,247 @@
+## No module context, no page exports
+
+```svelte before
+
+
+
+
+{sry}
+```
+
+```svelte after
+
+
+
+
+{sry}
+```
+
+## Module context that can be removed
+
+```svelte before
+
+
+
+```
+
+```svelte after
+
+```
+
+## Module context with moved imports
+
+```svelte before
+
+
+
+
+{sry}
+```
+
+```svelte after
+
+
+
+
+{sry}
+```
+
+## Module context with type imports only
+
+```svelte before
+
+```
+
+```svelte after
+```
+
+## Module context with type imports only but used in instance script
+
+```svelte before
+
+
+
+```
+
+```svelte after
+
+
+
+```
+
+## Module context with export * from '..'
+
+```svelte before
+
+```
+
+```svelte after
+
+```
+
+## Module context with named imports
+
+```svelte before
+
+
+
+```
+
+```svelte after
+
+
+
+```
+
+## Module context with named imports that have same name as a load option
+
+```svelte before
+
+
+
+```
+
+```svelte after
+
+
+
+```
diff --git a/packages/migrate/migrations/routes/migrate_server/index.js b/packages/migrate/migrations/routes/migrate_server/index.js
new file mode 100644
index 00000000..d9092d55
--- /dev/null
+++ b/packages/migrate/migrations/routes/migrate_server/index.js
@@ -0,0 +1,190 @@
+import ts from 'typescript';
+import {
+ automigration,
+ uppercase_migration,
+ error,
+ get_function_node,
+ get_object_nodes,
+ is_new,
+ is_string_like,
+ manual_return_migration,
+ parse,
+ rewrite_returns,
+ unwrap
+} from '../utils.js';
+import * as TASKS from '../tasks.js';
+import { dedent, guess_indent, indent_at_line } from '../../../utils.js';
+
+const give_up = `${error('Update +server.js', TASKS.STANDALONE_ENDPOINT)}\n\n`;
+
+/**
+ * @param {string} content
+ * @returns {string}
+ */
+export function migrate_server(content) {
+ const file = parse(content);
+ if (!file) return give_up + content;
+
+ const indent = guess_indent(content);
+
+ const methods = ['GET', 'POST', 'PUT', 'PATCH', 'DELETE'].filter((name) =>
+ file.exports.map.has(name)
+ );
+
+ // If user didn't do the uppercase verbs migration yet, do it here on the fly.
+ const uppercased = uppercase_migration(methods, file);
+ if (!uppercased) {
+ return give_up + content;
+ } else if (uppercased !== content) {
+ return migrate_server(uppercased);
+ }
+
+ const unmigrated = new Set(methods);
+
+ /** @type {Map} */
+ const imports = new Map();
+
+ for (const statement of file.ast.statements) {
+ for (const method of methods) {
+ const fn = get_function_node(statement, /** @type{string} */ (file.exports.map.get(method)));
+ if (fn?.body) {
+ rewrite_returns(fn.body, (expr, node) => {
+ // leave `() => new Response(...)` alone
+ if (is_new(expr, 'Response')) return;
+
+ const value = unwrap(expr);
+ const nodes = ts.isObjectLiteralExpression(value) && get_object_nodes(value);
+
+ if (nodes) {
+ const body_is_object_literal = nodes.body && ts.isObjectLiteralExpression(nodes.body);
+
+ if (body_is_object_literal || (nodes.body && ts.isIdentifier(nodes.body))) {
+ let result;
+
+ let name = 'json';
+ let i = 1;
+ while (content.includes(name)) name = `json$${i++}`;
+
+ imports.set('json', name);
+
+ const body = dedent(nodes.body.getText());
+
+ if (nodes.headers || (nodes.status && nodes.status.getText() !== '200')) {
+ const start = indent_at_line(content, expr.pos);
+ const properties = [];
+
+ if (nodes.status && nodes.status.getText() !== '200') {
+ properties.push(`status: ${nodes.status.getText()}`);
+ }
+
+ if (nodes.headers) {
+ properties.push(`headers: ${nodes.headers.getText()}`);
+ }
+
+ const ws = `\n${start}`;
+ const init = `{${ws}${indent}${properties.join(`,${ws}${indent}`)}${ws}}`;
+
+ result = `${name}(${body}, ${init})`;
+ } else {
+ result = `${name}(${body})`;
+ }
+
+ if (body_is_object_literal) {
+ automigration(expr, file.code, result);
+ } else {
+ manual_return_migration(
+ node || fn,
+ file.code,
+ TASKS.STANDALONE_ENDPOINT,
+ `return ${result};`
+ );
+ }
+
+ return;
+ }
+
+ let safe_headers = !nodes.headers || !ts.isObjectLiteralExpression(nodes.headers);
+ if (nodes.headers && ts.isObjectLiteralExpression(nodes.headers)) {
+ // if `headers` is an object literal, and it either doesn't contain
+ // `set-cookie` or `set-cookie` is a string, then the headers
+ // are safe to use in a `Response`
+ const set_cookie_value = nodes.headers.properties.find((prop) => {
+ return (
+ ts.isPropertyAssignment(prop) &&
+ ts.isStringLiteral(prop.name) &&
+ /set-cookie/i.test(prop.name.text)
+ );
+ });
+
+ if (!set_cookie_value || is_string_like(set_cookie_value)) {
+ safe_headers = true;
+ }
+ }
+
+ const safe_body =
+ !nodes.body ||
+ is_string_like(nodes.body) ||
+ (ts.isCallExpression(nodes.body) &&
+ nodes.body.expression.getText() === 'JSON.stringify');
+
+ if (safe_headers) {
+ const status = nodes.status ? nodes.status.getText() : '200';
+ const headers = nodes.headers?.getText();
+ const body = dedent(nodes.body?.getText() || 'undefined');
+
+ const multiline = /\n/.test(headers);
+
+ const init = [
+ status !== '200' && `status: ${status}`,
+ headers && `headers: ${headers}`
+ ].filter(Boolean);
+
+ const indent = indent_at_line(content, expr.getStart());
+ const end_whitespace = multiline ? `\n${indent}` : ' ';
+ const join_whitespace = multiline ? end_whitespace + guess_indent(content) : ' ';
+
+ const response =
+ init.length > 0
+ ? `new Response(${body}, {${join_whitespace}${init.join(
+ `,${join_whitespace}`
+ )}${end_whitespace}})`
+ : `new Response(${body})`;
+
+ if (safe_body) {
+ automigration(expr, file.code, response);
+ } else {
+ manual_return_migration(
+ node || fn,
+ file.code,
+ TASKS.STANDALONE_ENDPOINT,
+ `return ${response};`
+ );
+ }
+
+ return;
+ }
+ }
+
+ manual_return_migration(node || fn, file.code, TASKS.STANDALONE_ENDPOINT);
+ });
+
+ unmigrated.delete(method);
+ }
+ }
+ }
+
+ if (imports.size) {
+ const has_imports = file.ast.statements.some((statement) => ts.isImportDeclaration(statement));
+ const specifiers = Array.from(imports).map(([name, local]) =>
+ name === local ? name : `${name} as ${local}`
+ );
+ const declaration = `import { ${specifiers.join(', ')} } from '@sveltejs/kit';`;
+ file.code.prependLeft(0, declaration + (has_imports ? '\n' : '\n\n'));
+ }
+
+ if (unmigrated.size) {
+ return give_up + file.code.toString();
+ }
+
+ return file.code.toString();
+}
diff --git a/packages/migrate/migrations/routes/migrate_server/index.spec.js b/packages/migrate/migrations/routes/migrate_server/index.spec.js
new file mode 100644
index 00000000..8ad42453
--- /dev/null
+++ b/packages/migrate/migrations/routes/migrate_server/index.spec.js
@@ -0,0 +1,10 @@
+import { assert, test } from 'vitest';
+import { migrate_server } from './index.js';
+import { read_samples } from '../../../utils.js';
+
+for (const sample of read_samples(new URL('./samples.md', import.meta.url))) {
+ test(sample.description, () => {
+ const actual = migrate_server(sample.before);
+ assert.equal(actual, sample.after);
+ });
+}
diff --git a/packages/migrate/migrations/routes/migrate_server/samples.md b/packages/migrate/migrations/routes/migrate_server/samples.md
new file mode 100644
index 00000000..3bc08662
--- /dev/null
+++ b/packages/migrate/migrations/routes/migrate_server/samples.md
@@ -0,0 +1,212 @@
+## A GET function that returns a JSON object
+
+```js before
+export function GET() {
+ return {
+ body: {
+ a: 1
+ }
+ };
+}
+```
+
+```js after
+import { json } from '@sveltejs/kit';
+
+export function GET() {
+ return json({
+ a: 1
+ });
+}
+```
+
+## A GET function that returns a JSON object and already specifies a 'json' identifier
+
+```js before
+export function GET() {
+ const json = 'shadow';
+
+ return {
+ body: {
+ a: 1
+ }
+ };
+}
+```
+
+```js after
+import { json as json$1 } from '@sveltejs/kit';
+
+export function GET() {
+ const json = 'shadow';
+
+ return json$1({
+ a: 1
+ });
+}
+```
+
+## A GET function that returns a JSON object with custom headers
+
+```js before
+export function GET() {
+ return {
+ headers: {
+ 'x-foo': '123'
+ },
+ body: {
+ a: 1
+ }
+ };
+}
+```
+
+```js after
+import { json } from '@sveltejs/kit';
+
+export function GET() {
+ return json({
+ a: 1
+ }, {
+ headers: {
+ 'x-foo': '123'
+ }
+ });
+}
+```
+
+## A GET arrow function that returns a JSON object
+
+```js before
+export const GET = () => ({
+ body: {
+ a: 1
+ }
+});
+```
+
+```js after
+import { json } from '@sveltejs/kit';
+
+export const GET = () => json({
+ a: 1
+});
+```
+
+## GET returns we can't migrate
+
+```js before
+export function GET() {
+ if (a) {
+ return {
+ body
+ };
+ } else if (b) {
+ return {
+ body: new ReadableStream(),
+ headers: {
+ 'content-type': 'octasomething'
+ }
+ }
+ } else if (c) {
+ return {
+ body: 'string',
+ headers: {
+ 'x-foo': 'bar'
+ }
+ }
+ }
+}
+```
+
+```js after
+import { json } from '@sveltejs/kit';
+
+export function GET() {
+ if (a) {
+ throw new Error("@migration task: Migrate this return statement (https://github.com/sveltejs/kit/discussions/5774#discussioncomment-3292701)");
+ // Suggestion (check for correctness before using):
+ // return json(body);
+ return {
+ body
+ };
+ } else if (b) {
+ throw new Error("@migration task: Migrate this return statement (https://github.com/sveltejs/kit/discussions/5774#discussioncomment-3292701)");
+ // Suggestion (check for correctness before using):
+ // return new Response(new ReadableStream(), {
+ // headers: {
+ // 'content-type': 'octasomething'
+ // }
+ // });
+ return {
+ body: new ReadableStream(),
+ headers: {
+ 'content-type': 'octasomething'
+ }
+ }
+ } else if (c) {
+ return new Response('string', {
+ headers: {
+ 'x-foo': 'bar'
+ }
+ })
+ }
+}
+```
+
+## A function that returns a Response
+
+```js before
+export const GET = () => new Response('text');
+```
+
+```js after
+export const GET = () => new Response('text');
+```
+
+## A function that returns an unknown value
+
+```js before
+export const GET = () => createResponse('text');
+```
+
+```js after
+throw new Error("@migration task: Migrate this return statement (https://github.com/sveltejs/kit/discussions/5774#discussioncomment-3292701)");
+export const GET = () => createResponse('text');
+```
+
+## A function that returns nothing
+
+```js before
+export function GET() {
+ return;
+}
+```
+
+```js after
+export function GET() {
+ return;
+}
+```
+
+## A GET function that returns a JSON object
+
+```js before
+export function get() {
+ return {
+ body: {
+ a: 1
+ }
+ };
+}
+```
+
+```js after
+import { json } from '@sveltejs/kit';
+
+export function GET() {
+ return json({
+ a: 1
+ });
+}
+```
diff --git a/packages/migrate/migrations/routes/tasks.js b/packages/migrate/migrations/routes/tasks.js
new file mode 100644
index 00000000..77562323
--- /dev/null
+++ b/packages/migrate/migrations/routes/tasks.js
@@ -0,0 +1,5 @@
+export const STANDALONE_ENDPOINT = '3292701';
+export const PAGE_ENDPOINT = '3292699';
+export const PAGE_LOAD = '3292693';
+export const PAGE_MODULE_CTX = '3292722';
+export const PAGE_DATA_PROP = '3292707';
diff --git a/packages/migrate/migrations/routes/utils.js b/packages/migrate/migrations/routes/utils.js
new file mode 100644
index 00000000..0b8073e6
--- /dev/null
+++ b/packages/migrate/migrations/routes/utils.js
@@ -0,0 +1,374 @@
+import ts from 'typescript';
+import MagicString from 'magic-string';
+import { comment, indent_at_line } from '../../utils.js';
+
+/**
+ * @param {string} description
+ * @param {string} [comment_id]
+ */
+export function task(description, comment_id) {
+ return (
+ `@migration task: ${description}` +
+ (comment_id
+ ? ` (https://github.com/sveltejs/kit/discussions/5774#discussioncomment-${comment_id})`
+ : '')
+ );
+}
+
+/**
+ * @param {string} description
+ * @param {string} comment_id
+ */
+export function error(description, comment_id) {
+ return `throw new Error(${JSON.stringify(task(description, comment_id))});`;
+}
+
+/** @param {string} content */
+export function adjust_imports(content) {
+ try {
+ const ast = ts.createSourceFile(
+ 'filename.ts',
+ content,
+ ts.ScriptTarget.Latest,
+ true,
+ ts.ScriptKind.TS
+ );
+
+ const code = new MagicString(content);
+
+ /** @param {number} pos */
+ function adjust(pos) {
+ // TypeScript AST is a clusterfuck, we need to step forward to find
+ // where the node _actually_ starts
+ while (content[pos] !== '.') pos += 1;
+
+ // replace ../ with ../../ and ./ with ../
+ code.prependLeft(pos, content[pos + 1] === '.' ? '../' : '.');
+ }
+
+ /** @param {ts.Node} node */
+ function walk(node) {
+ if (ts.isImportDeclaration(node)) {
+ const text = /** @type {ts.StringLiteral} */ (node.moduleSpecifier).text;
+ if (text[0] === '.') adjust(node.moduleSpecifier.pos);
+ }
+
+ if (ts.isCallExpression(node) && node.expression.getText() === 'import') {
+ const arg = node.arguments[0];
+
+ if (ts.isStringLiteral(arg)) {
+ if (arg.text[0] === '.') adjust(arg.pos);
+ } else if (ts.isTemplateLiteral(arg) && !ts.isNoSubstitutionTemplateLiteral(arg)) {
+ if (arg.head.text[0] === '.') adjust(arg.head.pos);
+ }
+ }
+
+ node.forEachChild(walk);
+ }
+
+ ast.forEachChild(walk);
+
+ return code.toString();
+ } catch {
+ // this is enough of an edge case that it's probably fine to
+ // just leave the code as we found it
+ return content;
+ }
+}
+
+/**
+ *
+ * @param {ts.Node} node
+ * @param {MagicString} str
+ * @param {string} comment_nr
+ * @param {string} [suggestion]
+ */
+export function manual_return_migration(node, str, comment_nr, suggestion) {
+ manual_migration(node, str, 'Migrate this return statement', comment_nr, suggestion);
+}
+
+/**
+ * @param {ts.Node} node
+ * @param {MagicString} str
+ * @param {string} message
+ * @param {string} comment_nr
+ * @param {string} [suggestion]
+ */
+export function manual_migration(node, str, message, comment_nr, suggestion) {
+ // handle case where this is called on a (arrow) function
+ if (ts.isFunctionExpression(node) || ts.isArrowFunction(node)) {
+ node = node.parent.parent.parent;
+ }
+
+ const indent = indent_at_line(str.original, node.getStart());
+
+ let appended = '';
+
+ if (suggestion) {
+ appended = `\n${indent}// Suggestion (check for correctness before using):\n${indent}// ${comment(
+ suggestion,
+ indent
+ )}`;
+ }
+
+ str.prependLeft(node.getStart(), error(message, comment_nr) + appended + `\n${indent}`);
+}
+
+/**
+ *
+ * @param {ts.Node} node
+ * @param {MagicString} str
+ * @param {string} migration
+ */
+export function automigration(node, str, migration) {
+ str.overwrite(node.getStart(), node.getEnd(), migration);
+}
+
+/**
+ * @param {ts.ObjectLiteralExpression} node
+ */
+export function get_object_nodes(node) {
+ /** @type {Record} */
+ const obj = {};
+
+ for (const property of node.properties) {
+ if (ts.isPropertyAssignment(property) && ts.isIdentifier(property.name)) {
+ obj[property.name.text] = property.initializer;
+ } else if (ts.isShorthandPropertyAssignment(property)) {
+ obj[property.name.text] = property.name;
+ } else {
+ return null; // object contains funky stuff like computed properties/accessors — bail
+ }
+ }
+
+ return obj;
+}
+
+/**
+ * @param {ts.Node} node
+ */
+export function is_string_like(node) {
+ return (
+ ts.isStringLiteral(node) ||
+ ts.isTemplateExpression(node) ||
+ ts.isNoSubstitutionTemplateLiteral(node)
+ );
+}
+
+/** @param {ts.SourceFile} node */
+export function get_exports(node) {
+ /** @type {Map} */
+ const map = new Map();
+
+ let complex = false;
+
+ for (const statement of node.statements) {
+ if (
+ ts.isExportDeclaration(statement) &&
+ statement.exportClause &&
+ ts.isNamedExports(statement.exportClause)
+ ) {
+ // export { x }, export { x as y }
+ for (const specifier of statement.exportClause.elements) {
+ map.set(specifier.name.text, specifier.propertyName?.text || specifier.name.text);
+ }
+ } else if (
+ ts.isFunctionDeclaration(statement) &&
+ statement.name &&
+ ts.getModifiers(statement)?.[0]?.kind === ts.SyntaxKind.ExportKeyword
+ ) {
+ // export function x ...
+ map.set(statement.name.text, statement.name.text);
+ } else if (
+ ts.isVariableStatement(statement) &&
+ ts.getModifiers(statement)?.[0]?.kind === ts.SyntaxKind.ExportKeyword
+ ) {
+ // export const x = ..., y = ...
+ for (const declaration of statement.declarationList.declarations) {
+ if (ts.isIdentifier(declaration.name)) {
+ map.set(declaration.name.text, declaration.name.text);
+ } else {
+ // might need to bail out on encountering this edge case,
+ // because this stuff can get pretty intense
+ complex = true;
+ }
+ }
+ }
+ }
+
+ return { map, complex };
+}
+
+/**
+ * @param {ts.Node} statement
+ * @param {string[]} names
+ * @returns {ts.FunctionDeclaration | ts.FunctionExpression | ts.ArrowFunction | undefined}
+ */
+export function get_function_node(statement, ...names) {
+ if (
+ ts.isFunctionDeclaration(statement) &&
+ statement.name &&
+ names.includes(statement.name.text)
+ ) {
+ // export function x ...
+ return statement;
+ }
+
+ if (ts.isVariableStatement(statement)) {
+ for (const declaration of statement.declarationList.declarations) {
+ if (
+ ts.isIdentifier(declaration.name) &&
+ names.includes(declaration.name.text) &&
+ declaration.initializer &&
+ (ts.isArrowFunction(declaration.initializer) ||
+ ts.isFunctionExpression(declaration.initializer))
+ ) {
+ // export const x = ...
+ return declaration.initializer;
+ }
+ }
+ }
+}
+
+/**
+ * Utility for rewriting return statements.
+ * If `node` is `undefined`, it means it's a concise arrow function body (`() => ({}))`.
+ * Lone `return;` statements are left untouched.
+ * @param {ts.Block | ts.ConciseBody} block
+ * @param {(expression: ts.Expression, node: ts.ReturnStatement | undefined) => void} callback
+ */
+export function rewrite_returns(block, callback) {
+ if (ts.isBlock(block)) {
+ /** @param {ts.Node} node */
+ function walk(node) {
+ if (
+ ts.isArrowFunction(node) ||
+ ts.isFunctionExpression(node) ||
+ ts.isFunctionDeclaration(node)
+ ) {
+ // don't cross this boundary
+ return;
+ }
+
+ if (ts.isReturnStatement(node) && node.expression) {
+ callback(node.expression, node);
+ return;
+ }
+
+ node.forEachChild(walk);
+ }
+
+ block.forEachChild(walk);
+ } else {
+ callback(block, undefined);
+ }
+}
+
+/** @param {ts.Node} node */
+export function unwrap(node) {
+ if (ts.isParenthesizedExpression(node)) {
+ return node.expression;
+ }
+
+ return node;
+}
+
+/**
+ * @param {ts.Node} node
+ * @param {string} name
+ * @returns {node is ts.isNewExpression}
+ */
+export function is_new(node, name) {
+ return (
+ ts.isNewExpression(node) && ts.isIdentifier(node.expression) && node.expression.text === name
+ );
+}
+
+/** @param {string} content */
+export function parse(content) {
+ try {
+ const ast = ts.createSourceFile(
+ 'filename.ts',
+ content,
+ ts.ScriptTarget.Latest,
+ true,
+ ts.ScriptKind.TS
+ );
+
+ const code = new MagicString(content);
+
+ return {
+ ast,
+ code,
+ exports: get_exports(ast)
+ };
+ } catch {
+ return null;
+ }
+}
+
+/**
+ * @param {ts.Node} node
+ * @param {MagicString} code
+ * @param {string} old_type
+ * @param {string} new_type
+ */
+export function rewrite_type(node, code, old_type, new_type) {
+ // @ts-ignore
+ const jsDoc = node.jsDoc || node.parent?.parent?.parent?.jsDoc;
+ if (jsDoc) {
+ // @ts-ignore
+ for (const comment of jsDoc) {
+ const str = comment.getText();
+ const index = str.indexOf(old_type);
+
+ if (index !== -1) {
+ code.overwrite(comment.pos + index, comment.pos + index + old_type.length, new_type);
+ }
+ }
+ }
+
+ // @ts-ignore
+ const type = node.type || node.parent.type; // handle both fn and var declarations
+
+ if (type?.typeName?.escapedText.startsWith(old_type)) {
+ const start = type.getStart();
+ code.overwrite(start, start + old_type.length, new_type);
+ }
+}
+
+/**
+ * Does the HTTP verbs uppercase migration if it didn't happen yet. If a string
+ * is returned, the migration was done or wasn't needed. If undefined is returned,
+ * the migration is needed but couldn't be done.
+ *
+ * @param {string[]} methods
+ * @param {NonNullable>} file
+ */
+export function uppercase_migration(methods, file) {
+ const old_methods = new Set(
+ ['get', 'post', 'put', 'patch', 'del'].filter((name) => file.exports.map.has(name))
+ );
+
+ if (old_methods.size && !methods.length) {
+ for (const statement of file.ast.statements) {
+ for (const method of old_methods) {
+ const fn = get_function_node(
+ statement,
+ /** @type{string} */ (file.exports.map.get(method))
+ );
+ if (!fn?.name) {
+ continue;
+ }
+ file.code.overwrite(
+ fn.name.getStart(),
+ fn.name.getEnd(),
+ method === 'del' ? 'DELETE' : method.toUpperCase()
+ );
+ old_methods.delete(method);
+ }
+ }
+ }
+
+ return old_methods.size ? undefined : file.code.toString();
+}
diff --git a/packages/migrate/migrations/self-closing-tags/index.js b/packages/migrate/migrations/self-closing-tags/index.js
new file mode 100644
index 00000000..ced6ca30
--- /dev/null
+++ b/packages/migrate/migrations/self-closing-tags/index.js
@@ -0,0 +1,57 @@
+import colors from 'kleur';
+import fs from 'node:fs';
+import process from 'node:process';
+import prompts from 'prompts';
+import glob from 'tiny-glob/sync.js';
+import { remove_self_closing_tags } from './migrate.js';
+import { pathToFileURL } from 'node:url';
+import { resolve } from 'import-meta-resolve';
+
+export async function migrate() {
+ let compiler;
+ try {
+ compiler = await import_from_cwd('svelte/compiler');
+ } catch {
+ console.log(colors.bold().red('❌ Could not find a local Svelte installation.'));
+ return;
+ }
+
+ console.log(
+ colors.bold().yellow('\nThis will update .svelte files inside the current directory\n')
+ );
+
+ const response = await prompts({
+ type: 'confirm',
+ name: 'value',
+ message: 'Continue?',
+ initial: false
+ });
+
+ if (!response.value) {
+ process.exit(1);
+ }
+
+ const files = glob('**/*.svelte')
+ .map((file) => file.replace(/\\/g, '/'))
+ .filter((file) => !file.includes('/node_modules/'));
+
+ for (const file of files) {
+ try {
+ const code = await remove_self_closing_tags(compiler, fs.readFileSync(file, 'utf-8'));
+ fs.writeFileSync(file, code);
+ } catch {
+ // continue
+ }
+ }
+
+ console.log(colors.bold().green('✔ Your project has been updated'));
+ console.log(' If using Prettier, please upgrade to the latest prettier-plugin-svelte version');
+}
+
+/** @param {string} name */
+function import_from_cwd(name) {
+ const cwd = pathToFileURL(process.cwd()).href;
+ const url = resolve(name, cwd + '/x.js');
+
+ return import(url);
+}
diff --git a/packages/migrate/migrations/self-closing-tags/migrate.js b/packages/migrate/migrations/self-closing-tags/migrate.js
new file mode 100644
index 00000000..73f4ec0a
--- /dev/null
+++ b/packages/migrate/migrations/self-closing-tags/migrate.js
@@ -0,0 +1,192 @@
+import MagicString from 'magic-string';
+import { walk } from 'zimmerframe';
+
+const VoidElements = [
+ 'area',
+ 'base',
+ 'br',
+ 'col',
+ 'embed',
+ 'hr',
+ 'img',
+ 'input',
+ 'keygen',
+ 'link',
+ 'menuitem',
+ 'meta',
+ 'param',
+ 'source',
+ 'track',
+ 'wbr'
+];
+
+const SVGElements = [
+ 'altGlyph',
+ 'altGlyphDef',
+ 'altGlyphItem',
+ 'animate',
+ 'animateColor',
+ 'animateMotion',
+ 'animateTransform',
+ 'circle',
+ 'clipPath',
+ 'color-profile',
+ 'cursor',
+ 'defs',
+ 'desc',
+ 'discard',
+ 'ellipse',
+ 'feBlend',
+ 'feColorMatrix',
+ 'feComponentTransfer',
+ 'feComposite',
+ 'feConvolveMatrix',
+ 'feDiffuseLighting',
+ 'feDisplacementMap',
+ 'feDistantLight',
+ 'feDropShadow',
+ 'feFlood',
+ 'feFuncA',
+ 'feFuncB',
+ 'feFuncG',
+ 'feFuncR',
+ 'feGaussianBlur',
+ 'feImage',
+ 'feMerge',
+ 'feMergeNode',
+ 'feMorphology',
+ 'feOffset',
+ 'fePointLight',
+ 'feSpecularLighting',
+ 'feSpotLight',
+ 'feTile',
+ 'feTurbulence',
+ 'filter',
+ 'font',
+ 'font-face',
+ 'font-face-format',
+ 'font-face-name',
+ 'font-face-src',
+ 'font-face-uri',
+ 'foreignObject',
+ 'g',
+ 'glyph',
+ 'glyphRef',
+ 'hatch',
+ 'hatchpath',
+ 'hkern',
+ 'image',
+ 'line',
+ 'linearGradient',
+ 'marker',
+ 'mask',
+ 'mesh',
+ 'meshgradient',
+ 'meshpatch',
+ 'meshrow',
+ 'metadata',
+ 'missing-glyph',
+ 'mpath',
+ 'path',
+ 'pattern',
+ 'polygon',
+ 'polyline',
+ 'radialGradient',
+ 'rect',
+ 'set',
+ 'solidcolor',
+ 'stop',
+ 'svg',
+ 'switch',
+ 'symbol',
+ 'text',
+ 'textPath',
+ 'tref',
+ 'tspan',
+ 'unknown',
+ 'use',
+ 'view',
+ 'vkern'
+];
+
+/**
+ * @param {{ preprocess: any, parse: any }} svelte_compiler
+ * @param {string} source
+ */
+export async function remove_self_closing_tags({ preprocess, parse }, source) {
+ const preprocessed = await preprocess(source, {
+ /** @param {{ content: string }} input */
+ script: ({ content }) => ({
+ code: content
+ .split('\n')
+ .map((line) => ' '.repeat(line.length))
+ .join('\n')
+ }),
+ /** @param {{ content: string }} input */
+ style: ({ content }) => ({
+ code: content
+ .split('\n')
+ .map((line) => ' '.repeat(line.length))
+ .join('\n')
+ })
+ });
+ const ast = parse(preprocessed.code);
+ const ms = new MagicString(source);
+ /** @type {Array<() => void>} */
+ const updates = [];
+ let is_foreign = false;
+ let is_custom_element = false;
+
+ walk(ast.html, null, {
+ _(node, { next, stop }) {
+ if (node.type === 'Options') {
+ const namespace = node.attributes.find(
+ /** @param {any} a */
+ (a) => a.type === 'Attribute' && a.name === 'namespace'
+ );
+ if (namespace?.value[0].data === 'foreign') {
+ is_foreign = true;
+ stop();
+ return;
+ }
+
+ is_custom_element = node.attributes.some(
+ /** @param {any} a */
+ (a) => a.type === 'Attribute' && (a.name === 'customElement' || a.name === 'tag')
+ );
+ }
+
+ if (node.type === 'Element' || node.type === 'Slot') {
+ const is_self_closing = source[node.end - 2] === '/';
+ if (
+ !is_self_closing ||
+ VoidElements.includes(node.name) ||
+ SVGElements.includes(node.name) ||
+ !/^[a-z0-9_-]+$/.test(node.name)
+ ) {
+ next();
+ return;
+ }
+
+ let start = node.end - 2;
+ if (source[start - 1] === ' ') {
+ start--;
+ }
+ updates.push(() => {
+ if (node.type === 'Element' || is_custom_element) {
+ ms.update(start, node.end, `>${node.name}>`);
+ }
+ });
+ }
+
+ next();
+ }
+ });
+
+ if (is_foreign) {
+ return source;
+ }
+
+ updates.forEach((update) => update());
+ return ms.toString();
+}
diff --git a/packages/migrate/migrations/self-closing-tags/migrate.spec.js b/packages/migrate/migrations/self-closing-tags/migrate.spec.js
new file mode 100644
index 00000000..421f0051
--- /dev/null
+++ b/packages/migrate/migrations/self-closing-tags/migrate.spec.js
@@ -0,0 +1,32 @@
+import { assert, test } from 'vitest';
+import * as compiler from 'svelte/compiler';
+import { remove_self_closing_tags } from './migrate.js';
+
+/** @type {Record} */
+const tests = {
+ '': '',
+ '': '',
+ '': '',
+ '': '',
+ '': '',
+ '\t': '\t',
+ '': '',
+ '': '',
+ '': '',
+ '': '',
+ '': '',
+ '':
+ '',
+ '': '',
+ '': '',
+ '':
+ '',
+ '
': '
'
+};
+
+for (const input in tests) {
+ test(input, async () => {
+ const output = tests[input];
+ assert.equal(await remove_self_closing_tags(compiler, input), output);
+ });
+}
diff --git a/packages/migrate/migrations/svelte-4/index.js b/packages/migrate/migrations/svelte-4/index.js
new file mode 100644
index 00000000..4fea5173
--- /dev/null
+++ b/packages/migrate/migrations/svelte-4/index.js
@@ -0,0 +1,112 @@
+import colors from 'kleur';
+import fs from 'node:fs';
+import process from 'node:process';
+import prompts from 'prompts';
+import glob from 'tiny-glob/sync.js';
+import { bail, check_git, update_js_file, update_svelte_file } from '../../utils.js';
+import { transform_code, 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');
+ }
+
+ 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);
+ }
+
+ const migrate_transition = await prompts({
+ type: 'confirm',
+ name: 'value',
+ message:
+ 'Add the `|global` modifier to currently global transitions for backwards compatibility? More info at https://svelte.dev/docs/svelte/v4-migration-guide#transitions-are-local-by-default',
+ initial: true
+ });
+
+ update_pkg_json();
+
+ // const { default: config } = fs.existsSync('svelte.config.js')
+ // ? await import(pathToFileURL(path.resolve('svelte.config.js')).href)
+ // : { default: {} };
+
+ /** @type {string[]} */
+ const svelte_extensions = /* config.extensions ?? - disabled because it would break .svx */ [
+ '.svelte'
+ ];
+ const extensions = [...svelte_extensions, '.ts', '.js'];
+ // 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/'))
+ );
+
+ for (const file of files) {
+ if (extensions.some((ext) => file.endsWith(ext))) {
+ if (svelte_extensions.some((ext) => file.endsWith(ext))) {
+ update_svelte_file(file, transform_code, (code) =>
+ transform_svelte_code(code, migrate_transition.value)
+ );
+ } else {
+ update_js_file(file, transform_code);
+ }
+ }
+ }
+
+ console.log(colors.bold().green('✔ Your project has been migrated'));
+
+ console.log('\nRecommended next steps:\n');
+
+ const cyan = colors.bold().cyan;
+
+ const tasks = [
+ use_git && cyan('git commit -m "migration to Svelte 4"'),
+ 'Review the migration guide at https://svelte.dev/docs/svelte/v4-migration-guide',
+ 'Read the updated docs at https://svelte.dev/docs/svelte'
+ ].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/svelte-4/migrate.js b/packages/migrate/migrations/svelte-4/migrate.js
new file mode 100644
index 00000000..c459380e
--- /dev/null
+++ b/packages/migrate/migrations/svelte-4/migrate.js
@@ -0,0 +1,348 @@
+import fs from 'node:fs';
+import { Project, ts, Node, SyntaxKind } from 'ts-morph';
+import { log_migration, log_on_ts_modification, 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, [
+ ['svelte', '^4.0.0'],
+ ['svelte-check', '^3.4.3'],
+ ['svelte-preprocess', '^5.0.3'],
+ ['@sveltejs/kit', '^1.20.4'],
+ ['@sveltejs/vite-plugin-svelte', '^2.4.1'],
+ [
+ 'svelte-loader',
+ '^3.1.8',
+ ' (if you are still on webpack 4, you need to update to webpack 5)'
+ ],
+ ['rollup-plugin-svelte', '^7.1.5'],
+ ['prettier-plugin-svelte', '^2.10.1'],
+ ['eslint-plugin-svelte', '^2.30.0'],
+ [
+ 'eslint-plugin-svelte3',
+ '^4.0.0',
+ ' (this package is deprecated, use eslint-plugin-svelte instead. More info: https://svelte.dev/docs/svelte/v4-migration-guide#new-eslint-package)'
+ ],
+ [
+ 'typescript',
+ '^5.0.0',
+ ' (this might introduce new type errors due to breaking changes within TypeScript)'
+ ]
+ ]);
+}
+
+/**
+ * @param {string} code
+ * @param {boolean} is_ts
+ */
+export function transform_code(code, is_ts) {
+ const project = new Project({ useInMemoryFileSystem: true });
+ const source = project.createSourceFile('svelte.ts', code);
+ update_imports(source, is_ts);
+ update_typeof_svelte_component(source, is_ts);
+ update_action_types(source, is_ts);
+ update_action_return_types(source, is_ts);
+ return source.getFullText();
+}
+
+/**
+ * @param {string} code
+ * @param {boolean} migrate_transition
+ */
+export function transform_svelte_code(code, migrate_transition) {
+ code = update_svelte_options(code);
+ return update_transitions(code, migrate_transition);
+}
+
+/**
+ * ->
+ * @param {string} code
+ */
+function update_svelte_options(code) {
+ return code.replace(//, (match) => {
+ log_migration(
+ 'Replaced `svelte:options` `tag` attribute with `customElement` attribute: https://svelte.dev/docs/svelte/v4-migration-guide#custom-elements-with-svelte'
+ );
+ return match.replace('tag=', 'customElement=');
+ });
+}
+
+/**
+ * transition/in/out:x -> transition/in/out:x|global
+ * transition/in/out|local:x -> transition/in/out:x
+ * @param {string} code
+ * @param {boolean} migrate_transition
+ */
+function update_transitions(code, migrate_transition) {
+ if (migrate_transition) {
+ const replaced = code.replace(/(\s)(transition:|in:|out:)(\w+)(?=[\s>=])/g, '$1$2$3|global');
+ if (replaced !== code) {
+ log_migration(
+ 'Added `|global` to `transition`, `in`, and `out` directives (transitions are local by default now): https://svelte.dev/docs/svelte/v4-migration-guide#transitions-are-local-by-default'
+ );
+ }
+ code = replaced;
+ }
+ const replaced = code.replace(/(\s)(transition:|in:|out:)(\w+)(\|local)(?=[\s>=])/g, '$1$2$3');
+ if (replaced !== code) {
+ log_migration(
+ 'Removed `|local` from `transition`, `in`, and `out` directives (transitions are local by default now): https://svelte.dev/docs/svelte/v4-migration-guide#transitions-are-local-by-default'
+ );
+ }
+ return replaced;
+}
+
+/**
+ * Action -> Action
+ * @param {import('ts-morph').SourceFile} source
+ * @param {boolean} is_ts
+ */
+function update_action_types(source, is_ts) {
+ const logger = log_on_ts_modification(
+ source,
+ 'Updated `Action` interface usages: https://svelte.dev/docs/svelte/v4-migration-guide#stricter-types-for-svelte-functions'
+ );
+
+ const imports = get_imports(source, 'svelte/action', 'Action');
+ for (const namedImport of imports) {
+ const identifiers = find_identifiers(source, namedImport.getAliasNode()?.getText() ?? 'Action');
+ for (const id of identifiers) {
+ const parent = id.getParent();
+ if (Node.isTypeReference(parent)) {
+ const type_args = parent.getTypeArguments();
+ if (type_args.length === 1) {
+ parent.addTypeArgument('any');
+ } else if (type_args.length === 0) {
+ parent.addTypeArgument('HTMLElement');
+ parent.addTypeArgument('any');
+ }
+ }
+ }
+ }
+
+ if (!is_ts) {
+ replaceInJsDoc(source, (text) => {
+ return text.replace(
+ /import\((['"])svelte\/action['"]\).Action(<\w+>)?(?=[^<\w]|$)/g,
+ (_, quote, type) =>
+ `import(${quote}svelte/action${quote}).Action<${
+ type ? type.slice(1, -1) + '' : 'HTMLElement'
+ }, any>`
+ );
+ });
+ }
+
+ logger();
+}
+
+/**
+ * ActionReturn -> ActionReturn
+ * @param {import('ts-morph').SourceFile} source
+ * @param {boolean} is_ts
+ */
+function update_action_return_types(source, is_ts) {
+ const logger = log_on_ts_modification(
+ source,
+ 'Updated `ActionReturn` interface usages: https://svelte.dev/docs/svelte/v4-migration-guide#stricter-types-for-svelte-functions'
+ );
+
+ const imports = get_imports(source, 'svelte/action', 'ActionReturn');
+ for (const namedImport of imports) {
+ const identifiers = find_identifiers(
+ source,
+ namedImport.getAliasNode()?.getText() ?? 'ActionReturn'
+ );
+ for (const id of identifiers) {
+ const parent = id.getParent();
+ if (Node.isTypeReference(parent)) {
+ const type_args = parent.getTypeArguments();
+ if (type_args.length === 0) {
+ parent.addTypeArgument('any');
+ }
+ }
+ }
+ }
+
+ if (!is_ts) {
+ replaceInJsDoc(source, (text) => {
+ return text.replace(
+ /import\((['"])svelte\/action['"]\).ActionReturn(?=[^<\w]|$)/g,
+ 'import($1svelte/action$1).ActionReturn'
+ );
+ });
+ }
+
+ logger();
+}
+
+/**
+ * SvelteComponentTyped -> SvelteComponent
+ * @param {import('ts-morph').SourceFile} source
+ * @param {boolean} is_ts
+ */
+function update_imports(source, is_ts) {
+ const logger = log_on_ts_modification(
+ source,
+ 'Replaced `SvelteComponentTyped` imports with `SvelteComponent` imports: https://svelte.dev/docs/svelte/v4-migration-guide#stricter-types-for-svelte-functions'
+ );
+
+ const identifiers = find_identifiers(source, 'SvelteComponent');
+ const can_rename = identifiers.every((id) => {
+ const parent = id.getParent();
+ return (
+ (Node.isImportSpecifier(parent) &&
+ !parent.getAliasNode() &&
+ parent
+ .getParent()
+ .getParent()
+ .getParentIfKind(SyntaxKind.ImportDeclaration)
+ ?.getModuleSpecifier()
+ .getText() === 'svelte') ||
+ !is_declaration(parent)
+ );
+ });
+
+ const imports = get_imports(source, 'svelte', 'SvelteComponentTyped');
+ for (const namedImport of imports) {
+ if (can_rename) {
+ namedImport.renameAlias('SvelteComponent');
+ if (
+ namedImport
+ .getParent()
+ .getElements()
+ .some((e) => !e.getAliasNode() && e.getNameNode().getText() === 'SvelteComponent')
+ ) {
+ namedImport.remove();
+ } else {
+ namedImport.setName('SvelteComponent');
+ namedImport.removeAlias();
+ }
+ } else {
+ namedImport.renameAlias('SvelteComponentTyped');
+ namedImport.setName('SvelteComponent');
+ }
+ }
+
+ if (!is_ts) {
+ replaceInJsDoc(source, (text) => {
+ return text.replace(
+ /import\((['"])svelte['"]\)\.SvelteComponentTyped(?=\W|$)/g,
+ 'import($1svelte$1).SvelteComponent'
+ );
+ });
+ }
+
+ logger();
+}
+
+/**
+ * typeof SvelteComponent -> typeof SvelteComponent
+ * @param {import('ts-morph').SourceFile} source
+ * @param {boolean} is_ts
+ */
+function update_typeof_svelte_component(source, is_ts) {
+ const logger = log_on_ts_modification(
+ source,
+ 'Adjusted `typeof SvelteComponent` to `typeof SvelteComponent`: https://svelte.dev/docs/svelte/v4-migration-guide#stricter-types-for-svelte-functions'
+ );
+
+ const imports = get_imports(source, 'svelte', 'SvelteComponent');
+
+ for (const type of imports) {
+ if (type) {
+ const name = type.getAliasNode() ?? type.getNameNode();
+ if (Node.isIdentifier(name)) {
+ name.findReferencesAsNodes().forEach((ref) => {
+ const parent = ref.getParent();
+ if (parent && Node.isTypeQuery(parent)) {
+ const id = parent.getFirstChildByKind(ts.SyntaxKind.Identifier);
+ if (id?.getText() === name.getText()) {
+ const typeArguments = parent.getTypeArguments();
+ if (typeArguments.length === 0) {
+ parent.addTypeArgument('any');
+ }
+ }
+ }
+ });
+ }
+ }
+ }
+
+ if (!is_ts) {
+ replaceInJsDoc(source, (text) => {
+ return text.replace(
+ /typeof import\((['"])svelte['"]\)\.SvelteComponent(?=[^<\w]|$)/g,
+ 'typeof import($1svelte$1).SvelteComponent'
+ );
+ });
+ }
+
+ logger();
+}
+
+/**
+ * @param {import('ts-morph').SourceFile} source
+ * @param {string} from
+ * @param {string} name
+ */
+function get_imports(source, from, name) {
+ return source
+ .getImportDeclarations()
+ .filter((i) => i.getModuleSpecifierValue() === from)
+ .flatMap((i) => i.getNamedImports())
+ .filter((i) => i.getName() === name);
+}
+
+/**
+ * @param {import('ts-morph').SourceFile} source
+ * @param {string} name
+ */
+function find_identifiers(source, name) {
+ return source.getDescendantsOfKind(ts.SyntaxKind.Identifier).filter((i) => i.getText() === name);
+}
+
+/**
+ * Does not include imports
+ * @param {Node} node
+ */
+function is_declaration(node) {
+ return (
+ Node.isVariableDeclaration(node) ||
+ Node.isFunctionDeclaration(node) ||
+ Node.isClassDeclaration(node) ||
+ Node.isTypeAliasDeclaration(node) ||
+ Node.isInterfaceDeclaration(node)
+ );
+}
+
+/**
+ * @param {import('ts-morph').SourceFile} source
+ * @param {(text: string) => string | undefined} replacer
+ */
+function replaceInJsDoc(source, replacer) {
+ source.forEachChild((node) => {
+ if (Node.isJSDocable(node)) {
+ const tags = node.getJsDocs().flatMap((jsdoc) => jsdoc.getTags());
+ tags.forEach((t) =>
+ t.forEachChild((c) => {
+ if (Node.isJSDocTypeExpression(c)) {
+ const text = c.getText().slice(1, -1);
+ const replacement = replacer(text);
+ if (replacement && replacement !== text) {
+ c.replaceWithText(`{${replacement}}`);
+ }
+ }
+ })
+ );
+ }
+ });
+}
diff --git a/packages/migrate/migrations/svelte-4/migrate.spec.js b/packages/migrate/migrations/svelte-4/migrate.spec.js
new file mode 100644
index 00000000..f0c34cdd
--- /dev/null
+++ b/packages/migrate/migrations/svelte-4/migrate.spec.js
@@ -0,0 +1,382 @@
+import { assert, test } from 'vitest';
+import { transform_code, transform_svelte_code, update_pkg_json_content } from './migrate.js';
+
+test('Updates SvelteComponentTyped #1', () => {
+ const result = transform_code(
+ `import { SvelteComponentTyped } from 'svelte';
+
+export class Foo extends SvelteComponentTyped<{}> {}
+
+const bar: SvelteComponentTyped = null;`,
+ true
+ );
+ assert.equal(
+ result,
+ `import { SvelteComponent } from 'svelte';
+
+export class Foo extends SvelteComponent<{}> {}
+
+const bar: SvelteComponent = null;`
+ );
+});
+
+test('Updates SvelteComponentTyped #2', () => {
+ const result = transform_code(
+ `import { SvelteComponentTyped, SvelteComponent } from 'svelte';
+
+export class Foo extends SvelteComponentTyped<{}> {}
+
+const bar: SvelteComponentTyped = null;
+const baz: SvelteComponent = null;`,
+ true
+ );
+ assert.equal(
+ result,
+ `import { SvelteComponent } from 'svelte';
+
+export class Foo extends SvelteComponent<{}> {}
+
+const bar: SvelteComponent = null;
+const baz: SvelteComponent = null;`
+ );
+});
+
+test('Updates SvelteComponentTyped #3', () => {
+ const result = transform_code(
+ `import { SvelteComponentTyped } from 'svelte';
+
+interface SvelteComponent {}
+
+export class Foo extends SvelteComponentTyped<{}> {}
+
+const bar: SvelteComponentTyped = null;
+const baz: SvelteComponent = null;`,
+ true
+ );
+ assert.equal(
+ result,
+ `import { SvelteComponent as SvelteComponentTyped } from 'svelte';
+
+interface SvelteComponent {}
+
+export class Foo extends SvelteComponentTyped<{}> {}
+
+const bar: SvelteComponentTyped = null;
+const baz: SvelteComponent = null;`
+ );
+});
+
+test('Updates SvelteComponentTyped (jsdoc)', () => {
+ const result = transform_code(
+ `
+ /** @type {import('svelte').SvelteComponentTyped} */
+ const bar = null;
+ /** @type {import('svelte').SvelteComponentTyped} */
+ const baz = null;
+ `,
+ false
+ );
+ assert.equal(
+ result,
+ `
+ /** @type {import('svelte').SvelteComponent} */
+ const bar = null;
+ /** @type {import('svelte').SvelteComponent} */
+ const baz = null;
+ `
+ );
+});
+
+test('Updates typeof SvelteComponent', () => {
+ const result = transform_code(
+ `import { SvelteComponent } from 'svelte';
+ import { SvelteComponent as C } from 'svelte';
+
+ const a: typeof SvelteComponent = null;
+ function b(c: typeof SvelteComponent) {}
+ const c: typeof SvelteComponent = null;
+ const d: typeof C = null;
+ `,
+ true
+ );
+ assert.equal(
+ result,
+ `import { SvelteComponent } from 'svelte';
+ import { SvelteComponent as C } from 'svelte';
+
+ const a: typeof SvelteComponent = null;
+ function b(c: typeof SvelteComponent) {}
+ const c: typeof SvelteComponent = null;
+ const d: typeof C = null;
+ `
+ );
+});
+
+test('Updates typeof SvelteComponent (jsdoc)', () => {
+ const result = transform_code(
+ `
+ /** @type {typeof import('svelte').SvelteComponent} */
+ const a = null;
+ /** @type {typeof import('svelte').SvelteComponent} */
+ const c = null;
+ /** @type {typeof C} */
+ const d: typeof C = null;
+ `,
+ false
+ );
+ assert.equal(
+ result,
+ `
+ /** @type {typeof import('svelte').SvelteComponent} */
+ const a = null;
+ /** @type {typeof import('svelte').SvelteComponent} */
+ const c = null;
+ /** @type {typeof C} */
+ const d: typeof C = null;
+ `
+ );
+});
+
+test('Updates Action and ActionReturn', () => {
+ const result = transform_code(
+ `import type { Action, ActionReturn } from 'svelte/action';
+
+ const a: Action = () => {};
+ const b: Action = () => {};
+ const c: Action = () => {};
+ const d: Action = () => {};
+ const e: ActionReturn = () => {};
+ const f: ActionReturn = () => {};
+ const g: ActionReturn = () => {};
+ `,
+ true
+ );
+ assert.equal(
+ result,
+
+ `import type { Action, ActionReturn } from 'svelte/action';
+
+ const a: Action = () => {};
+ const b: Action = () => {};
+ const c: Action = () => {};
+ const d: Action = () => {};
+ const e: ActionReturn = () => {};
+ const f: ActionReturn = () => {};
+ const g: ActionReturn = () => {};
+ `
+ );
+});
+
+test('Updates Action and ActionReturn (jsdoc)', () => {
+ const result = transform_code(
+ `
+ /** @type {import('svelte/action').Action} */
+ const a = () => {};
+ /** @type {import('svelte/action').Action} */
+ const b = () => {};
+ /** @type {import('svelte/action').Action} */
+ const c = () => {};
+ /** @type {import('svelte/action').Action} */
+ const d = () => {};
+ /** @type {import('svelte/action').ActionReturn} */
+ const e = () => {};
+ /** @type {import('svelte/action').ActionReturn} */
+ const f = () => {};
+ /** @type {import('svelte/action').ActionReturn} */
+ const g = () => {};
+ `,
+ false
+ );
+ assert.equal(
+ result,
+
+ `
+ /** @type {import('svelte/action').Action} */
+ const a = () => {};
+ /** @type {import('svelte/action').Action} */
+ const b = () => {};
+ /** @type {import('svelte/action').Action} */
+ const c = () => {};
+ /** @type {import('svelte/action').Action} */
+ const d = () => {};
+ /** @type {import('svelte/action').ActionReturn} */
+ const e = () => {};
+ /** @type {import('svelte/action').ActionReturn} */
+ const f = () => {};
+ /** @type {import('svelte/action').ActionReturn} */
+ const g = () => {};
+ `
+ );
+});
+
+test('Updates svelte:options #1', () => {
+ const result = transform_svelte_code(
+ `
+
+ hi
`,
+ true
+ );
+ assert.equal(
+ result,
+ `
+
+ hi
`
+ );
+});
+
+test('Updates svelte:options #2', () => {
+ const result = transform_svelte_code(
+ `
+
+
+
+ hi
`,
+ true
+ );
+ assert.equal(
+ result,
+ `
+
+
+
+ hi
`
+ );
+});
+
+test('Updates transitions', () => {
+ const result = transform_svelte_code(
+ `
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ `,
+ true
+ );
+ assert.equal(
+ result,
+ `
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ `
+ );
+});
+
+test('Updates transitions #2', () => {
+ const result = transform_svelte_code(
+ `
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ `,
+ false
+ );
+ assert.equal(
+ result,
+ `
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ `
+ );
+});
+
+test('Update package.json', () => {
+ const result = update_pkg_json_content(`{
+ "name": "svelte-app",
+ "version": "1.0.0",
+ "devDependencies": {
+ "svelte": "^3.0.0",
+ "svelte-check": "^1.0.0",
+ "svelte-preprocess": "^5.0.0"
+ },
+ "dependencies": {
+ "@sveltejs/kit": "^1.0.0"
+ }
+}`);
+ assert.equal(
+ result,
+ `{
+ "name": "svelte-app",
+ "version": "1.0.0",
+ "devDependencies": {
+ "svelte": "^4.0.0",
+ "svelte-check": "^3.4.3",
+ "svelte-preprocess": "^5.0.3"
+ },
+ "dependencies": {
+ "@sveltejs/kit": "^1.20.4"
+ }
+}`
+ );
+});
+
+test('Does not downgrade versions', () => {
+ const result = update_pkg_json_content(`{
+ "devDependencies": {
+ "svelte": "^4.0.5",
+ "typescript": "github:idk"
+ }
+}`);
+ assert.equal(
+ result,
+ `{
+ "devDependencies": {
+ "svelte": "^4.0.5",
+ "typescript": "github:idk"
+ }
+}`
+ );
+});
diff --git a/packages/migrate/migrations/svelte-5/index.js b/packages/migrate/migrations/svelte-5/index.js
new file mode 100644
index 00000000..a6b17d42
--- /dev/null
+++ b/packages/migrate/migrations/svelte-5/index.js
@@ -0,0 +1,216 @@
+import { resolve } from 'import-meta-resolve';
+import colors from 'kleur';
+import { execSync } from 'node:child_process';
+import process from 'node:process';
+import fs from 'node:fs';
+import { dirname } from 'node:path';
+import { fileURLToPath, pathToFileURL } from 'node:url';
+import prompts from 'prompts';
+import semver from 'semver';
+import glob from 'tiny-glob/sync.js';
+import { bail, check_git, update_js_file, update_svelte_file } from '../../utils.js';
+import { migrate as migrate_svelte_4 } from '../svelte-4/index.js';
+import { migrate as migrate_sveltekit_2 } from '../sveltekit-2/index.js';
+import { transform_module_code, 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');
+ }
+
+ console.log(
+ 'This migration is experimental — please report any bugs to https://github.com/sveltejs/svelte/issues'
+ );
+
+ 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('4.0.0', svelte_dep)) {
+ console.log(
+ colors
+ .bold()
+ .yellow(
+ '\nDetected Svelte 3. You need to upgrade to Svelte version 4 first (`npx sv migrate svelte-4`).\n'
+ )
+ );
+ const response = await prompts({
+ type: 'confirm',
+ name: 'value',
+ message: 'Run svelte-4 migration now?',
+ initial: false
+ });
+ if (!response.value) {
+ process.exit(1);
+ } else {
+ await migrate_svelte_4();
+ console.log(
+ colors
+ .bold()
+ .green(
+ 'svelte-4 migration complete. Check that everything is ok, then run `npx sv migrate svelte-5` again to continue the Svelte 5 migration.\n'
+ )
+ );
+ process.exit(0);
+ }
+ }
+
+ 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()
+ .yellow(
+ '\nDetected SvelteKit 1. You need to upgrade to SvelteKit version 2 first (`npx sv migrate sveltekit-2`).\n'
+ )
+ );
+ const response = await prompts({
+ type: 'confirm',
+ name: 'value',
+ message: 'Run sveltekit-2 migration now?',
+ initial: false
+ });
+ if (!response.value) {
+ process.exit(1);
+ } else {
+ await migrate_sveltekit_2();
+ console.log(
+ colors
+ .bold()
+ .green(
+ 'sveltekit-2 migration complete. Check that everything is ok, then run `npx sv migrate svelte-5` again to continue the Svelte 5 migration.\n'
+ )
+ );
+ process.exit(0);
+ }
+ }
+
+ let migrate;
+ try {
+ try {
+ ({ migrate } = await import_from_cwd('svelte/compiler'));
+ if (!migrate) throw new Error('found Svelte 4');
+ } catch {
+ execSync('npm install svelte@^5.0.0 --no-save', {
+ stdio: 'inherit',
+ cwd: dirname(fileURLToPath(import.meta.url))
+ });
+ const url = resolve('svelte/compiler', import.meta.url);
+ ({ migrate } = await import(url));
+ }
+ } catch (e) {
+ console.log(e);
+ console.log(
+ colors
+ .bold()
+ .red(
+ '❌ Could not install Svelte. Manually bump the dependency to version 5 in your package.json, install it, then try again.'
+ )
+ );
+ return;
+ }
+
+ 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();
+
+ const use_ts = fs.existsSync('tsconfig.json');
+
+ // const { default: config } = fs.existsSync('svelte.config.js')
+ // ? await import(pathToFileURL(path.resolve('svelte.config.js')).href)
+ // : { default: {} };
+
+ /** @type {string[]} */
+ const svelte_extensions = /* config.extensions ?? - disabled because it would break .svx */ [
+ '.svelte'
+ ];
+ const extensions = [...svelte_extensions, '.ts', '.js'];
+ // 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/'))
+ );
+
+ for (const file of files) {
+ if (extensions.some((ext) => file.endsWith(ext))) {
+ if (svelte_extensions.some((ext) => file.endsWith(ext))) {
+ update_svelte_file(file, transform_module_code, (code) =>
+ transform_svelte_code(code, migrate, { filename: file, use_ts })
+ );
+ } else {
+ update_js_file(file, transform_module_code);
+ }
+ }
+ }
+
+ 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) " +
+ '(note that there may be peer dependency issues when not all your libraries officially support Svelte 5 yet. In this case try installing with the --force option)',
+ use_git && cyan('git commit -m "migration to Svelte 5"'),
+ 'Review the breaking changes at https://svelte-5-preview.vercel.app/docs/breaking-changes'
+ // replace with this once it's live:
+ // 'Review the migration guide at https://svelte.dev/docs/svelte/v5-migration-guide',
+ // 'Read the updated docs at https://svelte.dev/docs/svelte'
+ ].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`);
+ }
+}
+
+/** @param {string} name */
+function import_from_cwd(name) {
+ const cwd = pathToFileURL(process.cwd()).href;
+ const url = resolve(name, cwd + '/x.js');
+
+ return import(url);
+}
diff --git a/packages/migrate/migrations/svelte-5/migrate.js b/packages/migrate/migrations/svelte-5/migrate.js
new file mode 100644
index 00000000..eb9e750f
--- /dev/null
+++ b/packages/migrate/migrations/svelte-5/migrate.js
@@ -0,0 +1,129 @@
+import fs from 'node:fs';
+import { Project, ts, Node } from 'ts-morph';
+import { add_named_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, [
+ ['svelte', '^5.0.0'],
+ ['svelte-check', '^4.0.0'],
+ ['svelte-preprocess', '^6.0.0'],
+ ['@sveltejs/enhanced-img', '^0.3.9'],
+ ['@sveltejs/kit', '^2.5.27'],
+ ['@sveltejs/vite-plugin-svelte', '^4.0.0'],
+ [
+ 'svelte-loader',
+ '^3.2.3',
+ ' (if you are still on webpack 4, you need to update to webpack 5)'
+ ],
+ ['rollup-plugin-svelte', '^7.2.2'],
+ ['prettier', '^3.1.0'],
+ ['prettier-plugin-svelte', '^3.2.6'],
+ ['eslint-plugin-svelte', '^2.45.1'],
+ ['svelte-eslint-parser', '^0.42.0'],
+ [
+ 'eslint-plugin-svelte3',
+ '^4.0.0',
+ ' (this package is deprecated, use eslint-plugin-svelte instead. More info: https://svelte.dev/docs/svelte/v4-migration-guide#new-eslint-package)'
+ ],
+ [
+ 'typescript',
+ '^5.5.0',
+ ' (this might introduce new type errors due to breaking changes within TypeScript)'
+ ],
+ ['vite', '^5.4.4']
+ ]);
+}
+
+/**
+ * @param {string} code
+ */
+export function transform_module_code(code) {
+ const project = new Project({ useInMemoryFileSystem: true });
+ const source = project.createSourceFile('svelte.ts', code);
+ update_component_instantiation(source);
+ return source.getFullText();
+}
+
+/**
+ * @param {string} code
+ * @param {(source: string, options: { filename?: string, use_ts?: boolean }) => { code: string }} transform_code
+ * @param {{ filename?: string, use_ts?: boolean }} options
+ */
+export function transform_svelte_code(code, transform_code, options) {
+ return transform_code(code, options).code;
+}
+
+/**
+ * new Component(...) -> mount(Component, ...)
+ * @param {import('ts-morph').SourceFile} source
+ */
+function update_component_instantiation(source) {
+ const imports = source
+ .getImportDeclarations()
+ .filter((i) => i.getModuleSpecifierValue().endsWith('.svelte'))
+ .flatMap((i) => i.getDefaultImport() || []);
+
+ for (const defaultImport of imports) {
+ const identifiers = find_identifiers(source, defaultImport.getText());
+
+ for (const id of identifiers) {
+ const parent = id.getParent();
+
+ if (Node.isNewExpression(parent)) {
+ const args = parent.getArguments();
+
+ if (args.length === 1) {
+ const method =
+ Node.isObjectLiteralExpression(args[0]) && !!args[0].getProperty('hydrate')
+ ? 'hydrate'
+ : 'mount';
+
+ if (method === 'hydrate') {
+ /** @type {import('ts-morph').ObjectLiteralExpression} */ (args[0])
+ .getProperty('hydrate')
+ ?.remove();
+ }
+
+ add_named_import(source, 'svelte', method);
+
+ const declaration = parent
+ .getParentIfKind(ts.SyntaxKind.VariableDeclaration)
+ ?.getNameNode();
+ if (Node.isIdentifier(declaration)) {
+ const usages = declaration.findReferencesAsNodes();
+ for (const usage of usages) {
+ const parent = usage.getParent();
+ if (Node.isPropertyAccessExpression(parent) && parent.getName() === '$destroy') {
+ const call_expr = parent.getParentIfKind(ts.SyntaxKind.CallExpression);
+ if (call_expr) {
+ call_expr.replaceWithText(`unmount(${usage.getText()})`);
+ add_named_import(source, 'svelte', 'unmount');
+ }
+ }
+ }
+ }
+
+ parent.replaceWithText(`${method}(${id.getText()}, ${args[0].getText()})`);
+ }
+ }
+ }
+ }
+}
+
+/**
+ * @param {import('ts-morph').SourceFile} source
+ * @param {string} name
+ */
+function find_identifiers(source, name) {
+ return source.getDescendantsOfKind(ts.SyntaxKind.Identifier).filter((i) => i.getText() === name);
+}
diff --git a/packages/migrate/migrations/svelte-5/migrate.spec.js b/packages/migrate/migrations/svelte-5/migrate.spec.js
new file mode 100644
index 00000000..0f9e3e6d
--- /dev/null
+++ b/packages/migrate/migrations/svelte-5/migrate.spec.js
@@ -0,0 +1,135 @@
+import { assert, test } from 'vitest';
+import { transform_module_code, update_pkg_json_content } from './migrate.js';
+
+test('Updates component creation #1', () => {
+ const result = transform_module_code(
+ `import App from './App.svelte'
+
+const app = new App({
+ target: document.getElementById('app')!
+})
+
+export default app`
+ );
+ assert.equal(
+ result,
+ `import App from './App.svelte'
+import { mount } from "svelte";
+
+const app = mount(App, {
+ target: document.getElementById('app')!
+})
+
+export default app`
+ );
+});
+
+test('Updates component creation #2', () => {
+ const result = transform_module_code(
+ `import App from './App.svelte'
+
+new App({
+ target: document.getElementById('app')!,
+ hydrate: true
+})`
+ );
+ assert.equal(
+ result,
+ `import App from './App.svelte'
+import { hydrate } from "svelte";
+
+hydrate(App, {
+ target: document.getElementById('app')!
+})`
+ );
+});
+
+test('Updates component creation #3', () => {
+ const result = transform_module_code(
+ `import App from './App.svelte'
+
+const x = new App({
+ target: document.getElementById('app')!
+});
+
+function destroy() {
+ x.$destroy();
+}
+`
+ );
+ assert.equal(
+ result,
+ `import App from './App.svelte'
+import { mount, unmount } from "svelte";
+
+const x = mount(App, {
+ target: document.getElementById('app')!
+});
+
+function destroy() {
+ unmount(x);
+}
+`
+ );
+});
+
+test('Updates component creation with multiple components', () => {
+ const result = transform_module_code(
+ `import App from './App.svelte';
+import Child from './Child.svelte';
+
+const x = new App({
+ target: document.getElementById('app')!
+});
+const y = new Child({
+ target: document.getElementById('child')!
+});
+`
+ );
+ assert.equal(
+ result,
+ `import App from './App.svelte';
+import Child from './Child.svelte';
+import { mount } from "svelte";
+
+const x = mount(App, {
+ target: document.getElementById('app')!
+});
+const y = mount(Child, {
+ target: document.getElementById('child')!
+});
+`
+ );
+});
+
+test('Update package.json', () => {
+ const result = update_pkg_json_content(`{
+ "name": "svelte-app",
+ "version": "1.0.0",
+ "devDependencies": {
+ "svelte": "^4.0.0",
+ "svelte-check": "^3.0.0",
+ "svelte-preprocess": "^5.0.0",
+ "svelte-eslint-parser": "^0.41.1"
+ },
+ "dependencies": {
+ "@sveltejs/kit": "^2.0.0"
+ }
+}`);
+ assert.equal(
+ result,
+ `{
+ "name": "svelte-app",
+ "version": "1.0.0",
+ "devDependencies": {
+ "svelte": "^5.0.0",
+ "svelte-check": "^4.0.0",
+ "svelte-preprocess": "^6.0.0",
+ "svelte-eslint-parser": "^0.42.0"
+ },
+ "dependencies": {
+ "@sveltejs/kit": "^2.5.27"
+ }
+}`
+ );
+});
diff --git a/packages/migrate/migrations/sveltekit-2/index.js b/packages/migrate/migrations/sveltekit-2/index.js
new file mode 100644
index 00000000..c62442ab
--- /dev/null
+++ b/packages/migrate/migrations/sveltekit-2/index.js
@@ -0,0 +1,167 @@
+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_js_file,
+ update_svelte_file,
+ update_tsconfig
+} from '../../utils.js';
+import { migrate as migrate_svelte_4 } from '../svelte-4/index.js';
+import {
+ transform_code,
+ update_pkg_json,
+ update_svelte_config,
+ update_tsconfig_content
+} 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');
+ }
+
+ if (!fs.existsSync('svelte.config.js')) {
+ bail('Please re-run this script in a directory with a svelte.config.js');
+ }
+
+ console.log(
+ colors
+ .bold()
+ .yellow(
+ '\nThis will update files in the current directory\n' +
+ "If you're inside a monorepo, run this in individual project directories rather than the workspace root.\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 pkg = JSON.parse(fs.readFileSync('package.json', 'utf8'));
+ const svelte_dep = pkg.devDependencies?.svelte ?? pkg.dependencies?.svelte;
+ if (svelte_dep === undefined) {
+ bail('Please install Svelte before continuing');
+ }
+
+ if (semver.validRange(svelte_dep) && semver.gtr('4.0.0', svelte_dep)) {
+ console.log(
+ colors
+ .bold()
+ .yellow(
+ '\nSvelteKit 2 requires Svelte 4 or newer. We recommend running the `svelte-4` migration first (`npx sv migrate svelte-4`).\n'
+ )
+ );
+ const response = await prompts({
+ type: 'confirm',
+ name: 'value',
+ message: 'Run `svelte-4` migration now?',
+ initial: false
+ });
+ if (!response.value) {
+ process.exit(1);
+ } else {
+ await migrate_svelte_4();
+ console.log(
+ colors
+ .bold()
+ .green('`svelte-4` migration complete. Continue with `sveltekit-2` migration?\n')
+ );
+ 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 !== 'dist' &&
+ !dir.startsWith('.')
+ )
+ .map((dir) => ({ title: dir, value: dir, selected: dir === 'src' }))
+ });
+
+ if (!folders.value?.length) {
+ process.exit(1);
+ }
+
+ update_pkg_json();
+ update_tsconfig(update_tsconfig_content);
+ update_svelte_config();
+
+ // const { default: config } = fs.existsSync('svelte.config.js')
+ // ? await import(pathToFileURL(path.resolve('svelte.config.js')).href)
+ // : { default: {} };
+
+ /** @type {string[]} */
+ const svelte_extensions = /* config.extensions ?? - disabled because it would break .svx */ [
+ '.svelte'
+ ];
+ const extensions = [...svelte_extensions, '.ts', '.js'];
+ // 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/'))
+ );
+
+ for (const file of files) {
+ if (extensions.some((ext) => file.endsWith(ext))) {
+ if (svelte_extensions.some((ext) => file.endsWith(ext))) {
+ update_svelte_file(file, transform_code, (code) => code);
+ } else {
+ update_js_file(file, transform_code);
+ }
+ }
+ }
+
+ console.log(colors.bold().green('✔ Your project has been migrated'));
+
+ console.log('\nRecommended next steps:\n');
+
+ const cyan = colors.bold().cyan;
+
+ const tasks = [
+ 'Run npm install (or the corresponding installation command of your package manager)',
+ use_git && cyan('git commit -m "migration to SvelteKit 2"'),
+ 'Review the migration guide at https://svelte.dev/docs/kit/migrating-to-sveltekit-2',
+ 'Read the updated docs at https://svelte.dev/docs/kit'
+ ].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/sveltekit-2/migrate.js b/packages/migrate/migrations/sveltekit-2/migrate.js
new file mode 100644
index 00000000..9657be37
--- /dev/null
+++ b/packages/migrate/migrations/sveltekit-2/migrate.js
@@ -0,0 +1,318 @@
+import fs from 'node:fs';
+import { Project, Node, SyntaxKind } from 'ts-morph';
+import {
+ add_named_import,
+ log_migration,
+ log_on_ts_modification,
+ update_pkg
+} from '../../utils.js';
+import path from 'node:path';
+
+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, [
+ // All other bumps are done as part of the Svelte 4 migration
+ ['@sveltejs/kit', '^2.0.0'],
+ ['@sveltejs/adapter-static', '^3.0.0'],
+ ['@sveltejs/adapter-node', '^2.0.0'],
+ ['@sveltejs/adapter-vercel', '^4.0.0'],
+ ['@sveltejs/adapter-netlify', '^3.0.0'],
+ ['@sveltejs/adapter-cloudflare', '^3.0.0'],
+ ['@sveltejs/adapter-cloudflare-workers', '^2.0.0'],
+ ['@sveltejs/adapter-auto', '^3.0.0'],
+ ['vite', '^5.0.0'],
+ ['vitest', '^1.0.0'],
+ ['typescript', '^5.0.0'], // should already be done by Svelte 4 migration, but who knows
+ [
+ '@sveltejs/vite-plugin-svelte',
+ '^3.0.0',
+ ' (vite-plugin-svelte is a peer dependency of SvelteKit now)',
+ 'devDependencies'
+ ]
+ ]);
+}
+
+/** @param {string} content */
+export function update_tsconfig_content(content) {
+ if (!content.includes('"extends"')) {
+ // Don't touch the tsconfig if people opted out of our default config
+ return content;
+ }
+
+ let updated = content
+ .split('\n')
+ .filter(
+ (line) => !line.includes('importsNotUsedAsValues') && !line.includes('preserveValueImports')
+ )
+ .join('\n');
+ if (updated !== content) {
+ log_migration(
+ 'Removed deprecated `importsNotUsedAsValues` and `preserveValueImports`' +
+ ' from tsconfig.json: https://svelte.dev/docs/kit/migrating-to-sveltekit-2#updated-dependency-requirements'
+ );
+ }
+
+ content = updated;
+ updated = content.replace('"moduleResolution": "node"', '"moduleResolution": "bundler"');
+ if (updated !== content) {
+ log_migration(
+ 'Updated `moduleResolution` to `bundler`' +
+ ' in tsconfig.json: https://svelte.dev/docs/kit/migrating-to-sveltekit-2#updated-dependency-requirements'
+ );
+ }
+
+ if (content.includes('"paths":') || content.includes('"baseUrl":')) {
+ log_migration(
+ '`paths` and/or `baseUrl` detected in your tsconfig.json - remove it and use `kit.alias` instead: https://svelte.dev/docs/kit/migrating-to-sveltekit-2#generated-tsconfig-json-is-more-strict'
+ );
+ }
+
+ return updated;
+}
+
+export function update_svelte_config() {
+ fs.writeFileSync(
+ 'svelte.config.js',
+ update_svelte_config_content(fs.readFileSync('svelte.config.js', 'utf8'))
+ );
+}
+
+/**
+ * @param {string} code
+ */
+export function update_svelte_config_content(code) {
+ const regex = /\s*dangerZone:\s*{[^}]*},?/g;
+ const result = code.replace(regex, '');
+ if (result !== code) {
+ log_migration(
+ 'Removed `dangerZone` from svelte.config.js: https://svelte.dev/docs/kit/migrating-to-sveltekit-2#server-fetches-are-not-trackable-anymore'
+ );
+ }
+
+ const project = new Project({ useInMemoryFileSystem: true });
+ const source = project.createSourceFile('svelte.ts', result);
+
+ const namedImport = get_import(source, '@sveltejs/kit/vite', 'vitePreprocess');
+ if (!namedImport) return result;
+
+ const logger = log_on_ts_modification(
+ source,
+ 'Changed `vitePreprocess` import: https://svelte.dev/docs/kit/migrating-to-sveltekit-2#vitepreprocess-is-no-longer-exported-from-sveltejs-kit-vite'
+ );
+
+ if (namedImport.getParent().getParent().getNamedImports().length === 1) {
+ namedImport
+ .getParent()
+ .getParent()
+ .getParentIfKind(SyntaxKind.ImportDeclaration)
+ ?.setModuleSpecifier('@sveltejs/vite-plugin-svelte');
+ } else {
+ namedImport.remove();
+ add_named_import(source, '@sveltejs/vite-plugin-svelte', 'vitePreprocess');
+ }
+
+ logger();
+ return source.getFullText();
+}
+
+/**
+ * @param {string} code
+ * @param {boolean} _is_ts
+ * @param {string} file_path
+ */
+export function transform_code(code, _is_ts, file_path) {
+ const project = new Project({ useInMemoryFileSystem: true });
+ const source = project.createSourceFile('svelte.ts', code);
+ remove_throws(source);
+ add_cookie_note(file_path, source);
+ replace_resolve_path(source);
+ return source.getFullText();
+}
+
+/**
+ * `throw redirect(..)` -> `redirect(..)`
+ * @param {import('ts-morph').SourceFile} source
+ */
+function remove_throws(source) {
+ const logger = log_on_ts_modification(
+ source,
+ 'Removed `throw` from redirect/error functions: https://svelte.dev/docs/kit/migrating-to-sveltekit-2#redirect-and-error-are-no-longer-thrown-by-you'
+ );
+
+ /** @param {string} id */
+ function remove_throw(id) {
+ const named_import = get_import(source, '@sveltejs/kit', id);
+ if (!named_import) return;
+ const name_node = named_import.getNameNode();
+ if (Node.isIdentifier(name_node)) {
+ for (const id of name_node.findReferencesAsNodes()) {
+ const call_expression = id.getParent();
+ const throw_stmt = call_expression?.getParent();
+ if (Node.isCallExpression(call_expression) && Node.isThrowStatement(throw_stmt)) {
+ throw_stmt.replaceWithText((writer) => {
+ writer.setIndentationLevel(0);
+ writer.write(call_expression.getText() + ';');
+ });
+ }
+ }
+ }
+ }
+
+ remove_throw('redirect');
+ remove_throw('error');
+
+ logger();
+}
+
+/**
+ * Adds `path` option to `cookies.set/delete/serialize` calls
+ * @param {string} file_path
+ * @param {import('ts-morph').SourceFile} source
+ */
+function add_cookie_note(file_path, source) {
+ const basename = path.basename(file_path);
+ if (
+ basename !== '+page.js' &&
+ basename !== '+page.ts' &&
+ basename !== '+page.server.js' &&
+ basename !== '+page.server.ts' &&
+ basename !== '+server.js' &&
+ basename !== '+server.ts' &&
+ basename !== 'hooks.server.js' &&
+ basename !== 'hooks.server.ts'
+ ) {
+ return;
+ }
+
+ const logger = log_on_ts_modification(
+ source,
+ 'Search codebase for `@migration` and manually add the `path` option to `cookies.set/delete/serialize` calls: https://svelte.dev/docs/kit/migrating-to-sveltekit-2#path-is-now-a-required-option-for-cookies'
+ );
+
+ const calls = [];
+
+ for (const call of source.getDescendantsOfKind(SyntaxKind.CallExpression)) {
+ const expression = call.getExpression();
+ if (!Node.isPropertyAccessExpression(expression)) {
+ continue;
+ }
+
+ const name = expression.getName();
+ if (name !== 'set' && name !== 'delete' && name !== 'serialize') {
+ continue;
+ }
+
+ if (call.getText().includes('path')) {
+ continue;
+ }
+
+ const options_arg = call.getArguments()[name === 'delete' ? 1 : 2];
+ if (options_arg && !Node.isObjectLiteralExpression(options_arg)) {
+ continue;
+ }
+
+ const parent_function = call.getFirstAncestor(
+ /** @returns {ancestor is import('ts-morph').FunctionDeclaration | import('ts-morph').FunctionExpression | import('ts-morph').ArrowFunction} */
+ (ancestor) => {
+ // Check if this is inside a function
+ const fn_declaration = ancestor.asKind(SyntaxKind.FunctionDeclaration);
+ const fn_expression = ancestor.asKind(SyntaxKind.FunctionExpression);
+ const arrow_fn_expression = ancestor.asKind(SyntaxKind.ArrowFunction);
+ return !!fn_declaration || !!fn_expression || !!arrow_fn_expression;
+ }
+ );
+ if (!parent_function) {
+ continue;
+ }
+
+ const expression_text = expression.getExpression().getText();
+ if (
+ expression_text !== 'cookies' &&
+ (!expression_text.includes('.') ||
+ expression_text.split('.').pop() !== 'cookies' ||
+ !parent_function.getParameter(expression_text.split('.')[0]))
+ ) {
+ continue;
+ }
+
+ const parent = call.getFirstAncestorByKind(SyntaxKind.Block);
+ if (!parent) {
+ continue;
+ }
+
+ calls.push(() =>
+ call.replaceWithText((writer) => {
+ writer.setIndentationLevel(0); // prevent ts-morph from being unhelpful and adding its own indentation
+ writer.write('/* @migration task: add path argument */ ' + call.getText());
+ })
+ );
+ }
+
+ for (const call of calls) {
+ call();
+ }
+
+ logger();
+}
+
+/**
+ * `resolvePath` from `@sveltejs/kit` -> `resolveRoute` from `$app/paths`
+ * @param {import('ts-morph').SourceFile} source
+ */
+function replace_resolve_path(source) {
+ const named_import = get_import(source, '@sveltejs/kit', 'resolvePath');
+ if (!named_import) return;
+
+ const logger = log_on_ts_modification(
+ source,
+ 'Replaced `resolvePath` with `resolveRoute`: https://svelte.dev/docs/kit/migrating-to-sveltekit-2#resolvePath-has-been-removed'
+ );
+
+ const name_node = named_import.getNameNode();
+ if (Node.isIdentifier(name_node)) {
+ for (const id of name_node.findReferencesAsNodes()) {
+ id.replaceWithText('resolveRoute');
+ }
+ }
+ if (named_import.getParent().getParent().getNamedImports().length === 1) {
+ named_import.getParent().getParent().getParent().remove();
+ } else {
+ named_import.remove();
+ }
+
+ const paths_import = source.getImportDeclaration(
+ (i) => i.getModuleSpecifierValue() === '$app/paths'
+ );
+ if (paths_import) {
+ paths_import.addNamedImport('resolveRoute');
+ } else {
+ source.addImportDeclaration({
+ moduleSpecifier: '$app/paths',
+ namedImports: ['resolveRoute']
+ });
+ }
+
+ logger();
+}
+
+/**
+ * @param {import('ts-morph').SourceFile} source
+ * @param {string} from
+ * @param {string} name
+ */
+function get_import(source, from, name) {
+ return source
+ .getImportDeclarations()
+ .filter((i) => i.getModuleSpecifierValue() === from)
+ .flatMap((i) => i.getNamedImports())
+ .find((i) => i.getName() === name);
+}
diff --git a/packages/migrate/migrations/sveltekit-2/migrate.spec.js b/packages/migrate/migrations/sveltekit-2/migrate.spec.js
new file mode 100644
index 00000000..a297d947
--- /dev/null
+++ b/packages/migrate/migrations/sveltekit-2/migrate.spec.js
@@ -0,0 +1,32 @@
+import { assert, test } from 'vitest';
+import {
+ transform_code,
+ update_svelte_config_content,
+ update_tsconfig_content
+} from './migrate.js';
+import { read_samples } from '../../utils.js';
+
+for (const sample of read_samples(new URL('./svelte-config-samples.md', import.meta.url))) {
+ test('svelte.config.js: ' + sample.description, () => {
+ const actual = update_svelte_config_content(sample.before);
+ assert.equal(actual, sample.after);
+ });
+}
+
+for (const sample of read_samples(new URL('./tsconfig-samples.md', import.meta.url))) {
+ test('tsconfig.json: ' + sample.description, () => {
+ const actual = update_tsconfig_content(sample.before);
+ assert.equal(actual, sample.after);
+ });
+}
+
+for (const sample of read_samples(new URL('./tsjs-samples.md', import.meta.url))) {
+ test('JS/TS file: ' + sample.description, () => {
+ const actual = transform_code(
+ sample.before,
+ sample.filename?.endsWith('.ts') ?? false,
+ sample.filename ?? '+page.js'
+ );
+ assert.equal(actual, sample.after);
+ });
+}
diff --git a/packages/migrate/migrations/sveltekit-2/svelte-config-samples.md b/packages/migrate/migrations/sveltekit-2/svelte-config-samples.md
new file mode 100644
index 00000000..9a06096a
--- /dev/null
+++ b/packages/migrate/migrations/sveltekit-2/svelte-config-samples.md
@@ -0,0 +1,126 @@
+## Removes dangerZone (1)
+
+```js before
+export default {
+ kit: {
+ foo: bar,
+ dangerZone: {
+ trackServerFetches: true
+ },
+ baz: qux
+ }
+};
+```
+
+```js after
+export default {
+ kit: {
+ foo: bar,
+ baz: qux
+ }
+};
+```
+
+## Removes dangerZone (2)
+
+```js before
+export default {
+ kit: {
+ foo: bar,
+ dangerZone: {
+ trackServerFetches: true
+ }
+ }
+};
+```
+
+
+```js after
+export default {
+ kit: {
+ foo: bar,
+ }
+};
+```
+
+## Replaces vitePreprocess import (1)
+
+```js before
+import adapter from '@sveltejs/adapter-auto';
+import { vitePreprocess } from '@sveltejs/kit/vite';
+
+/** @type {import('@sveltejs/kit').Config} */
+const config = {
+ // Consult https://svelte.dev/docs/kit/integrations#preprocessors
+ // for more information about preprocessors
+ preprocess: vitePreprocess(),
+
+ kit: {
+ adapter: adapter()
+ }
+};
+
+export default config;
+```
+
+```js after
+import adapter from '@sveltejs/adapter-auto';
+import { vitePreprocess } from '@sveltejs/vite-plugin-svelte';
+
+/** @type {import('@sveltejs/kit').Config} */
+const config = {
+ // Consult https://svelte.dev/docs/kit/integrations#preprocessors
+ // for more information about preprocessors
+ preprocess: vitePreprocess(),
+
+ kit: {
+ adapter: adapter()
+ }
+};
+
+export default config;
+```
+
+## Replaces vitePreprocess import (2)
+
+```js before
+import adapter from '@sveltejs/adapter-auto';
+import { vitePreprocess, foo } from '@sveltejs/kit/vite';
+
+export default {
+ preprocess: vitePreprocess()
+};
+```
+
+
+```js after
+import adapter from '@sveltejs/adapter-auto';
+import { foo } from '@sveltejs/kit/vite';
+import { vitePreprocess } from "@sveltejs/vite-plugin-svelte";
+
+export default {
+ preprocess: vitePreprocess()
+};
+```
+
+## Replaces vitePreprocess import (3)
+
+```js before
+import adapter from '@sveltejs/adapter-auto';
+import { vitePreprocess, foo } from '@sveltejs/kit/vite';
+import { a } from '@sveltejs/vite-plugin-svelte';
+
+export default {
+ preprocess: vitePreprocess()
+};
+```
+
+```js after
+import adapter from '@sveltejs/adapter-auto';
+import { foo } from '@sveltejs/kit/vite';
+import { a, vitePreprocess } from '@sveltejs/vite-plugin-svelte';
+
+export default {
+ preprocess: vitePreprocess()
+};
+```
diff --git a/packages/migrate/migrations/sveltekit-2/tsconfig-samples.md b/packages/migrate/migrations/sveltekit-2/tsconfig-samples.md
new file mode 100644
index 00000000..93d357fa
--- /dev/null
+++ b/packages/migrate/migrations/sveltekit-2/tsconfig-samples.md
@@ -0,0 +1,40 @@
+## Removes importsNotUsedAsValues/preserveValueImports
+
+```json before
+{
+ "extends": "./.svelte-kit/tsconfig.json",
+ "compilerOptions": {
+ "importsNotUsedAsValues": "error",
+ "preserveValueImports": true
+ }
+}
+```
+
+
+```json after
+{
+ "extends": "./.svelte-kit/tsconfig.json",
+ "compilerOptions": {
+ }
+}
+```
+
+## Leaves tsconfig alone
+
+```json before
+{
+ "compilerOptions": {
+ "importsNotUsedAsValues": "error",
+ "preserveValueImports": true
+ }
+}
+```
+
+```json after
+{
+ "compilerOptions": {
+ "importsNotUsedAsValues": "error",
+ "preserveValueImports": true
+ }
+}
+```
diff --git a/packages/migrate/migrations/sveltekit-2/tsjs-samples.md b/packages/migrate/migrations/sveltekit-2/tsjs-samples.md
new file mode 100644
index 00000000..3fed9bf6
--- /dev/null
+++ b/packages/migrate/migrations/sveltekit-2/tsjs-samples.md
@@ -0,0 +1,170 @@
+## Removes throws
+
+```js before
+import { redirect, error } from '@sveltejs/kit';
+
+throw redirect();
+redirect();
+throw error();
+error();
+function x() {
+ let redirect = true;
+ throw redirect();
+}
+```
+
+```js after
+import { redirect, error } from '@sveltejs/kit';
+
+redirect();
+redirect();
+error();
+error();
+function x() {
+ let redirect = true;
+ throw redirect();
+}
+```
+
+## Leaves redirect/error from other sources alone
+
+```js before
+import { redirect, error } from 'somewhere-else';
+
+throw redirect();
+redirect();
+throw error();
+error();
+```
+
+```js after
+import { redirect, error } from 'somewhere-else';
+
+throw redirect();
+redirect();
+throw error();
+error();
+```
+
+## Notes cookie migration
+
+```js before
+export function load({ cookies }) {
+ cookies.set('foo', 'bar');
+}
+```
+
+```js after
+export function load({ cookies }) {
+ /* @migration task: add path argument */ cookies.set('foo', 'bar');
+}
+```
+
+## Notes cookie migration with multiple occurences
+
+```js before
+export function load({ cookies }) {
+ cookies.delete('foo');
+ cookies.set('x', 'y', { z: '' });
+}
+```
+
+```js after
+export function load({ cookies }) {
+ /* @migration task: add path argument */ cookies.delete('foo');
+ /* @migration task: add path argument */ cookies.set('x', 'y', { z: '' });
+}
+```
+
+## Handles non-destructured argument
+
+```js before
+export function load(event) {
+ event.cookies.set('x', 'y');
+}
+```
+
+```js after
+export function load(event) {
+ /* @migration task: add path argument */ event.cookies.set('x', 'y');
+}
+```
+
+## Recognizes cookies false positives
+
+```js before
+export function load({ cookies }) {
+ cookies.set('foo', 'bar', { path: '/' });
+}
+
+export function foo(event) {
+ x.cookies.set('foo', 'bar');
+}
+
+export function bar(event) {
+ event.x.set('foo', 'bar');
+}
+
+cookies.set('foo', 'bar');
+```
+
+```js after
+export function load({ cookies }) {
+ cookies.set('foo', 'bar', { path: '/' });
+}
+
+export function foo(event) {
+ x.cookies.set('foo', 'bar');
+}
+
+export function bar(event) {
+ event.x.set('foo', 'bar');
+}
+
+cookies.set('foo', 'bar');
+```
+
+## Replaces resolvePath
+
+```js before
+import { resolvePath } from '@sveltejs/kit';
+
+resolvePath('x', y);
+```
+
+
+```js after
+import { resolveRoute } from "$app/paths";
+
+resolveRoute('x', y);
+```
+
+## Replaces resolvePath taking care of imports
+
+```js before
+import { resolvePath, x } from '@sveltejs/kit';
+import { y } from '$app/paths';
+
+resolvePath('x');
+```
+
+```js after
+import { x } from '@sveltejs/kit';
+import { y, resolveRoute } from '$app/paths';
+
+resolveRoute('x');
+```
+
+## Doesn't replace resolvePath from other sources
+
+```js before
+import { resolvePath } from 'x';
+
+resolvePath('x');
+```
+
+```js after
+import { resolvePath } from 'x';
+
+resolvePath('x');
+```
diff --git a/packages/migrate/package.json b/packages/migrate/package.json
new file mode 100644
index 00000000..be1a7167
--- /dev/null
+++ b/packages/migrate/package.json
@@ -0,0 +1,54 @@
+{
+ "name": "svelte-migrate",
+ "version": "1.6.8",
+ "description": "A CLI for migrating Svelte(Kit) codebases",
+ "keywords": [
+ "migration",
+ "upgrade",
+ "svelte",
+ "sveltekit",
+ "tool"
+ ],
+ "repository": {
+ "type": "git",
+ "url": "https://github.com/sveltejs/kit",
+ "directory": "packages/migrate"
+ },
+ "license": "MIT",
+ "homepage": "https://svelte.dev",
+ "type": "module",
+ "bin": {
+ "svelte-migrate": "./bin.js"
+ },
+ "files": [
+ "bin.js",
+ "migrations",
+ "utils.js",
+ "!migrations/**/*.spec.js",
+ "!migrations/**/samples.md"
+ ],
+ "dependencies": {
+ "import-meta-resolve": "^4.1.0",
+ "kleur": "^4.1.5",
+ "magic-string": "^0.30.5",
+ "prompts": "^2.4.2",
+ "semver": "^7.5.4",
+ "tiny-glob": "^0.2.9",
+ "ts-morph": "^24.0.0",
+ "typescript": "^5.3.3",
+ "zimmerframe": "^1.1.2"
+ },
+ "devDependencies": {
+ "@types/node": "^18.19.48",
+ "@types/prompts": "^2.4.9",
+ "@types/semver": "^7.5.6",
+ "svelte": "^4.2.10",
+ "vitest": "^2.0.1"
+ },
+ "scripts": {
+ "test": "vitest run --silent",
+ "check": "tsc",
+ "lint": "prettier --check .",
+ "format": "pnpm lint --write"
+ }
+}
diff --git a/packages/migrate/tsconfig.json b/packages/migrate/tsconfig.json
new file mode 100644
index 00000000..26885cff
--- /dev/null
+++ b/packages/migrate/tsconfig.json
@@ -0,0 +1,12 @@
+{
+ "compilerOptions": {
+ "allowJs": true,
+ "checkJs": true,
+ "noEmit": true,
+ "strict": true,
+ "target": "es2022",
+ "module": "node16",
+ "moduleResolution": "node16",
+ "allowSyntheticDefaultImports": true
+ }
+}
diff --git a/packages/migrate/utils.js b/packages/migrate/utils.js
new file mode 100644
index 00000000..944461f0
--- /dev/null
+++ b/packages/migrate/utils.js
@@ -0,0 +1,421 @@
+import colors from 'kleur';
+import MagicString from 'magic-string';
+import { execFileSync, execSync } from 'node:child_process';
+import fs from 'node:fs';
+import path from 'node:path';
+import process from 'node:process';
+import semver from 'semver';
+import ts from 'typescript';
+
+/** @param {string} message */
+export function bail(message) {
+ console.error(colors.bold().red(message));
+ process.exit(1);
+}
+
+/** @param {string} file */
+export function relative(file) {
+ return path.relative('.', file);
+}
+/**
+ *
+ * @param {string} file
+ * @param {string} renamed
+ * @param {string} content
+ * @param {boolean} use_git
+ */
+export function move_file(file, renamed, content, use_git) {
+ if (use_git) {
+ execFileSync('git', ['mv', file, renamed]);
+ } else {
+ fs.unlinkSync(file);
+ }
+
+ fs.writeFileSync(renamed, content);
+}
+
+/**
+ * @param {string} contents
+ * @param {string} indent
+ */
+export function comment(contents, indent) {
+ return contents.replace(new RegExp(`^${indent}`, 'gm'), `${indent}// `);
+}
+
+/** @param {string} content */
+export function dedent(content) {
+ const indent = guess_indent(content);
+ if (!indent) return content;
+
+ /** @type {string[]} */
+ const substitutions = [];
+
+ try {
+ const ast = ts.createSourceFile(
+ 'filename.ts',
+ content,
+ ts.ScriptTarget.Latest,
+ true,
+ ts.ScriptKind.TS
+ );
+
+ const code = new MagicString(content);
+
+ /** @param {ts.Node} node */
+ function walk(node) {
+ if (ts.isTemplateLiteral(node)) {
+ let pos = node.pos;
+ while (/\s/.test(content[pos])) pos += 1;
+
+ code.overwrite(pos, node.end, `____SUBSTITUTION_${substitutions.length}____`);
+ substitutions.push(node.getText());
+ }
+
+ node.forEachChild(walk);
+ }
+
+ ast.forEachChild(walk);
+
+ return code
+ .toString()
+ .replace(new RegExp(`^${indent}`, 'gm'), '')
+ .replace(/____SUBSTITUTION_(\d+)____/g, (match, index) => substitutions[index]);
+ } catch {
+ // as above — ignore this edge case
+ return content;
+ }
+}
+
+/** @param {string} content */
+export function guess_indent(content) {
+ const lines = content.split('\n');
+
+ const tabbed = lines.filter((line) => /^\t+/.test(line));
+ const spaced = lines.filter((line) => /^ {2,}/.test(line));
+
+ if (tabbed.length === 0 && spaced.length === 0) {
+ return null;
+ }
+
+ // More lines tabbed than spaced? Assume tabs, and
+ // default to tabs in the case of a tie (or nothing
+ // to go on)
+ if (tabbed.length >= spaced.length) {
+ return '\t';
+ }
+
+ // Otherwise, we need to guess the multiple
+ const min = spaced.reduce((previous, current) => {
+ const count = /^ +/.exec(current)?.[0].length ?? 0;
+ return Math.min(count, previous);
+ }, Infinity);
+
+ return ' '.repeat(min);
+}
+
+/**
+ * @param {string} content
+ * @param {number} offset
+ */
+export function indent_at_line(content, offset) {
+ const substr = content.substring(content.lastIndexOf('\n', offset) + 1, offset);
+ return /\s*/.exec(substr)?.[0] ?? '';
+}
+
+/**
+ * @param {string} content
+ * @param {string} except
+ */
+export function except_str(content, except) {
+ const start = content.indexOf(except);
+ const end = start + except.length;
+ return content.substring(0, start) + content.substring(end);
+}
+
+/**
+ * @returns {boolean} True if git is installed
+ */
+export function check_git() {
+ let use_git = false;
+
+ let dir = process.cwd();
+ do {
+ if (fs.existsSync(path.join(dir, '.git'))) {
+ use_git = true;
+ break;
+ }
+ } while (dir !== (dir = path.dirname(dir)));
+
+ if (use_git) {
+ try {
+ const status = execSync('git status --porcelain', { stdio: 'pipe' }).toString();
+
+ if (status) {
+ const message =
+ 'Your git working directory is dirty — we recommend committing your changes before running this migration.\n';
+ console.log(colors.bold().red(message));
+ }
+ } catch {
+ // would be weird to have a .git folder if git is not installed,
+ // but always expect the unexpected
+ const message =
+ 'Could not detect a git installation. If this is unexpected, please raise an issue: https://github.com/sveltejs/kit.\n';
+ console.log(colors.bold().red(message));
+ use_git = false;
+ }
+ }
+
+ return use_git;
+}
+
+/**
+ * Get a list of all files in a directory
+ * @param {string} cwd - the directory to walk
+ * @param {boolean} [dirs] - whether to include directories in the result
+ */
+export function walk(cwd, dirs = false) {
+ /** @type {string[]} */
+ const all_files = [];
+
+ /** @param {string} dir */
+ function walk_dir(dir) {
+ const files = fs.readdirSync(path.join(cwd, dir));
+
+ for (const file of files) {
+ const joined = path.join(dir, file);
+ const stats = fs.statSync(path.join(cwd, joined));
+ if (stats.isDirectory()) {
+ if (dirs) all_files.push(joined);
+ walk_dir(joined);
+ } else {
+ all_files.push(joined);
+ }
+ }
+ }
+
+ return walk_dir(''), all_files;
+}
+
+/** @param {string} str */
+export function posixify(str) {
+ return str.replace(/\\/g, '/');
+}
+
+/**
+ * @param {string} content
+ * @param {Array<[string, string, string?, ('dependencies' | 'devDependencies')?]>} updates
+ */
+export function update_pkg(content, updates) {
+ const indent = content.split('\n')[1].match(/^\s+/)?.[0] || ' ';
+ const pkg = JSON.parse(content);
+
+ /**
+ * @param {string} name
+ * @param {string} version
+ * @param {string} [additional]
+ * @param {'dependencies' | 'devDependencies' | undefined} [insert]
+ */
+ function update_pkg(name, version, additional = '', insert) {
+ /**
+ * @param {string} type
+ */
+ const updateVersion = (type) => {
+ const existingRange = pkg[type]?.[name];
+
+ if (
+ existingRange &&
+ semver.validRange(existingRange) &&
+ !semver.subset(existingRange, version)
+ ) {
+ // Check if the new version range is an upgrade
+ const minExistingVersion = semver.minVersion(existingRange);
+ const minNewVersion = semver.minVersion(version);
+
+ if (minExistingVersion && minNewVersion && semver.gt(minNewVersion, minExistingVersion)) {
+ log_migration(`Updated ${name} to ${version}`);
+ pkg[type][name] = version;
+ }
+ }
+ };
+
+ updateVersion('dependencies');
+ updateVersion('devDependencies');
+
+ if (insert && !pkg[insert]?.[name]) {
+ if (!pkg[insert]) pkg[insert] = {};
+
+ // Insert the property in sorted position without adjusting other positions so diffs are easier to read
+ const sorted_keys = Object.keys(pkg[insert]).sort();
+ const index = sorted_keys.findIndex((key) => name.localeCompare(key) === -1);
+ const insert_index = index !== -1 ? index : sorted_keys.length;
+ const new_properties = Object.entries(pkg[insert]);
+ new_properties.splice(insert_index, 0, [name, version]);
+ pkg[insert] = Object.fromEntries(new_properties);
+
+ log_migration(`Added ${name} version ${version} ${additional}`);
+ }
+ }
+
+ for (const update of updates) {
+ update_pkg(...update);
+ }
+
+ const result = JSON.stringify(pkg, null, indent);
+ if (content.endsWith('\n')) return result + '\n';
+ return result;
+}
+
+const logged_migrations = new Set();
+
+/**
+ * @param {import('ts-morph').SourceFile} source
+ * @param {string} text
+ */
+export function log_on_ts_modification(source, text) {
+ let logged = false;
+ const log = () => {
+ if (!logged) {
+ logged = true;
+ log_migration(text);
+ }
+ };
+ source.onModified(log);
+ return () => source.onModified(log, false);
+}
+
+/** @param {string} text */
+export function log_migration(text) {
+ if (logged_migrations.has(text)) return;
+ console.log(text);
+ logged_migrations.add(text);
+}
+
+/**
+ * Parses the scripts contents and invoked `transform_script_code` with it, then runs the result through `transform_svelte_code`.
+ * The result is written back to disk.
+ * @param {string} file_path
+ * @param {(code: string, is_ts: boolean, file_path: string) => string} transform_script_code
+ * @param {(code: string, file_path: string) => string} transform_svelte_code
+ */
+export function update_svelte_file(file_path, transform_script_code, transform_svelte_code) {
+ try {
+ const content = fs.readFileSync(file_path, 'utf-8');
+ const updated = content.replace(
+ /${whitespace}`;
+ }
+ );
+ fs.writeFileSync(file_path, transform_svelte_code(updated, file_path), 'utf-8');
+ } catch (err) {
+ // TODO: change to import('svelte/compiler').Warning after upgrading to Svelte 5
+ const e = /** @type {any} */ (err);
+ console.warn(buildExtendedLogMessage(e), e.frame);
+ console.info(e.stack);
+ }
+}
+
+/**
+ * Reads the file and invokes `transform_code` with its contents. The result is written back to disk.
+ * @param {string} file_path
+ * @param {(code: string, is_ts: boolean, file_path: string) => string} transform_code
+ */
+export function update_js_file(file_path, transform_code) {
+ try {
+ const content = fs.readFileSync(file_path, 'utf-8');
+ const updated = transform_code(content, file_path.endsWith('.ts'), file_path);
+ fs.writeFileSync(file_path, updated, 'utf-8');
+ } catch (err) {
+ // TODO: change to import('svelte/compiler').Warning after upgrading to Svelte 5
+ const e = /** @type {any} */ (err);
+ console.warn(buildExtendedLogMessage(e), e.frame);
+ console.info(e.stack);
+ }
+}
+
+/**
+ * @param {any} w
+ */
+export function buildExtendedLogMessage(w) {
+ const parts = [];
+ if (w.filename) {
+ parts.push(w.filename);
+ }
+ if (w.start) {
+ parts.push(':', w.start.line, ':', w.start.column);
+ }
+ if (w.message) {
+ if (parts.length > 0) {
+ parts.push(' ');
+ }
+ parts.push(w.message);
+ }
+ return parts.join('');
+}
+
+/**
+ * Updates the tsconfig/jsconfig.json file with the provided function.
+ * @param {(content: string) => string} update_tsconfig_content
+ */
+export function update_tsconfig(update_tsconfig_content) {
+ const file = fs.existsSync('tsconfig.json')
+ ? 'tsconfig.json'
+ : fs.existsSync('jsconfig.json')
+ ? 'jsconfig.json'
+ : null;
+ if (file) {
+ fs.writeFileSync(file, update_tsconfig_content(fs.readFileSync(file, 'utf8')));
+ }
+}
+
+/** @param {string | URL} test_file */
+export function read_samples(test_file) {
+ const markdown = fs.readFileSync(test_file, 'utf8').replaceAll('\r\n', '\n');
+ const samples = markdown
+ .split(/^##/gm)
+ .slice(1)
+ .map((block) => {
+ const description = block.split('\n')[0];
+ const before = /```(js|ts|svelte) before\n([^]*?)\n```/.exec(block);
+ const after = /```(js|ts|svelte) after\n([^]*?)\n```/.exec(block);
+
+ const match = /> file: (.+)/.exec(block);
+
+ return {
+ description,
+ before: before ? before[2] : '',
+ after: after ? after[2] : '',
+ filename: match?.[1],
+ solo: block.includes('> solo')
+ };
+ });
+
+ if (samples.some((sample) => sample.solo)) {
+ return samples.filter((sample) => sample.solo);
+ }
+
+ return samples;
+}
+
+/**
+ * @param {import('ts-morph').SourceFile} source
+ * @param {string} _import
+ * @param {string} method
+ */
+export function add_named_import(source, _import, method) {
+ const existing = source.getImportDeclaration(_import);
+ if (existing) {
+ if (existing.getNamedImports().some((i) => i.getName() === method)) return;
+ existing?.addNamedImport(method);
+ } else {
+ source.addImportDeclaration({
+ moduleSpecifier: _import,
+ namedImports: [method]
+ });
+ }
+}
diff --git a/packages/migrate/utils.spec.js b/packages/migrate/utils.spec.js
new file mode 100644
index 00000000..a75641e6
--- /dev/null
+++ b/packages/migrate/utils.spec.js
@@ -0,0 +1,93 @@
+import { assert, test } from 'vitest';
+import { update_pkg } from './utils.js';
+
+test('Inserts package at correct position (1)', () => {
+ const result = update_pkg(
+ `{
+ "dependencies": {
+ "a": "1",
+ "z": "3",
+ "c": "4"
+ }
+}`,
+ [['b', '2', '', 'dependencies']]
+ );
+
+ assert.equal(
+ result,
+ `{
+ "dependencies": {
+ "a": "1",
+ "b": "2",
+ "z": "3",
+ "c": "4"
+ }
+}`
+ );
+});
+
+test('Inserts package at correct position (2)', () => {
+ const result = update_pkg(
+ `{
+ "dependencies": {
+ "a": "1",
+ "b": "2"
+ }
+}`,
+ [['c', '3', '', 'dependencies']]
+ );
+
+ assert.equal(
+ result,
+ `{
+ "dependencies": {
+ "a": "1",
+ "b": "2",
+ "c": "3"
+ }
+}`
+ );
+});
+
+test('Inserts package at correct position (3)', () => {
+ const result = update_pkg(
+ `{
+ "dependencies": {
+ "b": "2",
+ "c": "3"
+ }
+}`,
+ [['a', '1', '', 'dependencies']]
+ );
+
+ assert.equal(
+ result,
+ `{
+ "dependencies": {
+ "a": "1",
+ "b": "2",
+ "c": "3"
+ }
+}`
+ );
+});
+
+test('Does not downgrade versions', () => {
+ const result = update_pkg(
+ `{
+ "devDependencies": {
+ "@sveltejs/kit": "^2.4.3"
+ }
+}`,
+ [['@sveltejs/kit', '^2.0.0']]
+ );
+
+ assert.equal(
+ result,
+ `{
+ "devDependencies": {
+ "@sveltejs/kit": "^2.4.3"
+ }
+}`
+ );
+});
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 1a7619e0..bf920c59 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -233,6 +233,52 @@ importers:
specifier: ^0.2.9
version: 0.2.9
+ packages/migrate:
+ dependencies:
+ import-meta-resolve:
+ specifier: ^4.1.0
+ version: 4.1.0
+ kleur:
+ specifier: ^4.1.5
+ version: 4.1.5
+ magic-string:
+ specifier: ^0.30.5
+ version: 0.30.12
+ prompts:
+ specifier: ^2.4.2
+ version: 2.4.2
+ semver:
+ specifier: ^7.5.4
+ version: 7.6.3
+ tiny-glob:
+ specifier: ^0.2.9
+ version: 0.2.9
+ ts-morph:
+ specifier: ^24.0.0
+ version: 24.0.0
+ typescript:
+ specifier: ^5.3.3
+ version: 5.6.2
+ zimmerframe:
+ specifier: ^1.1.2
+ version: 1.1.2
+ devDependencies:
+ '@types/node':
+ specifier: ^18.19.48
+ version: 18.19.64
+ '@types/prompts':
+ specifier: ^2.4.9
+ version: 2.4.9
+ '@types/semver':
+ specifier: ^7.5.6
+ version: 7.5.8
+ svelte:
+ specifier: ^4.2.10
+ version: 4.2.19
+ vitest:
+ specifier: ^2.0.1
+ version: 2.0.5(@types/node@18.19.64)(@vitest/ui@2.0.5)
+
packages:
'@ampproject/remapping@2.3.0':
@@ -731,6 +777,9 @@ packages:
resolution: {integrity: sha512-qhUGGDHcpbY2zpjW3SwqchuW8J/5EzlPFud7xNntHKA7f3a/mx5+g+ruJKFHSAiVZYo30PALt+AyhmPUNKH/Og==}
engines: {node: ^14.13.1 || ^16.0.0 || >=18}
+ '@ts-morph/common@0.25.0':
+ resolution: {integrity: sha512-kMnZz+vGGHi4GoHnLmMhGNjm44kGtKUXGnOvrKmMwAuvNjM/PgKVGfUnL7IDvK7Jb2QQ82jq3Zmp04Gy+r3Dkg==}
+
'@types/estree@1.0.5':
resolution: {integrity: sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==}
@@ -740,12 +789,21 @@ packages:
'@types/node@12.20.55':
resolution: {integrity: sha512-J8xLz7q2OFulZ2cyGTLE1TbbZcjpno7FaN6zdJNrgAdrJ+DZzh/uFR6YrTb4C+nXakvud8Q4+rbhoIWlYQbUFQ==}
+ '@types/node@18.19.64':
+ resolution: {integrity: sha512-955mDqvO2vFf/oL7V3WiUtiz+BugyX8uVbaT2H8oj3+8dRyH2FLiNdowe7eNqRM7IOIZvzDH76EoAT+gwm6aIQ==}
+
'@types/node@22.5.4':
resolution: {integrity: sha512-FDuKUJQm/ju9fT/SeX/6+gBzoPzlVCzfzmGkwKvRHQVxi4BntVbyIwf6a4Xn62mrvndLiml6z/UBXIdEVjQLXg==}
+ '@types/prompts@2.4.9':
+ resolution: {integrity: sha512-qTxFi6Buiu8+50/+3DGIWLHM6QuWsEKugJnnP6iv2Mc4ncxE4A/OJkjuVOA+5X0X1S/nq5VJRa8Lu+nwcvbrKA==}
+
'@types/resolve@1.20.2':
resolution: {integrity: sha512-60BCwRFOZCQhDncwQdxxeOEEkbc5dIMccYLwbxsS4TUNeVECQ/pBJ0j09mrHOl/JJvpRPGwO9SvE4nR2Nb/a4Q==}
+ '@types/semver@7.5.8':
+ resolution: {integrity: sha512-I8EUhyrgfLrcTkzV3TSsGyl1tSuPrEDzr0yd5m90UgNxQkyDXULk3b6MlQqTCpZpNtWe1K0hzclnZkTcLBe2UQ==}
+
'@types/tar-fs@2.0.4':
resolution: {integrity: sha512-ipPec0CjTmVDWE+QKr9cTmIIoTl7dFG/yARCM5MqK8i6CNLIG1P8x4kwDsOQY1ChZOZjH0wO9nvfgBvWl4R3kA==}
@@ -969,6 +1027,12 @@ packages:
resolution: {integrity: sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ==}
engines: {node: '>=8'}
+ code-block-writer@13.0.3:
+ resolution: {integrity: sha512-Oofo0pq3IKnsFtuHqSF7TqBfr71aeyZDVJ0HpmqB7FBM2qEigL0iPONSCZSO9pE9dZTAxANe5XHG9Uy0YMv8cg==}
+
+ code-red@1.0.4:
+ resolution: {integrity: sha512-7qJWqItLA8/VPVlKJlFXU+NBlo/qyfs39aJcuMT/2ere32ZqvF5OSxgdM5xOfJJ7O429gg2HM47y8v9P+9wrNw==}
+
color-convert@2.0.1:
resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==}
engines: {node: '>=7.0.0'}
@@ -997,6 +1061,10 @@ packages:
resolution: {integrity: sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==}
engines: {node: '>= 8'}
+ css-tree@2.3.1:
+ resolution: {integrity: sha512-6Fv1DV/TYw//QF5IzQdqsNDjx/wc8TrMBZsqjL9eW01tWb7R7k/mq+/VXfJCl7SoD5emsJop9cOByJZfs8hYIw==}
+ engines: {node: ^10 || ^12.20.0 || ^14.13.0 || >=15.0.0}
+
cssesc@3.0.0:
resolution: {integrity: sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==}
engines: {node: '>=4'}
@@ -1241,6 +1309,14 @@ packages:
fastq@1.17.1:
resolution: {integrity: sha512-sRVD3lWVIXWg6By68ZN7vho9a1pQcN/WBFaAAsDDFzlJjvoGx0P8z7V1t72grFJfJhu3YPZBuu25f7Kaw2jN1w==}
+ fdir@6.4.2:
+ resolution: {integrity: sha512-KnhMXsKSPZlAhp7+IjUkRZKPb4fUyccpDrdFXbi4QL1qkmFh9kVY09Yox+n4MaOb3lHZ1Tv829C3oaaXoMYPDQ==}
+ peerDependencies:
+ picomatch: ^3 || ^4
+ peerDependenciesMeta:
+ picomatch:
+ optional: true
+
fflate@0.8.2:
resolution: {integrity: sha512-cPJU47OaAoCbg0pBvzsgpTPhmhqI5eJjh/JIu8tPj5q+T7iLvW/JAYUqmE7KOB4R1ZyEhzBaIQpQpardBF5z8A==}
@@ -1378,6 +1454,9 @@ packages:
resolution: {integrity: sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==}
engines: {node: '>=6'}
+ import-meta-resolve@4.1.0:
+ resolution: {integrity: sha512-I6fiaX09Xivtk+THaMfAwnA3MVA5Big1WHF1Dfx9hFuvNIWpXnorlkzhcQf6ehrqQiiZECRt1poOAkPmer3ruw==}
+
imurmurhash@0.1.4:
resolution: {integrity: sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==}
engines: {node: '>=0.8.19'}
@@ -1468,6 +1547,14 @@ packages:
keyv@4.5.4:
resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==}
+ kleur@3.0.3:
+ resolution: {integrity: sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==}
+ engines: {node: '>=6'}
+
+ kleur@4.1.5:
+ resolution: {integrity: sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ==}
+ engines: {node: '>=6'}
+
known-css-properties@0.34.0:
resolution: {integrity: sha512-tBECoUqNFbyAY4RrbqsBQqDFpGXAEbdD5QKr8kACx3+rnArmuuR22nKQWKazvp07N9yjTyDZaw/20UIH8tL9DQ==}
@@ -1514,6 +1601,9 @@ packages:
magic-string@0.30.12:
resolution: {integrity: sha512-Ea8I3sQMVXr8JhN4z+H/d8zwo+tYDgHE9+5G4Wnrwhs0gaK9fXTKx0Tw5Xwsd/bCPTTZNRAdpyzvoeORe9LYpw==}
+ mdn-data@2.0.30:
+ resolution: {integrity: sha512-GaqWWShW4kv/G9IEucWScBx9G1/vsFZZJUO+tD26M8J8z3Kw5RDQjaoZe03YAClgeS/SWPOcb4nkFBTEi5DUEA==}
+
merge-stream@2.0.0:
resolution: {integrity: sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==}
@@ -1638,6 +1728,9 @@ packages:
resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==}
engines: {node: '>=6'}
+ path-browserify@1.0.1:
+ resolution: {integrity: sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==}
+
path-exists@4.0.0:
resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==}
engines: {node: '>=8'}
@@ -1668,6 +1761,9 @@ packages:
resolution: {integrity: sha512-vE7JKRyES09KiunauX7nd2Q9/L7lhok4smP9RZTDeD4MVs72Dp2qNFVz39Nz5a0FVEW0BJR6C0DYrq6unoziZA==}
engines: {node: '>= 14.16'}
+ periscopic@3.1.0:
+ resolution: {integrity: sha512-vKiQ8RRtkl9P+r/+oefh25C3fhybptkHKCZSPlcXiJux2tJF55GnEj3BVn4A5gKfq9NWWXXrxkHBwVPUfH0opw==}
+
picocolors@1.1.0:
resolution: {integrity: sha512-TQ92mBOW0l3LeMeyLV6mzy/kWr8lkd/hp3mTg7wYK7zJhuBStmGMBG0BdeDZS/dZx1IukaX6Bk11zcln25o1Aw==}
@@ -1675,6 +1771,10 @@ packages:
resolution: {integrity: sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==}
engines: {node: '>=8.6'}
+ picomatch@4.0.2:
+ resolution: {integrity: sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==}
+ engines: {node: '>=12'}
+
pify@4.0.1:
resolution: {integrity: sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==}
engines: {node: '>=6'}
@@ -1743,6 +1843,10 @@ packages:
engines: {node: '>=14'}
hasBin: true
+ prompts@2.4.2:
+ resolution: {integrity: sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q==}
+ engines: {node: '>= 6'}
+
pseudomap@1.0.2:
resolution: {integrity: sha512-b/YwNhb8lk1Zz2+bXXpS/LK9OisiZZ1SNsSLxN1x2OXVEhW2Ckr/7mWE5vrC1ZTiJlD9g19jWszTmJsB+oEpFQ==}
@@ -1945,6 +2049,10 @@ packages:
svelte:
optional: true
+ svelte@4.2.19:
+ resolution: {integrity: sha512-IY1rnGr6izd10B0A8LqsBfmlT5OILVuZ7XsI0vdGPEvuonFV7NYEUK4dAkm9Zg2q0Um92kYjTpS1CAP3Nh/KWw==}
+ engines: {node: '>=16'}
+
svelte@5.0.0:
resolution: {integrity: sha512-jv2IvTtakG58DqZMo6fY3T6HFmGV4iDQH2lSUyfmCEYaoa+aCNcF+9rERbdDvT4XDF0nQBg6TEoJn0dirED8VQ==}
engines: {node: '>=18'}
@@ -1992,6 +2100,10 @@ packages:
tinyexec@0.3.0:
resolution: {integrity: sha512-tVGE0mVJPGb0chKhqmsoosjsS+qUnJVGJpZgsHYQcGoPlG3B51R3PouqTgEGH2Dc9jjFyOqOpix6ZHNMXp1FZg==}
+ tinyglobby@0.2.10:
+ resolution: {integrity: sha512-Zc+8eJlFMvgatPZTl6A9L/yht8QqdmUNtURHaKZLmKBE12hNPSrqNkUp2cs3M/UKmNVVAMFQYSjYIVHDjW5zew==}
+ engines: {node: '>=12.0.0'}
+
tinypool@1.0.1:
resolution: {integrity: sha512-URZYihUbRPcGv95En+sz6MfghfIc2OJ1sv/RmhWZLouPY0/8Vo80viwPvg3dlaS9fuq7fQMEfgRRK7BBZThBEA==}
engines: {node: ^18.0.0 || >=20.0.0}
@@ -2032,6 +2144,9 @@ packages:
ts-interface-checker@0.1.13:
resolution: {integrity: sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==}
+ ts-morph@24.0.0:
+ resolution: {integrity: sha512-2OAOg/Ob5yx9Et7ZX4CvTCc0UFoZHwLEJ+dpDPSUi5TgwwlTlX47w+iFRrEwzUZwYACjq83cgjS/Da50Ga37uw==}
+
tslib@2.6.3:
resolution: {integrity: sha512-xNvxJEOUiWPGhUuUdQgAJPKOOJfGnIyKySOc09XkKsgdUV/3E2zvwZYdejjmRgPCgcym1juLH3226yA7sEFJKQ==}
@@ -2053,6 +2168,9 @@ packages:
engines: {node: '>=14.17'}
hasBin: true
+ undici-types@5.26.5:
+ resolution: {integrity: sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==}
+
undici-types@6.19.8:
resolution: {integrity: sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==}
@@ -2702,18 +2820,35 @@ snapshots:
transitivePeerDependencies:
- encoding
+ '@ts-morph/common@0.25.0':
+ dependencies:
+ minimatch: 9.0.5
+ path-browserify: 1.0.1
+ tinyglobby: 0.2.10
+
'@types/estree@1.0.5': {}
'@types/gitignore-parser@0.0.3': {}
'@types/node@12.20.55': {}
+ '@types/node@18.19.64':
+ dependencies:
+ undici-types: 5.26.5
+
'@types/node@22.5.4':
dependencies:
undici-types: 6.19.8
+ '@types/prompts@2.4.9':
+ dependencies:
+ '@types/node': 18.19.64
+ kleur: 3.0.3
+
'@types/resolve@1.20.2': {}
+ '@types/semver@7.5.8': {}
+
'@types/tar-fs@2.0.4':
dependencies:
'@types/node': 22.5.4
@@ -2969,6 +3104,16 @@ snapshots:
ci-info@3.9.0: {}
+ code-block-writer@13.0.3: {}
+
+ code-red@1.0.4:
+ dependencies:
+ '@jridgewell/sourcemap-codec': 1.5.0
+ '@types/estree': 1.0.5
+ acorn: 8.12.1
+ estree-walker: 3.0.3
+ periscopic: 3.1.0
+
color-convert@2.0.1:
dependencies:
color-name: 1.1.4
@@ -2995,6 +3140,11 @@ snapshots:
shebang-command: 2.0.0
which: 2.0.2
+ css-tree@2.3.1:
+ dependencies:
+ mdn-data: 2.0.30
+ source-map-js: 1.2.1
+
cssesc@3.0.0: {}
dataloader@1.4.0: {}
@@ -3276,6 +3426,10 @@ snapshots:
dependencies:
reusify: 1.0.4
+ fdir@6.4.2(picomatch@4.0.2):
+ optionalDependencies:
+ picomatch: 4.0.2
+
fflate@0.8.2: {}
file-entry-cache@8.0.0:
@@ -3413,6 +3567,8 @@ snapshots:
parent-module: 1.0.1
resolve-from: 4.0.0
+ import-meta-resolve@4.1.0: {}
+
imurmurhash@0.1.4: {}
is-builtin-module@3.2.1:
@@ -3488,6 +3644,10 @@ snapshots:
dependencies:
json-buffer: 3.0.1
+ kleur@3.0.3: {}
+
+ kleur@4.1.5: {}
+
known-css-properties@0.34.0: {}
levn@0.4.1:
@@ -3532,6 +3692,8 @@ snapshots:
dependencies:
'@jridgewell/sourcemap-codec': 1.5.0
+ mdn-data@2.0.30: {}
+
merge-stream@2.0.0: {}
merge2@1.4.1: {}
@@ -3643,6 +3805,8 @@ snapshots:
dependencies:
callsites: 3.1.0
+ path-browserify@1.0.1: {}
+
path-exists@4.0.0: {}
path-key@3.1.1: {}
@@ -3662,10 +3826,18 @@ snapshots:
pathval@2.0.0: {}
+ periscopic@3.1.0:
+ dependencies:
+ '@types/estree': 1.0.5
+ estree-walker: 3.0.3
+ is-reference: 3.0.2
+
picocolors@1.1.0: {}
picomatch@2.3.1: {}
+ picomatch@4.0.2: {}
+
pify@4.0.1: {}
pirates@4.0.6: {}
@@ -3714,6 +3886,11 @@ snapshots:
prettier@3.3.3: {}
+ prompts@2.4.2:
+ dependencies:
+ kleur: 3.0.3
+ sisteransi: 1.0.5
+
pseudomap@1.0.2: {}
pump@3.0.2:
@@ -3926,6 +4103,23 @@ snapshots:
optionalDependencies:
svelte: 5.0.0
+ svelte@4.2.19:
+ dependencies:
+ '@ampproject/remapping': 2.3.0
+ '@jridgewell/sourcemap-codec': 1.5.0
+ '@jridgewell/trace-mapping': 0.3.25
+ '@types/estree': 1.0.5
+ acorn: 8.12.1
+ aria-query: 5.3.2
+ axobject-query: 4.1.0
+ code-red: 1.0.4
+ css-tree: 2.3.1
+ estree-walker: 3.0.3
+ is-reference: 3.0.2
+ locate-character: 3.0.0
+ magic-string: 0.30.12
+ periscopic: 3.1.0
+
svelte@5.0.0:
dependencies:
'@ampproject/remapping': 2.3.0
@@ -3990,6 +4184,11 @@ snapshots:
tinyexec@0.3.0: {}
+ tinyglobby@0.2.10:
+ dependencies:
+ fdir: 6.4.2(picomatch@4.0.2)
+ picomatch: 4.0.2
+
tinypool@1.0.1: {}
tinyrainbow@1.2.0: {}
@@ -4016,6 +4215,11 @@ snapshots:
ts-interface-checker@0.1.13: {}
+ ts-morph@24.0.0:
+ dependencies:
+ '@ts-morph/common': 0.25.0
+ code-block-writer: 13.0.3
+
tslib@2.6.3: {}
type-check@0.4.0:
@@ -4035,6 +4239,8 @@ snapshots:
typescript@5.6.2: {}
+ undici-types@5.26.5: {}
+
undici-types@6.19.8: {}
universalify@0.1.2: {}
@@ -4068,6 +4274,24 @@ snapshots:
optionalDependencies:
typescript: 5.6.2
+ vite-node@2.0.5(@types/node@18.19.64):
+ dependencies:
+ cac: 6.7.14
+ debug: 4.3.7
+ pathe: 1.1.2
+ tinyrainbow: 1.2.0
+ vite: 5.4.3(@types/node@18.19.64)
+ transitivePeerDependencies:
+ - '@types/node'
+ - less
+ - lightningcss
+ - sass
+ - sass-embedded
+ - stylus
+ - sugarss
+ - supports-color
+ - terser
+
vite-node@2.0.5(@types/node@22.5.4):
dependencies:
cac: 6.7.14
@@ -4086,6 +4310,15 @@ snapshots:
- supports-color
- terser
+ vite@5.4.3(@types/node@18.19.64):
+ dependencies:
+ esbuild: 0.21.5
+ postcss: 8.4.45
+ rollup: 4.21.2
+ optionalDependencies:
+ '@types/node': 18.19.64
+ fsevents: 2.3.3
+
vite@5.4.3(@types/node@22.5.4):
dependencies:
esbuild: 0.21.5
@@ -4095,6 +4328,40 @@ snapshots:
'@types/node': 22.5.4
fsevents: 2.3.3
+ vitest@2.0.5(@types/node@18.19.64)(@vitest/ui@2.0.5):
+ dependencies:
+ '@ampproject/remapping': 2.3.0
+ '@vitest/expect': 2.0.5
+ '@vitest/pretty-format': 2.0.5
+ '@vitest/runner': 2.0.5
+ '@vitest/snapshot': 2.0.5
+ '@vitest/spy': 2.0.5
+ '@vitest/utils': 2.0.5
+ chai: 5.1.1
+ debug: 4.3.7
+ execa: 8.0.1
+ magic-string: 0.30.12
+ pathe: 1.1.2
+ std-env: 3.7.0
+ tinybench: 2.9.0
+ tinypool: 1.0.1
+ tinyrainbow: 1.2.0
+ vite: 5.4.3(@types/node@18.19.64)
+ vite-node: 2.0.5(@types/node@18.19.64)
+ why-is-node-running: 2.3.0
+ optionalDependencies:
+ '@types/node': 18.19.64
+ '@vitest/ui': 2.0.5(vitest@2.0.5)
+ transitivePeerDependencies:
+ - less
+ - lightningcss
+ - sass
+ - sass-embedded
+ - stylus
+ - sugarss
+ - supports-color
+ - terser
+
vitest@2.0.5(@types/node@22.5.4)(@vitest/ui@2.0.5):
dependencies:
'@ampproject/remapping': 2.3.0
From e042c3df9e5b024a095c18633c444f014fed0c13 Mon Sep 17 00:00:00 2001
From: Ben McCann <322311+benmccann@users.noreply.github.com>
Date: Thu, 7 Nov 2024 11:36:49 -0800
Subject: [PATCH 2/6] chore: update package.json
---
packages/migrate/package.json | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/packages/migrate/package.json b/packages/migrate/package.json
index be1a7167..f9db2315 100644
--- a/packages/migrate/package.json
+++ b/packages/migrate/package.json
@@ -11,7 +11,7 @@
],
"repository": {
"type": "git",
- "url": "https://github.com/sveltejs/kit",
+ "url": "https://github.com/sveltejs/cli",
"directory": "packages/migrate"
},
"license": "MIT",
From 7fa63cebb1bf3b869ff1c9456d8ab3494a019c21 Mon Sep 17 00:00:00 2001
From: Ben McCann <322311+benmccann@users.noreply.github.com>
Date: Thu, 7 Nov 2024 11:43:57 -0800
Subject: [PATCH 3/6] additional config
---
.changeset/config.json | 2 +-
README.md | 7 ++++---
prettier.config.js | 5 ++++-
3 files changed, 9 insertions(+), 5 deletions(-)
diff --git a/.changeset/config.json b/.changeset/config.json
index addc29ba..01a284bb 100644
--- a/.changeset/config.json
+++ b/.changeset/config.json
@@ -7,5 +7,5 @@
"access": "public",
"baseBranch": "main",
"updateInternalDependencies": "patch",
- "ignore": [ "!sv" ]
+ "ignore": [ "!(sv|svelte-migrate)" ]
}
diff --git a/README.md b/README.md
index 5342c685..09a32e17 100644
--- a/README.md
+++ b/README.md
@@ -6,9 +6,10 @@ Read the [SvelteKit documentation](https://svelte.dev/docs/kit) for more details
### Packages
-| Package | Changelog |
-| ------------------ | -------------------------------------- |
-| [sv](packages/cli) | [Changelog](packages/cli/CHANGELOG.md) |
+| Package | Changelog |
+| ---------------------------------- | ------------------------------------------ |
+| [sv](packages/cli) | [Changelog](packages/cli/CHANGELOG.md) |
+| [svelte-migrate](packages/migrate) | [Changelog](packages/migrate/CHANGELOG.md) |
## Contributing
diff --git a/prettier.config.js b/prettier.config.js
index b12745ea..34cb78e9 100644
--- a/prettier.config.js
+++ b/prettier.config.js
@@ -65,7 +65,10 @@ export default {
}
},
{
- files: ['**/CHANGELOG.md'],
+ files: [
+ '**/CHANGELOG.md',
+ "packages/migrate/migrations/routes/*/samples.md"
+ ],
options: {
requirePragma: true
}
From 1434d2a789b9d2dd391a3f76f8cd9a40fa27440f Mon Sep 17 00:00:00 2001
From: Ben McCann <322311+benmccann@users.noreply.github.com>
Date: Thu, 7 Nov 2024 11:44:02 -0800
Subject: [PATCH 4/6] changeset
---
.changeset/thin-eggs-judge.md | 5 +++++
1 file changed, 5 insertions(+)
create mode 100644 .changeset/thin-eggs-judge.md
diff --git a/.changeset/thin-eggs-judge.md b/.changeset/thin-eggs-judge.md
new file mode 100644
index 00000000..02455063
--- /dev/null
+++ b/.changeset/thin-eggs-judge.md
@@ -0,0 +1,5 @@
+---
+'svelte-migrate': patch
+---
+
+chore: update repository information
From 0d09892b3afc10d77bfb4e05a46acd3d22215108 Mon Sep 17 00:00:00 2001
From: Ben McCann <322311+benmccann@users.noreply.github.com>
Date: Thu, 7 Nov 2024 11:46:29 -0800
Subject: [PATCH 5/6] sort package.json
---
packages/migrate/package.json | 34 +++++++++++++++++-----------------
1 file changed, 17 insertions(+), 17 deletions(-)
diff --git a/packages/migrate/package.json b/packages/migrate/package.json
index f9db2315..83253197 100644
--- a/packages/migrate/package.json
+++ b/packages/migrate/package.json
@@ -1,24 +1,20 @@
{
"name": "svelte-migrate",
"version": "1.6.8",
+ "type": "module",
"description": "A CLI for migrating Svelte(Kit) codebases",
- "keywords": [
- "migration",
- "upgrade",
- "svelte",
- "sveltekit",
- "tool"
- ],
+ "license": "MIT",
"repository": {
"type": "git",
"url": "https://github.com/sveltejs/cli",
"directory": "packages/migrate"
},
- "license": "MIT",
"homepage": "https://svelte.dev",
- "type": "module",
- "bin": {
- "svelte-migrate": "./bin.js"
+ "scripts": {
+ "check": "tsc",
+ "format": "pnpm lint --write",
+ "lint": "prettier --check .",
+ "test": "vitest run --silent"
},
"files": [
"bin.js",
@@ -27,6 +23,9 @@
"!migrations/**/*.spec.js",
"!migrations/**/samples.md"
],
+ "bin": {
+ "svelte-migrate": "./bin.js"
+ },
"dependencies": {
"import-meta-resolve": "^4.1.0",
"kleur": "^4.1.5",
@@ -45,10 +44,11 @@
"svelte": "^4.2.10",
"vitest": "^2.0.1"
},
- "scripts": {
- "test": "vitest run --silent",
- "check": "tsc",
- "lint": "prettier --check .",
- "format": "pnpm lint --write"
- }
+ "keywords": [
+ "migration",
+ "upgrade",
+ "svelte",
+ "sveltekit",
+ "tool"
+ ]
}
From f3e865a5968a36aa14a44fa82c905db63a0c0272 Mon Sep 17 00:00:00 2001
From: Ben McCann <322311+benmccann@users.noreply.github.com>
Date: Thu, 7 Nov 2024 11:49:17 -0800
Subject: [PATCH 6/6] format
---
prettier.config.js | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/prettier.config.js b/prettier.config.js
index 34cb78e9..1a3d9c70 100644
--- a/prettier.config.js
+++ b/prettier.config.js
@@ -67,7 +67,7 @@ export default {
{
files: [
'**/CHANGELOG.md',
- "packages/migrate/migrations/routes/*/samples.md"
+ 'packages/migrate/migrations/routes/*/samples.md'
],
options: {
requirePragma: true