Skip to content

Commit b665425

Browse files
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 <[email protected]>
1 parent 33ee958 commit b665425

File tree

7 files changed

+356
-4
lines changed

7 files changed

+356
-4
lines changed

.changeset/good-vans-bake.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'svelte': patch
3+
---
4+
5+
feat: support migration of `svelte:component`

packages/svelte/src/compiler/migrate/index.js

Lines changed: 78 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,18 @@
11
/** @import { VariableDeclarator, Node, Identifier } from 'estree' */
22
/** @import { Visitors } from 'zimmerframe' */
33
/** @import { ComponentAnalysis } from '../phases/types.js' */
4-
/** @import { Scope } from '../phases/scope.js' */
4+
/** @import { Scope, ScopeRoot } from '../phases/scope.js' */
55
/** @import { AST, Binding, SvelteNode, ValidatedCompileOptions } from '#compiler' */
66
import MagicString from 'magic-string';
77
import { walk } from 'zimmerframe';
88
import { parse } from '../phases/1-parse/index.js';
9+
import { regex_valid_component_name } from '../phases/1-parse/state/element.js';
910
import { analyze_component } from '../phases/2-analyze/index.js';
1011
import { get_rune } from '../phases/scope.js';
1112
import { reset, reset_warning_filter } from '../state.js';
1213
import { extract_identifiers } from '../utils/ast.js';
1314
import { migrate_svelte_ignore } from '../utils/extract_svelte_ignore.js';
15+
import { determine_slot } from '../utils/slot.js';
1416
import { validate_component_options } from '../validate-options.js';
1517

1618
const regex_style_tags = /(<style[^>]+>)([\S\s]*?)(<\/style>)/g;
@@ -85,7 +87,8 @@ export function migrate(source) {
8587
nonpassive: analysis.root.unique('nonpassive').name
8688
},
8789
legacy_imports: new Set(),
88-
script_insertions: new Set()
90+
script_insertions: new Set(),
91+
derived_components: new Map()
8992
};
9093

9194
if (parsed.module) {
@@ -108,6 +111,7 @@ export function migrate(source) {
108111

109112
const need_script =
110113
state.legacy_imports.size > 0 ||
114+
state.derived_components.size > 0 ||
111115
state.script_insertions.size > 0 ||
112116
state.props.length > 0 ||
113117
analysis.uses_rest_props ||
@@ -250,6 +254,17 @@ export function migrate(source) {
250254
}
251255
}
252256

257+
insertion_point = parsed.instance
258+
? /** @type {number} */ (parsed.instance.content.end)
259+
: insertion_point;
260+
261+
if (state.derived_components.size > 0) {
262+
str.appendRight(
263+
insertion_point,
264+
`\n${indent}${[...state.derived_components.entries()].map(([init, name]) => `const ${name} = $derived(${init});`).join(`\n${indent}`)}\n`
265+
);
266+
}
267+
253268
if (!parsed.instance && need_script) {
254269
str.appendRight(insertion_point, '\n</script>\n\n');
255270
}
@@ -273,7 +288,8 @@ export function migrate(source) {
273288
* end: number;
274289
* names: Record<string, string>;
275290
* legacy_imports: Set<string>;
276-
* script_insertions: Set<string>
291+
* script_insertions: Set<string>;
292+
* derived_components: Map<string, string>
277293
* }} State
278294
*/
279295

