Skip to content

Commit 687d9db

Browse files
authored
feat: migrate slot usages (#13500)
Now that snippets can fill slots, we can add logic to migrate slot usages
1 parent aa3f002 commit 687d9db

File tree

5 files changed

+418
-39
lines changed

5 files changed

+418
-39
lines changed

.changeset/small-suns-lie.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: migrate slot usages

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

Lines changed: 147 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,11 @@ import { regex_valid_component_name } from '../phases/1-parse/state/element.js';
1010
import { analyze_component } from '../phases/2-analyze/index.js';
1111
import { get_rune } from '../phases/scope.js';
1212
import { reset, reset_warning_filter } from '../state.js';
13-
import { extract_identifiers, extract_all_identifiers_from_expression } from '../utils/ast.js';
13+
import {
14+
extract_identifiers,
15+
extract_all_identifiers_from_expression,
16+
is_text_attribute
17+
} from '../utils/ast.js';
1418
import { migrate_svelte_ignore } from '../utils/extract_svelte_ignore.js';
1519
import { validate_component_options } from '../validate-options.js';
1620
import { is_svg, is_void } from '../../utils.js';
@@ -711,7 +715,8 @@ const template = {
711715
Identifier(node, { state, path }) {
712716
handle_identifier(node, state, path);
713717
},
714-
RegularElement(node, { state, next }) {
718+
RegularElement(node, { state, path, next }) {
719+
migrate_slot_usage(node, path, state);
715720
handle_events(node, state);
716721
// Strip off any namespace from the beginning of the node name.
717722
const node_name = node.name.replace(/[a-zA-Z-]*:/g, '');
@@ -724,7 +729,9 @@ const template = {
724729
}
725730
next();
726731
},
727-
SvelteElement(node, { state, next }) {
732+
SvelteElement(node, { state, path, next }) {
733+
migrate_slot_usage(node, path, state);
734+
728735
if (node.tag.type === 'Literal') {
729736
let is_static = true;
730737

@@ -748,9 +755,15 @@ const template = {
748755
handle_events(node, state);
749756
next();
750757
},
758+
Component(node, { state, path, next }) {
759+
next();
760+
migrate_slot_usage(node, path, state);
761+
},
751762
SvelteComponent(node, { state, next, path }) {
752763
next();
753764

765+
migrate_slot_usage(node, path, state);
766+
754767
let expression = state.str
755768
.snip(
756769
/** @type {number} */ (node.expression.start),
@@ -789,7 +802,7 @@ const template = {
789802
state.str.original.lastIndexOf('\n', position) + 1,
790803
position
791804
);
792-
state.str.prependLeft(
805+
state.str.appendRight(
793806
position,
794807
`{@const ${expression} = ${current_expression}}\n${indent}`
795808
);
@@ -816,6 +829,10 @@ const template = {
816829
const end_pos = state.str.original.indexOf('}', node.expression.end) + 1;
817830
state.str.remove(this_pos, end_pos);
818831
},
832+
SvelteFragment(node, { state, path, next }) {
833+
migrate_slot_usage(node, path, state);
834+
next();
835+
},
819836
SvelteWindow(node, { state, next }) {
820837
handle_events(node, state);
821838
next();
@@ -828,7 +845,9 @@ const template = {
828845
handle_events(node, state);
829846
next();
830847
},
831-
SlotElement(node, { state, next, visit }) {
848+
SlotElement(node, { state, path, next, visit }) {
849+
migrate_slot_usage(node, path, state);
850+
832851
if (state.analysis.custom_element) return;
833852
let name = 'children';
834853
let slot_name = 'default';
@@ -915,6 +934,129 @@ const template = {
915934
}
916935
};
917936

937+
/**
938+
* @param {AST.RegularElement | AST.SvelteElement | AST.SvelteComponent | AST.Component | AST.SlotElement | AST.SvelteFragment} node
939+
* @param {SvelteNode[]} path
940+
* @param {State} state
941+
*/
942+
function migrate_slot_usage(node, path, state) {
943+
const parent = path.at(-2);
944+
// Bail on custom element slot usage
945+
if (
946+
parent?.type !== 'Component' &&
947+
parent?.type !== 'SvelteComponent' &&
948+
node.type !== 'Component' &&
949+
node.type !== 'SvelteComponent'
950+
) {
951+
return;
952+
}
953+
954+
let snippet_name = 'children';
955+
let snippet_props = [];
956+
957+
for (let attribute of node.attributes) {
958+
if (
959+
attribute.type === 'Attribute' &&
960+
attribute.name === 'slot' &&
961+
is_text_attribute(attribute)
962+
) {
963+
snippet_name = attribute.value[0].data;
964+
state.str.remove(attribute.start, attribute.end);
965+
}
966+
if (attribute.type === 'LetDirective') {
967+
snippet_props.push(
968+
attribute.name +
969+
(attribute.expression
970+
? `: ${state.str.original.substring(/** @type {number} */ (attribute.expression.start), /** @type {number} */ (attribute.expression.end))}`
971+
: '')
972+
);
973+
state.str.remove(attribute.start, attribute.end);
974+
}
975+
}
976+
977+
if (node.type === 'SvelteFragment' && node.fragment.nodes.length > 0) {
978+
// remove node itself, keep content
979+
state.str.remove(node.start, node.fragment.nodes[0].start);
980+
state.str.remove(node.fragment.nodes[node.fragment.nodes.length - 1].end, node.end);
981+
}
982+
983+
const props = snippet_props.length > 0 ? `{ ${snippet_props.join(', ')} }` : '';
984+
985+
if (snippet_name === 'children' && node.type !== 'SvelteFragment') {
986+
if (snippet_props.length === 0) return; // nothing to do
987+
988+
let inner_start = 0;
989+
let inner_end = 0;
990+
for (let i = 0; i < node.fragment.nodes.length; i++) {
991+
const inner = node.fragment.nodes[i];
992+
const is_empty_text = inner.type === 'Text' && !inner.data.trim();
993+
994+
if (
995+
(inner.type === 'RegularElement' ||
996+
inner.type === 'SvelteElement' ||
997+
inner.type === 'Component' ||
998+
inner.type === 'SvelteComponent' ||
999+
inner.type === 'SlotElement' ||
1000+
inner.type === 'SvelteFragment') &&
1001+
inner.attributes.some((attr) => attr.type === 'Attribute' && attr.name === 'slot')
1002+
) {
1003+
if (inner_start && !inner_end) {
1004+
// End of default slot content
1005+
inner_end = inner.start;
1006+
}
1007+
} else if (!inner_start && !is_empty_text) {
1008+
// Start of default slot content
1009+
inner_start = inner.start;
1010+
} else if (inner_end && !is_empty_text) {
1011+
// There was default slot content before, then some named slot content, now some default slot content again.
1012+
// We're moving the last character back by one to avoid the closing {/snippet} tag inserted afterwards
1013+
// to come before the opening {#snippet} tag of the named slot.
1014+
state.str.update(inner_end - 1, inner_end, '');
1015+
state.str.prependLeft(inner_end - 1, state.str.original[inner_end - 1]);
1016+
state.str.move(inner.start, inner.end, inner_end - 1);
1017+
}
1018+
}
1019+
1020+
if (!inner_end) {
1021+
inner_end = node.fragment.nodes[node.fragment.nodes.length - 1].end;
1022+
}
1023+
1024+
state.str.appendLeft(
1025+
inner_start,
1026+
`{#snippet ${snippet_name}(${props})}\n${state.indent.repeat(path.length)}`
1027+
);
1028+
state.str.indent(state.indent, {
1029+
exclude: [
1030+
[0, inner_start],
1031+
[inner_end, state.str.original.length]
1032+
]
1033+
});
1034+
if (inner_end < node.fragment.nodes[node.fragment.nodes.length - 1].end) {
1035+
// Named slots coming afterwards
1036+
state.str.prependLeft(inner_end, `{/snippet}\n${state.indent.repeat(path.length)}`);
1037+
} else {
1038+
// No named slots coming afterwards
1039+
state.str.prependLeft(
1040+
inner_end,
1041+
`${state.indent.repeat(path.length)}{/snippet}\n${state.indent.repeat(path.length - 1)}`
1042+
);
1043+
}
1044+
} else {
1045+
// Named slot or `svelte:fragment`: wrap element itself in a snippet
1046+
state.str.prependLeft(
1047+
node.start,
1048+
`{#snippet ${snippet_name}(${props})}\n${state.indent.repeat(path.length - 2)}`
1049+
);
1050+
state.str.indent(state.indent, {
1051+
exclude: [
1052+
[0, node.start],
1053+
[node.end, state.str.original.length]
1054+
]
1055+
});
1056+
state.str.appendLeft(node.end, `\n${state.indent.repeat(path.length - 2)}{/snippet}`);
1057+
}
1058+
}
1059+
9181060
/**
9191061
* @param {VariableDeclarator} declarator
9201062
* @param {MagicString} str
Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
<Component>
2+
unchanged
3+
</Component>
4+
5+
<svelte:component this={Component}>
6+
unchanged
7+
</svelte:component>
8+
9+
<Component let:foo>
10+
<div>{foo}</div>
11+
</Component>
12+
13+
<Component let:foo={bar}>
14+
<div>{bar}</div>
15+
</Component>
16+
17+
<svelte:component this={Component} let:foo>
18+
<div>{foo}</div>
19+
</svelte:component>
20+
21+
<Component>
22+
<div slot="named">x</div>
23+
</Component>
24+
25+
<Component>
26+
<div slot="named">
27+
<p>multi</p>
28+
<p>line</p>
29+
</div>
30+
</Component>
31+
32+
<Component>
33+
<svelte:element this={'div'} slot="named">x</svelte:element>
34+
</Component>
35+
36+
<Component>
37+
<div slot="foo" let:foo>{foo}</div>
38+
<div slot="bar" let:foo={bar}>{bar}</div>
39+
</Component>
40+
41+
<Component let:foo>
42+
{foo}
43+
<div slot="named">x</div>
44+
</Component>
45+
46+
<Component>
47+
<svelte:fragment let:foo>{foo}</svelte:fragment>
48+
</Component>
49+
50+
<Component>
51+
<svelte:fragment slot="named" let:foo>{foo}</svelte:fragment>
52+
</Component>
53+
54+
<Component>
55+
<div slot="foo">foo</div>
56+
OMG WHY
57+
<div slot="bar">bar</div>
58+
</Component>
59+
60+
<Component>
61+
If you do mix slots like this
62+
<div slot="foo">foo</div>
63+
you're a monster
64+
<div slot="bar">bar</div>
65+
</Component>
66+
67+
<Component let:omg>
68+
<div slot="foo">foo</div>
69+
{omg} WHY
70+
<div slot="bar">bar</div>
71+
</Component>
72+
73+
<Component let:monster>
74+
If you do mix slots like this
75+
<div slot="foo">foo</div>
76+
you're a {monster}
77+
<div slot="bar">bar</div>
78+
</Component>
79+
80+
<c-e>
81+
<div slot="named">unchanged</div>
82+
</c-e>

0 commit comments

Comments
 (0)