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

fix: migration cleanup #10195

Merged
merged 13 commits into from
Jun 21, 2023
5 changes: 5 additions & 0 deletions .changeset/forty-ears-beg.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'svelte-migrate': patch
---

fix: finalize svelte-4 migration
18 changes: 14 additions & 4 deletions packages/migrate/migrations/svelte-4/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ export async function migrate() {
bail('Please re-run this script in a directory with a package.json');
}

console.log(colors.bold().yellow('\nThis will update files in the current directory\n'));
console.log(colors.bold().yellow('\nThis will update files in the current src/ directory\n'));
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is the path configurable?

Outside of Kit projects I've usually seen people put svelte files in client, app, components...

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It isn't right now. How could we go about it instead? Scan everything except node_modules / .svelte-kit or all things inside .gitignore? If a jsconfig/tsconfig exists and read the paths from there?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Probably keep it simple and add a clack prompt. The automated solutions would still miss things, whereas people can just order it to do the right thing for their codebase with an input.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ahh, there is already a prompt for the transitions change. Great.


const use_git = check_git();

Expand All @@ -25,12 +25,22 @@ export async function migrate() {
process.exit(1);
}

const migrate_transition = await prompts({
type: 'confirm',
name: 'value',
message:
'Add the `|global` modifier to currently global transitions for backwards compatibility? More info at https://svelte.dev/docs/v4-migration-guide#transitions-are-local-by-default',
initial: true
});

