-
-
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
Migration script #5773
Merged
Merged
Migration script #5773
Changes from all commits
Commits
Show all changes
71 commits
Select commit
Hold shift + click to select a range
47076c1
skeleton
dummdidumm 5b49486
add moved info to files to be able to rewrite imports later
dummdidumm 9f83032
add magic-string, start load extraction and manipulation logic
dummdidumm c10febb
Merge branch 'master' into migration-script
Rich-Harris 5410a5b
Merge branch 'master' into migration-script
Rich-Harris ccb5059
move migration logic into new svelte-migrate package
Rich-Harris 4387b2c
remove commented code
Rich-Harris 08906cc
lockfile
Rich-Harris dd7fdd4
move components, extract load
Rich-Harris 07ca11f
+page.server.js and +server.js
Rich-Harris 836227b
remove unused code
Rich-Harris 4c1334a
uncomment prompt
Rich-Harris 2243175
add some errors
Rich-Harris f08c6cd
fix detection
Rich-Harris 3a119b5
consistent casing
Rich-Harris f3a2d7c
not sure if typescript will ever fail to parse, but in any case its h…
Rich-Harris d9b988f
handle error with module context
Rich-Harris c3a2987
add some comments
Rich-Harris 16b312c
fix dynamic import on windows, fix js/ts file ending, handle index.js…
dummdidumm 02ab096
extract imports into Svelte migration task for QOL, don't adjust impo…
dummdidumm 2d01f30
show message either way, could be other things gone wrong
dummdidumm 4f88ead
windooows
dummdidumm 8ccb1f9
more sophisticated auto migrations
dummdidumm 6c9489d
fix regex, dont show props message on error page, more sophisticated …
dummdidumm d69ea50
pretend to make this more readable
dummdidumm 305caee
adjust import location for error and redirect
dummdidumm 9ad6691
only show short automigration message on success, only migrate where …
dummdidumm 5e37428
shorten redirect/error if no second argument given
dummdidumm de773cc
use git mv if available
dummdidumm 426e3ba
remove unused dep
Rich-Harris b4f0739
determine whether or not to use git upfront
Rich-Harris 81b80e0
add recommended next steps
Rich-Harris 2439151
tweak
Rich-Harris 4b495c2
i think we can omit the comment for successful automigrations
Rich-Harris ec53495
move code into separate modules, to make stuff easier to test
Rich-Harris 4076a2f
add the beginnings of a test suite
Rich-Harris 5fcb591
fix read_samples
Rich-Harris 0e282e0
only add page level migration task if something went wrong
Rich-Harris 21ef89f
tweak migrate_page function, add some tests
Rich-Harris 598a3f4
ugh
Rich-Harris 4f27cac
only inject error when necessary
Rich-Harris f6594a4
add migrate_page_server test file
Rich-Harris e0c5b6d
preserve semicolons
Rich-Harris f745a7e
refactor a bit, allow +page.server.js to get away without any migrati…
Rich-Harris 88e6a1f
oops
Rich-Harris f0dd49c
handle arrow functions
Rich-Harris bba4b3c
add test file for +server.js, only print file-level warning if we giv…
Rich-Harris 2f693ad
remove some no-longer-used code
Rich-Harris 078afd0
DRY out
Rich-Harris 0a8ec74
small cosmetic tweak
Rich-Harris 246157e
simplify
Rich-Harris 531ff9c
make check case-insensitive
Rich-Harris 2d9c096
more tests
Rich-Harris 29710cc
fix test
Rich-Harris 4afd5d5
refactor slightly
Rich-Harris 0ff7e0c
handle some more cases
Rich-Harris 1d3cb70
handle case where status: 200 is returned from GET
Rich-Harris dfbb031
move error where it belongs
Rich-Harris 075a2ee
all tests passing
Rich-Harris 4215e05
reuse keys
Rich-Harris c779ec2
fix
Rich-Harris 958083a
fix 400 error status bug, support throw statement on arrow function, …
dummdidumm fd10dc4
add is_new helper, handle more cases
Rich-Harris 7979d96
leave new Response alone
Rich-Harris 3af836d
handle some more Response cases
Rich-Harris 6c1fb8d
fix commenting logic, handle multiline headers
Rich-Harris 3b70d7c
oops
Rich-Harris a7b5acc
move some logic
Rich-Harris 60f4478
remove module context altogether if safe, leave error if not
Rich-Harris ed6c534
remove some no-longer-used code
Rich-Harris 813c9ed
oops
Rich-Harris 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
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,20 @@ | ||
#!/usr/bin/env node | ||
import fs from 'fs'; | ||
import { fileURLToPath } from 'url'; | ||
import colors from 'kleur'; | ||
|
||
const migration = process.argv[2]; | ||
const dir = fileURLToPath(new URL('.', import.meta.url)); | ||
|
||
const migrations = fs | ||
.readdirSync(`${dir}/migrations`) | ||
.filter((migration) => fs.existsSync(`${dir}/migrations/${migration}/index.js`)); | ||
|
||
if (migrations.includes(migration)) { | ||
const { migrate } = await import(`./migrations/${migration}/index.js`); | ||
migrate(); | ||
} else { | ||
console.error( | ||
colors.bold().red(`You must specify one of the following migrations: ${migrations.join(', ')}`) | ||
); | ||
} |
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,237 @@ | ||
import { execSync } from 'child_process'; | ||
import fs from 'fs'; | ||
import colors from 'kleur'; | ||
import path from 'path'; | ||
import prompts from 'prompts'; | ||
import glob from 'tiny-glob/sync.js'; | ||
import { pathToFileURL } from 'url'; | ||
import { migrate_scripts } from './migrate_scripts/index.js'; | ||
import { migrate_page } from './migrate_page_js/index.js'; | ||
import { migrate_page_server } from './migrate_page_server/index.js'; | ||
import { migrate_server } from './migrate_server/index.js'; | ||
import { adjust_imports, bail, dedent, move_file, relative, task } from './utils.js'; | ||
|
||
export async function migrate() { | ||
if (!fs.existsSync('svelte.config.js')) { | ||
bail('Please re-run this script in a directory with a svelte.config.js'); | ||
} | ||
|
||
const { default: config } = await import(pathToFileURL(path.resolve('svelte.config.js')).href); | ||
|
||
const routes = path.resolve(config.kit?.files?.routes ?? 'src/routes'); | ||
|
||
/** @type {string[]} */ | ||
const extensions = config.extensions ?? ['.svelte']; | ||
|
||
/** @type {string[]} */ | ||
const module_extensions = config.kit?.moduleExtensions ?? ['.js', '.ts']; | ||
|
||
/** @type {((filepath: string) => boolean)} */ | ||
const filter = | ||
config.kit?.routes ?? | ||
((filepath) => !/(?:(?:^_|\/_)|(?:^\.|\/\.)(?!well-known))/.test(filepath)); | ||
|
||
const files = glob(`${routes}/**`, { filesOnly: true }).map((file) => file.replace(/\\/g, '/')); | ||
|
||
// validate before proceeding | ||
for (const file of files) { | ||
const basename = path.basename(file); | ||
if ( | ||
basename.startsWith('+page.') || | ||
basename.startsWith('+layout.') || | ||
basename.startsWith('+server.') || | ||
basename.startsWith('+error.') | ||
) { | ||
bail(`It looks like this migration has already been run (found ${relative(file)}). Aborting`); | ||
} | ||
|
||
if (basename.startsWith('+')) { | ||
// prettier-ignore | ||
bail( | ||
`Please rename any files in ${relative(routes)} with a leading + character before running this migration (found ${relative(file)}). Aborting` | ||
); | ||
} | ||
} | ||
|
||
console.log(colors.bold().yellow('\nThis will overwrite files in the current directory!\n')); | ||
|
||
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; | ||
} | ||
} | ||
|
||
const response = await prompts({ | ||
type: 'confirm', | ||
name: 'value', | ||
message: 'Continue?', | ||
initial: false | ||
}); | ||
|
||
if (!response.value) { | ||
process.exit(1); | ||
} | ||
|
||
for (const file of files) { | ||
const basename = path.basename(file); | ||
if (!filter(file) && !basename.startsWith('__')) continue; | ||
|
||
// replace `./__types` or `./__types/foo` with `./$types` | ||
const content = fs.readFileSync(file, 'utf8').replace(/\.\/__types(?:\/[^'"]+)?/g, './$types'); | ||
|
||
const svelte_ext = extensions.find((ext) => file.endsWith(ext)); | ||
const module_ext = module_extensions.find((ext) => file.endsWith(ext)); | ||
|
||
if (svelte_ext) { | ||
// file is a component | ||
const bare = basename.slice(0, -svelte_ext.length); | ||
const [name, layout] = bare.split('@'); | ||
const is_error_page = bare === '__error'; | ||
|
||
/** | ||
* Whether file should be moved to a subdirectory — e.g. `src/routes/about.svelte` | ||
* should become `src/routes/about/+page.svelte` | ||
*/ | ||
let move_to_directory = false; | ||
|
||
/** | ||
* The new name of the file | ||
*/ | ||
let renamed = file.slice(0, -basename.length); | ||
|
||
/** | ||
* If a component has `<script context="module">`, the contents are moved | ||
* into a sibling module with the same name | ||
*/ | ||
let sibling; | ||
|
||
if (bare.startsWith('__layout')) { | ||
sibling = renamed + '+layout'; | ||
renamed += '+' + bare.slice(2); // account for __layout-foo etc | ||
} else if (is_error_page) { | ||
renamed += '+error'; | ||
// no sibling, because error files can no longer have load | ||
} else if (name === 'index') { | ||
sibling = renamed + '+page'; | ||
renamed += '+page' + (layout ? '@' + layout : ''); | ||
} else { | ||
sibling = `${renamed}${name}/+page`; | ||
renamed += `${name}/+page${layout ? '@' + layout : ''}`; | ||
|
||
move_to_directory = true; | ||
} | ||
|
||
renamed += svelte_ext; | ||
|
||
const { module, main } = migrate_scripts(content, is_error_page, move_to_directory); | ||
|
||
if (move_to_directory) { | ||
const dir = path.dirname(renamed); | ||
if (!fs.existsSync(dir)) fs.mkdirSync(dir); | ||
} | ||
|
||
move_file(file, renamed, main, use_git); | ||
|
||
// if component has a <script context="module">, move it to a sibling .js file | ||
if (module) { | ||
const ext = /<script[^>]+?lang=['"](ts|typescript)['"][^]*?>/.test(module) ? '.ts' : '.js'; | ||
|
||
fs.writeFileSync(sibling + ext, migrate_page(module)); | ||
} | ||
} else if (module_ext) { | ||
// file is a module | ||
const bare = basename.slice(0, -module_ext.length); | ||
const [name] = bare.split('@'); | ||
|
||
/** | ||
* Whether the file is paired with a page component, and should | ||
* therefore become `+page.server.js`, or not in which case | ||
* it should become `+server.js` | ||
*/ | ||
const is_page_endpoint = extensions.some((ext) => | ||
files.includes(`${file.slice(0, -module_ext.length)}${ext}`) | ||
); | ||
|
||
const type = is_page_endpoint ? '+page.server' : '+server'; | ||
|
||
const move_to_directory = name !== 'index'; | ||
const is_standalone_index = !is_page_endpoint && name.startsWith('index.'); | ||
|
||
let renamed = ''; | ||
if (is_standalone_index) { | ||
// handle <folder>/index.json.js -> <folder>.json/+server.js | ||
const dir = path.dirname(file); | ||
renamed = | ||
// prettier-ignore | ||
`${file.slice(0, -(basename.length + dir.length + 1))}${dir + name.slice('index'.length)}/+server${module_ext}`; | ||
} else if (move_to_directory) { | ||
renamed = `${file.slice(0, -basename.length)}${name}/${type}${module_ext}`; | ||
} else { | ||
renamed = `${file.slice(0, -basename.length)}${type}${module_ext}`; | ||
} | ||
|
||
// Standalone index endpoints are edge case enough that we don't spend time on trying to update all the imports correctly | ||
const edited = | ||
(is_standalone_index && /import/.test(content) ? `\n// ${task('Check imports')}\n` : '') + | ||
(!is_standalone_index && move_to_directory ? adjust_imports(content) : content); | ||
if (move_to_directory) { | ||
const dir = path.dirname(renamed); | ||
if (!fs.existsSync(dir)) fs.mkdirSync(dir); | ||
} | ||
|
||
move_file( | ||
file, | ||
renamed, | ||
is_page_endpoint ? migrate_page_server(edited) : migrate_server(edited), | ||
use_git | ||
); | ||
} | ||
} | ||
|
||
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 "svelte-migrate: renamed files"'), | ||
`Review the migration guide at https://github.com/sveltejs/kit/discussions/5774`, | ||
`Search codebase for ${cyan('"@migration"')} and manually complete migration tasks`, | ||
use_git && cyan('git add -A'), | ||
use_git && cyan('git commit -m "svelte-migrate: updated files"') | ||
].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`); | ||
} | ||
} |
104 changes: 104 additions & 0 deletions
104
packages/migrate/migrations/routes/migrate_page_js/index.js
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,104 @@ | ||
import ts from 'typescript'; | ||
import { | ||
automigration, | ||
dedent, | ||
error, | ||
get_function_node, | ||
get_object_nodes, | ||
is_new, | ||
is_string_like, | ||
manual_return_migration, | ||
parse, | ||
rewrite_returns | ||
} from '../utils.js'; | ||
import * as TASKS from '../tasks.js'; | ||
|
||
const give_up = `${error('Update load function', TASKS.PAGE_LOAD)}\n\n`; | ||
|
||
/** @param {string} content */ | ||
export function migrate_page(content) { | ||
// early out if we can tell there's no load function | ||
// without parsing the file | ||
if (!/load/.test(content)) return content; | ||
|
||
const file = parse(content); | ||
if (!file) return give_up + content; | ||
|
||
if (!file.exports.map.has('load') && !file.exports.complex) { | ||
// there's no load function here, so there's nothing to do | ||
return content; | ||
} | ||
|
||
const name = file.exports.map.get('load'); | ||
|
||
for (const statement of file.ast.statements) { | ||
const fn = get_function_node(statement, name); | ||
if (fn) { | ||
/** @type {Set<string>} */ | ||
const imports = new Set(); | ||
|
||
rewrite_returns(fn.body, (expr, node) => { | ||
const nodes = ts.isObjectLiteralExpression(expr) && get_object_nodes(expr); | ||
|
||
if (nodes) { | ||
const keys = Object.keys(nodes).sort().join(' '); | ||
|
||
if (keys === 'props') { | ||
automigration(expr, file.code, dedent(nodes.props.getText())); | ||
return; | ||
} | ||
|
||
// check for existence of `node`, otherwise it's an arrow function | ||
// with an implicit body, which we bail out on | ||
if (node) { | ||
const status = nodes.status && Number(nodes.status.getText()); | ||
|
||
// logic based on https://github.com/sveltejs/kit/blob/67e2342149847d267eb0c50809a1f93f41fa529b/packages/kit/src/runtime/load.js | ||
if (keys === 'redirect status' && status > 300 && status < 400) { | ||
automigration( | ||
node, | ||
file.code, | ||
`throw redirect(${status}, ${nodes.redirect.getText()});` | ||
); | ||
imports.add('redirect'); | ||
return; | ||
} | ||
|
||
if (nodes.error) { | ||
const message = is_string_like(nodes.error) | ||
? nodes.error.getText() | ||
: is_new(nodes.error, 'Error') && nodes.error.arguments[0].getText(); | ||
|
||
if (message) { | ||
automigration(node, file.code, `throw error(${status || 500}, ${message});`); | ||
imports.add('error'); | ||
return; | ||
} | ||
} else if (status >= 400) { | ||
automigration(node, file.code, `throw error(${status});`); | ||
imports.add('error'); | ||
return; | ||
} | ||
} | ||
} | ||
|
||
manual_return_migration(node || fn, file.code, TASKS.PAGE_LOAD); | ||
}); | ||
|
||
if (imports.size) { | ||
const has_imports = file.ast.statements.some((statement) => | ||
ts.isImportDeclaration(statement) | ||
); | ||
const declaration = `import { ${[...imports.keys()].join(', ')} } from '@sveltejs/kit';`; | ||
|
||
return declaration + (has_imports ? '\n' : '\n\n') + file.code.toString(); | ||
} | ||
|
||
return file.code.toString(); | ||
} | ||
} | ||
|
||
// we failed to rewrite the load function, so we inject | ||
// an error at the top of the file | ||
return give_up + content; | ||
} |
13 changes: 13 additions & 0 deletions
13
packages/migrate/migrations/routes/migrate_page_js/index.spec.js
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,13 @@ | ||
import { read_samples } from '../utils.js'; | ||
import { test } from 'uvu'; | ||
import * as assert from 'uvu/assert'; | ||
import { migrate_page } from './index.js'; | ||
|
||
for (const sample of read_samples(import.meta.url)) { | ||
test(sample.description, () => { | ||
const actual = migrate_page(sample.before); | ||
assert.equal(actual, sample.after); | ||
}); | ||
} | ||
|
||
test.run(); |
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.
oh huh — i thought it would always return paths with
/
regardless of platform. i may have confused input with output https://github.com/terkelg/tiny-glob#windows