diff --git a/.changeset/small-suns-lie.md b/.changeset/small-suns-lie.md new file mode 100644 index 0000000000..4295555164 --- /dev/null +++ b/.changeset/small-suns-lie.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +feat: migrate slot usages diff --git a/packages/svelte/src/compiler/migrate/index.js b/packages/svelte/src/compiler/migrate/index.js index 0bf6b371f4..393b34f5b2 100644 --- a/packages/svelte/src/compiler/migrate/index.js +++ b/packages/svelte/src/compiler/migrate/index.js @@ -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 diff --git a/packages/svelte/tests/migrate/samples/slot-usages/input.svelte b/packages/svelte/tests/migrate/samples/slot-usages/input.svelte new file mode 100644 index 0000000000..5646174c4c --- /dev/null +++ b/packages/svelte/tests/migrate/samples/slot-usages/input.svelte @@ -0,0 +1,82 @@ + + unchanged + + + + unchanged + + + +
{foo}
+
+ + +
{bar}
+
+ + +
{foo}
+
+ + +
x
+
+ + +
+

multi

+

line

+
+
+ + + x + + + +
{foo}
+
{bar}
+
+ + + {foo} +
x
+
+ + + {foo} + + + + {foo} + + + +
foo
+ OMG WHY +
bar
+
+ + + If you do mix slots like this +
foo
+ you're a monster +
bar
+
+ + +
foo
+ {omg} WHY +
bar
+
+ + + If you do mix slots like this +
foo
+ you're a {monster} +
bar
+
+ + +
unchanged
+
diff --git a/packages/svelte/tests/migrate/samples/slot-usages/output.svelte b/packages/svelte/tests/migrate/samples/slot-usages/output.svelte new file mode 100644 index 0000000000..42e219a567 --- /dev/null +++ b/packages/svelte/tests/migrate/samples/slot-usages/output.svelte @@ -0,0 +1,126 @@ + + unchanged + + + + unchanged + + + + {#snippet children({ foo })} +
{foo}
+ {/snippet} +
+ + + {#snippet children({ foo: bar })} +
{bar}
+ {/snippet} +
+ + + {#snippet children({ foo })} +
{foo}
+ {/snippet} +
+ + + {#snippet named()} +
x
+ {/snippet} +
+ + + {#snippet named()} +
+

multi

+

line

+
+ {/snippet} +
+ + + {#snippet named()} + x + {/snippet} + + + + {#snippet foo({ foo })} +
{foo}
+ {/snippet} + {#snippet bar({ foo: bar })} +
{bar}
+ {/snippet} +
+ + + {#snippet children({ foo })} + {foo} + {/snippet} + {#snippet named()} +
x
+ {/snippet} +
+ + + {#snippet children({ foo })} + {foo} + {/snippet} + + + + {#snippet named({ foo })} + {foo} + {/snippet} + + + + {#snippet foo()} +
foo
+ {/snippet} + OMG WHY + {#snippet bar()} +
bar
+ {/snippet} +
+ + + If you do mix slots like this + {#snippet foo()} +
foo
+ {/snippet} + you're a monster + {#snippet bar()} +
bar
+ {/snippet} +
+ + + {#snippet foo()} +
foo
+ {/snippet} + {#snippet children({ omg })} + {omg} WHY + {/snippet} + {#snippet bar()} +
bar
+ {/snippet} +
+ +{#snippet children({ monster })} + + If you do mix slots like this + + you're a {monster}{/snippet} + {#snippet foo()} +
foo
+ {/snippet} + {#snippet bar()} +
bar
+ {/snippet} +
+ + +
unchanged
+
\ No newline at end of file diff --git a/packages/svelte/tests/migrate/samples/svelte-component/output.svelte b/packages/svelte/tests/migrate/samples/svelte-component/output.svelte index bba6fc6fa7..26a007aa3c 100644 --- a/packages/svelte/tests/migrate/samples/svelte-component/output.svelte +++ b/packages/svelte/tests/migrate/samples/svelte-component/output.svelte @@ -7,74 +7,98 @@ const SvelteComponent_10 = $derived(Math.random() > .5 ? rest.heads : rest.tail); - - + + {#snippet children({ Comp })} + + {/snippet} - - {@const SvelteComponent = comp} + + {#snippet children({ comp })} + {@const SvelteComponent = comp} + {/snippet} - - {@const SvelteComponent_1 = stuff} + + {#snippet children({ comp: stuff })} + {@const SvelteComponent_1 = stuff} + {/snippet} - {@const SvelteComponent_2 = stuff} -
- -
+ {#snippet x({ comp: stuff })} + {@const SvelteComponent_2 = stuff} +
+ +
+ {/snippet}
+ {#snippet x({ comp: stuff })} {@const SvelteComponent_3 = stuff} - - - + + + + {/snippet} - {@const SvelteComponent_4 = stuff} - - - + {#snippet x({ comp: stuff })} + {@const SvelteComponent_4 = stuff} + + + + {/snippet} - - + + {#snippet children({ Comp })} + + {/snippet} - - {@const SvelteComponent_5 = comp} + + {#snippet children({ comp })} + {@const SvelteComponent_5 = comp} + {/snippet} - - {@const SvelteComponent_6 = stuff} + + {#snippet children({ comp: stuff })} + {@const SvelteComponent_6 = stuff} + {/snippet} - {@const SvelteComponent_7 = stuff} -
- -
+ {#snippet x({ comp: stuff })} + {@const SvelteComponent_7 = stuff} +
+ +
+ {/snippet}
+ {#snippet x({ comp: stuff })} {@const SvelteComponent_8 = stuff} - - - + + + + {/snippet} - {@const SvelteComponent_9 = stuff} - - - + {#snippet x({ comp: stuff })} + {@const SvelteComponent_9 = stuff} + + + + {/snippet}