@@ -586,6 +602,65 @@ const template = {
586602
handle_events(node, state);
587603
next();
588604
},
605+
SvelteComponent(node, { state, next, path }) {
606+
next();
607+
608+
let expression = state.str
609+
.snip(
610+
/** @type {number} */ (node.expression.start),
611+
/** @type {number} */ (node.expression.end)
612+
)
613+
.toString();
614+
615+
if (
616+
(node.expression.type !== 'Identifier' && node.expression.type !== 'MemberExpression') ||
617+
!regex_valid_component_name.test(expression)
618+
) {
619+
let current_expression = expression;
620+
expression = state.scope.generate('SvelteComponent');
621+
let needs_derived = true;
622+
for (let i = path.length - 1; i >= 0; i--) {
623+
const part = path[i];
624+
if (
625+
part.type === 'EachBlock' ||
626+
part.type === 'AwaitBlock' ||
627+
part.type === 'IfBlock' ||
628+
part.type === 'KeyBlock' ||
629+
part.type === 'SnippetBlock' ||
630+
part.type === 'Component' ||
631+
part.type === 'SvelteComponent'
632+
) {
633+
const indent = state.str.original.substring(
634+
state.str.original.lastIndexOf('\n', node.start) + 1,
635+
node.start
636+
);
637+
state.str.prependLeft(
638+
node.start,
639+
`{@const ${expression} = ${current_expression}}\n${indent}`
640+
);
641+
needs_derived = false;
642+
continue;
643+
}
644+
}
645+
if (needs_derived) {
646+
if (state.derived_components.has(current_expression)) {
647+
expression = /** @type {string} */ (state.derived_components.get(current_expression));
648+
} else {
649+
state.derived_components.set(current_expression, expression);
650+
}
651+
}
652+
}
653+
654+
state.str.overwrite(node.start + 1, node.start + node.name.length + 1, expression);
655+
656+
if (state.str.original.substring(node.end - node.name.length - 1, node.end - 1) === node.name) {
657+
state.str.overwrite(node.end - node.name.length - 1, node.end - 1, expression);
658+
}
659+
let this_pos = state.str.original.lastIndexOf('this', node.expression.start);
660+
while (!state.str.original.charAt(this_pos - 1).trim()) this_pos--;
661+
const end_pos = state.str.original.indexOf('}', node.expression.end) + 1;
662+
state.str.remove(this_pos, end_pos);
663+
},
589664
SvelteWindow(node, { state, next }) {
590665
handle_events(node, state);
591666
next();

packages/svelte/src/compiler/phases/1-parse/state/element.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ const regex_starts_with_quote_characters = /^["']/;
2323
const regex_attribute_value = /^(?:"([^"]*)"|'([^'])*'|([^>\s]+))/;
2424
const regex_valid_element_name =
2525
/^(?:![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])$/;
26-
const regex_valid_component_name =
26+
export const regex_valid_component_name =
2727
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Lexical_grammar#identifiers adjusted for our needs
2828
// (must start with uppercase letter if no dots, can contain dots)
2929
/^(?:\p{Lu}[$\u200c\u200d\p{ID_Continue}.]*|\p{ID_Start}[$\u200c\u200d\p{ID_Continue}]*(?:\.[$\u200c\u200d\p{ID_Continue}]+)+)$/u;

packages/svelte/src/compiler/phases/2-analyze/visitors/SvelteComponent.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,5 +12,7 @@ export function SvelteComponent(node, context) {
1212
w.svelte_component_deprecated(node);
1313
}
1414

15+
context.visit(node.expression);
16+
1517
visit_component(node, context);
1618
}

