UNPKG

svelte-migrate

Version:

A CLI for migrating Svelte(Kit) codebases

319 lines (280 loc) 9.34 kB
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); }