From 116de3eb8e21de5bf50789f81e959c4e7a401eac Mon Sep 17 00:00:00 2001 From: Dominic Gannaway Date: Thu, 11 Jul 2024 18:24:42 +0100 Subject: [PATCH 01/19] feat: add createRawSnippet API --- .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 | 29 ++++++++++++++++- packages/svelte/src/internal/server/index.js | 18 ++++++++++- .../samples/snippet-raw-args/_config.js | 17 ++++++++++ .../samples/snippet-raw-args/main.svelte | 32 +++++++++++++++++++ .../samples/snippet-raw/_config.js | 8 +++++ .../samples/snippet-raw/main.svelte | 16 ++++++++++ packages/svelte/types/index.d.ts | 15 +++++++-- 10 files changed, 139 insertions(+), 5 deletions(-) create mode 100644 .changeset/fresh-zoos-burn.md create mode 100644 packages/svelte/tests/runtime-runes/samples/snippet-raw-args/_config.js create mode 100644 packages/svelte/tests/runtime-runes/samples/snippet-raw-args/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 000000000000..6b68a0272656 --- /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 b4f5e9a12b68..b6a53bb40aa6 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 5bcb13e5f2ef..6cea28c9f30a 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/index.js'; diff --git a/packages/svelte/src/internal/client/dom/blocks/snippet.js b/packages/svelte/src/internal/client/dom/blocks/snippet.js index c0fc350ce45c..31b02a153f14 100644 --- a/packages/svelte/src/internal/client/dom/blocks/snippet.js +++ b/packages/svelte/src/internal/client/dom/blocks/snippet.js @@ -6,7 +6,8 @@ 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 { assign_nodes } from '../template.js'; /** * @template {(node: TemplateNode, ...args: any[]) => void} SnippetFn @@ -60,3 +61,29 @@ export function wrap_snippet(component, fn) { } }); } + +/** + * Create a snippet imperatively using mount, hyrdate and render functions. + * @param {{ + * mount: (...params: any[]) => Element, + * hydrate?: (element: Element, ...params: any[]) => void, + * render: (...params: any[]) => string + * }} options + */ +export function createRawSnippet({ mount, hydrate }) { + var snippet_fn = (/** @type {TemplateNode} */ anchor, /** @type {any[]} */ ...params) => { + var element; + if (hydrating) { + element = hydrate_node; + hydrate_next(); + if (hydrate !== undefined) hydrate(/** @type {Element} */ (element), ...params); + } else { + element = mount(...params); + anchor.before(element); + } + assign_nodes(element, element); + }; + add_snippet_symbol(snippet_fn); + + return snippet_fn; +} diff --git a/packages/svelte/src/internal/server/index.js b/packages/svelte/src/internal/server/index.js index 841794e54496..65e55ffad0f1 100644 --- a/packages/svelte/src/internal/server/index.js +++ b/packages/svelte/src/internal/server/index.js @@ -13,7 +13,7 @@ import { escape_html } from '../../escaping.js'; import { DEV } from 'esm-env'; import { current_component, pop, push } from './context.js'; import { EMPTY_COMMENT, BLOCK_CLOSE, BLOCK_OPEN } from './hydration.js'; -import { validate_store } from '../shared/validate.js'; +import { add_snippet_symbol, validate_store } from '../shared/validate.js'; // https://html.spec.whatwg.org/multipage/syntax.html#attributes-2 // https://infra.spec.whatwg.org/#noncharacter @@ -155,6 +155,22 @@ export function head(payload, fn) { head_payload.out += BLOCK_CLOSE; } +/** + * Create a snippet imperatively using mount, hyrdate and render functions. + * @param {{ + * mount: (...params: any[]) => Element, + * hydrate?: (element: Element, ...params: any[]) => void, + * render: (...params: any[]) => string + * }} options + */ +export function createRawSnippet({ render }) { + const snippet_fn = (/** @type {Payload} */ payload, /** @type {any[]} */ ...args) => { + payload.out += render(...args); + }; + add_snippet_symbol(snippet_fn); + return snippet_fn; +} + /** * @template V * @param {string} name diff --git a/packages/svelte/tests/runtime-runes/samples/snippet-raw-args/_config.js b/packages/svelte/tests/runtime-runes/samples/snippet-raw-args/_config.js new file mode 100644 index 000000000000..a22776d7e7f8 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/snippet-raw-args/_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: `
0
`, + + test({ assert, target }) { + const [b1] = target.querySelectorAll('button'); + + b1?.click(); + flushSync(); + assert.htmlEqual(target.innerHTML, `
1
`); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/snippet-raw-args/main.svelte b/packages/svelte/tests/runtime-runes/samples/snippet-raw-args/main.svelte new file mode 100644 index 000000000000..6568abd987e9 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/snippet-raw-args/main.svelte @@ -0,0 +1,32 @@ + + +
+ {@render snippet(count)} +
+ 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 000000000000..129a5734028c --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/snippet-raw/_config.js @@ -0,0 +1,8 @@ +import { test } from '../../test'; + +export default test({ + compileOptions: { + dev: true // Render in dev mode to check that the validation error is not thrown + }, + html: `

hello world

