From c287bd503d5b73af1b3aff9ef210341bfcddc642 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Wed, 17 Jul 2024 01:49:44 -0700 Subject: [PATCH 1/5] Raw snippet alternative (#12425) * feat: add createRawSnippet API * handle missing hydrate function, improve types * fix * tweak types * beef up test * build * types * oops this was temporary * typo * regenerate types * make mount/render optional, error if missing * move code to new module * test hydration * simpler createRawSnippet API * regenerate types * change signature * docs * h1 -> node * allow `setup` to return a teardown function --------- Co-authored-by: Dominic Gannaway --- .changeset/fresh-zoos-burn.md | 5 +++ packages/svelte/src/index-client.js | 2 + packages/svelte/src/index-server.js | 2 + .../src/internal/client/dom/blocks/snippet.js | 45 ++++++++++++++++++- .../src/internal/server/blocks/snippet.js | 22 +++++++++ .../svelte/src/internal/shared/types.d.ts | 4 ++ .../svelte/src/internal/shared/validate.js | 3 ++ .../samples/snippet-raw-hydrate/_config.js | 9 ++++ .../snippet-raw-hydrate/_expected.html | 1 + .../samples/snippet-raw-hydrate/main.svelte | 14 ++++++ .../samples/snippet-raw-teardown/_config.js | 11 +++++ .../samples/snippet-raw-teardown/main.svelte | 18 ++++++++ .../samples/snippet-raw/_config.js | 17 +++++++ .../samples/snippet-raw/main.svelte | 20 +++++++++ packages/svelte/types/index.d.ts | 10 +++++ .../routes/docs/content/01-api/03-snippets.md | 4 ++ .../routes/docs/content/01-api/05-imports.md | 31 +++++++++++++ 17 files changed, 216 insertions(+), 2 deletions(-) create mode 100644 .changeset/fresh-zoos-burn.md create mode 100644 packages/svelte/src/internal/server/blocks/snippet.js create mode 100644 packages/svelte/tests/hydration/samples/snippet-raw-hydrate/_config.js create mode 100644 packages/svelte/tests/hydration/samples/snippet-raw-hydrate/_expected.html create mode 100644 packages/svelte/tests/hydration/samples/snippet-raw-hydrate/main.svelte create mode 100644 packages/svelte/tests/runtime-runes/samples/snippet-raw-teardown/_config.js create mode 100644 packages/svelte/tests/runtime-runes/samples/snippet-raw-teardown/main.svelte create mode 100644 packages/svelte/tests/runtime-runes/samples/snippet-raw/_config.js create mode 100644 packages/svelte/tests/runtime-runes/samples/snippet-raw/main.svelte diff --git a/.changeset/fresh-zoos-burn.md b/.changeset/fresh-zoos-burn.md new file mode 100644 index 0000000000..6b68a02726 --- /dev/null +++ b/.changeset/fresh-zoos-burn.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +feat: add createRawSnippet API diff --git a/packages/svelte/src/index-client.js b/packages/svelte/src/index-client.js index 37fdb9cf6c..24f8d99a12 100644 --- a/packages/svelte/src/index-client.js +++ b/packages/svelte/src/index-client.js @@ -190,3 +190,5 @@ export { tick, untrack } from './internal/client/runtime.js'; + +export { createRawSnippet } from './internal/client/dom/blocks/snippet.js'; diff --git a/packages/svelte/src/index-server.js b/packages/svelte/src/index-server.js index 5bcb13e5f2..e5590fba50 100644 --- a/packages/svelte/src/index-server.js +++ b/packages/svelte/src/index-server.js @@ -35,3 +35,5 @@ export function unmount() { export async function tick() {} export { getAllContexts, getContext, hasContext, setContext } from './internal/server/context.js'; + +export { createRawSnippet } from './internal/server/blocks/snippet.js'; diff --git a/packages/svelte/src/internal/client/dom/blocks/snippet.js b/packages/svelte/src/internal/client/dom/blocks/snippet.js index c0fc350ce4..a920f6db3e 100644 --- a/packages/svelte/src/internal/client/dom/blocks/snippet.js +++ b/packages/svelte/src/internal/client/dom/blocks/snippet.js @@ -1,12 +1,16 @@ +/** @import { Snippet } from 'svelte' */ /** @import { Effect, TemplateNode } from '#client' */ +/** @import { Getters } from '#shared' */ import { add_snippet_symbol } from '../../../shared/validate.js'; import { EFFECT_TRANSPARENT } from '../../constants.js'; -import { branch, block, destroy_effect } from '../../reactivity/effects.js'; +import { branch, block, destroy_effect, teardown } from '../../reactivity/effects.js'; import { dev_current_component_function, set_dev_current_component_function } from '../../runtime.js'; -import { hydrate_node, hydrating } from '../hydration.js'; +import { hydrate_next, hydrate_node, hydrating } from '../hydration.js'; +import { create_fragment_from_html } from '../reconciler.js'; +import { assign_nodes } from '../template.js'; /** * @template {(node: TemplateNode, ...args: any[]) => void} SnippetFn @@ -60,3 +64,40 @@ export function wrap_snippet(component, fn) { } }); } + +/** + * Create a snippet programmatically + * @template {unknown[]} Params + * @param {(...params: Getters) => { + * render: () => string + * setup?: (element: Element) => void + * }} fn + * @returns {Snippet} + */ +export function createRawSnippet(fn) { + return add_snippet_symbol( + (/** @type {TemplateNode} */ anchor, /** @type {Getters} */ ...params) => { + var snippet = fn(...params); + + /** @type {Element} */ + var element; + + if (hydrating) { + element = /** @type {Element} */ (hydrate_node); + hydrate_next(); + } else { + var html = snippet.render().trim(); + var fragment = create_fragment_from_html(html); + element = /** @type {Element} */ (fragment.firstChild); + anchor.before(element); + } + + const result = snippet.setup?.(element); + assign_nodes(element, element); + + if (typeof result === 'function') { + teardown(result); + } + } + ); +} diff --git a/packages/svelte/src/internal/server/blocks/snippet.js b/packages/svelte/src/internal/server/blocks/snippet.js new file mode 100644 index 0000000000..b9f72063f4 --- /dev/null +++ b/packages/svelte/src/internal/server/blocks/snippet.js @@ -0,0 +1,22 @@ +/** @import { Snippet } from 'svelte' */ +/** @import { Payload } from '#server' */ +/** @import { Getters } from '#shared' */ +import { add_snippet_symbol } from '../../shared/validate.js'; + +/** + * Create a snippet programmatically + * @template {unknown[]} Params + * @param {(...params: Getters) => { + * render: () => string + * setup?: (element: Element) => void + * }} fn + * @returns {Snippet} + */ +export function createRawSnippet(fn) { + return add_snippet_symbol((/** @type {Payload} */ payload, /** @type {Params} */ ...args) => { + var getters = /** @type {Getters} */ (args.map((value) => () => value)); + payload.out += fn(...getters) + .render() + .trim(); + }); +} diff --git a/packages/svelte/src/internal/shared/types.d.ts b/packages/svelte/src/internal/shared/types.d.ts index 76340531e9..a97a61af67 100644 --- a/packages/svelte/src/internal/shared/types.d.ts +++ b/packages/svelte/src/internal/shared/types.d.ts @@ -7,4 +7,8 @@ export type SourceLocation = | [line: number, column: number] | [line: number, column: number, SourceLocation[]]; +export type Getters = { + [K in keyof T]: () => T[K]; +}; + export type Snapshot = ReturnType>; diff --git a/packages/svelte/src/internal/shared/validate.js b/packages/svelte/src/internal/shared/validate.js index 12d852d537..e08f3ddab1 100644 --- a/packages/svelte/src/internal/shared/validate.js +++ b/packages/svelte/src/internal/shared/validate.js @@ -1,3 +1,5 @@ +/** @import { TemplateNode } from '#client' */ +/** @import { Getters } from '#shared' */ import { is_void } from '../../constants.js'; import * as w from './warnings.js'; import * as e from './errors.js'; @@ -6,6 +8,7 @@ const snippet_symbol = Symbol.for('svelte.snippet'); /** * @param {any} fn + * @returns {import('svelte').Snippet} */ export function add_snippet_symbol(fn) { fn[snippet_symbol] = true; diff --git a/packages/svelte/tests/hydration/samples/snippet-raw-hydrate/_config.js b/packages/svelte/tests/hydration/samples/snippet-raw-hydrate/_config.js new file mode 100644 index 0000000000..a2f55c2641 --- /dev/null +++ b/packages/svelte/tests/hydration/samples/snippet-raw-hydrate/_config.js @@ -0,0 +1,9 @@ +import { test } from '../../test'; + +export default test({ + snapshot(target) { + return { + p: target.querySelector('p') + }; + } +}); diff --git a/packages/svelte/tests/hydration/samples/snippet-raw-hydrate/_expected.html b/packages/svelte/tests/hydration/samples/snippet-raw-hydrate/_expected.html new file mode 100644 index 0000000000..8d9dde52c1 --- /dev/null +++ b/packages/svelte/tests/hydration/samples/snippet-raw-hydrate/_expected.html @@ -0,0 +1 @@ +

hydrated

diff --git a/packages/svelte/tests/hydration/samples/snippet-raw-hydrate/main.svelte b/packages/svelte/tests/hydration/samples/snippet-raw-hydrate/main.svelte new file mode 100644 index 0000000000..84e1722908 --- /dev/null +++ b/packages/svelte/tests/hydration/samples/snippet-raw-hydrate/main.svelte @@ -0,0 +1,14 @@ + + +{@render snippet()} diff --git a/packages/svelte/tests/runtime-runes/samples/snippet-raw-teardown/_config.js b/packages/svelte/tests/runtime-runes/samples/snippet-raw-teardown/_config.js new file mode 100644 index 0000000000..e05d82e929 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/snippet-raw-teardown/_config.js @@ -0,0 +1,11 @@ +import { flushSync } from 'svelte'; +import { test } from '../../test'; + +export default test({ + test({ target, assert, logs }) { + const button = target.querySelector('button'); + + flushSync(() => button?.click()); + assert.deepEqual(logs, ['tearing down']); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/snippet-raw-teardown/main.svelte b/packages/svelte/tests/runtime-runes/samples/snippet-raw-teardown/main.svelte new file mode 100644 index 0000000000..026208645c --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/snippet-raw-teardown/main.svelte @@ -0,0 +1,18 @@ + + + + +{#if show} + {@render snippet()} +{/if} diff --git a/packages/svelte/tests/runtime-runes/samples/snippet-raw/_config.js b/packages/svelte/tests/runtime-runes/samples/snippet-raw/_config.js new file mode 100644 index 0000000000..818d81f117 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/snippet-raw/_config.js @@ -0,0 +1,17 @@ +import { flushSync } from 'svelte'; +import { test } from '../../test'; + +export default test({ + compileOptions: { + dev: true // Render in dev mode to check that the validation error is not thrown + }, + + html: `

clicks: 0

`, + + test({ target, assert }) { + const button = target.querySelector('button'); + + flushSync(() => button?.click()); + assert.htmlEqual(target.innerHTML, `

clicks: 1

`); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/snippet-raw/main.svelte b/packages/svelte/tests/runtime-runes/samples/snippet-raw/main.svelte new file mode 100644 index 0000000000..ab23de4f36 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/snippet-raw/main.svelte @@ -0,0 +1,20 @@ + + + + +{@render hello(count)} diff --git a/packages/svelte/types/index.d.ts b/packages/svelte/types/index.d.ts index dff48ce8a1..b0c8c026d3 100644 --- a/packages/svelte/types/index.d.ts +++ b/packages/svelte/types/index.d.ts @@ -365,6 +365,13 @@ declare module 'svelte' { export function flushSync(fn?: (() => void) | undefined): void; /** Anything except a function */ type NotFunction = T extends Function ? never : T; + /** + * Create a snippet programmatically + * */ + export function createRawSnippet(fn: (...params: Getters) => { + render: () => string; + setup?: (element: Element) => void; + }): Snippet; /** * Mounts a component to the given target and returns the exports and potentially the props (if compiled with `accessors: true`) of the component. * Transitions will play during the initial render unless the `intro` option is set to `false`. @@ -450,6 +457,9 @@ declare module 'svelte' { * https://svelte.dev/docs/svelte#getallcontexts * */ export function getAllContexts = Map>(): T; + type Getters = { + [K in keyof T]: () => T[K]; + }; export {}; } diff --git a/sites/svelte-5-preview/src/routes/docs/content/01-api/03-snippets.md b/sites/svelte-5-preview/src/routes/docs/content/01-api/03-snippets.md index e10443f750..516300f2ea 100644 --- a/sites/svelte-5-preview/src/routes/docs/content/01-api/03-snippets.md +++ b/sites/svelte-5-preview/src/routes/docs/content/01-api/03-snippets.md @@ -256,6 +256,10 @@ We can tighten things up further by declaring a generic, so that `data` and `row ``` +## Creating snippets programmatically + +In advanced scenarios, you may need to create a snippet programmatically. For this, you can use [`createRawSnippet`](/docs/imports#svelte-createrawsnippet) + ## Snippets and slots In Svelte 4, content can be passed to components using [slots](https://svelte.dev/docs/special-elements#slot). Snippets are more powerful and flexible, and as such slots are deprecated in Svelte 5. diff --git a/sites/svelte-5-preview/src/routes/docs/content/01-api/05-imports.md b/sites/svelte-5-preview/src/routes/docs/content/01-api/05-imports.md index af4118eaab..d364f82429 100644 --- a/sites/svelte-5-preview/src/routes/docs/content/01-api/05-imports.md +++ b/sites/svelte-5-preview/src/routes/docs/content/01-api/05-imports.md @@ -93,6 +93,37 @@ To prevent something from being treated as an `$effect`/`$derived` dependency, u ``` +### `createRawSnippet` + +An advanced API designed for people building frameworks that integrate with Svelte, `createRawSnippet` allows you to create [snippets](/docs/snippets) programmatically for use with `{@render ...}` tags: + +```js +import { createRawSnippet } from 'svelte'; + +const greet = createRawSnippet((name) => { + return { + render: () => ` +

Hello ${name()}!

+ `, + setup: (node) => { + $effect(() => { + node.textContent = `Hello ${name()}!`; + }); + } + }; +}); +``` + +The `render` function is called during server-side rendering, or during `mount` (but not during `hydrate`, because it already ran on the server), and must return HTML representing a single element. + +The `setup` function is called during `mount` or `hydrate` with that same element as its sole argument. It is responsible for ensuring that the DOM is updated when the arguments change their value — in this example, when `name` changes: + +```svelte +{@render greet(name)} +``` + +If `setup` returns a function, it will be called when the snippet is unmounted. If the snippet is fully static, you can omit the `setup` function altogether. + ## `svelte/reactivity` Svelte provides reactive `SvelteMap`, `SvelteSet`, `SvelteDate` and `SvelteURL` classes. These can be imported from `svelte/reactivity` and used just like their native counterparts. [Demo:](https://svelte-5-preview.vercel.app/#H4sIAAAAAAAAE32QwUrEMBBAf2XMpQrb9t7tFrx7UjxZYWM6NYFkEpJJ16X03yWK9OQeZ3iPecwqZmMxie5tFSQdik48hiAOgq-hDGlByygOIvkcVdn0SUUTeBhpZOOCjwwrvPxgr89PsMEcvYPqV2wjSsVmMXytjiMVR3lKDDlaOAHhZVfvK80cUte2-CVdsNgo79ogWVcPx5H6dj9M_V1dg9KSPjEBe2CNCZumgboeRuoNhczwYWjqFmkzntYcbROiZ6-83f5HtE9c3nADKUF_yEi9jnvQxVgLOUySEc464nwGSRMsRiEsGJO8mVeEbRAH4fxkZoOT6Dhm3N63b9_bGfOlAQAA) From 3c3a542c058b2b1e68e57e77f5255bca6842c793 Mon Sep 17 00:00:00 2001 From: Simon H <5968653+dummdidumm@users.noreply.github.com> Date: Wed, 17 Jul 2024 14:06:24 +0200 Subject: [PATCH 2/5] fix: error on snippet rest parameters (#12472) closes #12464 --- packages/svelte/messages/compile-errors/template.md | 2 +- packages/svelte/src/compiler/errors.js | 4 ++-- .../svelte/src/compiler/phases/2-analyze/validation.js | 6 ++++++ .../compiler-errors/samples/snippet-rest-args/_config.js | 9 +++++++++ .../samples/snippet-rest-args/main.svelte | 3 +++ 5 files changed, 21 insertions(+), 3 deletions(-) create mode 100644 packages/svelte/tests/compiler-errors/samples/snippet-rest-args/_config.js create mode 100644 packages/svelte/tests/compiler-errors/samples/snippet-rest-args/main.svelte diff --git a/packages/svelte/messages/compile-errors/template.md b/packages/svelte/messages/compile-errors/template.md index 517e5e730f..9b43e99820 100644 --- a/packages/svelte/messages/compile-errors/template.md +++ b/packages/svelte/messages/compile-errors/template.md @@ -250,7 +250,7 @@ ## snippet_invalid_rest_parameter -> snippets do not support rest parameters; use an array instead +> Snippets do not support rest parameters; use an array instead ## snippet_shadowing_prop diff --git a/packages/svelte/src/compiler/errors.js b/packages/svelte/src/compiler/errors.js index b092e262db..b4dcad4e4c 100644 --- a/packages/svelte/src/compiler/errors.js +++ b/packages/svelte/src/compiler/errors.js @@ -1108,12 +1108,12 @@ export function snippet_conflict(node) { } /** - * snippets do not support rest parameters; use an array instead + * Snippets do not support rest parameters; use an array instead * @param {null | number | NodeLike} node * @returns {never} */ export function snippet_invalid_rest_parameter(node) { - e(node, "snippet_invalid_rest_parameter", "snippets do not support rest parameters; use an array instead"); + e(node, "snippet_invalid_rest_parameter", "Snippets do not support rest parameters; use an array instead"); } /** diff --git a/packages/svelte/src/compiler/phases/2-analyze/validation.js b/packages/svelte/src/compiler/phases/2-analyze/validation.js index fcbb227a71..a5b22c666f 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/validation.js +++ b/packages/svelte/src/compiler/phases/2-analyze/validation.js @@ -674,6 +674,12 @@ const validation = { SnippetBlock(node, context) { validate_block_not_empty(node.body, context); + for (const arg of node.parameters) { + if (arg.type === 'RestElement') { + e.snippet_invalid_rest_parameter(arg); + } + } + context.next({ ...context.state, parent_element: null }); const { path } = context; diff --git a/packages/svelte/tests/compiler-errors/samples/snippet-rest-args/_config.js b/packages/svelte/tests/compiler-errors/samples/snippet-rest-args/_config.js new file mode 100644 index 0000000000..f305520831 --- /dev/null +++ b/packages/svelte/tests/compiler-errors/samples/snippet-rest-args/_config.js @@ -0,0 +1,9 @@ +import { test } from '../../test'; + +export default test({ + error: { + code: 'snippet_invalid_rest_parameter', + message: 'Snippets do not support rest parameters; use an array instead', + position: [19, 26] + } +}); diff --git a/packages/svelte/tests/compiler-errors/samples/snippet-rest-args/main.svelte b/packages/svelte/tests/compiler-errors/samples/snippet-rest-args/main.svelte new file mode 100644 index 0000000000..f345bf1956 --- /dev/null +++ b/packages/svelte/tests/compiler-errors/samples/snippet-rest-args/main.svelte @@ -0,0 +1,3 @@ +{#snippet children(...args)} + {args} +{/snippet} From 0c15a7f98e0723412b2aa42ab64c597e6a682963 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Wed, 17 Jul 2024 09:19:04 -0400 Subject: [PATCH 3/5] Version Packages (next) (#12470) Co-authored-by: github-actions[bot] --- .changeset/pre.json | 1 + packages/svelte/CHANGELOG.md | 6 ++++++ packages/svelte/package.json | 2 +- packages/svelte/src/version.js | 2 +- 4 files changed, 9 insertions(+), 2 deletions(-) diff --git a/.changeset/pre.json b/.changeset/pre.json index cffe3c6681..71843dcd8b 100644 --- a/.changeset/pre.json +++ b/.changeset/pre.json @@ -196,6 +196,7 @@ "fresh-walls-bathe", "fresh-weeks-trade", "fresh-wombats-learn", + "fresh-zoos-burn", "friendly-candles-relate", "friendly-clouds-rhyme", "friendly-lies-camp", diff --git a/packages/svelte/CHANGELOG.md b/packages/svelte/CHANGELOG.md index da0265a571..72fa175624 100644 --- a/packages/svelte/CHANGELOG.md +++ b/packages/svelte/CHANGELOG.md @@ -1,5 +1,11 @@ # svelte +## 5.0.0-next.189 + +### Patch Changes + +- feat: add createRawSnippet API ([#12425](https://github.com/sveltejs/svelte/pull/12425)) + ## 5.0.0-next.188 ### Patch Changes diff --git a/packages/svelte/package.json b/packages/svelte/package.json index 40f7488dc0..2268831fd7 100644 --- a/packages/svelte/package.json +++ b/packages/svelte/package.json @@ -2,7 +2,7 @@ "name": "svelte", "description": "Cybernetically enhanced web apps", "license": "MIT", - "version": "5.0.0-next.188", + "version": "5.0.0-next.189", "type": "module", "types": "./types/index.d.ts", "engines": { diff --git a/packages/svelte/src/version.js b/packages/svelte/src/version.js index 09cc9b42d8..480ef6414c 100644 --- a/packages/svelte/src/version.js +++ b/packages/svelte/src/version.js @@ -6,5 +6,5 @@ * https://svelte.dev/docs/svelte-compiler#svelte-version * @type {string} */ -export const VERSION = '5.0.0-next.188'; +export const VERSION = '5.0.0-next.189'; export const PUBLIC_VERSION = '5'; From 4fd6d292274a91e4a3b7acdd6e62451780d3ceb4 Mon Sep 17 00:00:00 2001 From: Simon H <5968653+dummdidumm@users.noreply.github.com> Date: Wed, 17 Jul 2024 16:07:31 +0200 Subject: [PATCH 4/5] fix: assign correct scope to attributes of named slot (#12476) fixes #12213 --- .changeset/cold-shrimps-hug.md | 5 +++++ packages/svelte/src/compiler/phases/scope.js | 14 +++++++++----- .../component-slot-named-scope/Child.svelte | 7 +++++++ .../component-slot-named-scope/Parent.svelte | 1 + .../samples/component-slot-named-scope/_config.js | 12 ++++++++++++ .../samples/component-slot-named-scope/main.svelte | 8 ++++++++ 6 files changed, 42 insertions(+), 5 deletions(-) create mode 100644 .changeset/cold-shrimps-hug.md create mode 100644 packages/svelte/tests/runtime-legacy/samples/component-slot-named-scope/Child.svelte create mode 100644 packages/svelte/tests/runtime-legacy/samples/component-slot-named-scope/Parent.svelte create mode 100644 packages/svelte/tests/runtime-legacy/samples/component-slot-named-scope/_config.js create mode 100644 packages/svelte/tests/runtime-legacy/samples/component-slot-named-scope/main.svelte diff --git a/.changeset/cold-shrimps-hug.md b/.changeset/cold-shrimps-hug.md new file mode 100644 index 0000000000..be5dc91d2f --- /dev/null +++ b/.changeset/cold-shrimps-hug.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: assign correct scope to attributes of named slot diff --git a/packages/svelte/src/compiler/phases/scope.js b/packages/svelte/src/compiler/phases/scope.js index 7b23486d9a..f9f208d6ae 100644 --- a/packages/svelte/src/compiler/phases/scope.js +++ b/packages/svelte/src/compiler/phases/scope.js @@ -386,16 +386,20 @@ export function create_scopes(ast, root, allow_reactive_declarations, parent) { Component(node, { state, visit, path }) { state.scope.reference(b.id(node.name), path); - for (const attribute of node.attributes) { - visit(attribute); - } - // let:x is super weird: // - for the default slot, its scope only applies to children that are not slots themselves // - for named slots, its scope applies to the component itself, too const [scope, is_default_slot] = analyze_let_directives(node, state.scope); - if (!is_default_slot) { + if (is_default_slot) { + for (const attribute of node.attributes) { + visit(attribute); + } + } else { scopes.set(node, scope); + + for (const attribute of node.attributes) { + visit(attribute, { ...state, scope }); + } } for (const child of node.fragment.nodes) { diff --git a/packages/svelte/tests/runtime-legacy/samples/component-slot-named-scope/Child.svelte b/packages/svelte/tests/runtime-legacy/samples/component-slot-named-scope/Child.svelte new file mode 100644 index 0000000000..c46c742315 --- /dev/null +++ b/packages/svelte/tests/runtime-legacy/samples/component-slot-named-scope/Child.svelte @@ -0,0 +1,7 @@ + + + diff --git a/packages/svelte/tests/runtime-legacy/samples/component-slot-named-scope/Parent.svelte b/packages/svelte/tests/runtime-legacy/samples/component-slot-named-scope/Parent.svelte new file mode 100644 index 0000000000..1d9ecbcc2d --- /dev/null +++ b/packages/svelte/tests/runtime-legacy/samples/component-slot-named-scope/Parent.svelte @@ -0,0 +1 @@ + diff --git a/packages/svelte/tests/runtime-legacy/samples/component-slot-named-scope/_config.js b/packages/svelte/tests/runtime-legacy/samples/component-slot-named-scope/_config.js new file mode 100644 index 0000000000..f11eb14e71 --- /dev/null +++ b/packages/svelte/tests/runtime-legacy/samples/component-slot-named-scope/_config.js @@ -0,0 +1,12 @@ +import { flushSync } from 'svelte'; +import { test } from '../../test'; + +export default test({ + test({ assert, logs, target }) { + const btn = target.querySelector('button'); + + btn?.click(); + flushSync(); + assert.deepEqual(logs, [1]); + } +}); diff --git a/packages/svelte/tests/runtime-legacy/samples/component-slot-named-scope/main.svelte b/packages/svelte/tests/runtime-legacy/samples/component-slot-named-scope/main.svelte new file mode 100644 index 0000000000..d0a38e4d08 --- /dev/null +++ b/packages/svelte/tests/runtime-legacy/samples/component-slot-named-scope/main.svelte @@ -0,0 +1,8 @@ + + + + console.log(item)}>asd + From bc9907aa1c3a3be983f03654572256361438019d Mon Sep 17 00:00:00 2001 From: Simon H <5968653+dummdidumm@users.noreply.github.com> Date: Wed, 17 Jul 2024 16:08:27 +0200 Subject: [PATCH 5/5] fix: hydrate multiple `` elements correctly (#12475) The head hydration anchor didn't update after hydrating the contents of one `` element, which meant subsequent `` elements would always start at the beginning of the head. This PR fixes that. The test was updated such that the shape of each `` content is sufficiently different to throw an error if this wasn't fixed. fixes #12458 --- .changeset/clever-toys-laugh.md | 5 +++++ .../src/internal/client/dom/blocks/svelte-head.js | 1 + .../samples/head-html-and-component/HeadNested.svelte | 2 +- .../samples/head-html-and-component/Nested.svelte | 6 +++++- .../samples/head-html-and-component/main.svelte | 11 +++++++---- 5 files changed, 19 insertions(+), 6 deletions(-) create mode 100644 .changeset/clever-toys-laugh.md diff --git a/.changeset/clever-toys-laugh.md b/.changeset/clever-toys-laugh.md new file mode 100644 index 0000000000..d1f72545bd --- /dev/null +++ b/.changeset/clever-toys-laugh.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: hydrate multiple `` elements correctly diff --git a/packages/svelte/src/internal/client/dom/blocks/svelte-head.js b/packages/svelte/src/internal/client/dom/blocks/svelte-head.js index bfd4291728..0fc2efa1eb 100644 --- a/packages/svelte/src/internal/client/dom/blocks/svelte-head.js +++ b/packages/svelte/src/internal/client/dom/blocks/svelte-head.js @@ -51,6 +51,7 @@ export function head(render_fn) { block(() => render_fn(anchor), HEAD_EFFECT); } finally { if (was_hydrating) { + head_anchor = hydrate_node; // so that next head block starts from the correct node set_hydrate_node(/** @type {TemplateNode} */ (previous_hydrate_node)); } } diff --git a/packages/svelte/tests/hydration/samples/head-html-and-component/HeadNested.svelte b/packages/svelte/tests/hydration/samples/head-html-and-component/HeadNested.svelte index 33bbdd1fd1..e9872ad897 100644 --- a/packages/svelte/tests/hydration/samples/head-html-and-component/HeadNested.svelte +++ b/packages/svelte/tests/hydration/samples/head-html-and-component/HeadNested.svelte @@ -1,2 +1,2 @@ {@html ''} - + diff --git a/packages/svelte/tests/hydration/samples/head-html-and-component/Nested.svelte b/packages/svelte/tests/hydration/samples/head-html-and-component/Nested.svelte index 28f5371910..308f44de38 100644 --- a/packages/svelte/tests/hydration/samples/head-html-and-component/Nested.svelte +++ b/packages/svelte/tests/hydration/samples/head-html-and-component/Nested.svelte @@ -1,5 +1,9 @@ + {@html ''} - + + diff --git a/packages/svelte/tests/hydration/samples/head-html-and-component/main.svelte b/packages/svelte/tests/hydration/samples/head-html-and-component/main.svelte index 188ecace6b..f11f24e42d 100644 --- a/packages/svelte/tests/hydration/samples/head-html-and-component/main.svelte +++ b/packages/svelte/tests/hydration/samples/head-html-and-component/main.svelte @@ -4,9 +4,12 @@ - {@html ''} - - + + {#if true} + {@html ''} + + + {/if} - +