feat: support migration of `svelte:component` (#13437)

* feat: allow migration of  `svelte:component`

* chore: simplify a lot (thanks @dummdidumm)

* chore: update output

* chore: use `next()` and `snip` instead of walking the AST

* fix: migrate nested `svelte:component`

* Update .changeset/good-vans-bake.md

---------

Co-authored-by: Simon H <5968653+dummdidumm@users.noreply.github.com>
main
Paolo Ricciuti 1 day ago committed by GitHub
parent 33ee958087
commit b665425e5d
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -0,0 +1,5 @@
---
'svelte': patch
---
feat: support migration of `svelte:component`

@ -1,16 +1,18 @@
/** @import { VariableDeclarator, Node, Identifier } from 'estree' */
/** @import { Visitors } from 'zimmerframe' */
/** @import { ComponentAnalysis } from '../phases/types.js' */
/** @import { Scope } from '../phases/scope.js' */
/** @import { Scope, ScopeRoot } from '../phases/scope.js' */
/** @import { AST, Binding, SvelteNode, ValidatedCompileOptions } from '#compiler' */
import MagicString from 'magic-string';
import { walk } from 'zimmerframe';
import { parse } from '../phases/1-parse/index.js';
import { regex_valid_component_name } from '../phases/1-parse/state/element.js';
import { analyze_component } from '../phases/2-analyze/index.js';
import { get_rune } from '../phases/scope.js';
import { reset, reset_warning_filter } from '../state.js';
import { extract_identifiers } from '../utils/ast.js';
import { migrate_svelte_ignore } from '../utils/extract_svelte_ignore.js';
import { determine_slot } from '../utils/slot.js';
import { validate_component_options } from '../validate-options.js';
const regex_style_tags = /(<style[^>]+>)([\S\s]*?)(<\/style>)/g;
@ -85,7 +87,8 @@ export function migrate(source) {
nonpassive: analysis.root.unique('nonpassive').name
},
legacy_imports: new Set(),
script_insertions: new Set()
script_insertions: new Set(),
derived_components: new Map()
};
if (parsed.module) {
@ -108,6 +111,7 @@ export function migrate(source) {
const need_script =
state.legacy_imports.size > 0 ||
state.derived_components.size > 0 ||
state.script_insertions.size > 0 ||
state.props.length > 0 ||
analysis.uses_rest_props ||
@ -250,6 +254,17 @@ export function migrate(source) {
}
}
insertion_point = parsed.instance
? /** @type {number} */ (parsed.instance.content.end)
: insertion_point;
if (state.derived_components.size > 0) {
str.appendRight(
insertion_point,
`\n${indent}${[...state.derived_components.entries()].map(([init, name]) => `const ${name} = $derived(${init});`).join(`\n${indent}`)}\n`
);
}
if (!parsed.instance && need_script) {
str.appendRight(insertion_point, '\n</script>\n\n');
}
@ -273,7 +288,8 @@ export function migrate(source) {
* end: number;
* names: Record<string, string>;
* legacy_imports: Set<string>;
* script_insertions: Set<string>
* script_insertions: Set<string>;
* derived_components: Map<string, string>
* }} State
*/
@ -586,6 +602,65 @@ const template = {
handle_events(node, state);
next();
},
SvelteComponent(node, { state, next, path }) {
next();
let expression = state.str
.snip(
/** @type {number} */ (node.expression.start),
/** @type {number} */ (node.expression.end)
)
.toString();
if (
(node.expression.type !== 'Identifier' && node.expression.type !== 'MemberExpression') ||
!regex_valid_component_name.test(expression)
) {
let current_expression = expression;
expression = state.scope.generate('SvelteComponent');
let needs_derived = true;
for (let i = path.length - 1; i >= 0; i--) {
const part = path[i];
if (
part.type === 'EachBlock' ||
part.type === 'AwaitBlock' ||
part.type === 'IfBlock' ||
part.type === 'KeyBlock' ||
part.type === 'SnippetBlock' ||
part.type === 'Component' ||
part.type === 'SvelteComponent'
) {
const indent = state.str.original.substring(
state.str.original.lastIndexOf('\n', node.start) + 1,
node.start
);
state.str.prependLeft(
node.start,
`{@const ${expression} = ${current_expression}}\n${indent}`
);
needs_derived = false;
continue;
}
}
if (needs_derived) {
if (state.derived_components.has(current_expression)) {
expression = /** @type {string} */ (state.derived_components.get(current_expression));
} else {
state.derived_components.set(current_expression, expression);
}
}
}
state.str.overwrite(node.start + 1, node.start + node.name.length + 1, expression);
if (state.str.original.substring(node.end - node.name.length - 1, node.end - 1) === node.name) {
state.str.overwrite(node.end - node.name.length - 1, node.end - 1, expression);
}
let this_pos = state.str.original.lastIndexOf('this', node.expression.start);
while (!state.str.original.charAt(this_pos - 1).trim()) this_pos--;
const end_pos = state.str.original.indexOf('}', node.expression.end) + 1;
state.str.remove(this_pos, end_pos);
},
SvelteWindow(node, { state, next }) {
handle_events(node, state);
next();

@ -23,7 +23,7 @@ const regex_starts_with_quote_characters = /^["']/;
const regex_attribute_value = /^(?:"([^"]*)"|'([^'])*'|([^>\s]+))/;
const regex_valid_element_name =
/^(?:![a-zA-Z]+|[a-zA-Z](?:[a-zA-Z0-9-]*[a-zA-Z0-9])?|[a-zA-Z][a-zA-Z0-9]*:[a-zA-Z][a-zA-Z0-9-]*[a-zA-Z0-9])$/;
const regex_valid_component_name =
export const regex_valid_component_name =
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Lexical_grammar#identifiers adjusted for our needs
// (must start with uppercase letter if no dots, can contain dots)
/^(?:\p{Lu}[$\u200c\u200d\p{ID_Continue}.]*|\p{ID_Start}[$\u200c\u200d\p{ID_Continue}]*(?:\.[$\u200c\u200d\p{ID_Continue}]+)+)$/u;

@ -12,5 +12,7 @@ export function SvelteComponent(node, context) {
w.svelte_component_deprecated(node);
}
context.visit(node.expression);
visit_component(node, context);
}

