From 378a17e8c8f7c0b6afaa168d42ef16bfdc471b56 Mon Sep 17 00:00:00 2001 From: Simon Holthausen Date: Wed, 13 Mar 2024 23:14:38 +0100 Subject: [PATCH 01/15] feat: provide isSnippet type, deduplicate children prop from default slot fixes #10790 part of #9774 --- .changeset/cold-cheetahs-judge.md | 5 ++++ .changeset/famous-grapes-refuse.md | 5 ++++ .../3-transform/client/visitors/template.js | 26 ++++++++++-------- .../3-transform/server/transform-server.js | 27 ++++++++++--------- .../src/internal/client/dom/blocks/snippet.js | 19 +++++++++++++ .../src/internal/client/dom/legacy/misc.js | 15 +++++++++++ .../svelte/src/internal/client/validate.js | 15 +++-------- packages/svelte/src/internal/server/index.js | 2 ++ packages/svelte/src/legacy/legacy-client.js | 2 +- packages/svelte/src/main/main-client.js | 3 ++- packages/svelte/src/main/main-server.js | 3 ++- .../samples/slot-children-prop/A.svelte | 6 +++++ .../samples/slot-children-prop/_config.js | 5 ++++ .../samples/slot-children-prop/main.svelte | 7 +++++ packages/svelte/tests/types/snippet.ts | 7 ++++- packages/svelte/types/index.d.ts | 18 ++++++++++++- 16 files changed, 125 insertions(+), 40 deletions(-) create mode 100644 .changeset/cold-cheetahs-judge.md create mode 100644 .changeset/famous-grapes-refuse.md create mode 100644 packages/svelte/tests/runtime-legacy/samples/slot-children-prop/A.svelte create mode 100644 packages/svelte/tests/runtime-legacy/samples/slot-children-prop/_config.js create mode 100644 packages/svelte/tests/runtime-legacy/samples/slot-children-prop/main.svelte diff --git a/.changeset/cold-cheetahs-judge.md b/.changeset/cold-cheetahs-judge.md new file mode 100644 index 000000000000..63302f29e866 --- /dev/null +++ b/.changeset/cold-cheetahs-judge.md @@ -0,0 +1,5 @@ +--- +"svelte": patch +--- + +fix: deduplicate children prop and default slot diff --git a/.changeset/famous-grapes-refuse.md b/.changeset/famous-grapes-refuse.md new file mode 100644 index 000000000000..54246e918a91 --- /dev/null +++ b/.changeset/famous-grapes-refuse.md @@ -0,0 +1,5 @@ +--- +"svelte": patch +--- + +feat: provide `isSnippet` function to determine whether a given value is a snippet diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/template.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/template.js index e5302a3a7c3b..11b62fa05661 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/template.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/template.js @@ -774,6 +774,13 @@ function serialize_inline_component(node, component_name, context) { */ let slot_scope_applies_to_itself = false; + /** + * Components may have a children prop and also have child nodes. In this case, we assume + * that the child component isn't using render tags yet and pass the slot as $$slots.default. + * We're not doing it for spread attributes, as this would result in too many false positives. + */ + let has_children_prop = false; + /** * @param {import('estree').Property} prop */ @@ -823,6 +830,10 @@ function serialize_inline_component(node, component_name, context) { slot_scope_applies_to_itself = true; } + if (attribute.name === 'children') { + has_children_prop = true; + } + const [, value] = serialize_attribute_value(attribute.value, context); if (attribute.metadata.dynamic) { @@ -944,13 +955,8 @@ function serialize_inline_component(node, component_name, context) { b.block([...(slot_name === 'default' && !slot_scope_applies_to_itself ? lets : []), ...body]) ); - if (slot_name === 'default') { - push_prop( - b.init( - 'children', - context.state.options.dev ? b.call('$.add_snippet_symbol', slot_fn) : slot_fn - ) - ); + if (slot_name === 'default' && !has_children_prop) { + push_prop(b.init('children', b.call('$.add_snippet_symbol', slot_fn))); } else { serialized_slots.push(b.init(slot_name, slot_fn)); } @@ -2685,9 +2691,7 @@ export const template_visitors = { } else { context.state.init.push(b.const(node.expression, b.arrow(args, body))); } - if (context.state.options.dev) { - context.state.init.push(b.stmt(b.call('$.add_snippet_symbol', node.expression))); - } + context.state.init.push(b.stmt(b.call('$.add_snippet_symbol', node.expression))); }, FunctionExpression: function_visitor, ArrowFunctionExpression: function_visitor, @@ -3106,7 +3110,7 @@ export const template_visitors = { ); const expression = is_default - ? b.member(b.id('$$props'), b.id('children')) + ? b.call('$.default_slot', b.id('$$props')) : b.member(b.member(b.id('$$props'), b.id('$$slots')), name, true, true); const slot = b.call('$.slot', context.state.node, expression, props_expression, fallback); diff --git a/packages/svelte/src/compiler/phases/3-transform/server/transform-server.js b/packages/svelte/src/compiler/phases/3-transform/server/transform-server.js index 8ff7d2c31687..6946d4362ffb 100644 --- a/packages/svelte/src/compiler/phases/3-transform/server/transform-server.js +++ b/packages/svelte/src/compiler/phases/3-transform/server/transform-server.js @@ -947,6 +947,13 @@ function serialize_inline_component(node, component_name, context) { */ let slot_scope_applies_to_itself = false; + /** + * Components may have a children prop and also have child nodes. In this case, we assume + * that the child component isn't using render tags yet and pass the slot as $$slots.default. + * We're not doing it for spread attributes, as this would result in too many false positives. + */ + let has_children_prop = false; + /** * @param {import('estree').Property} prop */ @@ -975,6 +982,10 @@ function serialize_inline_component(node, component_name, context) { slot_scope_applies_to_itself = true; } + if (attribute.name === 'children') { + has_children_prop = true; + } + const value = serialize_attribute_value(attribute.value, context, false, true); push_prop(b.prop('init', b.key(attribute.name), value)); } else if (attribute.type === 'BindDirective' && attribute.name !== 'this') { @@ -1049,14 +1060,8 @@ function serialize_inline_component(node, component_name, context) { b.block([...(slot_name === 'default' && !slot_scope_applies_to_itself ? lets : []), ...body]) ); - if (slot_name === 'default') { - push_prop( - b.prop( - 'init', - b.id('children'), - context.state.options.dev ? b.call('$.add_snippet_symbol', slot_fn) : slot_fn - ) - ); + if (slot_name === 'default' && !has_children_prop) { + push_prop(b.prop('init', b.id('children'), b.call('$.add_snippet_symbol', slot_fn))); } else { const slot = b.prop('init', b.literal(slot_name), slot_fn); serialized_slots.push(slot); @@ -1614,9 +1619,7 @@ const template_visitors = { ) ); - if (context.state.options.dev) { - context.state.init.push(b.stmt(b.call('$.add_snippet_symbol', node.expression))); - } + context.state.init.push(b.stmt(b.call('$.add_snippet_symbol', node.expression))); }, Component(node, context) { const state = context.state; @@ -1744,7 +1747,7 @@ const template_visitors = { const lets = []; /** @type {import('estree').Expression} */ - let expression = b.member_id('$$props.children'); + let expression = b.call('$.default_slot', b.id('$$props')); for (const attribute of node.attributes) { if (attribute.type === 'SpreadAttribute') { diff --git a/packages/svelte/src/internal/client/dom/blocks/snippet.js b/packages/svelte/src/internal/client/dom/blocks/snippet.js index c07eca4d441c..4bb1c1cc57b3 100644 --- a/packages/svelte/src/internal/client/dom/blocks/snippet.js +++ b/packages/svelte/src/internal/client/dom/blocks/snippet.js @@ -38,3 +38,22 @@ export function snippet(get_snippet, node, ...args) { }; }, block); } + +const snippet_symbol = Symbol.for('svelte.snippet'); + +/** + * @param {any} fn + */ +export function add_snippet_symbol(fn) { + fn[snippet_symbol] = true; + return fn; +} + +/** + * Returns true if given parameter is a snippet. + * @param {any} maybeSnippet + * @returns {maybeSnippet is import('svelte').Snippet} + */ +export function isSnippet(maybeSnippet) { + return /** @type {any} */ (maybeSnippet)?.[snippet_symbol] === true; +} diff --git a/packages/svelte/src/internal/client/dom/legacy/misc.js b/packages/svelte/src/internal/client/dom/legacy/misc.js index a4b52efe283c..d137a1068d3a 100644 --- a/packages/svelte/src/internal/client/dom/legacy/misc.js +++ b/packages/svelte/src/internal/client/dom/legacy/misc.js @@ -1,6 +1,7 @@ import { set, source } from '../../reactivity/sources.js'; import { get } from '../../runtime.js'; import { is_array } from '../../utils.js'; +import { isSnippet } from '../blocks/snippet.js'; /** * Under some circumstances, imports may be reactive in legacy mode. In that case, @@ -66,3 +67,17 @@ export function update_legacy_props($$new_props) { } } } + +/** + * @param {Record} $$props + */ +export function default_slot($$props) { + var children = $$props.$$slots?.default; + if (children) { + return children; + } + children = $$props.children; + if (isSnippet(children)) { + return children; + } +} diff --git a/packages/svelte/src/internal/client/validate.js b/packages/svelte/src/internal/client/validate.js index 442d91190b9a..1dde94a493e7 100644 --- a/packages/svelte/src/internal/client/validate.js +++ b/packages/svelte/src/internal/client/validate.js @@ -1,3 +1,4 @@ +import { isSnippet } from './dom/blocks/snippet.js'; import { untrack } from './runtime.js'; import { is_array } from './utils.js'; @@ -103,22 +104,12 @@ export function loop_guard(timeout) { }; } -const snippet_symbol = Symbol.for('svelte.snippet'); - -/** - * @param {any} fn - */ -export function add_snippet_symbol(fn) { - fn[snippet_symbol] = true; - return fn; -} - /** * Validate that the function handed to `{@render ...}` is a snippet function, and not some other kind of function. * @param {any} snippet_fn */ export function validate_snippet(snippet_fn) { - if (snippet_fn && snippet_fn[snippet_symbol] !== true) { + if (snippet_fn && !isSnippet(snippet_fn)) { throw new Error( 'The argument to `{@render ...}` must be a snippet function, not a component or some other kind of function. ' + 'If you want to dynamically render one snippet or another, use `$derived` and pass its result to `{@render ...}`.' @@ -132,7 +123,7 @@ export function validate_snippet(snippet_fn) { * @param {any} component_fn */ export function validate_component(component_fn) { - if (component_fn?.[snippet_symbol] === true) { + if (isSnippet(component_fn)) { throw new Error('A snippet must be rendered with `{@render ...}`'); } return component_fn; diff --git a/packages/svelte/src/internal/server/index.js b/packages/svelte/src/internal/server/index.js index a6b0001d33ad..9ea6b91f4c8c 100644 --- a/packages/svelte/src/internal/server/index.js +++ b/packages/svelte/src/internal/server/index.js @@ -11,6 +11,8 @@ import { DEV } from 'esm-env'; import { UNINITIALIZED } from '../client/constants.js'; export * from '../client/validate.js'; +export { add_snippet_symbol } from '../client/dom/blocks/snippet.js'; +export { default_slot } from '../client/dom/legacy/misc.js'; /** * @typedef {{ diff --git a/packages/svelte/src/legacy/legacy-client.js b/packages/svelte/src/legacy/legacy-client.js index 09f122e37023..482628660e94 100644 --- a/packages/svelte/src/legacy/legacy-client.js +++ b/packages/svelte/src/legacy/legacy-client.js @@ -12,7 +12,7 @@ import * as $ from '../internal/index.js'; * @template {Record} Slots * * @param {import('../main/public.js').ComponentConstructorOptions & { - * component: import('../main/public.js').SvelteComponent; + * component: typeof import('../main/public.js').SvelteComponent; * immutable?: boolean; * hydrate?: boolean; * recover?: boolean; diff --git a/packages/svelte/src/main/main-client.js b/packages/svelte/src/main/main-client.js index 58461692b63f..99086b7ab54e 100644 --- a/packages/svelte/src/main/main-client.js +++ b/packages/svelte/src/main/main-client.js @@ -181,5 +181,6 @@ export { hasContext, getContext, getAllContexts, - setContext + setContext, + isSnippet } from '../internal/index.js'; diff --git a/packages/svelte/src/main/main-server.js b/packages/svelte/src/main/main-server.js index 58ee20e9aea5..d332520a0f0c 100644 --- a/packages/svelte/src/main/main-server.js +++ b/packages/svelte/src/main/main-server.js @@ -12,7 +12,8 @@ export { tick, unmount, untrack, - createRoot + createRoot, + isSnippet } from './main-client.js'; /** @returns {void} */ diff --git a/packages/svelte/tests/runtime-legacy/samples/slot-children-prop/A.svelte b/packages/svelte/tests/runtime-legacy/samples/slot-children-prop/A.svelte new file mode 100644 index 000000000000..32735e38d6fa --- /dev/null +++ b/packages/svelte/tests/runtime-legacy/samples/slot-children-prop/A.svelte @@ -0,0 +1,6 @@ + + +{children} + diff --git a/packages/svelte/tests/runtime-legacy/samples/slot-children-prop/_config.js b/packages/svelte/tests/runtime-legacy/samples/slot-children-prop/_config.js new file mode 100644 index 000000000000..3add2f34f319 --- /dev/null +++ b/packages/svelte/tests/runtime-legacy/samples/slot-children-prop/_config.js @@ -0,0 +1,5 @@ +import { test } from '../../test'; + +export default test({ + html: `foo bar` +}); diff --git a/packages/svelte/tests/runtime-legacy/samples/slot-children-prop/main.svelte b/packages/svelte/tests/runtime-legacy/samples/slot-children-prop/main.svelte new file mode 100644 index 000000000000..15d898bdd381 --- /dev/null +++ b/packages/svelte/tests/runtime-legacy/samples/slot-children-prop/main.svelte @@ -0,0 +1,7 @@ + + + + bar + \ No newline at end of file diff --git a/packages/svelte/tests/types/snippet.ts b/packages/svelte/tests/types/snippet.ts index edc5aba12378..75553419b4a9 100644 --- a/packages/svelte/tests/types/snippet.ts +++ b/packages/svelte/tests/types/snippet.ts @@ -1,4 +1,4 @@ -import type { Snippet } from 'svelte'; +import { type Snippet, isSnippet } from 'svelte'; const return_type: ReturnType = null as any; @@ -38,3 +38,8 @@ const h: Snippet<[{ a: true }]> = (a) => { const i: Snippet = () => { return return_type; }; + +let j = null as any; +if (isSnippet(j)) { + let x: Snippet = j; +} diff --git a/packages/svelte/types/index.d.ts b/packages/svelte/types/index.d.ts index 567f0b0dcf98..4b994010d4c6 100644 --- a/packages/svelte/types/index.d.ts +++ b/packages/svelte/types/index.d.ts @@ -291,6 +291,12 @@ declare module 'svelte' { * Anything except a function */ type NotFunction = T extends Function ? never : T; + /** + * Returns true if given parameter is a snippet. + * */ + export function isSnippet(maybeSnippet: any): maybeSnippet is (this: void) => unique symbol & { + _: "functions passed to {@render ...} tags must use the `Snippet` type imported from \"svelte\""; + }; /** * @deprecated Use `mount` or `hydrate` instead */ @@ -1729,7 +1735,17 @@ declare module 'svelte/legacy' { * * */ export function createClassComponent, Exports extends Record, Events extends Record, Slots extends Record>(options: ComponentConstructorOptions & { - component: SvelteComponent; + component: { + new (options: ComponentConstructorOptions unique symbol & { + _: "functions passed to {@render ...} tags must use the `Snippet` type imported from \"svelte\""; + }) | undefined; + } : {})>): SvelteComponent; + }; immutable?: boolean | undefined; hydrate?: boolean | undefined; recover?: boolean | undefined; From b1973f9a55d5f10ceb94edf3b3f2dfdbe5057a45 Mon Sep 17 00:00:00 2001 From: Simon Holthausen Date: Thu, 14 Mar 2024 09:16:53 +0100 Subject: [PATCH 02/15] fix ce bug --- .../src/internal/client/dom/elements/custom-element.js | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/packages/svelte/src/internal/client/dom/elements/custom-element.js b/packages/svelte/src/internal/client/dom/elements/custom-element.js index cd2d2dfc6002..b35c111a88f7 100644 --- a/packages/svelte/src/internal/client/dom/elements/custom-element.js +++ b/packages/svelte/src/internal/client/dom/elements/custom-element.js @@ -2,6 +2,7 @@ import { createClassComponent } from '../../../../legacy/legacy-client.js'; import { destroy_effect, render_effect } from '../../reactivity/effects.js'; import { open, close } from '../template.js'; import { define_property } from '../../utils.js'; +import { add_snippet_symbol } from '../blocks/snippet.js'; /** * @typedef {Object} CustomElementPropDefinition @@ -113,8 +114,8 @@ if (typeof HTMLElement === 'function') { const existing_slots = get_custom_elements_slots(this); for (const name of this.$$s) { if (name in existing_slots) { - if (name === 'default') { - this.$$d.children = create_slot(name); + if (name === 'default' && !this.$$d.children) { + this.$$d.children = add_snippet_symbol(create_slot(name)); } else { $$slots[name] = create_slot(name); } From 9ffad5d69399248823e21b0d8a757fa6136e88f3 Mon Sep 17 00:00:00 2001 From: Simon Holthausen Date: Thu, 14 Mar 2024 10:46:58 +0100 Subject: [PATCH 03/15] remove isSnippet type, adjust test --- packages/svelte/src/internal/client/dom/blocks/snippet.js | 2 +- packages/svelte/src/internal/client/dom/legacy/misc.js | 4 ++-- packages/svelte/src/internal/client/validate.js | 6 +++--- packages/svelte/src/main/main-client.js | 3 +-- packages/svelte/src/main/main-server.js | 3 +-- .../_expected/client/index.svelte.js | 4 ++-- .../_expected/server/index.svelte.js | 4 ++-- packages/svelte/types/index.d.ts | 6 ------ 8 files changed, 12 insertions(+), 20 deletions(-) diff --git a/packages/svelte/src/internal/client/dom/blocks/snippet.js b/packages/svelte/src/internal/client/dom/blocks/snippet.js index 4bb1c1cc57b3..1f63967cd2f3 100644 --- a/packages/svelte/src/internal/client/dom/blocks/snippet.js +++ b/packages/svelte/src/internal/client/dom/blocks/snippet.js @@ -54,6 +54,6 @@ export function add_snippet_symbol(fn) { * @param {any} maybeSnippet * @returns {maybeSnippet is import('svelte').Snippet} */ -export function isSnippet(maybeSnippet) { +export function is_snippet(maybeSnippet) { return /** @type {any} */ (maybeSnippet)?.[snippet_symbol] === true; } diff --git a/packages/svelte/src/internal/client/dom/legacy/misc.js b/packages/svelte/src/internal/client/dom/legacy/misc.js index d137a1068d3a..a1c33221010b 100644 --- a/packages/svelte/src/internal/client/dom/legacy/misc.js +++ b/packages/svelte/src/internal/client/dom/legacy/misc.js @@ -1,7 +1,7 @@ import { set, source } from '../../reactivity/sources.js'; import { get } from '../../runtime.js'; import { is_array } from '../../utils.js'; -import { isSnippet } from '../blocks/snippet.js'; +import { is_snippet } from '../blocks/snippet.js'; /** * Under some circumstances, imports may be reactive in legacy mode. In that case, @@ -77,7 +77,7 @@ export function default_slot($$props) { return children; } children = $$props.children; - if (isSnippet(children)) { + if (is_snippet(children)) { return children; } } diff --git a/packages/svelte/src/internal/client/validate.js b/packages/svelte/src/internal/client/validate.js index 1dde94a493e7..577101fd390e 100644 --- a/packages/svelte/src/internal/client/validate.js +++ b/packages/svelte/src/internal/client/validate.js @@ -1,4 +1,4 @@ -import { isSnippet } from './dom/blocks/snippet.js'; +import { is_snippet } from './dom/blocks/snippet.js'; import { untrack } from './runtime.js'; import { is_array } from './utils.js'; @@ -109,7 +109,7 @@ export function loop_guard(timeout) { * @param {any} snippet_fn */ export function validate_snippet(snippet_fn) { - if (snippet_fn && !isSnippet(snippet_fn)) { + if (snippet_fn && !is_snippet(snippet_fn)) { throw new Error( 'The argument to `{@render ...}` must be a snippet function, not a component or some other kind of function. ' + 'If you want to dynamically render one snippet or another, use `$derived` and pass its result to `{@render ...}`.' @@ -123,7 +123,7 @@ export function validate_snippet(snippet_fn) { * @param {any} component_fn */ export function validate_component(component_fn) { - if (isSnippet(component_fn)) { + if (is_snippet(component_fn)) { throw new Error('A snippet must be rendered with `{@render ...}`'); } return component_fn; diff --git a/packages/svelte/src/main/main-client.js b/packages/svelte/src/main/main-client.js index 99086b7ab54e..58461692b63f 100644 --- a/packages/svelte/src/main/main-client.js +++ b/packages/svelte/src/main/main-client.js @@ -181,6 +181,5 @@ export { hasContext, getContext, getAllContexts, - setContext, - isSnippet + setContext } from '../internal/index.js'; diff --git a/packages/svelte/src/main/main-server.js b/packages/svelte/src/main/main-server.js index d332520a0f0c..58ee20e9aea5 100644 --- a/packages/svelte/src/main/main-server.js +++ b/packages/svelte/src/main/main-server.js @@ -12,8 +12,7 @@ export { tick, unmount, untrack, - createRoot, - isSnippet + createRoot } from './main-client.js'; /** @returns {void} */ diff --git a/packages/svelte/tests/snapshot/samples/function-prop-no-getter/_expected/client/index.svelte.js b/packages/svelte/tests/snapshot/samples/function-prop-no-getter/_expected/client/index.svelte.js index f34a4933a345..c228be30270e 100644 --- a/packages/svelte/tests/snapshot/samples/function-prop-no-getter/_expected/client/index.svelte.js +++ b/packages/svelte/tests/snapshot/samples/function-prop-no-getter/_expected/client/index.svelte.js @@ -21,14 +21,14 @@ export default function Function_prop_no_getter($$anchor, $$props) { onmousedown: () => $.set(count, $.get(count) + 1), onmouseup, onmouseenter: () => $.set(count, $.proxy(plusOne($.get(count)))), - children: ($$anchor, $$slotProps) => { + children: $.add_snippet_symbol(($$anchor, $$slotProps) => { /* Init */ var text = $.space_frag($$anchor); /* Update */ $.text_effect(text, () => `clicks: ${$.stringify($.get(count))}`); $.close($$anchor, text); - } + }) }); $.close_frag($$anchor, fragment); diff --git a/packages/svelte/tests/snapshot/samples/function-prop-no-getter/_expected/server/index.svelte.js b/packages/svelte/tests/snapshot/samples/function-prop-no-getter/_expected/server/index.svelte.js index f678bb6ad67d..03534f14c2ce 100644 --- a/packages/svelte/tests/snapshot/samples/function-prop-no-getter/_expected/server/index.svelte.js +++ b/packages/svelte/tests/snapshot/samples/function-prop-no-getter/_expected/server/index.svelte.js @@ -20,9 +20,9 @@ export default function Function_prop_no_getter($$payload, $$props) { onmousedown: () => count += 1, onmouseup, onmouseenter: () => count = plusOne(count), - children: ($$payload, $$slotProps) => { + children: $.add_snippet_symbol(($$payload, $$slotProps) => { $$payload.out += `clicks: ${$.escape(count)}`; - } + }) }); $$payload.out += `${anchor}`; diff --git a/packages/svelte/types/index.d.ts b/packages/svelte/types/index.d.ts index 4b994010d4c6..b8c1090f94f2 100644 --- a/packages/svelte/types/index.d.ts +++ b/packages/svelte/types/index.d.ts @@ -291,12 +291,6 @@ declare module 'svelte' { * Anything except a function */ type NotFunction = T extends Function ? never : T; - /** - * Returns true if given parameter is a snippet. - * */ - export function isSnippet(maybeSnippet: any): maybeSnippet is (this: void) => unique symbol & { - _: "functions passed to {@render ...} tags must use the `Snippet` type imported from \"svelte\""; - }; /** * @deprecated Use `mount` or `hydrate` instead */ From 5b78c24baf92917696715d61c54e1e1b46d8597d Mon Sep 17 00:00:00 2001 From: Simon Holthausen Date: Thu, 14 Mar 2024 10:51:10 +0100 Subject: [PATCH 04/15] fix types --- packages/svelte/tests/types/component.ts | 2 +- packages/svelte/tests/types/snippet.ts | 7 +------ 2 files changed, 2 insertions(+), 7 deletions(-) diff --git a/packages/svelte/tests/types/component.ts b/packages/svelte/tests/types/component.ts index e2fc094bab43..8ef52740161b 100644 --- a/packages/svelte/tests/types/component.ts +++ b/packages/svelte/tests/types/component.ts @@ -170,5 +170,5 @@ asLegacyComponent.anExport; const x: typeof asLegacyComponent = createClassComponent({ target: null as any, hydrate: true, - component: newComponent + component: NewComponent }); diff --git a/packages/svelte/tests/types/snippet.ts b/packages/svelte/tests/types/snippet.ts index 75553419b4a9..dee0a93603a6 100644 --- a/packages/svelte/tests/types/snippet.ts +++ b/packages/svelte/tests/types/snippet.ts @@ -1,4 +1,4 @@ -import { type Snippet, isSnippet } from 'svelte'; +import { type Snippet } from 'svelte'; const return_type: ReturnType = null as any; @@ -38,8 +38,3 @@ const h: Snippet<[{ a: true }]> = (a) => { const i: Snippet = () => { return return_type; }; - -let j = null as any; -if (isSnippet(j)) { - let x: Snippet = j; -} From 50a84aac1ef869714bf988c096343f21568a306a Mon Sep 17 00:00:00 2001 From: Simon Holthausen Date: Thu, 14 Mar 2024 11:30:41 +0100 Subject: [PATCH 05/15] revert unrelated changes --- packages/svelte/src/legacy/legacy-client.js | 2 +- packages/svelte/tests/types/component.ts | 2 +- packages/svelte/tests/types/snippet.ts | 2 +- packages/svelte/types/index.d.ts | 12 +----------- 4 files changed, 4 insertions(+), 14 deletions(-) diff --git a/packages/svelte/src/legacy/legacy-client.js b/packages/svelte/src/legacy/legacy-client.js index 482628660e94..09f122e37023 100644 --- a/packages/svelte/src/legacy/legacy-client.js +++ b/packages/svelte/src/legacy/legacy-client.js @@ -12,7 +12,7 @@ import * as $ from '../internal/index.js'; * @template {Record} Slots * * @param {import('../main/public.js').ComponentConstructorOptions & { - * component: typeof import('../main/public.js').SvelteComponent; + * component: import('../main/public.js').SvelteComponent; * immutable?: boolean; * hydrate?: boolean; * recover?: boolean; diff --git a/packages/svelte/tests/types/component.ts b/packages/svelte/tests/types/component.ts index 8ef52740161b..e2fc094bab43 100644 --- a/packages/svelte/tests/types/component.ts +++ b/packages/svelte/tests/types/component.ts @@ -170,5 +170,5 @@ asLegacyComponent.anExport; const x: typeof asLegacyComponent = createClassComponent({ target: null as any, hydrate: true, - component: NewComponent + component: newComponent }); diff --git a/packages/svelte/tests/types/snippet.ts b/packages/svelte/tests/types/snippet.ts index dee0a93603a6..edc5aba12378 100644 --- a/packages/svelte/tests/types/snippet.ts +++ b/packages/svelte/tests/types/snippet.ts @@ -1,4 +1,4 @@ -import { type Snippet } from 'svelte'; +import type { Snippet } from 'svelte'; const return_type: ReturnType = null as any; diff --git a/packages/svelte/types/index.d.ts b/packages/svelte/types/index.d.ts index b8c1090f94f2..567f0b0dcf98 100644 --- a/packages/svelte/types/index.d.ts +++ b/packages/svelte/types/index.d.ts @@ -1729,17 +1729,7 @@ declare module 'svelte/legacy' { * * */ export function createClassComponent, Exports extends Record, Events extends Record, Slots extends Record>(options: ComponentConstructorOptions & { - component: { - new (options: ComponentConstructorOptions unique symbol & { - _: "functions passed to {@render ...} tags must use the `Snippet` type imported from \"svelte\""; - }) | undefined; - } : {})>): SvelteComponent; - }; + component: SvelteComponent; immutable?: boolean | undefined; hydrate?: boolean | undefined; recover?: boolean | undefined; From 5729d53d8be0f5dd876d63447aff33cff4d94116 Mon Sep 17 00:00:00 2001 From: Simon Holthausen Date: Thu, 14 Mar 2024 11:31:04 +0100 Subject: [PATCH 06/15] remove changeset --- .changeset/famous-grapes-refuse.md | 5 ----- 1 file changed, 5 deletions(-) delete mode 100644 .changeset/famous-grapes-refuse.md diff --git a/.changeset/famous-grapes-refuse.md b/.changeset/famous-grapes-refuse.md deleted file mode 100644 index 54246e918a91..000000000000 --- a/.changeset/famous-grapes-refuse.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"svelte": patch ---- - -feat: provide `isSnippet` function to determine whether a given value is a snippet From a770803bf8a20441c0b9cb87b6cec06956c2c46e Mon Sep 17 00:00:00 2001 From: Simon Holthausen Date: Thu, 14 Mar 2024 11:40:11 +0100 Subject: [PATCH 07/15] enhance test --- .../runtime-legacy/samples/slot-children-prop/_config.js | 2 +- .../runtime-legacy/samples/slot-children-prop/main.svelte | 4 +++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/packages/svelte/tests/runtime-legacy/samples/slot-children-prop/_config.js b/packages/svelte/tests/runtime-legacy/samples/slot-children-prop/_config.js index 3add2f34f319..48b7168f308c 100644 --- a/packages/svelte/tests/runtime-legacy/samples/slot-children-prop/_config.js +++ b/packages/svelte/tests/runtime-legacy/samples/slot-children-prop/_config.js @@ -1,5 +1,5 @@ import { test } from '../../test'; export default test({ - html: `foo bar` + html: `foo bar foo` }); diff --git a/packages/svelte/tests/runtime-legacy/samples/slot-children-prop/main.svelte b/packages/svelte/tests/runtime-legacy/samples/slot-children-prop/main.svelte index 15d898bdd381..63c5cc3aae74 100644 --- a/packages/svelte/tests/runtime-legacy/samples/slot-children-prop/main.svelte +++ b/packages/svelte/tests/runtime-legacy/samples/slot-children-prop/main.svelte @@ -4,4 +4,6 @@ bar - \ No newline at end of file + + + From e8f1a1674d552cc34441e113bda76bd02f1da932 Mon Sep 17 00:00:00 2001 From: Simon Holthausen Date: Wed, 3 Apr 2024 23:10:51 +0200 Subject: [PATCH 08/15] fix --- packages/svelte/src/internal/shared/validate.js | 15 +++------------ 1 file changed, 3 insertions(+), 12 deletions(-) diff --git a/packages/svelte/src/internal/shared/validate.js b/packages/svelte/src/internal/shared/validate.js index 692a62fa070f..14d3cb39c394 100644 --- a/packages/svelte/src/internal/shared/validate.js +++ b/packages/svelte/src/internal/shared/validate.js @@ -1,21 +1,12 @@ import { is_void } from '../../compiler/phases/1-parse/utils/names.js'; - -const snippet_symbol = Symbol.for('svelte.snippet'); - -/** - * @param {any} fn - */ -export function add_snippet_symbol(fn) { - fn[snippet_symbol] = true; - return fn; -} +import { is_snippet } from '../client/dom/blocks/snippet.js'; /** * Validate that the function handed to `{@render ...}` is a snippet function, and not some other kind of function. * @param {any} snippet_fn */ export function validate_snippet(snippet_fn) { - if (snippet_fn && snippet_fn[snippet_symbol] !== true) { + if (snippet_fn && !is_snippet(snippet_fn)) { throw new Error( 'The argument to `{@render ...}` must be a snippet function, not a component or some other kind of function. ' + 'If you want to dynamically render one snippet or another, use `$derived` and pass its result to `{@render ...}`.' @@ -29,7 +20,7 @@ export function validate_snippet(snippet_fn) { * @param {any} component_fn */ export function validate_component(component_fn) { - if (component_fn?.[snippet_symbol] === true) { + if (is_snippet(component_fn)) { throw new Error('A snippet must be rendered with `{@render ...}`'); } return component_fn; From 97b696df4cd1b9f8a1f94483995aee6470786435 Mon Sep 17 00:00:00 2001 From: Simon Holthausen Date: Wed, 3 Apr 2024 23:18:24 +0200 Subject: [PATCH 09/15] fix --- packages/svelte/src/internal/client/index.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/packages/svelte/src/internal/client/index.js b/packages/svelte/src/internal/client/index.js index 0a2eeaa19aab..87fbbf0f2c4a 100644 --- a/packages/svelte/src/internal/client/index.js +++ b/packages/svelte/src/internal/client/index.js @@ -5,7 +5,7 @@ export { key_block as key } from './dom/blocks/key.js'; export { css_props } from './dom/blocks/css-props.js'; export { each_keyed, each_indexed } from './dom/blocks/each.js'; export { html } from './dom/blocks/html.js'; -export { snippet } from './dom/blocks/snippet.js'; +export { snippet, add_snippet_symbol } from './dom/blocks/snippet.js'; export { component } from './dom/blocks/svelte-component.js'; export { element } from './dom/blocks/svelte-element.js'; export { head } from './dom/blocks/svelte-head.js'; @@ -137,7 +137,6 @@ export { } from './dom/operations.js'; export { noop } from '../shared/utils.js'; export { - add_snippet_symbol, validate_component, validate_dynamic_element_tag, validate_snippet, From 75b761dc4c0cb040997ea3261fe969de4935b1a3 Mon Sep 17 00:00:00 2001 From: Simon Holthausen Date: Wed, 3 Apr 2024 23:33:44 +0200 Subject: [PATCH 10/15] fix --- packages/svelte/src/internal/client/index.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/svelte/src/internal/client/index.js b/packages/svelte/src/internal/client/index.js index 87fbbf0f2c4a..27db021f03bf 100644 --- a/packages/svelte/src/internal/client/index.js +++ b/packages/svelte/src/internal/client/index.js @@ -57,7 +57,8 @@ export { add_legacy_event_listener, bubble_event, reactive_import, - update_legacy_props + update_legacy_props, + default_slot } from './dom/legacy/misc.js'; export { append, From 1bdf919904bc736d07d3ee34169eea3c51a22aea Mon Sep 17 00:00:00 2001 From: Simon Holthausen Date: Tue, 14 May 2024 13:06:34 +0200 Subject: [PATCH 11/15] fix, different approach without needing symbol --- .../3-transform/client/visitors/template.js | 3 +++ .../phases/3-transform/server/transform-server.js | 15 +++++++++++++-- .../client/dom/elements/custom-element.js | 4 ++-- packages/svelte/src/internal/client/validate.js | 1 - packages/svelte/src/internal/server/index.js | 12 ++++++------ .../_expected/client/index.svelte.js | 5 +++-- .../_expected/server/index.svelte.js | 5 +++-- 7 files changed, 30 insertions(+), 15 deletions(-) diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/template.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/template.js index 0136bc592beb..85fcf7afe30a 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/template.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/template.js @@ -840,6 +840,9 @@ function serialize_inline_component(node, component_name, context) { push_prop( b.init('children', context.state.options.dev ? b.call('$.wrap_snippet', slot_fn) : slot_fn) ); + // We additionally add the default slot as a boolean, so that the slot render function on the other + // side knows it should get the content to render from $$props.children + serialized_slots.push(b.init(slot_name, b.true)); } else { serialized_slots.push(b.init(slot_name, slot_fn)); } diff --git a/packages/svelte/src/compiler/phases/3-transform/server/transform-server.js b/packages/svelte/src/compiler/phases/3-transform/server/transform-server.js index f5ae15850215..660ba7dbf12c 100644 --- a/packages/svelte/src/compiler/phases/3-transform/server/transform-server.js +++ b/packages/svelte/src/compiler/phases/3-transform/server/transform-server.js @@ -1084,7 +1084,16 @@ function serialize_inline_component(node, component_name, context) { ); if (slot_name === 'default' && !has_children_prop) { - push_prop(b.prop('init', b.id('children'), b.call('$.add_snippet_symbol', slot_fn))); + push_prop( + b.prop( + 'init', + b.id('children'), + context.state.options.dev ? b.call('$.add_snippet_symbol', slot_fn) : slot_fn + ) + ); + // We additionally add the default slot as a boolean, so that the slot render function on the other + // side knows it should get the content to render from $$props.children + serialized_slots.push(b.init('default', b.true)); } else { const slot = b.prop('init', b.literal(slot_name), slot_fn); serialized_slots.push(slot); @@ -1619,7 +1628,9 @@ const template_visitors = { // TODO hoist where possible context.state.init.push(fn); - context.state.init.push(b.stmt(b.call('$.add_snippet_symbol', node.expression))); + if (context.state.options.dev) { + context.state.init.push(b.stmt(b.call('$.add_snippet_symbol', node.expression))); + } }, Component(node, context) { const state = context.state; diff --git a/packages/svelte/src/internal/client/dom/elements/custom-element.js b/packages/svelte/src/internal/client/dom/elements/custom-element.js index cceb4311b350..ae966651e8c9 100644 --- a/packages/svelte/src/internal/client/dom/elements/custom-element.js +++ b/packages/svelte/src/internal/client/dom/elements/custom-element.js @@ -1,6 +1,5 @@ import { createClassComponent } from '../../../../legacy/legacy-client.js'; import { destroy_effect, render_effect } from '../../reactivity/effects.js'; -import { add_snippet_symbol } from '../blocks/snippet.js'; import { append } from '../template.js'; import { define_property, object_keys } from '../../utils.js'; @@ -111,7 +110,8 @@ if (typeof HTMLElement === 'function') { for (const name of this.$$s) { if (name in existing_slots) { if (name === 'default' && !this.$$d.children) { - this.$$d.children = add_snippet_symbol(create_slot(name)); + this.$$d.children = create_slot(name); + $$slots.default = true; } else { $$slots[name] = create_slot(name); } diff --git a/packages/svelte/src/internal/client/validate.js b/packages/svelte/src/internal/client/validate.js index 0de636a1503e..2685e0c1ad33 100644 --- a/packages/svelte/src/internal/client/validate.js +++ b/packages/svelte/src/internal/client/validate.js @@ -1,4 +1,3 @@ -import { is_snippet } from './dom/blocks/snippet.js'; import { untrack } from './runtime.js'; import { get_descriptor, is_array } from './utils.js'; import * as e from './errors.js'; diff --git a/packages/svelte/src/internal/server/index.js b/packages/svelte/src/internal/server/index.js index efaddc46fb11..3928b897b7fd 100644 --- a/packages/svelte/src/internal/server/index.js +++ b/packages/svelte/src/internal/server/index.js @@ -10,10 +10,6 @@ import { } from '../../constants.js'; import { escape_html } from '../../escaping.js'; import { DEV } from 'esm-env'; - -export * from '../client/validate.js'; -export { add_snippet_symbol } from '../client/dom/blocks/snippet.js'; -export { default_slot } from '../client/dom/legacy/misc.js'; import { current_component, pop, push } from './context.js'; import { BLOCK_CLOSE, BLOCK_OPEN } from './hydration.js'; import { validate_store } from '../shared/validate.js'; @@ -280,8 +276,9 @@ export function spread_attributes(attrs, lowercase_attributes, is_html, class_ha for (let i = 0; i < attrs.length; i++) { const obj = attrs[i]; for (key in obj) { - // omit functions - if (typeof obj[key] !== 'function') { + // omit functions and internal svelte properties + const prefix = key[0] + key[1]; // this is faster than key.slice(0, 2) + if (typeof obj[key] !== 'function' && prefix !== '$$') { merged_attrs[key] = obj[key]; } } @@ -626,6 +623,7 @@ export function once(get_value) { export { push, pop } from './context.js'; export { + add_snippet_symbol, validate_component, validate_dynamic_element_tag, validate_snippet, @@ -633,3 +631,5 @@ export { } from '../shared/validate.js'; export { escape_html as escape }; + +export { default_slot } from '../client/dom/legacy/misc.js'; diff --git a/packages/svelte/tests/snapshot/samples/function-prop-no-getter/_expected/client/index.svelte.js b/packages/svelte/tests/snapshot/samples/function-prop-no-getter/_expected/client/index.svelte.js index db6d3808ef10..c44f9f143305 100644 --- a/packages/svelte/tests/snapshot/samples/function-prop-no-getter/_expected/client/index.svelte.js +++ b/packages/svelte/tests/snapshot/samples/function-prop-no-getter/_expected/client/index.svelte.js @@ -16,12 +16,13 @@ export default function Function_prop_no_getter($$anchor) { onmousedown: () => $.set(count, $.get(count) + 1), onmouseup, onmouseenter: () => $.set(count, $.proxy(plusOne($.get(count)))), - children: $.add_snippet_symbol(($$anchor, $$slotProps) => { + children: ($$anchor, $$slotProps) => { var text = $.text($$anchor); $.template_effect(() => $.set_text(text, `clicks: ${$.stringify($.get(count))}`)); $.append($$anchor, text); - }) + }, + $$slots: { default: true } }); $.append($$anchor, fragment); diff --git a/packages/svelte/tests/snapshot/samples/function-prop-no-getter/_expected/server/index.svelte.js b/packages/svelte/tests/snapshot/samples/function-prop-no-getter/_expected/server/index.svelte.js index 93cf1c9b8989..b787ffefd114 100644 --- a/packages/svelte/tests/snapshot/samples/function-prop-no-getter/_expected/server/index.svelte.js +++ b/packages/svelte/tests/snapshot/samples/function-prop-no-getter/_expected/server/index.svelte.js @@ -17,9 +17,10 @@ export default function Function_prop_no_getter($$payload, $$props) { onmousedown: () => count += 1, onmouseup, onmouseenter: () => count = plusOne(count), - children: $.add_snippet_symbol(($$payload, $$slotProps) => { + children: ($$payload, $$slotProps) => { $$payload.out += `clicks: ${$.escape(count)}`; - }) + }, + $$slots: { default: true } }); $$payload.out += ``; From 3494f4cc6b1dbeed12836bf53076705b8b1c7050 Mon Sep 17 00:00:00 2001 From: Simon Holthausen Date: Tue, 14 May 2024 15:12:57 +0200 Subject: [PATCH 12/15] allow passing snippets rendered as slots --- .../3-transform/client/visitors/template.js | 19 +++++++++++------- .../3-transform/server/transform-server.js | 20 +++++++++++++------ .../src/internal/client/dom/legacy/misc.js | 12 ----------- packages/svelte/src/internal/client/index.js | 3 +-- packages/svelte/src/internal/client/render.js | 14 +++++++++++-- packages/svelte/src/internal/server/index.js | 16 +++++++++++---- .../samples/snippet-slot-interop/Child.svelte | 2 ++ .../samples/snippet-slot-interop/_config.js | 5 +++++ .../samples/snippet-slot-interop/main.svelte | 10 ++++++++++ 9 files changed, 68 insertions(+), 33 deletions(-) create mode 100644 packages/svelte/tests/runtime-runes/samples/snippet-slot-interop/Child.svelte create mode 100644 packages/svelte/tests/runtime-runes/samples/snippet-slot-interop/_config.js create mode 100644 packages/svelte/tests/runtime-runes/samples/snippet-slot-interop/main.svelte diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/template.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/template.js index 85fcf7afe30a..fcf8d00d4f1d 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/template.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/template.js @@ -789,6 +789,8 @@ function serialize_inline_component(node, component_name, context) { /** @type {import('estree').Statement[]} */ const snippet_declarations = []; + /** @type {import('estree').Property[]} */ + const serialized_slots = []; // Group children by slot for (const child of node.fragment.nodes) { @@ -802,6 +804,8 @@ function serialize_inline_component(node, component_name, context) { }); push_prop(b.prop('init', child.expression, child.expression)); + // Back/forward compatibility: allows people to pass snippets when component still uses slots + serialized_slots.push(b.init(child.expression.name, b.true)); continue; } @@ -825,8 +829,6 @@ function serialize_inline_component(node, component_name, context) { } // Serialize each slot - /** @type {import('estree').Property[]} */ - const serialized_slots = []; for (const slot_name of Object.keys(children)) { const body = create_block(node, `${node.name}_${slot_name}`, children[slot_name], context); if (body.length === 0) continue; @@ -3070,11 +3072,14 @@ export const template_visitors = { b.block(create_block(node, 'fallback', node.fragment.nodes, context)) ); - const expression = is_default - ? b.call('$.default_slot', b.id('$$props')) - : b.member(b.member(b.id('$$props'), b.id('$$slots')), name, true, true); - - const slot = b.call('$.slot', context.state.node, expression, props_expression, fallback); + const slot = b.call( + '$.slot', + context.state.node, + b.id('$$props'), + name, + props_expression, + fallback + ); context.state.init.push(b.stmt(slot)); }, SvelteHead(node, context) { diff --git a/packages/svelte/src/compiler/phases/3-transform/server/transform-server.js b/packages/svelte/src/compiler/phases/3-transform/server/transform-server.js index 660ba7dbf12c..f5d7e2c3ee69 100644 --- a/packages/svelte/src/compiler/phases/3-transform/server/transform-server.js +++ b/packages/svelte/src/compiler/phases/3-transform/server/transform-server.js @@ -1037,6 +1037,8 @@ function serialize_inline_component(node, component_name, context) { /** @type {import('estree').Statement[]} */ const snippet_declarations = []; + /** @type {import('estree').Property[]} */ + const serialized_slots = []; // Group children by slot for (const child of node.fragment.nodes) { @@ -1050,6 +1052,8 @@ function serialize_inline_component(node, component_name, context) { }); push_prop(b.prop('init', child.expression, child.expression)); + // Back/forward compatibility: allows people to pass snippets when component still uses slots + serialized_slots.push(b.init(child.expression.name, b.true)); continue; } @@ -1071,9 +1075,6 @@ function serialize_inline_component(node, component_name, context) { } // Serialize each slot - /** @type {import('estree').Property[]} */ - const serialized_slots = []; - for (const slot_name of Object.keys(children)) { const body = create_block(node, children[slot_name], context); if (body.length === 0) continue; @@ -1750,7 +1751,7 @@ const template_visitors = { const lets = []; /** @type {import('estree').Expression} */ - let expression = b.call('$.default_slot', b.id('$$props')); + let slot_name = b.literal('default'); for (const attribute of node.attributes) { if (attribute.type === 'SpreadAttribute') { @@ -1758,7 +1759,7 @@ const template_visitors = { } else if (attribute.type === 'Attribute') { const value = serialize_attribute_value(attribute.value, context, false, true); if (attribute.name === 'name') { - expression = b.member(b.member_id('$$props.$$slots'), value, true, true); + slot_name = value; } else if (attribute.name !== 'slot') { if (attribute.metadata.dynamic) { props.push(b.get(attribute.name, [b.return(value)])); @@ -1782,7 +1783,14 @@ const template_visitors = { node.fragment.nodes.length === 0 ? b.literal(null) : b.thunk(b.block(create_block(node, node.fragment.nodes, context))); - const slot = b.call('$.slot', b.id('$$payload'), expression, props_expression, fallback); + const slot = b.call( + '$.slot', + b.id('$$payload'), + b.id('$$props'), + slot_name, + props_expression, + fallback + ); state.template.push(t_statement(b.stmt(slot))); state.template.push(block_close); diff --git a/packages/svelte/src/internal/client/dom/legacy/misc.js b/packages/svelte/src/internal/client/dom/legacy/misc.js index b9ef9fc53a5b..a4b52efe283c 100644 --- a/packages/svelte/src/internal/client/dom/legacy/misc.js +++ b/packages/svelte/src/internal/client/dom/legacy/misc.js @@ -66,15 +66,3 @@ export function update_legacy_props($$new_props) { } } } - -/** - * @param {Record} $$props - */ -export function default_slot($$props) { - var children = $$props.$$slots?.default; - if (children === true) { - return $$props.children; - } else { - return children; - } -} diff --git a/packages/svelte/src/internal/client/index.js b/packages/svelte/src/internal/client/index.js index 9c65c9896386..c54de90fe5ee 100644 --- a/packages/svelte/src/internal/client/index.js +++ b/packages/svelte/src/internal/client/index.js @@ -72,8 +72,7 @@ export { add_legacy_event_listener, bubble_event, reactive_import, - update_legacy_props, - default_slot + update_legacy_props } from './dom/legacy/misc.js'; export { append, diff --git a/packages/svelte/src/internal/client/render.js b/packages/svelte/src/internal/client/render.js index ee9cfbfc30de..534d386ef16a 100644 --- a/packages/svelte/src/internal/client/render.js +++ b/packages/svelte/src/internal/client/render.js @@ -63,11 +63,21 @@ export function set_text(dom, value) { /** * @param {Comment} anchor - * @param {void | ((anchor: Comment, slot_props: Record) => void)} slot_fn + * @param {Record} $$props + * @param {string} slot_name * @param {Record} slot_props * @param {null | ((anchor: Comment) => void)} fallback_fn */ -export function slot(anchor, slot_fn, slot_props, fallback_fn) { +export function slot(anchor, $$props, slot_name, slot_props, fallback_fn) { + var slot_fn = $$props.$$slots?.[slot_name]; + if (slot_fn === true) { + if (slot_name === 'default') { + slot_fn = $$props.children; + } else { + slot_fn = $$props[slot_name]; + } + } + if (slot_fn === undefined) { if (fallback_fn !== null) { fallback_fn(anchor); diff --git a/packages/svelte/src/internal/server/index.js b/packages/svelte/src/internal/server/index.js index 3928b897b7fd..cd5d56354586 100644 --- a/packages/svelte/src/internal/server/index.js +++ b/packages/svelte/src/internal/server/index.js @@ -499,12 +499,22 @@ export async function value_or_fallback_async(value, fallback) { /** * @param {Payload} payload - * @param {void | ((payload: Payload, props: Record) => void)} slot_fn + * @param {Record} $$props + * @param {string} slot_name * @param {Record} slot_props * @param {null | (() => void)} fallback_fn * @returns {void} */ -export function slot(payload, slot_fn, slot_props, fallback_fn) { +export function slot(payload, $$props, slot_name, slot_props, fallback_fn) { + var slot_fn = $$props.$$slots?.[slot_name]; + if (slot_fn === true) { + if (slot_name === 'default') { + slot_fn = $$props.children; + } else { + slot_fn = $$props[slot_name]; + } + } + if (slot_fn === undefined) { if (fallback_fn !== null) { fallback_fn(); @@ -631,5 +641,3 @@ export { } from '../shared/validate.js'; export { escape_html as escape }; - -export { default_slot } from '../client/dom/legacy/misc.js'; diff --git a/packages/svelte/tests/runtime-runes/samples/snippet-slot-interop/Child.svelte b/packages/svelte/tests/runtime-runes/samples/snippet-slot-interop/Child.svelte new file mode 100644 index 000000000000..c14bed8d5566 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/snippet-slot-interop/Child.svelte @@ -0,0 +1,2 @@ +

