feat: migrate slot usages (#13500)

Now that snippets can fill slots, we can add logic to migrate slot usages
pull/13515/head
Simon H 11 months ago committed by GitHub
parent aa3f002465
commit 687d9dbef2
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -0,0 +1,5 @@
---
'svelte': patch
---
feat: migrate slot usages

@ -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';
@ -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, '');
@ -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;
@ -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),
@ -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}`
);
@ -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();
@ -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';
@ -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

@ -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>

@ -0,0 +1,126 @@
<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()}
<div >
<p>multi</p>
<p>line</p>
</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>
<Component>
{#snippet foo()}
<div >foo</div>
{/snippet}
OMG WHY
{#snippet bar()}
<div >bar</div>
{/snippet}
</Component>
<Component>
If you do mix slots like this
{#snippet foo()}
<div >foo</div>
{/snippet}
you're a monster
{#snippet bar()}
<div >bar</div>
{/snippet}
</Component>
<Component >
{#snippet foo()}
<div >foo</div>
{/snippet}
{#snippet children({ omg })}
{omg} WHY
{/snippet}
{#snippet bar()}
<div >bar</div>
{/snippet}
</Component>
<Component >{#snippet children({ monster })}
If you do mix slots like this
you're a {monster}{/snippet}
{#snippet foo()}
<div >foo</div>
{/snippet}
{#snippet bar()}
<div >bar</div>
{/snippet}
</Component>
<c-e>
<div slot="named">unchanged</div>
</c-e>

@ -7,74 +7,98 @@
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}>
<SvelteComponent_2 />
</div>
{#snippet x({ comp: stuff })}
{@const SvelteComponent_2 = stuff}
<div >
<SvelteComponent_2 />
</div>
{/snippet}
</Component>
<Component>
{#snippet x({ comp: stuff })}
{@const SvelteComponent_3 = stuff}
<svelte:fragment slot="x" let:comp={stuff}>
<SvelteComponent_3 />
</svelte:fragment>
<SvelteComponent_3 />
{/snippet}
</Component>
<Component>
{@const SvelteComponent_4 = stuff}
<svelte:element this={"div"} slot="x" let:comp={stuff}>
<SvelteComponent_4 />
</svelte:element>
{#snippet x({ comp: stuff })}
{@const SvelteComponent_4 = stuff}
<svelte:element this={"div"} >
<SvelteComponent_4 />
</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}>
<SvelteComponent_7 />
</div>
{#snippet x({ comp: stuff })}
{@const SvelteComponent_7 = stuff}
<div >
<SvelteComponent_7 />
</div>
{/snippet}
</Component>
<Component>
{#snippet x({ comp: stuff })}
{@const SvelteComponent_8 = stuff}
<svelte:fragment slot="x" let:comp={stuff}>
<SvelteComponent_8 />
</svelte:fragment>
<SvelteComponent_8 />
{/snippet}
</Component>
<Component>
{@const SvelteComponent_9 = stuff}
<svelte:element this={"div"} slot="x" let:comp={stuff}>
<SvelteComponent_9 />
</svelte:element>
{#snippet x({ comp: stuff })}
{@const SvelteComponent_9 = stuff}
<svelte:element this={"div"} >
<SvelteComponent_9 />
</svelte:element>
{/snippet}
</Component>
<Component />

Loading…
Cancel
Save