@ -391,6 +391,7 @@ export function create_scopes(ast, root, allow_reactive_declarations, parent) {
if (node.expression) {
for (const id of extract_identifiers_from_destructuring(node.expression)) {
const binding = scope.declare(id, 'template', 'const');
scope.reference(id, [context.path[context.path.length - 1], node]);
bindings.push(binding);
}
} else {
@ -402,6 +403,7 @@ export function create_scopes(ast, root, allow_reactive_declarations, parent) {
end: node.end
};
const binding = scope.declare(id, 'template', 'const');
scope.reference(id, [context.path[context.path.length - 1], node]);
bindings.push(binding);
}
},

@ -0,0 +1,125 @@
<script>
let Component;
let fallback;
</script>
<Component let:Comp>
<svelte:component this={Comp} />
</Component>
<Component let:comp>
<svelte:component this={comp} />
</Component>
<Component let:comp={stuff}>
<svelte:component this={stuff} />
</Component>
<Component>
<div slot="x" let:comp={stuff}>
<svelte:component this={stuff} />
</div>
</Component>
<Component>
<svelte:fragment slot="x" let:comp={stuff}>
<svelte:component this={stuff} />
</svelte:fragment>
</Component>
<Component>
<svelte:element this={"div"} slot="x" let:comp={stuff}>
<svelte:component this={stuff} />
</svelte:element>
</Component>
<svelte:component this={Component} let:Comp>
<svelte:component this={Comp} />
</svelte:component>
<svelte:component this={Component} let:comp>
<svelte:component this={comp} />
</svelte:component>
<svelte:component this={Component} let:comp={stuff}>
<svelte:component this={stuff} />
</svelte:component>
<svelte:component this={Component}>
<div slot="x" let:comp={stuff}>
<svelte:component this={stuff} />
</div>
</svelte:component>
<svelte:component this={Component}>
<svelte:fragment slot="x" let:comp={stuff}>
<svelte:component this={stuff} />
</svelte:fragment>
</svelte:component>
<svelte:component this={Component}>
<svelte:element this={"div"} slot="x" let:comp={stuff}>
<svelte:component this={stuff} />
</svelte:element>
</svelte:component>
<svelte:component this={Component} />
<svelte:component this={Component} prop value="" on:click on:click={()=>''} />
<svelte:component this={Math.random() > .5 ? $$restProps.heads : $$restProps.tail} prop value="" on:click on:click={()=>''}/>
<svelte:component
this={Component}
prop value=""
on:click
on:click={()=>''}
/>
<svelte:component
this={Math.random() > .5 ? $$restProps.heads : $$restProps.tail}
prop value=""
on:click
on:click={()=>''}
/>
{#if true}
{@const x = {Component}}
<svelte:component this={x['Component']} />
{/if}
{#if true}
{@const x = {Component}}
<svelte:component this={x.Component} />
{/if}
{#each [] as component}
<svelte:component this={component} />
{/each}
{#each [] as Component}
<svelte:component this={Component} />
{/each}
{#each [] as component}
{@const Comp = component.component}
<svelte:component this={Comp} />
{/each}
{#each [] as component}
{@const comp = component.component}
<svelte:component this={comp} />
{/each}
{#await Promise.resolve()}
<svelte:component this={Component} />
<svelte:component this={fallback} />
{:then something}
<svelte:component this={something} />
{:catch e}
<svelte:component this={e} />
{/await}
{#await Promise.resolve() then Something}
<svelte:component this={Something} />
{:catch Error}
<svelte:component this={Error} />
{/await}

@ -0,0 +1,143 @@
<script>
/** @type {{Record<string, any>}} */
let { ...rest } = $props();
let Component;
let fallback;
const SvelteComponent_10 = $derived(Math.random() > .5 ? rest.heads : rest.tail);
</script>
<Component let:Comp>
<Comp />
</Component>
<Component let:comp>
{@const SvelteComponent = comp}
<SvelteComponent />
</Component>
<Component let:comp={stuff}>
{@const SvelteComponent_1 = stuff}
<SvelteComponent_1 />
</Component>
<Component>
<div slot="x" let:comp={stuff}>
{@const SvelteComponent_2 = stuff}
<SvelteComponent_2 />
</div>
</Component>
<Component>
<svelte:fragment slot="x" let:comp={stuff}>
{@const SvelteComponent_3 = stuff}
<SvelteComponent_3 />
</svelte:fragment>
</Component>
<Component>
<svelte:element this={"div"} slot="x" let:comp={stuff}>
{@const SvelteComponent_4 = stuff}
<SvelteComponent_4 />
</svelte:element>
</Component>
<Component let:Comp>
<Comp />
</Component>
<Component let:comp>
{@const SvelteComponent_5 = comp}
<SvelteComponent_5 />
</Component>
<Component let:comp={stuff}>
{@const SvelteComponent_6 = stuff}
<SvelteComponent_6 />
</Component>
<Component>
<div slot="x" let:comp={stuff}>
{@const SvelteComponent_7 = stuff}
<SvelteComponent_7 />
</div>
</Component>
<Component>
<svelte:fragment slot="x" let:comp={stuff}>
{@const SvelteComponent_8 = stuff}
<SvelteComponent_8 />
</svelte:fragment>
</Component>
<Component>
<svelte:element this={"div"} slot="x" let:comp={stuff}>
{@const SvelteComponent_9 = stuff}
<SvelteComponent_9 />
</svelte:element>
</Component>
<Component />
<Component prop value="" on:click on:click={()=>''} />
<SvelteComponent_10 prop value="" on:click on:click={()=>''}/>
<Component
prop value=""
on:click
on:click={()=>''}
/>
<SvelteComponent_10
prop value=""
on:click
on:click={()=>''}
/>
{#if true}
{@const x = {Component}}
{@const SvelteComponent_12 = x['Component']}
<SvelteComponent_12 />
{/if}
{#if true}
{@const x = {Component}}
<x.Component />
{/if}
{#each [] as component}
{@const SvelteComponent_13 = component}
<SvelteComponent_13 />
{/each}
{#each [] as Component}
<Component />
{/each}
{#each [] as component}
{@const Comp = component.component}
<Comp />
{/each}
{#each [] as component}
{@const comp = component.component}
{@const SvelteComponent_14 = comp}
<SvelteComponent_14 />
{/each}
{#await Promise.resolve()}
<Component />
{@const SvelteComponent_15 = fallback}
<SvelteComponent_15 />
{:then something}
{@const SvelteComponent_16 = something}
<SvelteComponent_16 />
{:catch e}
{@const SvelteComponent_17 = e}
<SvelteComponent_17 />
{/await}
{#await Promise.resolve() then Something}
<Something />
{:catch Error}
<Error />
{/await}
Loading…
Cancel
Save