// 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 svelte_extensions = /* config.extensions ?? - disabled because it would break .svx */ [
'.svelte'
];
const extensions = [...svelte_extensions, '.ts', '.js'];
// TODO read tsconfig/jsconfig if available? src/** will be good for 99% of cases
const files = glob('src/**', { filesOnly: true, dot: true }).map((file) =>
Expand All @@ -40,7 +50,7 @@ export async function migrate() {
for (const file of files) {
if (extensions.some((ext) => file.endsWith(ext))) {
if (svelte_extensions.some((ext) => file.endsWith(ext))) {
update_svelte_file(file);
update_svelte_file(file, migrate_transition.value);
} else {
update_js_file(file);
}
Expand All @@ -55,7 +65,7 @@ export async function migrate() {

const tasks = [
use_git && cyan('git commit -m "migration to Svelte 4"'),
'Review the migration guide at TODO',
'Review the migration guide at https://svelte.dev/docs/v4-migration-guide',
'Read the updated docs at https://svelte.dev/docs'
].filter(Boolean);

Expand Down
195 changes: 174 additions & 21 deletions packages/migrate/migrations/svelte-4/migrate.js
Original file line number Diff line number Diff line change
@@ -1,39 +1,53 @@
import fs from 'node:fs';
import { Project, ts, Node } from 'ts-morph';

/** @param {string} file_path */
export function update_svelte_file(file_path) {
/**
* @param {string} file_path
* @param {boolean} migrate_transition
*/
export function update_svelte_file(file_path, migrate_transition) {
const content = fs.readFileSync(file_path, 'utf-8');
const updated = content.replace(
/<script([^]*?)>([^]+?)<\/script>(\n*)/g,
(_match, attrs, contents, whitespace) => {
return `<script${attrs}>${transform_code(contents)}</script>${whitespace}`;
return `<script${attrs}>${transform_code(
contents,
(attrs.includes('lang=') || attrs.includes('type=')) &&
(attrs.includes('ts') || attrs.includes('typescript'))
)}</script>${whitespace}`;
}
);
fs.writeFileSync(file_path, transform_svelte_code(updated), 'utf-8');
fs.writeFileSync(file_path, transform_svelte_code(updated, migrate_transition), 'utf-8');
}

/** @param {string} file_path */
export function update_js_file(file_path) {
const content = fs.readFileSync(file_path, 'utf-8');
const updated = transform_code(content);
const updated = transform_code(content, file_path.endsWith('.ts'));
fs.writeFileSync(file_path, updated, 'utf-8');
}

/** @param {string} code */
export function transform_code(code) {
/**
* @param {string} code
* @param {boolean} is_ts
*/
export function transform_code(code, is_ts) {
const project = new Project({ useInMemoryFileSystem: true });
const source = project.createSourceFile('svelte.ts', code);
update_imports(source);
update_typeof_svelte_component(source);
update_action_types(source);
update_action_return_types(source);
const source = project.createSourceFile(`svelte.${is_ts ? 'ts' : 'js'}`, code);
update_imports(source, is_ts);
update_typeof_svelte_component(source, is_ts);
update_action_types(source, is_ts);
update_action_return_types(source, is_ts);
return source.getFullText();
}

/** @param {string} code */
export function transform_svelte_code(code) {
return update_transitions(update_svelte_options(code));
/**
* @param {string} code
* @param {boolean} migrate_transition
*/
export function transform_svelte_code(code, migrate_transition) {
code = update_svelte_options(code);
return update_transitions(code, migrate_transition);
}

/**
Expand All @@ -42,23 +56,49 @@ export function transform_svelte_code(code) {
*/
function update_svelte_options(code) {
return code.replace(/<svelte:options([^]*?)\stag=([^]*?)\/?>/, (match) => {
log_migration(
'Replaced `svelte:options` `tag` attribute with `customElement` attribute: https://svelte.dev/docs/v4-migration-guide#custom-elements-with-svelte'
);
return match.replace('tag=', 'customElement=');
});
}

/**
* transition/in/out:x -> transition/in/out:x|global
* transition/in/out|local:x -> transition/in/out:x
* @param {string} code
* @param {boolean} migrate_transition
*/
function update_transitions(code) {
return code.replace(/(\s)(transition:|in:|out:)(\w+)(?=[\s>=])/g, '$1$2$3|global');
function update_transitions(code, migrate_transition) {
if (migrate_transition) {
const replaced = code.replace(/(\s)(transition:|in:|out:)(\w+)(?=[\s>=])/g, '$1$2$3|global');
if (replaced !== code) {
log_migration(
'Added `|global` to `transition`, `in`, and `out` directives (transitions are local by default now): https://svelte.dev/docs/v4-migration-guide#transitions-are-local-by-default'
);
}
code = replaced;
}
const replaced = code.replace(/(\s)(transition:|in:|out:)(\w+)(\|local)(?=[\s>=])/g, '$1$2$3');
if (replaced !== code) {
log_migration(
'Removed `|local` from `transition`, `in`, and `out` directives (transitions are local by default now): https://svelte.dev/docs/v4-migration-guide#transitions-are-local-by-default'
);
}
return replaced;
}

/**
* Action<T> -> Action<T, any>
* @param {import('ts-morph').SourceFile} source
* @param {boolean} is_ts
*/
function update_action_types(source) {
function update_action_types(source, is_ts) {
const logger = log_on_ts_modification(
source,
'Updated `Action` interface usages: https://svelte.dev/docs/v4-migration-guide#stricter-types-for-svelte-functions'
);

const imports = get_imports(source, 'svelte/action', 'Action');
for (const namedImport of imports) {
const identifiers = find_identifiers(source, namedImport.getAliasNode()?.getText() ?? 'Action');
Expand All @@ -75,13 +115,33 @@ function update_action_types(source) {
}
}
}

if (!is_ts) {
replaceInJsDoc(source, (text) => {
return text.replace(
/import\((['"])svelte\/action['"]\).Action(<\w+>)?(?=[^<\w]|$)/g,
(_, quote, type) =>
`import(${quote}svelte/action${quote}).Action<${
type ? type.slice(1, -1) + '' : 'HTMLElement'
}, any>`
);
});
}

logger();
}

/**
* ActionReturn -> ActionReturn<any>
* @param {import('ts-morph').SourceFile} source
* @param {boolean} is_ts
*/
function update_action_return_types(source) {
function update_action_return_types(source, is_ts) {
const logger = log_on_ts_modification(
source,
'Updated `ActionReturn` interface usages: https://svelte.dev/docs/v4-migration-guide#stricter-types-for-svelte-functions'
);

const imports = get_imports(source, 'svelte/action', 'ActionReturn');
for (const namedImport of imports) {
const identifiers = find_identifiers(
Expand All @@ -98,13 +158,30 @@ function update_action_return_types(source) {
}
}
}

if (!is_ts) {
replaceInJsDoc(source, (text) => {
return text.replace(
/import\((['"])svelte\/action['"]\).ActionReturn(?=[^<\w]|$)/g,
'import($1svelte/action$1).ActionReturn<any>'
);
});
}

logger();
}

/**
* SvelteComponentTyped -> SvelteComponent
* @param {import('ts-morph').SourceFile} source
* @param {boolean} is_ts
*/
function update_imports(source) {
function update_imports(source, is_ts) {
const logger = log_on_ts_modification(
source,
'Replaced `SvelteComponentTyped` imports with `SvelteComponent` imports: https://svelte.dev/docs/v4-migration-guide#stricter-types-for-svelte-functions'
);

const identifiers = find_identifiers(source, 'SvelteComponent');
const can_rename = identifiers.every((id) => {
const parent = id.getParent();
Expand Down Expand Up @@ -136,13 +213,30 @@ function update_imports(source) {
namedImport.setName('SvelteComponent');
}
}

if (!is_ts) {
replaceInJsDoc(source, (text) => {
return text.replace(
/import\((['"])svelte['"]\)\.SvelteComponentTyped(?=\W|$)/g,
'import($1svelte$1).SvelteComponent'
);
});
}

logger();
}

/**
* typeof SvelteComponent -> typeof SvelteComponent<any>
* @param {import('ts-morph').SourceFile} source
* @param {boolean} is_ts
*/
function update_typeof_svelte_component(source) {
function update_typeof_svelte_component(source, is_ts) {
const logger = log_on_ts_modification(
source,
'Adjusted `typeof SvelteComponent` to `typeof SvelteComponent<any>`: https://svelte.dev/docs/v4-migration-guide#stricter-types-for-svelte-functions'
);

const imports = get_imports(source, 'svelte', 'SvelteComponent');

for (const type of imports) {
Expand All @@ -162,6 +256,17 @@ function update_typeof_svelte_component(source) {
});
}
}

if (!is_ts) {
replaceInJsDoc(source, (text) => {
return text.replace(
/typeof import\((['"])svelte['"]\)\.SvelteComponent(?=[^<\w]|$)/g,
'typeof import($1svelte$1).SvelteComponent<any>'
);
});
}

logger();
}

/**
Expand Down Expand Up @@ -198,3 +303,51 @@ function is_declaration(node) {
Node.isInterfaceDeclaration(node)
);
}

/**
* @param {import('ts-morph').SourceFile} source
* @param {(text: string) => string | undefined} replacer
*/
function replaceInJsDoc(source, replacer) {
source.forEachChild((node) => {
if (Node.isJSDocable(node)) {
const tags = node.getJsDocs().flatMap((jsdoc) => jsdoc.getTags());
tags.forEach((t) =>
t.forEachChild((c) => {
if (Node.isJSDocTypeExpression(c)) {
const text = c.getText().slice(1, -1);
const replacement = replacer(text);
if (replacement && replacement !== text) {
c.replaceWithText(`{${replacement}}`);
}
}
})
);
}
});
}

const logged_migrations = new Set();

/**
* @param {import('ts-morph').SourceFile} source
* @param {string} text
*/
function log_on_ts_modification(source, text) {
let logged = false;
const log = () => {
if (!logged) {
logged = true;
log_migration(text);
}
};
source.onModified(log);
return () => source.onModified(log, false);
}

/** @param {string} text */
function log_migration(text) {
if (logged_migrations.has(text)) return;
console.log(text);
logged_migrations.add(text);
}
Loading