mirror of https://github.com/sveltejs/svelte
Provides the start of a migration function, exported as `migrate` from `svelte/compiler`, which tries its best to automatically migrate towards runes, render tags (instead of slots) and event attributes (instead of event handlers) The preview REPL was updated with a migrate button so people can try it out in the playground. closes #9239pull/11334/head
parent
6ad5cd4461
commit
9886329fd8
@ -0,0 +1,369 @@
|
|||||||
|
import MagicString from 'magic-string';
|
||||||
|
import { walk } from 'zimmerframe';
|
||||||
|
import { parse } from '../phases/1-parse/index.js';
|
||||||
|
import { analyze_component } from '../phases/2-analyze/index.js';
|
||||||
|
import { validate_component_options } from '../validate-options.js';
|
||||||
|
import { get_rune } from '../phases/scope.js';
|
||||||
|
import { reset_warnings } from '../warnings.js';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Does a best-effort migration of Svelte code towards using runes, event attributes and render tags.
|
||||||
|
* @param {string} source
|
||||||
|
* @returns {string}
|
||||||
|
*/
|
||||||
|
export function migrate(source) {
|
||||||
|
try {
|
||||||
|
reset_warnings({ source, filename: 'migrate.svelte' });
|
||||||
|
|
||||||
|
let parsed = parse(source);
|
||||||
|
|
||||||
|
const { customElement: customElementOptions, ...parsed_options } = parsed.options || {};
|
||||||
|
|
||||||
|
/** @type {import('#compiler').ValidatedCompileOptions} */
|
||||||
|
const combined_options = {
|
||||||
|
...validate_component_options({}, ''),
|
||||||
|
...parsed_options,
|
||||||
|
customElementOptions
|
||||||
|
};
|
||||||
|
|
||||||
|
const str = new MagicString(source);
|
||||||
|
const analysis = analyze_component(parsed, source, combined_options);
|
||||||
|
|
||||||
|
/** @type {State} */
|
||||||
|
let state = {
|
||||||
|
scope: analysis.instance.scope,
|
||||||
|
analysis,
|
||||||
|
str,
|
||||||
|
props: [],
|
||||||
|
props_insertion_point: 0,
|
||||||
|
has_props_rune: false,
|
||||||
|
props_name: analysis.root.unique('props').name,
|
||||||
|
rest_props_name: analysis.root.unique('rest').name
|
||||||
|
};
|
||||||
|
|
||||||
|
if (parsed.instance) {
|
||||||
|
walk(parsed.instance.content, state, instance_script);
|
||||||
|
}
|
||||||
|
|
||||||
|
state = { ...state, scope: analysis.template.scope };
|
||||||
|
walk(parsed.fragment, state, template);
|
||||||
|
|
||||||
|
if (state.props.length > 0 || analysis.uses_rest_props || analysis.uses_props) {
|
||||||
|
let props = '';
|
||||||
|
if (analysis.uses_props) {
|
||||||
|
props = `...${state.props_name}`;
|
||||||
|
} else {
|
||||||
|
props = state.props
|
||||||
|
.map((prop) => {
|
||||||
|
let prop_str =
|
||||||
|
prop.local === prop.exported ? prop.local : `${prop.exported}: ${prop.local}`;
|
||||||
|
if (prop.bindable) {
|
||||||
|
prop_str += ` = $bindable(${prop.init})`;
|
||||||
|
} else if (prop.init) {
|
||||||
|
prop_str += ` = ${prop.init}`;
|
||||||
|
}
|
||||||
|
return prop_str;
|
||||||
|
})
|
||||||
|
.join(', ');
|
||||||
|
|
||||||
|
if (analysis.uses_rest_props) {
|
||||||
|
props += `, ...${state.rest_props_name}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (state.has_props_rune) {
|
||||||
|
// some render tags or forwarded event attributes to add
|
||||||
|
str.appendRight(state.props_insertion_point, ` ${props},`);
|
||||||
|
} else {
|
||||||
|
const props_declaration = `let { ${props} } = $props();`;
|
||||||
|
if (parsed.instance) {
|
||||||
|
if (state.props_insertion_point === 0) {
|
||||||
|
// no regular props found, but render tags or events to forward found, $props() will be first in the script tag
|
||||||
|
str.appendRight(
|
||||||
|
/** @type {number} */ (parsed.instance.content.start),
|
||||||
|
`\n\t${props_declaration}`
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
str.appendRight(state.props_insertion_point, props_declaration);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
str.prepend(`<script>${props_declaration}</script>`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return str.toString();
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Error while migrating Svelte code');
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @typedef {{
|
||||||
|
* scope: import('../phases/scope.js').Scope;
|
||||||
|
* str: MagicString;
|
||||||
|
* analysis: import('../phases/types.js').ComponentAnalysis;
|
||||||
|
* props: Array<{ local: string; exported: string; init: string; bindable: boolean }>;
|
||||||
|
* props_insertion_point: number;
|
||||||
|
* has_props_rune: boolean;
|
||||||
|
* props_name: string;
|
||||||
|
* rest_props_name: string;
|
||||||
|
* }} State
|
||||||
|
*/
|
||||||
|
|
||||||
|
/** @type {import('zimmerframe').Visitors<import('../types/template.js').SvelteNode, State>} */
|
||||||
|
const instance_script = {
|
||||||
|
Identifier(node, { state }) {
|
||||||
|
handle_identifier(node, state);
|
||||||
|
},
|
||||||
|
VariableDeclaration(node, { state, path }) {
|
||||||
|
if (state.scope !== state.analysis.instance.scope) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let nr_of_props = 0;
|
||||||
|
|
||||||
|
for (const declarator of node.declarations) {
|
||||||
|
if (state.analysis.runes) {
|
||||||
|
if (get_rune(declarator.init, state.scope) === '$props') {
|
||||||
|
state.props_insertion_point = /** @type {number} */ (declarator.id.start) + 1;
|
||||||
|
state.has_props_rune = true;
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const bindings = state.scope.get_bindings(declarator);
|
||||||
|
const has_state = bindings.some((binding) => binding.kind === 'state');
|
||||||
|
const has_props = bindings.some((binding) => binding.kind === 'bindable_prop');
|
||||||
|
|
||||||
|
if (!has_state && !has_props) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (has_props) {
|
||||||
|
nr_of_props++;
|
||||||
|
|
||||||
|
if (declarator.id.type !== 'Identifier') {
|
||||||
|
// TODO
|
||||||
|
// Turn export let into props. It's really really weird because export let { x: foo, z: [bar]} = ..
|
||||||
|
// means that foo and bar are the props (i.e. the leafs are the prop names), not x and z.
|
||||||
|
// const tmp = state.scope.generate('tmp');
|
||||||
|
// const paths = extract_paths(declarator.id);
|
||||||
|
// state.props_pre.push(
|
||||||
|
// b.declaration('const', b.id(tmp), visit(declarator.init!) as Expression)
|
||||||
|
// );
|
||||||
|
// for (const path of paths) {
|
||||||
|
// const name = (path.node as Identifier).name;
|
||||||
|
// const binding = state.scope.get(name)!;
|
||||||
|
// const value = path.expression!(b.id(tmp));
|
||||||
|
// if (binding.kind === 'bindable_prop' || binding.kind === 'rest_prop') {
|
||||||
|
// state.props.push({
|
||||||
|
// local: name,
|
||||||
|
// exported: binding.prop_alias ? binding.prop_alias : name,
|
||||||
|
// init: value
|
||||||
|
// });
|
||||||
|
// state.props_insertion_point = /** @type {number} */(declarator.end);
|
||||||
|
// } else {
|
||||||
|
// declarations.push(b.declarator(path.node, value));
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const binding = /** @type {import('#compiler').Binding} */ (
|
||||||
|
state.scope.get(declarator.id.name)
|
||||||
|
);
|
||||||
|
|
||||||
|
if (
|
||||||
|
state.analysis.uses_props &&
|
||||||
|
(declarator.init || binding.mutated || binding.reassigned)
|
||||||
|
) {
|
||||||
|
throw new Error(
|
||||||
|
'$$props is used together with named props in a way that cannot be automatically migrated.'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
state.props.push({
|
||||||
|
local: declarator.id.name,
|
||||||
|
exported: binding.prop_alias ? binding.prop_alias : declarator.id.name,
|
||||||
|
init: declarator.init
|
||||||
|
? state.str.original.substring(
|
||||||
|
/** @type {number} */ (declarator.init.start),
|
||||||
|
/** @type {number} */ (declarator.init.end)
|
||||||
|
)
|
||||||
|
: '',
|
||||||
|
bindable: binding.mutated || binding.reassigned
|
||||||
|
});
|
||||||
|
state.props_insertion_point = /** @type {number} */ (declarator.end);
|
||||||
|
state.str.update(
|
||||||
|
/** @type {number} */ (declarator.start),
|
||||||
|
/** @type {number} */ (declarator.end),
|
||||||
|
''
|
||||||
|
);
|
||||||
|
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// state
|
||||||
|
if (declarator.init) {
|
||||||
|
state.str.prependLeft(/** @type {number} */ (declarator.init.start), '$state(');
|
||||||
|
state.str.appendRight(/** @type {number} */ (declarator.init.end), ')');
|
||||||
|
} else {
|
||||||
|
state.str.prependLeft(/** @type {number} */ (declarator.id.end), ' = $state()');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (nr_of_props === node.declarations.length) {
|
||||||
|
let start = /** @type {number} */ (node.start);
|
||||||
|
let end = /** @type {number} */ (node.end);
|
||||||
|
|
||||||
|
const parent = path.at(-1);
|
||||||
|
if (parent?.type === 'ExportNamedDeclaration') {
|
||||||
|
start = /** @type {number} */ (parent.start);
|
||||||
|
end = /** @type {number} */ (parent.end);
|
||||||
|
}
|
||||||
|
state.str.update(start, end, '');
|
||||||
|
}
|
||||||
|
},
|
||||||
|
LabeledStatement(node, { path, state }) {
|
||||||
|
if (state.analysis.runes) return;
|
||||||
|
if (path.length > 1) return;
|
||||||
|
if (node.label.name !== '$') return;
|
||||||
|
|
||||||
|
if (
|
||||||
|
node.body.type === 'ExpressionStatement' &&
|
||||||
|
node.body.expression.type === 'AssignmentExpression'
|
||||||
|
) {
|
||||||
|
// $derived
|
||||||
|
// TODO $: ({ x } = ...)
|
||||||
|
state.str.update(
|
||||||
|
/** @type {number} */ (node.start),
|
||||||
|
/** @type {number} */ (node.body.expression.start),
|
||||||
|
'let '
|
||||||
|
);
|
||||||
|
state.str.prependLeft(/** @type {number} */ (node.body.expression.right.start), '$derived(');
|
||||||
|
state.str.appendRight(/** @type {number} */ (node.body.expression.right.end), ')');
|
||||||
|
} else {
|
||||||
|
// $effect.pre, to be precise, but we gloss over that
|
||||||
|
// TODO try to find out if we can use $derived.by instead?
|
||||||
|
// TODO SSR mode variant needed
|
||||||
|
state.str.update(
|
||||||
|
/** @type {number} */ (node.start),
|
||||||
|
/** @type {number} */ (node.body.start),
|
||||||
|
'$effect(() => {'
|
||||||
|
);
|
||||||
|
state.str.appendRight(/** @type {number} */ (node.end), '})');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/** @type {import('zimmerframe').Visitors<import('../types/template.js').SvelteNode, State>} */
|
||||||
|
const template = {
|
||||||
|
Identifier(node, { state }) {
|
||||||
|
handle_identifier(node, state);
|
||||||
|
},
|
||||||
|
OnDirective(node, { state, path }) {
|
||||||
|
const parent = path.at(-1);
|
||||||
|
if (
|
||||||
|
parent?.type === 'SvelteSelf' ||
|
||||||
|
parent?.type === 'SvelteComponent' ||
|
||||||
|
parent?.type === 'Component'
|
||||||
|
) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (node.expression) {
|
||||||
|
// remove : from on:click
|
||||||
|
state.str.update(node.start, node.start + 3, 'on');
|
||||||
|
} else {
|
||||||
|
// turn on:click into a prop
|
||||||
|
// Check if prop already set, could happen when on:click on different elements
|
||||||
|
// TODO what to do when this results in a variable name clash?
|
||||||
|
if (!state.props.some((prop) => prop.local === node.name)) {
|
||||||
|
state.props.push({
|
||||||
|
local: node.name,
|
||||||
|
exported: node.name,
|
||||||
|
init: '',
|
||||||
|
bindable: false
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
state.str.update(node.start, node.end, `{${node.name}}`);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
SlotElement(node, { state }) {
|
||||||
|
let name = 'children';
|
||||||
|
let slot_props = '{ ';
|
||||||
|
|
||||||
|
for (const attr of node.attributes) {
|
||||||
|
if (attr.type === 'SpreadAttribute') {
|
||||||
|
slot_props += `...${state.str.original.substring(/** @type {number} */ (attr.expression.start), attr.expression.end)}, `;
|
||||||
|
} else if (attr.type === 'Attribute') {
|
||||||
|
if (attr.name === 'name') {
|
||||||
|
name = /** @type {any} */ (attr.value)[0].data;
|
||||||
|
} else {
|
||||||
|
const value =
|
||||||
|
attr.value !== true
|
||||||
|
? state.str.original.substring(
|
||||||
|
attr.value[0].start,
|
||||||
|
attr.value[attr.value.length - 1].end
|
||||||
|
)
|
||||||
|
: 'true';
|
||||||
|
slot_props += value === attr.name ? `${value}, ` : `${attr.name}: ${value}, `;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
slot_props += '}';
|
||||||
|
if (slot_props === '{ }') {
|
||||||
|
slot_props = '';
|
||||||
|
}
|
||||||
|
|
||||||
|
state.props.push({
|
||||||
|
local: name,
|
||||||
|
exported: name,
|
||||||
|
init: '',
|
||||||
|
bindable: false
|
||||||
|
});
|
||||||
|
|
||||||
|
if (node.fragment.nodes.length > 0) {
|
||||||
|
state.str.update(
|
||||||
|
node.start,
|
||||||
|
node.fragment.nodes[0].start,
|
||||||
|
`{#if ${name}}{@render ${name}(${slot_props})}{:else}`
|
||||||
|
);
|
||||||
|
state.str.update(node.fragment.nodes[node.fragment.nodes.length - 1].end, node.end, '{/if}');
|
||||||
|
} else {
|
||||||
|
state.str.update(node.start, node.end, `{@render ${name}?.(${slot_props})}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {import('estree').Identifier} node
|
||||||
|
* @param {State} state
|
||||||
|
*/
|
||||||
|
function handle_identifier(node, state) {
|
||||||
|
if (state.analysis.uses_props) {
|
||||||
|
if (node.name === '$$props' || node.name === '$$restProps') {
|
||||||
|
// not 100% correct for $$restProps but it'll do
|
||||||
|
state.str.update(
|
||||||
|
/** @type {number} */ (node.start),
|
||||||
|
/** @type {number} */ (node.end),
|
||||||
|
state.props_name
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
const binding = state.scope.get(node.name);
|
||||||
|
if (binding?.kind === 'bindable_prop') {
|
||||||
|
state.str.prependLeft(/** @type {number} */ (node.start), `${state.props_name}.`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (node.name === '$$restProps' && state.analysis.uses_rest_props) {
|
||||||
|
state.str.update(
|
||||||
|
/** @type {number} */ (node.start),
|
||||||
|
/** @type {number} */ (node.end),
|
||||||
|
state.rest_props_name
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,22 @@
|
|||||||
|
<script>
|
||||||
|
import { get_repl_context } from '$lib/context.js';
|
||||||
|
|
||||||
|
const { migrate } = get_repl_context();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="container">
|
||||||
|
<button on:click={migrate} title="Migrate this component towards the new syntax">migrate</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
button {
|
||||||
|
position: relative;
|
||||||
|
display: flex;
|
||||||
|
text-transform: uppercase;
|
||||||
|
font-size: 1.4rem;
|
||||||
|
padding: 0.8rem;
|
||||||
|
gap: 0.5rem;
|
||||||
|
margin-right: 0.3rem;
|
||||||
|
z-index: 9999;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
Loading…
Reference in new issue