-
-
Notifications
You must be signed in to change notification settings - Fork 1.9k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: add Svelte 5 migration (#12519)
- Loading branch information
1 parent
e9ed772
commit 05624c3
Showing
5 changed files
with
463 additions
and
5 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
--- | ||
"svelte-migrate": minor | ||
--- | ||
|
||
feat: add Svelte 5 migration |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,187 @@ | ||
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 { 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. We recommend running the `svelte-4` migration first (`npx svelte-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 `svelte-5` migration?\n') | ||
); | ||
const response = await prompts({ | ||
type: 'confirm', | ||
name: 'value', | ||
message: 'Continue?', | ||
initial: false | ||
}); | ||
if (!response.value) { | ||
process.exit(1); | ||
} | ||
} | ||
} | ||
|
||
let migrate; | ||
try { | ||
try { | ||
({ migrate } = await import_from_cwd('svelte/compiler')); | ||
if (!migrate) throw new Error('found Svelte 4'); | ||
} catch { | ||
// TODO replace with svelte@5 once it's released | ||
execSync('npm install svelte@next --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 { 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) | ||
); | ||
} 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); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,134 @@ | ||
import fs from 'node:fs'; | ||
import { Project, ts, Node } from 'ts-morph'; | ||
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.6'], | ||
['@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.43.0'], | ||
[ | ||
'eslint-plugin-svelte3', | ||
'^4.0.0', | ||
' (this package is deprecated, use eslint-plugin-svelte instead. More info: https://svelte.dev/docs/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: code) => { code: string }} transform_code | ||
*/ | ||
export function transform_svelte_code(code, transform_code) { | ||
return transform_code(code).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(); | ||
} | ||
|
||
if (source.getImportDeclaration('svelte')) { | ||
source.getImportDeclaration('svelte')?.addNamedImport(method); | ||
} else { | ||
source.addImportDeclaration({ | ||
moduleSpecifier: 'svelte', | ||
namedImports: [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()})`); | ||
source.getImportDeclaration('svelte')?.addNamedImport('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); | ||
} |
Oops, something went wrong.