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 `${body}${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 `${contents}${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, `>`); + } + }); + } + + 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( + /([^]+?)<\/script>(\n*)/g, + (_match, attrs, contents, whitespace) => { + return `${transform_script_code( + contents, + (attrs.includes('lang=') || attrs.includes('type=')) && + (attrs.includes('ts') || attrs.includes('typescript')), + file_path + )}${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