-
-
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.
* skeleton * add moved info to files to be able to rewrite imports later * add magic-string, start load extraction and manipulation logic * move migration logic into new svelte-migrate package * remove commented code * lockfile * move components, extract load * +page.server.js and +server.js * remove unused code * uncomment prompt * add some errors * fix detection * consistent casing * not sure if typescript will ever fail to parse, but in any case its handled now * handle error with module context * add some comments * fix dynamic import on windows, fix js/ts file ending, handle index.json.js endpoint case * extract imports into Svelte migration task for QOL, don't adjust imports in standalone index edge case * show message either way, could be other things gone wrong * windooows * more sophisticated auto migrations - unpack props/body - redirect/error in load - standalone endpoint * fix regex, dont show props message on error page, more sophisticated error/redirect checks, set-cookie suggestion * pretend to make this more readable * adjust import location for error and redirect * only show short automigration message on success, only migrate where we are confident it works, annotate non-migrated return statements, handle export { X } case * shorten redirect/error if no second argument given * use git mv if available * remove unused dep * determine whether or not to use git upfront * add recommended next steps * tweak * i think we can omit the comment for successful automigrations * move code into separate modules, to make stuff easier to test * add the beginnings of a test suite * fix read_samples * only add page level migration task if something went wrong * tweak migrate_page function, add some tests * ugh * only inject error when necessary * add migrate_page_server test file * preserve semicolons * refactor a bit, allow +page.server.js to get away without any migration task errors if all goes well * oops * handle arrow functions * add test file for +server.js, only print file-level warning if we give up * remove some no-longer-used code * DRY out * small cosmetic tweak * simplify * make check case-insensitive * more tests * fix test * refactor slightly * handle some more cases * handle case where status: 200 is returned from GET * move error where it belongs * all tests passing * reuse keys * fix * fix 400 error status bug, support throw statement on arrow function, support server arrow function migration, extract migration into own file and add tests * add is_new helper, handle more cases * leave new Response alone * handle some more Response cases * fix commenting logic, handle multiline headers * oops * move some logic * remove module context altogether if safe, leave error if not * remove some no-longer-used code * oops Co-authored-by: Simon Holthausen <simon.holthausen@vercel.com>
- Loading branch information
1 parent
69deeb7
commit 743f1c0
Showing
20 changed files
with
1,805 additions
and
2 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
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.