packages/svelte/src/compiler/phases/scope.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -391,6 +391,7 @@ export function create_scopes(ast, root, allow_reactive_declarations, parent) {
391391
if (node.expression) {
392392
for (const id of extract_identifiers_from_destructuring(node.expression)) {
393393
const binding = scope.declare(id, 'template', 'const');
394+
scope.reference(id, [context.path[context.path.length - 1], node]);
394395
bindings.push(binding);
395396
}
396397
} else {
@@ -402,6 +403,7 @@ export function create_scopes(ast, root, allow_reactive_declarations, parent) {
402403
end: node.end
403404
};
404405
const binding = scope.declare(id, 'template', 'const');
406+
scope.reference(id, [context.path[context.path.length - 1], node]);
405407
bindings.push(binding);
406408
}
407409
},
Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,125 @@
1+
<script>
2+
let Component;
3+
let fallback;
4+
</script>
5+
6+
<Component let:Comp>
7+
<svelte:component this={Comp} />
8+
</Component>
9+
10+
<Component let:comp>
11+
<svelte:component this={comp} />
12+
</Component>
13+
14+
<Component let:comp={stuff}>
15+
<svelte:component this={stuff} />
16+
</Component>
17+
18+
<Component>
19+
<div slot="x" let:comp={stuff}>
20+
<svelte:component this={stuff} />
21+
</div>
22+
</Component>
23+
24+
<Component>
25+
<svelte:fragment slot="x" let:comp={stuff}>
26+
<svelte:component this={stuff} />
27+
</svelte:fragment>
28+
</Component>
29+
30+
<Component>
31+
<svelte:element this={"div"} slot="x" let:comp={stuff}>
32+
<svelte:component this={stuff} />
33+
</svelte:element>
34+
</Component>
35+
36+
<svelte:component this={Component} let:Comp>
37+
<svelte:component this={Comp} />
38+
</svelte:component>
39+
40+
<svelte:component this={Component} let:comp>
41+
<svelte:component this={comp} />
42+
</svelte:component>
43+
44+
<svelte:component this={Component} let:comp={stuff}>
45+
<svelte:component this={stuff} />
46+
</svelte:component>
47+
48+
<svelte:component this={Component}>
49+
<div slot="x" let:comp={stuff}>
50+
<svelte:component this={stuff} />
51+
</div>
52+
</svelte:component>
53+
54+
<svelte:component this={Component}>
55+
<svelte:fragment slot="x" let:comp={stuff}>
56+
<svelte:component this={stuff} />
57+
</svelte:fragment>
58+
</svelte:component>
59+
60+
<svelte:component this={Component}>
61+
<svelte:element this={"div"} slot="x" let:comp={stuff}>
62+
<svelte:component this={stuff} />
63+
</svelte:element>
64+
</svelte:component>
65+
66+
<svelte:component this={Component} />
67+
<svelte:component this={Component} prop value="" on:click on:click={()=>''} />
68+
<svelte:component this={Math.random() > .5 ? $$restProps.heads : $$restProps.tail} prop value="" on:click on:click={()=>''}/>
69+
70+
<svelte:component
71+
this={Component}
72+
prop value=""
73+
on:click
74+
on:click={()=>''}
75+
/>
76+
77+
<svelte:component
78+
this={Math.random() > .5 ? $$restProps.heads : $$restProps.tail}
79+
prop value=""
80+
on:click
81+
on:click={()=>''}
82+
/>
83+
84+
{#if true}
85+
{@const x = {Component}}
86+
<svelte:component this={x['Component']} />
87+
{/if}
88+
89+
{#if true}
90+
{@const x = {Component}}
91+
<svelte:component this={x.Component} />
92+
{/if}
93+
94+
{#each [] as component}
95+
<svelte:component this={component} />
96+
{/each}
97+
98+
{#each [] as Component}
99+
<svelte:component this={Component} />
100+
{/each}
101+
102+
{#each [] as component}
103+
{@const Comp = component.component}
104+
<svelte:component this={Comp} />
105+
{/each}
106+
107+
{#each [] as component}
108+
{@const comp = component.component}
109+
<svelte:component this={comp} />
110+
{/each}
111+
112+
{#await Promise.resolve()}
113+
<svelte:component this={Component} />
114+
<svelte:component this={fallback} />
115+
{:then something}
116+
<svelte:component this={something} />
117+
{:catch e}
118+
<svelte:component this={e} />
119+
{/await}
120+
121+
{#await Promise.resolve() then Something}
122+
<svelte:component this={Something} />
123+
{:catch Error}
124+
<svelte:component this={Error} />
125+
{/await}

0 commit comments

Comments
 (0)