-
-
Notifications
You must be signed in to change notification settings - Fork 1.9k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
feat: add Svelte 5 migration #12519
Merged
Merged
feat: add Svelte 5 migration #12519
Changes from 4 commits
Commits
Show all changes
20 commits
Select commit
Hold shift + click to select a range
b00cc22
feat: add Svelte 5 migration
dummdidumm 9521438
lint
dummdidumm efe0075
fixes
dummdidumm 997862a
remove error handling to let outer utility method handle it more grac…
dummdidumm 3abcb9b
Merge branch 'main' into svelte-5-migration
dummdidumm eea7b15
preserve eol
dummdidumm 6a074ee
use `@next` for now
dummdidumm a9b22c9
Update packages/migrate/migrations/svelte-5/migrate.js
dummdidumm 66c11ce
Update packages/migrate/migrations/svelte-5/migrate.js
dummdidumm 506285e
Update packages/migrate/migrations/svelte-5/migrate.js
dummdidumm 5d1d2d3
format
benmccann 53e5b49
reuse component instantiation migration
dummdidumm ebc22f1
migrate component.$destroy() calls
dummdidumm fabb4f7
lint
dummdidumm 4dc9f6b
remove commented-out code
dummdidumm 87c8cf6
fix
dummdidumm 05c4437
Merge branch 'main' into svelte-5-migration
dummdidumm e0cf4fa
note experimental, note dependencies, link to functioning site
dummdidumm 4e5253e
better error handling
benmccann b9e7c6f
format
benmccann File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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,175 @@ | ||
import { resolve } from 'import-meta-resolve'; | ||
import colors from 'kleur'; | ||
import { execSync } from 'node:child_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 } 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'); | ||
} | ||
|
||
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 { | ||
execSync('npm install svelte@5 --no-save', { | ||
dummdidumm marked this conversation as resolved.
Show resolved
Hide resolved
|
||
stdio: 'inherit', | ||
cwd: dirname(fileURLToPath(import.meta.url)) | ||
benmccann marked this conversation as resolved.
Show resolved
Hide resolved
|
||
}); | ||
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_js_file(file, (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 = [ | ||
use_git && cyan('git commit -m "migration to Svelte 5"'), | ||
'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,122 @@ | ||
import fs from 'node:fs'; | ||
import { Project, ts, Node } from 'ts-morph'; | ||
import { 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, [ | ||
dummdidumm marked this conversation as resolved.
Show resolved
Hide resolved
|
||
['svelte', '^5.0.0'], | ||
['svelte-check', '^4.0.0'], | ||
['svelte-preprocess', '^6.0.0'], | ||
['@sveltejs/kit', '^2.5.18'], | ||
dummdidumm marked this conversation as resolved.
Show resolved
Hide resolved
|
||
['@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)' | ||
] | ||
dummdidumm marked this conversation as resolved.
Show resolved
Hide resolved
|
||
]); | ||
} | ||
|
||
/** | ||
* @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 logger = log_on_ts_modification( | ||
source, | ||
'Updates component instantiation: https://svelte.dev/docs/svelte/TODO (you may need to update usages of the component instance)' | ||
); | ||
|
||
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] | ||
}); | ||
} | ||
|
||
parent.replaceWithText(`${method}(${id.getText()}, ${args[0].getText()})`); | ||
} | ||
} | ||
} | ||
} | ||
|
||
logger(); | ||
} | ||
|
||
/** | ||
* @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); | ||
} |
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,75 @@ | ||
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('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" | ||
}, | ||
"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" | ||
}, | ||
"dependencies": { | ||
"@sveltejs/kit": "^2.5.18" | ||
} | ||
}` | ||
); | ||
}); |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
you sure they're not running svelte 1 or 2? 🤣
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
what-year-is-it.jpg