` +}); 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 000000000000..6a1dd3915ab2 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/snippet-raw/main.svelte @@ -0,0 +1,16 @@ + + +{@render hello()} diff --git a/packages/svelte/types/index.d.ts b/packages/svelte/types/index.d.ts index 399107815348..4219e14d52ee 100644 --- a/packages/svelte/types/index.d.ts +++ b/packages/svelte/types/index.d.ts @@ -365,12 +365,20 @@ declare module 'svelte' { export function flushSync(fn?: (() => void) | undefined): void; /** Anything except a function */ type NotFunction = T extends Function ? never : T; + /** + * Create a snippet imperatively using mount, hyrdate and render functions. + * */ + export function createRawSnippet({ mount, hydrate }: { + mount: (...params: any[]) => Element; + hydrate?: (element: Element, ...params: any[]) => void; + render: (...params: any[]) => string; + }): (anchor: TemplateNode, ...params: any[]) => void; /** * 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`. * * */ - export function mount, Exports extends Record>(component: ComponentType> | Component, options: {} extends Props ? { + function mount_1, Exports extends Record>(component: ComponentType> | Component, options: {} extends Props ? { target: Document | Element | ShadowRoot; anchor?: Node; props?: Props; @@ -389,7 +397,7 @@ declare module 'svelte' { * Hydrates a component on the given target and returns the exports and potentially the props (if compiled with `accessors: true`) of the component * * */ - export function hydrate, Exports extends Record>(component: ComponentType> | Component, options: {} extends Props ? { + function hydrate_1, Exports extends Record>(component: ComponentType> | Component, options: {} extends Props ? { target: Document | Element | ShadowRoot; props?: Props; events?: Record any>; @@ -450,8 +458,9 @@ declare module 'svelte' { * https://svelte.dev/docs/svelte#getallcontexts * */ export function getAllContexts = Map>(): T; + type TemplateNode = Text | Element | Comment; - export {}; + export { hydrate_1 as hydrate, mount_1 as mount }; } declare module 'svelte/action' { From 2b41c4b2fa171d50751ac22bc594abd69289031e Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Thu, 11 Jul 2024 18:02:30 -0400 Subject: [PATCH 02/19] handle missing hydrate function, improve types --- .../src/internal/client/dom/blocks/snippet.js | 36 +++++++++++++------ .../svelte/src/internal/shared/validate.js | 6 +++- packages/svelte/types/index.d.ts | 12 +++---- 3 files changed, 37 insertions(+), 17 deletions(-) diff --git a/packages/svelte/src/internal/client/dom/blocks/snippet.js b/packages/svelte/src/internal/client/dom/blocks/snippet.js index 31b02a153f14..7fdc2b1f7e26 100644 --- a/packages/svelte/src/internal/client/dom/blocks/snippet.js +++ b/packages/svelte/src/internal/client/dom/blocks/snippet.js @@ -62,28 +62,44 @@ export function wrap_snippet(component, fn) { }); } +/** + * @template T + * @typedef {{ + * [K in keyof T]: () => T[K]; + * }} LazyParams + */ + /** * Create a snippet imperatively using mount, hyrdate and render functions. + * @template {unknown[]} Params * @param {{ - * mount: (...params: any[]) => Element, - * hydrate?: (element: Element, ...params: any[]) => void, - * render: (...params: any[]) => string + * mount: (...params: LazyParams) => Element, + * hydrate?: (element: Element, ...params: LazyParams) => void, + * render: (...params: Params) => string * }} options + * @returns {import('svelte').Snippet} */ export function createRawSnippet({ mount, hydrate }) { - var snippet_fn = (/** @type {TemplateNode} */ anchor, /** @type {any[]} */ ...params) => { + return add_snippet_symbol((anchor, ...params) => { + /** @type {Element} */ var element; + if (hydrating) { - element = hydrate_node; + element = /** @type {Element} */ (hydrate_node); + + if (hydrate === undefined) { + element = mount(...params); + hydrate_node.replaceWith(element); + } else { + hydrate(element, ...params); + } + hydrate_next(); - if (hydrate !== undefined) hydrate(/** @type {Element} */ (element), ...params); } else { element = mount(...params); anchor.before(element); } - assign_nodes(element, element); - }; - add_snippet_symbol(snippet_fn); - return snippet_fn; + assign_nodes(element, element); + }); } diff --git a/packages/svelte/src/internal/shared/validate.js b/packages/svelte/src/internal/shared/validate.js index 1fb1644d97e0..6617907f4c13 100644 --- a/packages/svelte/src/internal/shared/validate.js +++ b/packages/svelte/src/internal/shared/validate.js @@ -1,3 +1,4 @@ +/** @import { TemplateNode } from '#client' */ import { is_void } from '../../constants.js'; import * as w from './warnings.js'; import * as e from './errors.js'; @@ -5,10 +6,13 @@ import * as e from './errors.js'; const snippet_symbol = Symbol.for('svelte.snippet'); /** - * @param {any} fn + * @param {(anchor: TemplateNode, ...args: any[]) => void} fn + * @returns {import('svelte').Snippet} */ export function add_snippet_symbol(fn) { + // @ts-expect-error fn[snippet_symbol] = true; + // @ts-expect-error return fn; } diff --git a/packages/svelte/types/index.d.ts b/packages/svelte/types/index.d.ts index 4219e14d52ee..f5d3b5f9c35d 100644 --- a/packages/svelte/types/index.d.ts +++ b/packages/svelte/types/index.d.ts @@ -368,11 +368,12 @@ declare module 'svelte' { /** * Create a snippet imperatively using mount, hyrdate and render functions. * */ - export function createRawSnippet({ mount, hydrate }: { - mount: (...params: any[]) => Element; - hydrate?: (element: Element, ...params: any[]) => void; - render: (...params: any[]) => string; - }): (anchor: TemplateNode, ...params: any[]) => void; + export function createRawSnippet({ mount, hydrate }: { + mount: (...params: LazyParams) => Element; + hydrate?: (element: Element, ...params: LazyParams) => void; + render: (...params: Params) => string; + }): import("svelte").Snippet; + type LazyParams = { [K in keyof T]: () => T[K]; }; /** * 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`. @@ -458,7 +459,6 @@ declare module 'svelte' { * https://svelte.dev/docs/svelte#getallcontexts * */ export function getAllContexts = Map>(): T; - type TemplateNode = Text | Element | Comment; export { hydrate_1 as hydrate, mount_1 as mount }; } From 263ef5f3a3408920a929a75f94270bbbbd7332c2 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Thu, 11 Jul 2024 23:26:18 -0400 Subject: [PATCH 03/19] fix --- packages/svelte/src/internal/client/dom/blocks/snippet.js | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/packages/svelte/src/internal/client/dom/blocks/snippet.js b/packages/svelte/src/internal/client/dom/blocks/snippet.js index 7fdc2b1f7e26..a0fbef93372c 100644 --- a/packages/svelte/src/internal/client/dom/blocks/snippet.js +++ b/packages/svelte/src/internal/client/dom/blocks/snippet.js @@ -86,15 +86,13 @@ export function createRawSnippet({ mount, hydrate }) { if (hydrating) { element = /** @type {Element} */ (hydrate_node); + hydrate_next(); if (hydrate === undefined) { - element = mount(...params); - hydrate_node.replaceWith(element); + element.replaceWith((element = mount(...params))); } else { hydrate(element, ...params); } - - hydrate_next(); } else { element = mount(...params); anchor.before(element); From 5f77f80c6df02659ab8092d85da2c8bbdc8585cf Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Thu, 11 Jul 2024 23:34:40 -0400 Subject: [PATCH 04/19] tweak types --- packages/svelte/src/internal/client/dom/blocks/snippet.js | 5 +++-- packages/svelte/src/internal/shared/types.d.ts | 4 ++++ packages/svelte/src/internal/shared/validate.js | 4 +++- 3 files changed, 10 insertions(+), 3 deletions(-) diff --git a/packages/svelte/src/internal/client/dom/blocks/snippet.js b/packages/svelte/src/internal/client/dom/blocks/snippet.js index a0fbef93372c..9be9ccf9df4f 100644 --- a/packages/svelte/src/internal/client/dom/blocks/snippet.js +++ b/packages/svelte/src/internal/client/dom/blocks/snippet.js @@ -1,4 +1,5 @@ /** @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'; @@ -73,8 +74,8 @@ export function wrap_snippet(component, fn) { * Create a snippet imperatively using mount, hyrdate and render functions. * @template {unknown[]} Params * @param {{ - * mount: (...params: LazyParams) => Element, - * hydrate?: (element: Element, ...params: LazyParams) => void, + * mount: (...params: Getters) => Element, + * hydrate?: (element: Element, ...params: Getters) => void, * render: (...params: Params) => string * }} options * @returns {import('svelte').Snippet} diff --git a/packages/svelte/src/internal/shared/types.d.ts b/packages/svelte/src/internal/shared/types.d.ts index 426d928ce3e2..926954c8dc51 100644 --- a/packages/svelte/src/internal/shared/types.d.ts +++ b/packages/svelte/src/internal/shared/types.d.ts @@ -6,3 +6,7 @@ export type Store = { export type SourceLocation = | [line: number, column: number] | [line: number, column: number, SourceLocation[]]; + +export type Getters = { + [K in keyof T]: () => T[K]; +}; diff --git a/packages/svelte/src/internal/shared/validate.js b/packages/svelte/src/internal/shared/validate.js index 6617907f4c13..bd03c7f99944 100644 --- a/packages/svelte/src/internal/shared/validate.js +++ b/packages/svelte/src/internal/shared/validate.js @@ -1,4 +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,7 +7,8 @@ import * as e from './errors.js'; const snippet_symbol = Symbol.for('svelte.snippet'); /** - * @param {(anchor: TemplateNode, ...args: any[]) => void} fn + * @template {unknown[]} Params + * @param {(anchor: TemplateNode, ...args: Getters) => void} fn * @returns {import('svelte').Snippet} */ export function add_snippet_symbol(fn) { From 4e7279af5eaf0eefaf81afe5df25789e7c5f0139 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Thu, 11 Jul 2024 23:34:52 -0400 Subject: [PATCH 05/19] beef up test --- .../samples/snippet-raw/_config.js | 11 ++++++++++- .../samples/snippet-raw/main.svelte | 18 +++++++++++++----- 2 files changed, 23 insertions(+), 6 deletions(-) diff --git a/packages/svelte/tests/runtime-runes/samples/snippet-raw/_config.js b/packages/svelte/tests/runtime-runes/samples/snippet-raw/_config.js index 129a5734028c..818d81f11702 100644 --- a/packages/svelte/tests/runtime-runes/samples/snippet-raw/_config.js +++ b/packages/svelte/tests/runtime-runes/samples/snippet-raw/_config.js @@ -1,8 +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: `

hello world

` + + 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 index 6a1dd3915ab2..0f65e0f89a01 100644 --- a/packages/svelte/tests/runtime-runes/samples/snippet-raw/main.svelte +++ b/packages/svelte/tests/runtime-runes/samples/snippet-raw/main.svelte @@ -1,16 +1,24 @@ -{@render hello()} + + +{@render hello(count)} From 12e5fe9f7d090e04bb26e9d230ced04b06e86526 Mon Sep 17 00:00:00 2001 From: Dominic Gannaway Date: Fri, 12 Jul 2024 09:35:01 +0100 Subject: [PATCH 06/19] build --- packages/svelte/types/index.d.ts | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/packages/svelte/types/index.d.ts b/packages/svelte/types/index.d.ts index f5d3b5f9c35d..73474a1e52d0 100644 --- a/packages/svelte/types/index.d.ts +++ b/packages/svelte/types/index.d.ts @@ -369,11 +369,10 @@ declare module 'svelte' { * Create a snippet imperatively using mount, hyrdate and render functions. * */ export function createRawSnippet({ mount, hydrate }: { - mount: (...params: LazyParams) => Element; - hydrate?: (element: Element, ...params: LazyParams) => void; + mount: (...params: Getters) => Element; + hydrate?: (element: Element, ...params: Getters) => void; render: (...params: Params) => string; }): import("svelte").Snippet; - type LazyParams = { [K in keyof T]: () => T[K]; }; /** * 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`. @@ -459,6 +458,9 @@ declare module 'svelte' { * https://svelte.dev/docs/svelte#getallcontexts * */ export function getAllContexts = Map>(): T; + type Getters = { + [K in keyof T]: () => T[K]; + }; export { hydrate_1 as hydrate, mount_1 as mount }; } From 011cef69f9145b5827b1adbcf18e1a36d0174475 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Fri, 12 Jul 2024 08:35:49 -0400 Subject: [PATCH 07/19] types --- .../src/internal/client/dom/blocks/snippet.js | 32 ++++++++++--------- .../svelte/src/internal/shared/validate.js | 5 +-- 2 files changed, 18 insertions(+), 19 deletions(-) diff --git a/packages/svelte/src/internal/client/dom/blocks/snippet.js b/packages/svelte/src/internal/client/dom/blocks/snippet.js index 9be9ccf9df4f..48e02be669db 100644 --- a/packages/svelte/src/internal/client/dom/blocks/snippet.js +++ b/packages/svelte/src/internal/client/dom/blocks/snippet.js @@ -81,24 +81,26 @@ export function wrap_snippet(component, fn) { * @returns {import('svelte').Snippet} */ export function createRawSnippet({ mount, hydrate }) { - return add_snippet_symbol((anchor, ...params) => { - /** @type {Element} */ - var element; + return add_snippet_symbol( + (/** @type {TemplateNode} */ anchor, /** @type {Getters} */ ...params) => { + /** @type {Element} */ + var element; - if (hydrating) { - element = /** @type {Element} */ (hydrate_node); - hydrate_next(); + if (hydrating) { + element = /** @type {Element} */ (hydrate_node); + hydrate_next(); - if (hydrate === undefined) { - element.replaceWith((element = mount(...params))); + if (hydrate === undefined) { + element.replaceWith((element = mount(...params))); + } else { + hydrate(element, ...params); + } } else { - hydrate(element, ...params); + element = mount(...params); + anchor.before(element); } - } else { - element = mount(...params); - anchor.before(element); - } - assign_nodes(element, element); - }); + assign_nodes(element, element); + } + ); } diff --git a/packages/svelte/src/internal/shared/validate.js b/packages/svelte/src/internal/shared/validate.js index bd03c7f99944..35f11974b409 100644 --- a/packages/svelte/src/internal/shared/validate.js +++ b/packages/svelte/src/internal/shared/validate.js @@ -7,14 +7,11 @@ import * as e from './errors.js'; const snippet_symbol = Symbol.for('svelte.snippet'); /** - * @template {unknown[]} Params - * @param {(anchor: TemplateNode, ...args: Getters) => void} fn + * @param {any} fn * @returns {import('svelte').Snippet} */ export function add_snippet_symbol(fn) { - // @ts-expect-error fn[snippet_symbol] = true; - // @ts-expect-error return fn; } From 9b89700b1a667576daa7488f4f7290dcd86da9b1 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Fri, 12 Jul 2024 08:57:12 -0400 Subject: [PATCH 08/19] oops this was temporary --- packages/svelte/src/internal/client/dom/blocks/snippet.js | 7 ------- 1 file changed, 7 deletions(-) diff --git a/packages/svelte/src/internal/client/dom/blocks/snippet.js b/packages/svelte/src/internal/client/dom/blocks/snippet.js index 48e02be669db..f9a3eb207b67 100644 --- a/packages/svelte/src/internal/client/dom/blocks/snippet.js +++ b/packages/svelte/src/internal/client/dom/blocks/snippet.js @@ -63,13 +63,6 @@ export function wrap_snippet(component, fn) { }); } -/** - * @template T - * @typedef {{ - * [K in keyof T]: () => T[K]; - * }} LazyParams - */ - /** * Create a snippet imperatively using mount, hyrdate and render functions. * @template {unknown[]} Params From 806f7a74675511dd6d6cca7875c6685dbba03035 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Fri, 12 Jul 2024 08:57:28 -0400 Subject: [PATCH 09/19] typo --- packages/svelte/src/internal/client/dom/blocks/snippet.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/svelte/src/internal/client/dom/blocks/snippet.js b/packages/svelte/src/internal/client/dom/blocks/snippet.js index f9a3eb207b67..c2aa9174a83f 100644 --- a/packages/svelte/src/internal/client/dom/blocks/snippet.js +++ b/packages/svelte/src/internal/client/dom/blocks/snippet.js @@ -64,7 +64,7 @@ export function wrap_snippet(component, fn) { } /** - * Create a snippet imperatively using mount, hyrdate and render functions. + * Create a snippet imperatively using mount, hydrate and render functions. * @template {unknown[]} Params * @param {{ * mount: (...params: Getters) => Element, From 08d2350affeacfa6eef73b1db36869ea6218531f Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Fri, 12 Jul 2024 09:01:49 -0400 Subject: [PATCH 10/19] regenerate types --- packages/svelte/types/index.d.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/svelte/types/index.d.ts b/packages/svelte/types/index.d.ts index 73474a1e52d0..f7eb80914143 100644 --- a/packages/svelte/types/index.d.ts +++ b/packages/svelte/types/index.d.ts @@ -366,7 +366,7 @@ declare module 'svelte' { /** Anything except a function */ type NotFunction = T extends Function ? never : T; /** - * Create a snippet imperatively using mount, hyrdate and render functions. + * Create a snippet imperatively using mount, hydrate and render functions. * */ export function createRawSnippet({ mount, hydrate }: { mount: (...params: Getters) => Element; From 442334f59f4218a5de0ccbbf58eb792541975de0 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Fri, 12 Jul 2024 10:20:18 -0400 Subject: [PATCH 11/19] make mount/render optional, error if missing --- packages/svelte/messages/client-errors/errors.md | 4 ++++ .../svelte/messages/server-errors/lifecycle.md | 4 ++++ .../src/internal/client/dom/blocks/snippet.js | 9 +++++++-- packages/svelte/src/internal/client/errors.js | 16 ++++++++++++++++ packages/svelte/src/internal/server/errors.js | 11 +++++++++++ packages/svelte/src/internal/server/index.js | 9 +++++++-- packages/svelte/types/index.d.ts | 4 ++-- 7 files changed, 51 insertions(+), 6 deletions(-) diff --git a/packages/svelte/messages/client-errors/errors.md b/packages/svelte/messages/client-errors/errors.md index e51242d8f63f..896c762bb320 100644 --- a/packages/svelte/messages/client-errors/errors.md +++ b/packages/svelte/messages/client-errors/errors.md @@ -60,6 +60,10 @@ > The `%rune%` rune is only available inside `.svelte` and `.svelte.js/ts` files +## snippet_missing_mount + +> Snippets created with `createRawSnippet(...)` and used on the client must specify a `mount` function + ## state_prototype_fixed > Cannot set prototype of `$state` object diff --git a/packages/svelte/messages/server-errors/lifecycle.md b/packages/svelte/messages/server-errors/lifecycle.md index 80830f79034c..d0e28050a1f3 100644 --- a/packages/svelte/messages/server-errors/lifecycle.md +++ b/packages/svelte/messages/server-errors/lifecycle.md @@ -1,3 +1,7 @@ ## lifecycle_function_unavailable > `%name%(...)` is not available on the server + +## snippet_missing_render + +> Snippets created with `createRawSnippet(...)` and used on the server must specify a `render` function diff --git a/packages/svelte/src/internal/client/dom/blocks/snippet.js b/packages/svelte/src/internal/client/dom/blocks/snippet.js index c2aa9174a83f..ddd6e99d6762 100644 --- a/packages/svelte/src/internal/client/dom/blocks/snippet.js +++ b/packages/svelte/src/internal/client/dom/blocks/snippet.js @@ -9,6 +9,7 @@ import { } from '../../runtime.js'; import { hydrate_next, hydrate_node, hydrating } from '../hydration.js'; import { assign_nodes } from '../template.js'; +import * as e from '../../errors.js'; /** * @template {(node: TemplateNode, ...args: any[]) => void} SnippetFn @@ -67,13 +68,17 @@ export function wrap_snippet(component, fn) { * Create a snippet imperatively using mount, hydrate and render functions. * @template {unknown[]} Params * @param {{ - * mount: (...params: Getters) => Element, + * mount?: (...params: Getters) => Element, * hydrate?: (element: Element, ...params: Getters) => void, - * render: (...params: Params) => string + * render?: (...params: Params) => string * }} options * @returns {import('svelte').Snippet} */ export function createRawSnippet({ mount, hydrate }) { + if (mount === undefined) { + e.snippet_missing_mount(); + } + return add_snippet_symbol( (/** @type {TemplateNode} */ anchor, /** @type {Getters} */ ...params) => { /** @type {Element} */ diff --git a/packages/svelte/src/internal/client/errors.js b/packages/svelte/src/internal/client/errors.js index f7ab6597af20..5ff07dfbea02 100644 --- a/packages/svelte/src/internal/client/errors.js +++ b/packages/svelte/src/internal/client/errors.js @@ -262,6 +262,22 @@ export function rune_outside_svelte(rune) { } } +/** + * Snippets created with `createRawSnippet(...)` and used on the client must specify a `mount` function + * @returns {never} + */ +export function snippet_missing_mount() { + if (DEV) { + const error = new Error(`snippet_missing_mount\nSnippets created with \`createRawSnippet(...)\` and used on the client must specify a \`mount\` function`); + + error.name = 'Svelte error'; + throw error; + } else { + // TODO print a link to the documentation + throw new Error("snippet_missing_mount"); + } +} + /** * Cannot set prototype of `$state` object * @returns {never} diff --git a/packages/svelte/src/internal/server/errors.js b/packages/svelte/src/internal/server/errors.js index 67f4a2dfc6a4..8afa90354070 100644 --- a/packages/svelte/src/internal/server/errors.js +++ b/packages/svelte/src/internal/server/errors.js @@ -8,6 +8,17 @@ export function lifecycle_function_unavailable(name) { const error = new Error(`lifecycle_function_unavailable\n\`${name}(...)\` is not available on the server`); + error.name = 'Svelte error'; + throw error; +} + +/** + * Snippets created with `createRawSnippet(...)` and used on the server must specify a `render` function + * @returns {never} + */ +export function snippet_missing_render() { + const error = new Error(`snippet_missing_render\nSnippets created with \`createRawSnippet(...)\` and used on the server must specify a \`render\` function`); + error.name = 'Svelte error'; throw error; } \ No newline at end of file diff --git a/packages/svelte/src/internal/server/index.js b/packages/svelte/src/internal/server/index.js index 65e55ffad0f1..7a020bc159d1 100644 --- a/packages/svelte/src/internal/server/index.js +++ b/packages/svelte/src/internal/server/index.js @@ -14,6 +14,7 @@ import { DEV } from 'esm-env'; import { current_component, pop, push } from './context.js'; import { EMPTY_COMMENT, BLOCK_CLOSE, BLOCK_OPEN } from './hydration.js'; import { add_snippet_symbol, validate_store } from '../shared/validate.js'; +import * as e from './errors.js'; // https://html.spec.whatwg.org/multipage/syntax.html#attributes-2 // https://infra.spec.whatwg.org/#noncharacter @@ -158,12 +159,16 @@ export function head(payload, fn) { /** * Create a snippet imperatively using mount, hyrdate and render functions. * @param {{ - * mount: (...params: any[]) => Element, + * mount?: (...params: any[]) => Element, * hydrate?: (element: Element, ...params: any[]) => void, - * render: (...params: any[]) => string + * render?: (...params: any[]) => string * }} options */ export function createRawSnippet({ render }) { + if (render === undefined) { + e.snippet_missing_render(); + } + const snippet_fn = (/** @type {Payload} */ payload, /** @type {any[]} */ ...args) => { payload.out += render(...args); }; diff --git a/packages/svelte/types/index.d.ts b/packages/svelte/types/index.d.ts index f7eb80914143..0ae6296afd60 100644 --- a/packages/svelte/types/index.d.ts +++ b/packages/svelte/types/index.d.ts @@ -369,9 +369,9 @@ declare module 'svelte' { * Create a snippet imperatively using mount, hydrate and render functions. * */ export function createRawSnippet({ mount, hydrate }: { - mount: (...params: Getters) => Element; + mount?: (...params: Getters) => Element; hydrate?: (element: Element, ...params: Getters) => void; - render: (...params: Params) => string; + render?: (...params: Params) => string; }): import("svelte").Snippet; /** * Mounts a component to the given target and returns the exports and potentially the props (if compiled with `accessors: true`) of the component. From e361a20608fb2142083c42e67bbb41d6c828188a Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Fri, 12 Jul 2024 10:23:20 -0400 Subject: [PATCH 12/19] move code to new module --- packages/svelte/src/index-server.js | 2 +- .../src/internal/server/blocks/snippet.js | 23 +++++++++++++++++++ packages/svelte/src/internal/server/index.js | 23 +------------------ 3 files changed, 25 insertions(+), 23 deletions(-) create mode 100644 packages/svelte/src/internal/server/blocks/snippet.js diff --git a/packages/svelte/src/index-server.js b/packages/svelte/src/index-server.js index 6cea28c9f30a..e5590fba504a 100644 --- a/packages/svelte/src/index-server.js +++ b/packages/svelte/src/index-server.js @@ -36,4 +36,4 @@ export async function tick() {} export { getAllContexts, getContext, hasContext, setContext } from './internal/server/context.js'; -export { createRawSnippet } from './internal/server/index.js'; +export { createRawSnippet } from './internal/server/blocks/snippet.js'; 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 000000000000..7b9805ce7b41 --- /dev/null +++ b/packages/svelte/src/internal/server/blocks/snippet.js @@ -0,0 +1,23 @@ +/** @import { Payload } from '#server' */ +import { add_snippet_symbol } from '../../shared/validate.js'; +import * as e from '../errors.js'; + +/** + * Create a snippet imperatively using mount, hyrdate and render functions. + * @param {{ + * mount?: (...params: any[]) => Element, + * hydrate?: (element: Element, ...params: any[]) => void, + * render?: (...params: any[]) => string + * }} options + */ +export function createRawSnippet({ render }) { + if (render === undefined) { + e.snippet_missing_render(); + } + + const snippet_fn = (/** @type {Payload} */ payload, /** @type {any[]} */ ...args) => { + payload.out += render(...args); + }; + add_snippet_symbol(snippet_fn); + return snippet_fn; +} diff --git a/packages/svelte/src/internal/server/index.js b/packages/svelte/src/internal/server/index.js index 7a020bc159d1..841794e54496 100644 --- a/packages/svelte/src/internal/server/index.js +++ b/packages/svelte/src/internal/server/index.js @@ -13,8 +13,7 @@ import { escape_html } from '../../escaping.js'; import { DEV } from 'esm-env'; import { current_component, pop, push } from './context.js'; import { EMPTY_COMMENT, BLOCK_CLOSE, BLOCK_OPEN } from './hydration.js'; -import { add_snippet_symbol, validate_store } from '../shared/validate.js'; -import * as e from './errors.js'; +import { validate_store } from '../shared/validate.js'; // https://html.spec.whatwg.org/multipage/syntax.html#attributes-2 // https://infra.spec.whatwg.org/#noncharacter @@ -156,26 +155,6 @@ export function head(payload, fn) { head_payload.out += BLOCK_CLOSE; } -/** - * Create a snippet imperatively using mount, hyrdate and render functions. - * @param {{ - * mount?: (...params: any[]) => Element, - * hydrate?: (element: Element, ...params: any[]) => void, - * render?: (...params: any[]) => string - * }} options - */ -export function createRawSnippet({ render }) { - if (render === undefined) { - e.snippet_missing_render(); - } - - const snippet_fn = (/** @type {Payload} */ payload, /** @type {any[]} */ ...args) => { - payload.out += render(...args); - }; - add_snippet_symbol(snippet_fn); - return snippet_fn; -} - /** * @template V * @param {string} name From 40d119158c584b52cdcc501409c0939690509da2 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Fri, 12 Jul 2024 10:57:33 -0400 Subject: [PATCH 13/19] test hydration --- .../samples/snippet-raw-hydrate/_config.js | 9 +++++++++ .../snippet-raw-hydrate/_expected.html | 1 + .../samples/snippet-raw-hydrate/main.svelte | 19 +++++++++++++++++++ .../samples/snippet-raw-mount/_config.js | 3 +++ .../samples/snippet-raw-mount/_expected.html | 1 + .../samples/snippet-raw-mount/main.svelte | 16 ++++++++++++++++ 6 files changed, 49 insertions(+) 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/hydration/samples/snippet-raw-mount/_config.js create mode 100644 packages/svelte/tests/hydration/samples/snippet-raw-mount/_expected.html create mode 100644 packages/svelte/tests/hydration/samples/snippet-raw-mount/main.svelte 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 000000000000..a2f55c2641b1 --- /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 000000000000..8d9dde52c112 --- /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 000000000000..51a1cab1b22e --- /dev/null +++ b/packages/svelte/tests/hydration/samples/snippet-raw-hydrate/main.svelte @@ -0,0 +1,19 @@ + + +{@render snippet()} diff --git a/packages/svelte/tests/hydration/samples/snippet-raw-mount/_config.js b/packages/svelte/tests/hydration/samples/snippet-raw-mount/_config.js new file mode 100644 index 000000000000..f47bee71df87 --- /dev/null +++ b/packages/svelte/tests/hydration/samples/snippet-raw-mount/_config.js @@ -0,0 +1,3 @@ +import { test } from '../../test'; + +export default test({}); diff --git a/packages/svelte/tests/hydration/samples/snippet-raw-mount/_expected.html b/packages/svelte/tests/hydration/samples/snippet-raw-mount/_expected.html new file mode 100644 index 000000000000..f93cda107826 --- /dev/null +++ b/packages/svelte/tests/hydration/samples/snippet-raw-mount/_expected.html @@ -0,0 +1 @@ +

mounted

diff --git a/packages/svelte/tests/hydration/samples/snippet-raw-mount/main.svelte b/packages/svelte/tests/hydration/samples/snippet-raw-mount/main.svelte new file mode 100644 index 000000000000..0677a0cf4f47 --- /dev/null +++ b/packages/svelte/tests/hydration/samples/snippet-raw-mount/main.svelte @@ -0,0 +1,16 @@ + + +{@render snippet()} From 1e4c6a2cbfb3789ab37920c88a52a1779a38d870 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Fri, 12 Jul 2024 14:36:32 -0400 Subject: [PATCH 14/19] simpler createRawSnippet API --- .../svelte/messages/client-errors/errors.md | 4 --- .../messages/server-errors/lifecycle.md | 4 --- .../src/internal/client/dom/blocks/snippet.js | 27 ++++++---------- packages/svelte/src/internal/client/errors.js | 16 ---------- .../src/internal/server/blocks/snippet.js | 20 ++++++------ packages/svelte/src/internal/server/errors.js | 11 ------- .../samples/snippet-raw-hydrate/main.svelte | 11 ++----- .../samples/snippet-raw-mount/_config.js | 3 -- .../samples/snippet-raw-mount/_expected.html | 1 - .../samples/snippet-raw-mount/main.svelte | 16 ---------- .../samples/snippet-raw-args/_config.js | 17 ---------- .../samples/snippet-raw-args/main.svelte | 32 ------------------- .../samples/snippet-raw/main.svelte | 12 +++---- 13 files changed, 26 insertions(+), 148 deletions(-) delete mode 100644 packages/svelte/tests/hydration/samples/snippet-raw-mount/_config.js delete mode 100644 packages/svelte/tests/hydration/samples/snippet-raw-mount/_expected.html delete mode 100644 packages/svelte/tests/hydration/samples/snippet-raw-mount/main.svelte delete mode 100644 packages/svelte/tests/runtime-runes/samples/snippet-raw-args/_config.js delete mode 100644 packages/svelte/tests/runtime-runes/samples/snippet-raw-args/main.svelte diff --git a/packages/svelte/messages/client-errors/errors.md b/packages/svelte/messages/client-errors/errors.md index 896c762bb320..e51242d8f63f 100644 --- a/packages/svelte/messages/client-errors/errors.md +++ b/packages/svelte/messages/client-errors/errors.md @@ -60,10 +60,6 @@ > The `%rune%` rune is only available inside `.svelte` and `.svelte.js/ts` files -## snippet_missing_mount - -> Snippets created with `createRawSnippet(...)` and used on the client must specify a `mount` function - ## state_prototype_fixed > Cannot set prototype of `$state` object diff --git a/packages/svelte/messages/server-errors/lifecycle.md b/packages/svelte/messages/server-errors/lifecycle.md index d0e28050a1f3..80830f79034c 100644 --- a/packages/svelte/messages/server-errors/lifecycle.md +++ b/packages/svelte/messages/server-errors/lifecycle.md @@ -1,7 +1,3 @@ ## lifecycle_function_unavailable > `%name%(...)` is not available on the server - -## snippet_missing_render - -> Snippets created with `createRawSnippet(...)` and used on the server must specify a `render` function diff --git a/packages/svelte/src/internal/client/dom/blocks/snippet.js b/packages/svelte/src/internal/client/dom/blocks/snippet.js index ddd6e99d6762..f4a3bf7db66a 100644 --- a/packages/svelte/src/internal/client/dom/blocks/snippet.js +++ b/packages/svelte/src/internal/client/dom/blocks/snippet.js @@ -1,5 +1,6 @@ /** @import { Effect, TemplateNode } from '#client' */ /** @import { Getters } from '#shared' */ +import { run } from '../../../shared/utils.js'; import { add_snippet_symbol } from '../../../shared/validate.js'; import { EFFECT_TRANSPARENT } from '../../constants.js'; import { branch, block, destroy_effect } from '../../reactivity/effects.js'; @@ -8,8 +9,8 @@ import { set_dev_current_component_function } from '../../runtime.js'; import { hydrate_next, hydrate_node, hydrating } from '../hydration.js'; +import { create_fragment_from_html } from '../reconciler.js'; import { assign_nodes } from '../template.js'; -import * as e from '../../errors.js'; /** * @template {(node: TemplateNode, ...args: any[]) => void} SnippetFn @@ -65,20 +66,15 @@ export function wrap_snippet(component, fn) { } /** - * Create a snippet imperatively using mount, hydrate and render functions. + * Create a snippet programmatically * @template {unknown[]} Params * @param {{ - * mount?: (...params: Getters) => Element, - * hydrate?: (element: Element, ...params: Getters) => void, - * render?: (...params: Params) => string + * render: (...params: Params) => string + * update?: (element: Element, ...params: Getters) => void, * }} options * @returns {import('svelte').Snippet} */ -export function createRawSnippet({ mount, hydrate }) { - if (mount === undefined) { - e.snippet_missing_mount(); - } - +export function createRawSnippet({ render, update }) { return add_snippet_symbol( (/** @type {TemplateNode} */ anchor, /** @type {Getters} */ ...params) => { /** @type {Element} */ @@ -87,17 +83,14 @@ export function createRawSnippet({ mount, hydrate }) { if (hydrating) { element = /** @type {Element} */ (hydrate_node); hydrate_next(); - - if (hydrate === undefined) { - element.replaceWith((element = mount(...params))); - } else { - hydrate(element, ...params); - } } else { - element = mount(...params); + var html = render(.../** @type {Params} */ (params.map(run))); + var fragment = create_fragment_from_html(html); + element = /** @type {Element} */ (fragment.firstChild); anchor.before(element); } + update?.(element, ...params); assign_nodes(element, element); } ); diff --git a/packages/svelte/src/internal/client/errors.js b/packages/svelte/src/internal/client/errors.js index 5ff07dfbea02..f7ab6597af20 100644 --- a/packages/svelte/src/internal/client/errors.js +++ b/packages/svelte/src/internal/client/errors.js @@ -262,22 +262,6 @@ export function rune_outside_svelte(rune) { } } -/** - * Snippets created with `createRawSnippet(...)` and used on the client must specify a `mount` function - * @returns {never} - */ -export function snippet_missing_mount() { - if (DEV) { - const error = new Error(`snippet_missing_mount\nSnippets created with \`createRawSnippet(...)\` and used on the client must specify a \`mount\` function`); - - error.name = 'Svelte error'; - throw error; - } else { - // TODO print a link to the documentation - throw new Error("snippet_missing_mount"); - } -} - /** * Cannot set prototype of `$state` object * @returns {never} diff --git a/packages/svelte/src/internal/server/blocks/snippet.js b/packages/svelte/src/internal/server/blocks/snippet.js index 7b9805ce7b41..f9856f07ea37 100644 --- a/packages/svelte/src/internal/server/blocks/snippet.js +++ b/packages/svelte/src/internal/server/blocks/snippet.js @@ -1,23 +1,21 @@ +/** @import { Snippet } from 'svelte' */ /** @import { Payload } from '#server' */ +/** @import { Getters } from '#shared' */ import { add_snippet_symbol } from '../../shared/validate.js'; -import * as e from '../errors.js'; /** - * Create a snippet imperatively using mount, hyrdate and render functions. + * Create a snippet programmatically + * @template {unknown[]} Params * @param {{ - * mount?: (...params: any[]) => Element, - * hydrate?: (element: Element, ...params: any[]) => void, - * render?: (...params: any[]) => string + * render: (...params: Params) => string + * update?: (element: Element, ...params: Getters) => void, * }} options + * @returns {Snippet} */ export function createRawSnippet({ render }) { - if (render === undefined) { - e.snippet_missing_render(); - } - - const snippet_fn = (/** @type {Payload} */ payload, /** @type {any[]} */ ...args) => { + const snippet_fn = (/** @type {Payload} */ payload, /** @type {Params} */ ...args) => { payload.out += render(...args); }; add_snippet_symbol(snippet_fn); - return snippet_fn; + return /** @type {Snippet} */ (snippet_fn); } diff --git a/packages/svelte/src/internal/server/errors.js b/packages/svelte/src/internal/server/errors.js index 8afa90354070..67f4a2dfc6a4 100644 --- a/packages/svelte/src/internal/server/errors.js +++ b/packages/svelte/src/internal/server/errors.js @@ -8,17 +8,6 @@ export function lifecycle_function_unavailable(name) { const error = new Error(`lifecycle_function_unavailable\n\`${name}(...)\` is not available on the server`); - error.name = 'Svelte error'; - throw error; -} - -/** - * Snippets created with `createRawSnippet(...)` and used on the server must specify a `render` function - * @returns {never} - */ -export function snippet_missing_render() { - const error = new Error(`snippet_missing_render\nSnippets created with \`createRawSnippet(...)\` and used on the server must specify a \`render\` function`); - error.name = 'Svelte error'; throw error; } \ No newline at end of file diff --git a/packages/svelte/tests/hydration/samples/snippet-raw-hydrate/main.svelte b/packages/svelte/tests/hydration/samples/snippet-raw-hydrate/main.svelte index 51a1cab1b22e..52bfd8f1ed9c 100644 --- a/packages/svelte/tests/hydration/samples/snippet-raw-hydrate/main.svelte +++ b/packages/svelte/tests/hydration/samples/snippet-raw-hydrate/main.svelte @@ -2,16 +2,11 @@ import { createRawSnippet } from 'svelte'; const snippet = createRawSnippet({ - mount() { - const p = document.createElement('p'); - p.textContent = 'mounted'; - return p; - }, - hydrate(p) { - p.textContent = 'hydrated'; - }, render() { return `

rendered

`; + }, + update(p) { + p.textContent = 'hydrated'; } }); diff --git a/packages/svelte/tests/hydration/samples/snippet-raw-mount/_config.js b/packages/svelte/tests/hydration/samples/snippet-raw-mount/_config.js deleted file mode 100644 index f47bee71df87..000000000000 --- a/packages/svelte/tests/hydration/samples/snippet-raw-mount/_config.js +++ /dev/null @@ -1,3 +0,0 @@ -import { test } from '../../test'; - -export default test({}); diff --git a/packages/svelte/tests/hydration/samples/snippet-raw-mount/_expected.html b/packages/svelte/tests/hydration/samples/snippet-raw-mount/_expected.html deleted file mode 100644 index f93cda107826..000000000000 --- a/packages/svelte/tests/hydration/samples/snippet-raw-mount/_expected.html +++ /dev/null @@ -1 +0,0 @@ -

mounted

diff --git a/packages/svelte/tests/hydration/samples/snippet-raw-mount/main.svelte b/packages/svelte/tests/hydration/samples/snippet-raw-mount/main.svelte deleted file mode 100644 index 0677a0cf4f47..000000000000 --- a/packages/svelte/tests/hydration/samples/snippet-raw-mount/main.svelte +++ /dev/null @@ -1,16 +0,0 @@ - - -{@render snippet()} diff --git a/packages/svelte/tests/runtime-runes/samples/snippet-raw-args/_config.js b/packages/svelte/tests/runtime-runes/samples/snippet-raw-args/_config.js deleted file mode 100644 index a22776d7e7f8..000000000000 --- a/packages/svelte/tests/runtime-runes/samples/snippet-raw-args/_config.js +++ /dev/null @@ -1,17 +0,0 @@ -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: `
0
`, - - test({ assert, target }) { - const [b1] = target.querySelectorAll('button'); - - b1?.click(); - flushSync(); - assert.htmlEqual(target.innerHTML, `
1
`); - } -}); diff --git a/packages/svelte/tests/runtime-runes/samples/snippet-raw-args/main.svelte b/packages/svelte/tests/runtime-runes/samples/snippet-raw-args/main.svelte deleted file mode 100644 index 6568abd987e9..000000000000 --- a/packages/svelte/tests/runtime-runes/samples/snippet-raw-args/main.svelte +++ /dev/null @@ -1,32 +0,0 @@ - - -
- {@render snippet(count)} -
- diff --git a/packages/svelte/tests/runtime-runes/samples/snippet-raw/main.svelte b/packages/svelte/tests/runtime-runes/samples/snippet-raw/main.svelte index 0f65e0f89a01..88290285d2c0 100644 --- a/packages/svelte/tests/runtime-runes/samples/snippet-raw/main.svelte +++ b/packages/svelte/tests/runtime-runes/samples/snippet-raw/main.svelte @@ -4,17 +4,13 @@ let count = $state(0); const hello = createRawSnippet({ - mount(count) { - const p = document.createElement('p') - + render(count) { + return `

clicks: ${count}

`; + }, + update(p, count) { $effect(() => { p.textContent = `clicks: ${count()}` }); - - return p; - }, - render(count) { - return `

clicks: ${count}

`; } }); From 62300f7286a971de94b737656a49b9174a6800cb Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Fri, 12 Jul 2024 14:37:08 -0400 Subject: [PATCH 15/19] regenerate types --- packages/svelte/types/index.d.ts | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/packages/svelte/types/index.d.ts b/packages/svelte/types/index.d.ts index 0ae6296afd60..321cab5d0f47 100644 --- a/packages/svelte/types/index.d.ts +++ b/packages/svelte/types/index.d.ts @@ -366,19 +366,18 @@ declare module 'svelte' { /** Anything except a function */ type NotFunction = T extends Function ? never : T; /** - * Create a snippet imperatively using mount, hydrate and render functions. + * Create a snippet programmatically * */ - export function createRawSnippet({ mount, hydrate }: { - mount?: (...params: Getters) => Element; - hydrate?: (element: Element, ...params: Getters) => void; - render?: (...params: Params) => string; + export function createRawSnippet({ render, update }: { + render: (...params: Params) => string; + update?: (element: Element, ...params: Getters) => void; }): import("svelte").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`. * * */ - function mount_1, Exports extends Record>(component: ComponentType> | Component, options: {} extends Props ? { + export function mount, Exports extends Record>(component: ComponentType> | Component, options: {} extends Props ? { target: Document | Element | ShadowRoot; anchor?: Node; props?: Props; @@ -397,7 +396,7 @@ declare module 'svelte' { * Hydrates a component on the given target and returns the exports and potentially the props (if compiled with `accessors: true`) of the component * * */ - function hydrate_1, Exports extends Record>(component: ComponentType> | Component, options: {} extends Props ? { + export function hydrate, Exports extends Record>(component: ComponentType> | Component, options: {} extends Props ? { target: Document | Element | ShadowRoot; props?: Props; events?: Record any>; @@ -462,7 +461,7 @@ declare module 'svelte' { [K in keyof T]: () => T[K]; }; - export { hydrate_1 as hydrate, mount_1 as mount }; + export {}; } declare module 'svelte/action' { From b133c04008867835f602ba90656a3c6a2a97f1f1 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Tue, 16 Jul 2024 16:38:56 -0400 Subject: [PATCH 16/19] change signature --- .../src/internal/client/dom/blocks/snippet.js | 20 ++++++++++-------- .../src/internal/server/blocks/snippet.js | 21 ++++++++++--------- .../samples/snippet-raw-hydrate/main.svelte | 12 +++++------ .../samples/snippet-raw/main.svelte | 12 +++++------ packages/svelte/types/index.d.ts | 8 +++---- 5 files changed, 38 insertions(+), 35 deletions(-) diff --git a/packages/svelte/src/internal/client/dom/blocks/snippet.js b/packages/svelte/src/internal/client/dom/blocks/snippet.js index f4a3bf7db66a..db8d4fdcce4c 100644 --- a/packages/svelte/src/internal/client/dom/blocks/snippet.js +++ b/packages/svelte/src/internal/client/dom/blocks/snippet.js @@ -1,6 +1,6 @@ +/** @import { Snippet } from 'svelte' */ /** @import { Effect, TemplateNode } from '#client' */ /** @import { Getters } from '#shared' */ -import { run } from '../../../shared/utils.js'; import { add_snippet_symbol } from '../../../shared/validate.js'; import { EFFECT_TRANSPARENT } from '../../constants.js'; import { branch, block, destroy_effect } from '../../reactivity/effects.js'; @@ -68,15 +68,17 @@ export function wrap_snippet(component, fn) { /** * Create a snippet programmatically * @template {unknown[]} Params - * @param {{ - * render: (...params: Params) => string - * update?: (element: Element, ...params: Getters) => void, - * }} options - * @returns {import('svelte').Snippet} + * @param {(...params: Getters) => { + * render: () => string + * setup?: (element: Element) => void + * }} fn + * @returns {Snippet} */ -export function createRawSnippet({ render, update }) { +export function createRawSnippet(fn) { return add_snippet_symbol( (/** @type {TemplateNode} */ anchor, /** @type {Getters} */ ...params) => { + var snippet = fn(...params); + /** @type {Element} */ var element; @@ -84,13 +86,13 @@ export function createRawSnippet({ render, update }) { element = /** @type {Element} */ (hydrate_node); hydrate_next(); } else { - var html = render(.../** @type {Params} */ (params.map(run))); + var html = snippet.render().trim(); var fragment = create_fragment_from_html(html); element = /** @type {Element} */ (fragment.firstChild); anchor.before(element); } - update?.(element, ...params); + snippet.setup?.(element); assign_nodes(element, element); } ); diff --git a/packages/svelte/src/internal/server/blocks/snippet.js b/packages/svelte/src/internal/server/blocks/snippet.js index f9856f07ea37..b9f72063f4d7 100644 --- a/packages/svelte/src/internal/server/blocks/snippet.js +++ b/packages/svelte/src/internal/server/blocks/snippet.js @@ -6,16 +6,17 @@ import { add_snippet_symbol } from '../../shared/validate.js'; /** * Create a snippet programmatically * @template {unknown[]} Params - * @param {{ - * render: (...params: Params) => string - * update?: (element: Element, ...params: Getters) => void, - * }} options + * @param {(...params: Getters) => { + * render: () => string + * setup?: (element: Element) => void + * }} fn * @returns {Snippet} */ -export function createRawSnippet({ render }) { - const snippet_fn = (/** @type {Payload} */ payload, /** @type {Params} */ ...args) => { - payload.out += render(...args); - }; - add_snippet_symbol(snippet_fn); - return /** @type {Snippet} */ (snippet_fn); +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/tests/hydration/samples/snippet-raw-hydrate/main.svelte b/packages/svelte/tests/hydration/samples/snippet-raw-hydrate/main.svelte index 52bfd8f1ed9c..84e1722908b2 100644 --- a/packages/svelte/tests/hydration/samples/snippet-raw-hydrate/main.svelte +++ b/packages/svelte/tests/hydration/samples/snippet-raw-hydrate/main.svelte @@ -1,14 +1,14 @@ {@render snippet()} diff --git a/packages/svelte/tests/runtime-runes/samples/snippet-raw/main.svelte b/packages/svelte/tests/runtime-runes/samples/snippet-raw/main.svelte index 88290285d2c0..ab23de4f36f2 100644 --- a/packages/svelte/tests/runtime-runes/samples/snippet-raw/main.svelte +++ b/packages/svelte/tests/runtime-runes/samples/snippet-raw/main.svelte @@ -3,16 +3,16 @@ let count = $state(0); - const hello = createRawSnippet({ - render(count) { - return `

clicks: ${count}

`; - }, - update(p, count) { + const hello = createRawSnippet((count) => ({ + render: () => ` +

clicks: ${count()}

+ `, + setup(p) { $effect(() => { p.textContent = `clicks: ${count()}` }); } - }); + })); diff --git a/packages/svelte/types/index.d.ts b/packages/svelte/types/index.d.ts index 82f9efd522c0..b0c8c026d36c 100644 --- a/packages/svelte/types/index.d.ts +++ b/packages/svelte/types/index.d.ts @@ -368,10 +368,10 @@ declare module 'svelte' { /** * Create a snippet programmatically * */ - export function createRawSnippet({ render, update }: { - render: (...params: Params) => string; - update?: (element: Element, ...params: Getters) => void; - }): import("svelte").Snippet; + 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`. From fde31d4e610aa26467f2d4480fbaa7f5163bd36c Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Tue, 16 Jul 2024 17:54:58 -0400 Subject: [PATCH 17/19] docs --- .../routes/docs/content/01-api/03-snippets.md | 4 +++ .../routes/docs/content/01-api/05-imports.md | 31 +++++++++++++++++++ 2 files changed, 35 insertions(+) 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 e10443f7505b..516300f2ea56 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 af4118eaabae..1c3aa7cf87e6 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: (h1) => { + $effect(() => { + h1.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 the snippet is fully static, you can omit the `setup` function. + ## `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 92cb9f89f1f65a805f4464bbf12ce29d8413e182 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Tue, 16 Jul 2024 19:25:28 -0400 Subject: [PATCH 18/19] h1 -> node --- .../src/routes/docs/content/01-api/05-imports.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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 1c3aa7cf87e6..8d12aa47be24 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 @@ -105,9 +105,9 @@ const greet = createRawSnippet((name) => { render: () => `

Hello ${name()}!

`, - setup: (h1) => { + setup: (node) => { $effect(() => { - h1.textContent = `Hello ${name()}!`; + node.textContent = `Hello ${name()}!`; }); } }; From 7239cbbcccf668e663d3d49cda7571d7855cbc8e Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Tue, 16 Jul 2024 20:47:38 -0400 Subject: [PATCH 19/19] allow `setup` to return a teardown function --- .../src/internal/client/dom/blocks/snippet.js | 8 ++++++-- .../samples/snippet-raw-teardown/_config.js | 11 +++++++++++ .../samples/snippet-raw-teardown/main.svelte | 18 ++++++++++++++++++ .../routes/docs/content/01-api/05-imports.md | 2 +- 4 files changed, 36 insertions(+), 3 deletions(-) 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 diff --git a/packages/svelte/src/internal/client/dom/blocks/snippet.js b/packages/svelte/src/internal/client/dom/blocks/snippet.js index db8d4fdcce4c..a920f6db3eda 100644 --- a/packages/svelte/src/internal/client/dom/blocks/snippet.js +++ b/packages/svelte/src/internal/client/dom/blocks/snippet.js @@ -3,7 +3,7 @@ /** @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 @@ -92,8 +92,12 @@ export function createRawSnippet(fn) { anchor.before(element); } - snippet.setup?.(element); + const result = snippet.setup?.(element); assign_nodes(element, element); + + if (typeof result === 'function') { + teardown(result); + } } ); } 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 000000000000..e05d82e929a2 --- /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 000000000000..026208645c6e --- /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/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 8d12aa47be24..d364f8242942 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 @@ -122,7 +122,7 @@ The `setup` function is called during `mount` or `hydrate` with that same elemen {@render greet(name)} ``` -If the snippet is fully static, you can omit the `setup` function. +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`