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
+
+
+
+
+
+
+
+ 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()}
+
+ {/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}