+

diff --git a/packages/svelte/tests/runtime-runes/samples/snippet-slot-interop/_config.js b/packages/svelte/tests/runtime-runes/samples/snippet-slot-interop/_config.js new file mode 100644 index 000000000000..99e1719bf3fb --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/snippet-slot-interop/_config.js @@ -0,0 +1,5 @@ +import { test } from '../../test'; + +export default test({ + html: `

Default

Named

` +}); diff --git a/packages/svelte/tests/runtime-runes/samples/snippet-slot-interop/main.svelte b/packages/svelte/tests/runtime-runes/samples/snippet-slot-interop/main.svelte new file mode 100644 index 000000000000..d439d5bd69f2 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/snippet-slot-interop/main.svelte @@ -0,0 +1,10 @@ + + + + Default + {#snippet named()} + Named + {/snippet} + From 46dcfca36f067d0a6193979dececd45f73007eff Mon Sep 17 00:00:00 2001 From: Simon Holthausen Date: Tue, 14 May 2024 15:14:13 +0200 Subject: [PATCH 13/15] we can do sanitize_slots like this now (more correct anyway since it should be booleans) --- packages/svelte/src/internal/client/render.js | 7 +++++-- packages/svelte/src/internal/server/index.js | 7 +++++-- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/packages/svelte/src/internal/client/render.js b/packages/svelte/src/internal/client/render.js index 534d386ef16a..40307fb980eb 100644 --- a/packages/svelte/src/internal/client/render.js +++ b/packages/svelte/src/internal/client/render.js @@ -322,8 +322,11 @@ export function unmount(component) { * @returns {Record} */ export function sanitize_slots(props) { - const sanitized = { ...props.$$slots }; - if (props.children) sanitized.default = props.children; + /** @type {Record} */ + const sanitized = {}; + for (const key in props.$$slots) { + sanitized[key] = true; + } return sanitized; } diff --git a/packages/svelte/src/internal/server/index.js b/packages/svelte/src/internal/server/index.js index cd5d56354586..2d8632063d8d 100644 --- a/packages/svelte/src/internal/server/index.js +++ b/packages/svelte/src/internal/server/index.js @@ -555,8 +555,11 @@ export function sanitize_props(props) { * @returns {Record} */ export function sanitize_slots(props) { - const sanitized = { ...props.$$slots }; - if (props.children) sanitized.default = props.children; + /** @type {Record} */ + const sanitized = {}; + for (const key in props.$$slots) { + sanitized[key] = true; + } return sanitized; } From 9be0fdd753d116573c894f121c8cc5769d009d3a Mon Sep 17 00:00:00 2001 From: Simon Holthausen Date: Tue, 14 May 2024 20:28:35 +0200 Subject: [PATCH 14/15] interop for passing slots but rendering them with render tags --- .../3-transform/client/visitors/template.js | 18 ++++++++- .../3-transform/server/transform-server.js | 39 ++++++++++++++----- .../src/internal/client/dom/blocks/snippet.js | 31 +++++++++++++++ packages/svelte/src/internal/client/index.js | 2 +- packages/svelte/src/internal/server/index.js | 20 ++++++++++ .../snippet-slot-interop-2/Child.svelte | 6 +++ .../samples/snippet-slot-interop-2/_config.js | 5 +++ .../snippet-slot-interop-2/main.svelte | 8 ++++ 8 files changed, 118 insertions(+), 11 deletions(-) create mode 100644 packages/svelte/tests/runtime-runes/samples/snippet-slot-interop-2/Child.svelte create mode 100644 packages/svelte/tests/runtime-runes/samples/snippet-slot-interop-2/_config.js create mode 100644 packages/svelte/tests/runtime-runes/samples/snippet-slot-interop-2/main.svelte diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/template.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/template.js index fcf8d00d4f1d..4286e3ac1908 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/template.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/template.js @@ -1786,6 +1786,10 @@ export const template_visitors = { const raw_args = unwrap_optional(node.expression).arguments; const is_reactive = callee.type !== 'Identifier' || context.state.scope.get(callee.name)?.kind !== 'normal'; + const needs_backwards_compat = + callee.type === 'Identifier' && + raw_args.length < 2 && + context.state.scope.get(callee.name)?.kind === 'prop'; /** @type {import('estree').Expression[]} */ const args = [context.state.node]; @@ -1798,7 +1802,19 @@ export const template_visitors = { snippet_function = b.call('$.validate_snippet', snippet_function); } - if (is_reactive) { + if (needs_backwards_compat) { + context.state.init.push( + b.stmt( + b.call( + '$.render_snippet_or_slot', + b.thunk(snippet_function), + b.id('$$props'), + b.literal(callee.name), + ...args + ) + ) + ); + } else if (is_reactive) { context.state.init.push(b.stmt(b.call('$.snippet', b.thunk(snippet_function), ...args))); } else { context.state.init.push( diff --git a/packages/svelte/src/compiler/phases/3-transform/server/transform-server.js b/packages/svelte/src/compiler/phases/3-transform/server/transform-server.js index f5d7e2c3ee69..7a9bec71e9e8 100644 --- a/packages/svelte/src/compiler/phases/3-transform/server/transform-server.js +++ b/packages/svelte/src/compiler/phases/3-transform/server/transform-server.js @@ -1303,6 +1303,10 @@ const template_visitors = { const callee = unwrap_optional(node.expression).callee; const raw_args = unwrap_optional(node.expression).arguments; + const needs_backwards_compat = + callee.type === 'Identifier' && + raw_args.length < 2 && + context.state.scope.get(callee.name)?.kind === 'prop'; const expression = /** @type {import('estree').Expression} */ (context.visit(callee)); const snippet_function = state.options.dev @@ -1313,17 +1317,34 @@ const template_visitors = { return /** @type {import('estree').Expression} */ (context.visit(arg)); }); - state.template.push( - t_statement( - b.stmt( - (node.expression.type === 'CallExpression' ? b.call : b.maybe_call)( - snippet_function, - b.id('$$payload'), - ...snippet_args + if (needs_backwards_compat) { + state.template.push( + t_statement( + b.stmt( + b.call( + '$.render_snippet_or_slot', + snippet_function, + b.id('$$props'), + b.literal(callee.name), + b.id('$$payload'), + ...snippet_args + ) ) ) - ) - ); + ); + } else { + state.template.push( + t_statement( + b.stmt( + (node.expression.type === 'CallExpression' ? b.call : b.maybe_call)( + snippet_function, + b.id('$$payload'), + ...snippet_args + ) + ) + ) + ); + } state.template.push(block_close); }, diff --git a/packages/svelte/src/internal/client/dom/blocks/snippet.js b/packages/svelte/src/internal/client/dom/blocks/snippet.js index 5bf13a45d8d4..7902df421115 100644 --- a/packages/svelte/src/internal/client/dom/blocks/snippet.js +++ b/packages/svelte/src/internal/client/dom/blocks/snippet.js @@ -57,3 +57,34 @@ export function wrap_snippet(fn) { } ); } + +/** + * Remove this once slots are gone + * @param {any} snippet_fn + * @param {Record} $$props + * @param {string} name + * @param {Element} node + * @param {any} slot_props + */ +export function render_snippet_or_slot(snippet_fn, $$props, name, node, slot_props) { + if ($$props.$$slots) { + const slot = $$props.$$slots[name === 'children' ? 'default' : name]; + if (typeof slot === 'function') { + let props = undefined; + if (slot_props) { + props = new Proxy( + {}, + { + get(_, key) { + return slot_props()?.[key]; + } + } + ); + } + slot(node, props); + return; + } + } + + snippet(snippet_fn, node, slot_props); +} diff --git a/packages/svelte/src/internal/client/index.js b/packages/svelte/src/internal/client/index.js index c54de90fe5ee..27161906e5e6 100644 --- a/packages/svelte/src/internal/client/index.js +++ b/packages/svelte/src/internal/client/index.js @@ -15,7 +15,7 @@ export { key_block as key } from './dom/blocks/key.js'; export { css_props } from './dom/blocks/css-props.js'; export { index, each } from './dom/blocks/each.js'; export { html } from './dom/blocks/html.js'; -export { snippet, wrap_snippet } from './dom/blocks/snippet.js'; +export { snippet, wrap_snippet, render_snippet_or_slot } from './dom/blocks/snippet.js'; export { component } from './dom/blocks/svelte-component.js'; export { element } from './dom/blocks/svelte-element.js'; export { head } from './dom/blocks/svelte-head.js'; diff --git a/packages/svelte/src/internal/server/index.js b/packages/svelte/src/internal/server/index.js index 2d8632063d8d..888cd48f6e90 100644 --- a/packages/svelte/src/internal/server/index.js +++ b/packages/svelte/src/internal/server/index.js @@ -563,6 +563,26 @@ export function sanitize_slots(props) { return sanitized; } +/** + * Remove this once slots are gone + * @param {any} snippet_fn + * @param {Record} $$props + * @param {string} name + * @param {Payload} payload + * @param {any} slot_props + */ +export function render_snippet_or_slot(snippet_fn, $$props, name, payload, slot_props) { + if ($$props.$$slots) { + const slot = $$props.$$slots[name === 'children' ? 'default' : name]; + if (typeof slot === 'function') { + slot(payload, slot_props); + return; + } + } + + snippet_fn?.(payload, slot_props); +} + /** * Legacy mode: If the prop has a fallback and is bound in the * parent component, propagate the fallback value upwards. diff --git a/packages/svelte/tests/runtime-runes/samples/snippet-slot-interop-2/Child.svelte b/packages/svelte/tests/runtime-runes/samples/snippet-slot-interop-2/Child.svelte new file mode 100644 index 000000000000..9f6882342216 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/snippet-slot-interop-2/Child.svelte @@ -0,0 +1,6 @@ + + +

{@render children()}

+{@render named({foo: 'foo'})} diff --git a/packages/svelte/tests/runtime-runes/samples/snippet-slot-interop-2/_config.js b/packages/svelte/tests/runtime-runes/samples/snippet-slot-interop-2/_config.js new file mode 100644 index 000000000000..fb0f428cf45e --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/snippet-slot-interop-2/_config.js @@ -0,0 +1,5 @@ +import { test } from '../../test'; + +export default test({ + html: `

Default

Named foo

` +}); diff --git a/packages/svelte/tests/runtime-runes/samples/snippet-slot-interop-2/main.svelte b/packages/svelte/tests/runtime-runes/samples/snippet-slot-interop-2/main.svelte new file mode 100644 index 000000000000..7079d5a1b2d1 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/snippet-slot-interop-2/main.svelte @@ -0,0 +1,8 @@ + + + + Default +

Named {foo}

+
From be3b08cbe5042de0f7ecae53eaeb3d69b1a0cbf7 Mon Sep 17 00:00:00 2001 From: Simon Holthausen Date: Tue, 14 May 2024 23:09:49 +0200 Subject: [PATCH 15/15] fix --- packages/svelte/src/internal/client/render.js | 4 +++- .../runtime-runes/samples/snippet-slot-interop/Child.svelte | 2 +- .../runtime-runes/samples/snippet-slot-interop/_config.js | 2 +- .../runtime-runes/samples/snippet-slot-interop/main.svelte | 4 ++-- 4 files changed, 7 insertions(+), 5 deletions(-) diff --git a/packages/svelte/src/internal/client/render.js b/packages/svelte/src/internal/client/render.js index 40307fb980eb..c65d697a1bb7 100644 --- a/packages/svelte/src/internal/client/render.js +++ b/packages/svelte/src/internal/client/render.js @@ -69,11 +69,13 @@ export function set_text(dom, value) { * @param {null | ((anchor: Comment) => void)} fallback_fn */ export function slot(anchor, $$props, slot_name, slot_props, fallback_fn) { + var is_snippet = false; var slot_fn = $$props.$$slots?.[slot_name]; if (slot_fn === true) { if (slot_name === 'default') { slot_fn = $$props.children; } else { + is_snippet = true; slot_fn = $$props[slot_name]; } } @@ -83,7 +85,7 @@ export function slot(anchor, $$props, slot_name, slot_props, fallback_fn) { fallback_fn(anchor); } } else { - slot_fn(anchor, slot_props); + slot_fn(anchor, is_snippet ? () => slot_props : slot_props); } } diff --git a/packages/svelte/tests/runtime-runes/samples/snippet-slot-interop/Child.svelte b/packages/svelte/tests/runtime-runes/samples/snippet-slot-interop/Child.svelte index c14bed8d5566..49ef446e25d1 100644 --- a/packages/svelte/tests/runtime-runes/samples/snippet-slot-interop/Child.svelte +++ b/packages/svelte/tests/runtime-runes/samples/snippet-slot-interop/Child.svelte @@ -1,2 +1,2 @@

-

+

diff --git a/packages/svelte/tests/runtime-runes/samples/snippet-slot-interop/_config.js b/packages/svelte/tests/runtime-runes/samples/snippet-slot-interop/_config.js index 99e1719bf3fb..718ca9b50b07 100644 --- a/packages/svelte/tests/runtime-runes/samples/snippet-slot-interop/_config.js +++ b/packages/svelte/tests/runtime-runes/samples/snippet-slot-interop/_config.js @@ -1,5 +1,5 @@ import { test } from '../../test'; export default test({ - html: `

Default

Named

` + html: `

Default

Named foo

` }); diff --git a/packages/svelte/tests/runtime-runes/samples/snippet-slot-interop/main.svelte b/packages/svelte/tests/runtime-runes/samples/snippet-slot-interop/main.svelte index d439d5bd69f2..01b827fa8350 100644 --- a/packages/svelte/tests/runtime-runes/samples/snippet-slot-interop/main.svelte +++ b/packages/svelte/tests/runtime-runes/samples/snippet-slot-interop/main.svelte @@ -4,7 +4,7 @@ Default - {#snippet named()} - Named + {#snippet named({ foo })} + Named {foo} {/snippet}