Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/small-suns-lie.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'svelte': patch
---

feat: migrate slot usages
119 changes: 115 additions & 4 deletions packages/svelte/src/compiler/migrate/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,11 @@ 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, extract_all_identifiers_from_expression } from '../utils/ast.js';
import {
extract_identifiers,
extract_all_identifiers_from_expression,
is_text_attribute
} from '../utils/ast.js';
import { migrate_svelte_ignore } from '../utils/extract_svelte_ignore.js';
import { validate_component_options } from '../validate-options.js';
import { is_svg, is_void } from '../../utils.js';
Expand Down Expand Up @@ -711,7 +715,8 @@ const template = {
Identifier(node, { state, path }) {
handle_identifier(node, state, path);
},
RegularElement(node, { state, next }) {
RegularElement(node, { state, path, next }) {
migrate_slot_usage(node, path, state);
handle_events(node, state);
// Strip off any namespace from the beginning of the node name.
const node_name = node.name.replace(/[a-zA-Z-]*:/g, '');
Expand All @@ -724,7 +729,9 @@ const template = {
}
next();
},
SvelteElement(node, { state, next }) {
SvelteElement(node, { state, path, next }) {
migrate_slot_usage(node, path, state);

if (node.tag.type === 'Literal') {
let is_static = true;

Expand All @@ -748,9 +755,15 @@ const template = {
handle_events(node, state);
next();
},
Component(node, { state, path, next }) {
next();
migrate_slot_usage(node, path, state);
},
SvelteComponent(node, { state, next, path }) {
next();

migrate_slot_usage(node, path, state);

let expression = state.str
.snip(
/** @type {number} */ (node.expression.start),
Expand Down Expand Up @@ -816,6 +829,10 @@ const template = {
const end_pos = state.str.original.indexOf('}', node.expression.end) + 1;
state.str.remove(this_pos, end_pos);
},
SvelteFragment(node, { state, path, next }) {
migrate_slot_usage(node, path, state);
next();
},
SvelteWindow(node, { state, next }) {
handle_events(node, state);
next();
Expand All @@ -828,7 +845,9 @@ const template = {
handle_events(node, state);
next();
},
SlotElement(node, { state, next, visit }) {
SlotElement(node, { state, path, next, visit }) {
migrate_slot_usage(node, path, state);

if (state.analysis.custom_element) return;
let name = 'children';
let slot_name = 'default';
Expand Down Expand Up @@ -915,6 +934,98 @@ const template = {
}
};

/**
* @param {AST.RegularElement | AST.SvelteElement | AST.SvelteComponent | AST.Component | AST.SlotElement | AST.SvelteFragment} node
* @param {SvelteNode[]} path
* @param {State} state
*/
function migrate_slot_usage(node, path, state) {
const parent = path.at(-2);
// Bail on custom element slot usage
if (
parent?.type !== 'Component' &&
parent?.type !== 'SvelteComponent' &&
node.type !== 'Component' &&
node.type !== 'SvelteComponent'
) {
return;
}

let snippet_name = 'children';
let snippet_props = [];

for (let attribute of node.attributes) {
if (
attribute.type === 'Attribute' &&
attribute.name === 'slot' &&
is_text_attribute(attribute)
) {
snippet_name = attribute.value[0].data;
state.str.remove(attribute.start, attribute.end);
}
if (attribute.type === 'LetDirective') {
snippet_props.push(
attribute.name +
(attribute.expression
? `: ${state.str.original.substring(/** @type {number} */ (attribute.expression.start), /** @type {number} */ (attribute.expression.end))}`
: '')
);
state.str.remove(attribute.start, attribute.end);
}
}

if (node.type === 'SvelteFragment' && node.fragment.nodes.length > 0) {
// remove node itself, keep content
state.str.remove(node.start, node.fragment.nodes[0].start);
state.str.remove(node.fragment.nodes[node.fragment.nodes.length - 1].end, node.end);
}

const props = snippet_props.length > 0 ? `{ ${snippet_props.join(', ')} }` : '';

if (snippet_name === 'children' && node.type !== 'SvelteFragment') {
if (snippet_props.length === 0) return; // nothing to do

// Default slot: wrap children in a snippet (TODO: named slots)
const inner_start = node.fragment.nodes[0].start;
let inner_end = node.fragment.nodes[node.fragment.nodes.length - 1].end;
for (let i = 0; i < node.fragment.nodes.length; i++) {
const inner = node.fragment.nodes[i];
if (
(inner.type === 'RegularElement' ||
inner.type === 'SvelteElement' ||
inner.type === 'Component' ||
inner.type === 'SvelteComponent' ||
inner.type === 'SlotElement' ||
inner.type === 'SvelteFragment') &&
inner.attributes.some((attr) => attr.type === 'Attribute' && attr.name === 'slot')
) {
// Assumption: People don't have default content mixed with named slots
inner_end = inner.start;
break;
}
}

state.str.appendLeft(
inner_start,
`\n${state.indent.repeat(path.length)}{#snippet ${snippet_name}(${props})}`
);
state.str.indent(state.indent, {
exclude: [
[0, inner_start],
[inner_end, state.str.original.length]
]
});
state.str.prependLeft(
inner_end,
`${state.indent.repeat(path.length)}{/snippet}\n${state.indent.repeat(path.length - 1)}`
);
} else {
// Named slot or `svelte:fragment`: wrap element itself in a snippet
state.str.prependLeft(node.start, `{#snippet ${snippet_name}(${props})}`);
state.str.appendRight(node.end, `{/snippet}`);
}
}

/**
* @param {VariableDeclarator} declarator
* @param {MagicString} str
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export default { solo: false };
49 changes: 49 additions & 0 deletions packages/svelte/tests/migrate/samples/slot-usages/input.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
<Component>
unchanged
</Component>

<svelte:component this={Component}>
unchanged
</svelte:component>

<Component let:foo>
<div>{foo}</div>
</Component>

<Component let:foo={bar}>
<div>{bar}</div>
</Component>

<svelte:component this={Component} let:foo>
<div>{foo}</div>
</svelte:component>

<Component>
<div slot="named">x</div>
</Component>

<Component>
<svelte:element this={'div'} slot="named">x</svelte:element>
</Component>

<Component>
<div slot="foo" let:foo>{foo}</div>
<div slot="bar" let:foo={bar}>{bar}</div>
</Component>

<Component let:foo>
{foo}
<div slot="named">x</div>
</Component>

<Component>
<svelte:fragment let:foo>{foo}</svelte:fragment>
</Component>

<Component>
<svelte:fragment slot="named" let:foo>{foo}</svelte:fragment>
</Component>

<c-e>
<div slot="named">unchanged</div>
</c-e>
57 changes: 57 additions & 0 deletions packages/svelte/tests/migrate/samples/slot-usages/output.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
<Component>
unchanged
</Component>

<Component>
unchanged
</Component>

<Component >
{#snippet children({ foo })}
<div>{foo}</div>
{/snippet}
</Component>

<Component >
{#snippet children({ foo: bar })}
<div>{bar}</div>
{/snippet}
</Component>

<Component >
{#snippet children({ foo })}
<div>{foo}</div>
{/snippet}
</Component>

<Component>
{#snippet named()}<div >x</div>{/snippet}
</Component>

<Component>
{#snippet named()}<svelte:element this={'div'} >x</svelte:element>{/snippet}
</Component>

<Component>
{#snippet foo({ foo })}<div >{foo}</div>{/snippet}
{#snippet bar({ foo: bar })}<div >{bar}</div>{/snippet}
</Component>

<Component >
{#snippet children({ foo })}
{foo}
{/snippet}
{#snippet named()}<div >x</div>{/snippet}
</Component>

<Component>
{#snippet children({ foo })}{foo}{/snippet}
</Component>

<Component>
{#snippet named({ foo })}{foo}{/snippet}
</Component>

<c-e>
<div slot="named">unchanged</div>
</c-e>
Original file line number Diff line number Diff line change
Expand Up @@ -7,74 +7,86 @@
const SvelteComponent_10 = $derived(Math.random() > .5 ? rest.heads : rest.tail);
</script>

<Component let:Comp>
<Comp />
<Component >
{#snippet children({ Comp })}
<Comp />
{/snippet}
</Component>

<Component let:comp>
{@const SvelteComponent = comp}
<Component >
{#snippet children({ comp })}
{@const SvelteComponent = comp}
<SvelteComponent />
{/snippet}
</Component>

<Component let:comp={stuff}>
{@const SvelteComponent_1 = stuff}
<Component >
{#snippet children({ comp: stuff })}
{@const SvelteComponent_1 = stuff}
<SvelteComponent_1 />
{/snippet}
</Component>

<Component>
{@const SvelteComponent_2 = stuff}
<div slot="x" let:comp={stuff}>
{#snippet x({ comp: stuff })}<div >
<SvelteComponent_2 />
</div>
</div>{/snippet}
</Component>

<Component>
{@const SvelteComponent_3 = stuff}
<svelte:fragment slot="x" let:comp={stuff}>
{#snippet x({ comp: stuff })}
<SvelteComponent_3 />
</svelte:fragment>
{/snippet}
</Component>

<Component>
{@const SvelteComponent_4 = stuff}
<svelte:element this={"div"} slot="x" let:comp={stuff}>
{#snippet x({ comp: stuff })}<svelte:element this={"div"} >
<SvelteComponent_4 />
</svelte:element>
</svelte:element>{/snippet}
</Component>

<Component let:Comp>
<Comp />
<Component >
{#snippet children({ Comp })}
<Comp />
{/snippet}
</Component>

<Component let:comp>
{@const SvelteComponent_5 = comp}
<Component >
{#snippet children({ comp })}
{@const SvelteComponent_5 = comp}
<SvelteComponent_5 />
{/snippet}
</Component>

<Component let:comp={stuff}>
{@const SvelteComponent_6 = stuff}
<Component >
{#snippet children({ comp: stuff })}
{@const SvelteComponent_6 = stuff}
<SvelteComponent_6 />
{/snippet}
</Component>

<Component>
{@const SvelteComponent_7 = stuff}
<div slot="x" let:comp={stuff}>
{#snippet x({ comp: stuff })}<div >
<SvelteComponent_7 />
</div>
</div>{/snippet}
</Component>

<Component>
{@const SvelteComponent_8 = stuff}
<svelte:fragment slot="x" let:comp={stuff}>
{#snippet x({ comp: stuff })}
<SvelteComponent_8 />
</svelte:fragment>
{/snippet}
</Component>

<Component>
{@const SvelteComponent_9 = stuff}
<svelte:element this={"div"} slot="x" let:comp={stuff}>
{#snippet x({ comp: stuff })}<svelte:element this={"div"} >
<SvelteComponent_9 />
</svelte:element>
</svelte:element>{/snippet}
</Component>

<Component />
Expand Down