Skip to content
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 71 commits into from
Aug 4, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
71 commits
Select commit Hold shift + click to select a range
47076c1
skeleton
dummdidumm Jul 29, 2022
5b49486
add moved info to files to be able to rewrite imports later
dummdidumm Jul 29, 2022
9f83032
add magic-string, start load extraction and manipulation logic
dummdidumm Jul 29, 2022
c10febb
Merge branch 'master' into migration-script
Rich-Harris Jul 31, 2022
5410a5b
Merge branch 'master' into migration-script
Rich-Harris Jul 31, 2022
ccb5059
move migration logic into new svelte-migrate package
Rich-Harris Jul 31, 2022
4387b2c
remove commented code
Rich-Harris Jul 31, 2022
08906cc
lockfile
Rich-Harris Jul 31, 2022
dd7fdd4
move components, extract load
Rich-Harris Jul 31, 2022
07ca11f
+page.server.js and +server.js
Rich-Harris Jul 31, 2022
836227b
remove unused code
Rich-Harris Jul 31, 2022
4c1334a
uncomment prompt
Rich-Harris Jul 31, 2022
2243175
add some errors
Rich-Harris Jul 31, 2022
f08c6cd
fix detection
Rich-Harris Jul 31, 2022
3a119b5
consistent casing
Rich-Harris Jul 31, 2022
f3a2d7c
not sure if typescript will ever fail to parse, but in any case its h…
Rich-Harris Jul 31, 2022
d9b988f
handle error with module context
Rich-Harris Aug 1, 2022
c3a2987
add some comments
Rich-Harris Aug 1, 2022
16b312c
fix dynamic import on windows, fix js/ts file ending, handle index.js…
dummdidumm Aug 1, 2022
02ab096
extract imports into Svelte migration task for QOL, don't adjust impo…
dummdidumm Aug 1, 2022
2d01f30
show message either way, could be other things gone wrong
dummdidumm Aug 1, 2022
4f88ead
windooows
dummdidumm Aug 1, 2022
8ccb1f9
more sophisticated auto migrations
dummdidumm Aug 1, 2022
6c9489d
fix regex, dont show props message on error page, more sophisticated …
dummdidumm Aug 2, 2022
d69ea50
pretend to make this more readable
dummdidumm Aug 2, 2022
305caee
adjust import location for error and redirect
dummdidumm Aug 2, 2022
9ad6691
only show short automigration message on success, only migrate where …
dummdidumm Aug 2, 2022
5e37428
shorten redirect/error if no second argument given
dummdidumm Aug 2, 2022
de773cc
use git mv if available
dummdidumm Aug 2, 2022
426e3ba
remove unused dep
Rich-Harris Aug 3, 2022
b4f0739
determine whether or not to use git upfront
Rich-Harris Aug 3, 2022
81b80e0
add recommended next steps
Rich-Harris Aug 3, 2022
2439151
tweak
Rich-Harris Aug 3, 2022
4b495c2
i think we can omit the comment for successful automigrations
Rich-Harris Aug 3, 2022
ec53495
move code into separate modules, to make stuff easier to test
Rich-Harris Aug 3, 2022
4076a2f
add the beginnings of a test suite
Rich-Harris Aug 3, 2022
5fcb591
fix read_samples
Rich-Harris Aug 3, 2022
0e282e0
only add page level migration task if something went wrong
Rich-Harris Aug 3, 2022
21ef89f
tweak migrate_page function, add some tests
Rich-Harris Aug 4, 2022
598a3f4
ugh
Rich-Harris Aug 4, 2022
4f27cac
only inject error when necessary
Rich-Harris Aug 4, 2022
f6594a4
add migrate_page_server test file
Rich-Harris Aug 4, 2022
e0c5b6d
preserve semicolons
Rich-Harris Aug 4, 2022
f745a7e
refactor a bit, allow +page.server.js to get away without any migrati…
Rich-Harris Aug 4, 2022
88e6a1f
oops
Rich-Harris Aug 4, 2022
f0dd49c
handle arrow functions
Rich-Harris Aug 4, 2022
bba4b3c
add test file for +server.js, only print file-level warning if we giv…
Rich-Harris Aug 4, 2022
2f693ad
remove some no-longer-used code
Rich-Harris Aug 4, 2022
078afd0
DRY out
Rich-Harris Aug 4, 2022
0a8ec74
small cosmetic tweak
Rich-Harris Aug 4, 2022
246157e
simplify
Rich-Harris Aug 4, 2022
531ff9c
make check case-insensitive
Rich-Harris Aug 4, 2022
2d9c096
more tests
Rich-Harris Aug 4, 2022
29710cc
fix test
Rich-Harris Aug 4, 2022
4afd5d5
refactor slightly
Rich-Harris Aug 4, 2022
0ff7e0c
handle some more cases
Rich-Harris Aug 4, 2022
1d3cb70
handle case where status: 200 is returned from GET
Rich-Harris Aug 4, 2022
dfbb031
move error where it belongs
Rich-Harris Aug 4, 2022
075a2ee
all tests passing
Rich-Harris Aug 4, 2022
4215e05
reuse keys
Rich-Harris Aug 4, 2022
c779ec2
fix
Rich-Harris Aug 4, 2022
958083a
fix 400 error status bug, support throw statement on arrow function, …
dummdidumm Aug 4, 2022
fd10dc4
add is_new helper, handle more cases
Rich-Harris Aug 4, 2022
7979d96
leave new Response alone
Rich-Harris Aug 4, 2022
3af836d
handle some more Response cases
Rich-Harris Aug 4, 2022
6c1fb8d
fix commenting logic, handle multiline headers
Rich-Harris Aug 4, 2022
3b70d7c
oops
Rich-Harris Aug 4, 2022
a7b5acc
move some logic
Rich-Harris Aug 4, 2022
60f4478
remove module context altogether if safe, leave error if not
Rich-Harris Aug 4, 2022
ed6c534
remove some no-longer-used code
Rich-Harris Aug 4, 2022
813c9ed
oops
Rich-Harris Aug 4, 2022
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion .prettierrc
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,8 @@
"packages/kit/src/packaging/test/fixtures/**/expected/**/*",
"packages/kit/src/packaging/test/watch/expected/**/*",
"packages/kit/src/packaging/test/watch/package/**/*",
"packages/kit/src/core/prerender/fixtures/**/*"
"packages/kit/src/core/prerender/fixtures/**/*",
"packages/migrate/migrations/routes/*/samples.md"
],
"options": {
"requirePragma": true
Expand Down
20 changes: 20 additions & 0 deletions packages/migrate/bin.js
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(', ')}`)
);
}
237 changes: 237 additions & 0 deletions packages/migrate/migrations/routes/index.js
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, '/'));
Copy link
Member Author

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


// 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 packages/migrate/migrations/routes/migrate_page_js/index.js
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 packages/migrate/migrations/routes/migrate_page_js/index.spec.js
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();
Loading