Skip to content

Commit

Permalink
feat: migrate slot usages (#13500)
Browse files Browse the repository at this point in the history
Now that snippets can fill slots, we can add logic to migrate slot usages
  • Loading branch information
dummdidumm authored Oct 4, 2024
1 parent aa3f002 commit 687d9db
Show file tree
Hide file tree
Showing 5 changed files with 418 additions and 39 deletions.
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
152 changes: 147 additions & 5 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 @@ -789,7 +802,7 @@ const template = {
state.str.original.lastIndexOf('\n', position) + 1,
position
);
state.str.prependLeft(
state.str.appendRight(
position,
`{@const ${expression} = ${current_expression}}\n${indent}`
);
Expand All @@ -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,129 @@ 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

let inner_start = 0;
let inner_end = 0;
for (let i = 0; i < node.fragment.nodes.length; i++) {
const inner = node.fragment.nodes[i];
const is_empty_text = inner.type === 'Text' && !inner.data.trim();

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')
) {
if (inner_start && !inner_end) {
// End of default slot content
inner_end = inner.start;
}
} else if (!inner_start && !is_empty_text) {
// Start of default slot content
inner_start = inner.start;
} else if (inner_end && !is_empty_text) {
// There was default slot content before, then some named slot content, now some default slot content again.
// We're moving the last character back by one to avoid the closing {/snippet} tag inserted afterwards
// to come before the opening {#snippet} tag of the named slot.
state.str.update(inner_end - 1, inner_end, '');
state.str.prependLeft(inner_end - 1, state.str.original[inner_end - 1]);
state.str.move(inner.start, inner.end, inner_end - 1);
}
}

if (!inner_end) {
inner_end = node.fragment.nodes[node.fragment.nodes.length - 1].end;
}

state.str.appendLeft(
inner_start,
`{#snippet ${snippet_name}(${props})}\n${state.indent.repeat(path.length)}`
);
state.str.indent(state.indent, {
exclude: [
[0, inner_start],
[inner_end, state.str.original.length]
]
});
if (inner_end < node.fragment.nodes[node.fragment.nodes.length - 1].end) {
// Named slots coming afterwards
state.str.prependLeft(inner_end, `{/snippet}\n${state.indent.repeat(path.length)}`);
} else {
// No named slots coming afterwards
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})}\n${state.indent.repeat(path.length - 2)}`
);
state.str.indent(state.indent, {
exclude: [
[0, node.start],
[node.end, state.str.original.length]
]
});
state.str.appendLeft(node.end, `\n${state.indent.repeat(path.length - 2)}{/snippet}`);
}
}

/**
* @param {VariableDeclarator} declarator
* @param {MagicString} str
Expand Down
82 changes: 82 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,82 @@
<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>
<div slot="named">
<p>multi</p>
<p>line</p>
</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>

<Component>
<div slot="foo">foo</div>
OMG WHY
<div slot="bar">bar</div>
</Component>

<Component>
If you do mix slots like this
<div slot="foo">foo</div>
you're a monster
<div slot="bar">bar</div>
</Component>

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

<Component let:monster>
If you do mix slots like this
<div slot="foo">foo</div>
you're a {monster}
<div slot="bar">bar</div>
</Component>

<c-e>
<div slot="named">unchanged</div>
</c-e>
Loading

0 comments on commit 687d9db

Please sign in to comment.