diff --git a/.changeset/ninety-pandas-move.md b/.changeset/ninety-pandas-move.md deleted file mode 100644 index 65f57ddbbf..0000000000 --- a/.changeset/ninety-pandas-move.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'svelte': patch ---- - -fix: flush effects scheduled during boundary's pending phase diff --git a/.github/workflows/ecosystem-ci-trigger.yml b/.github/workflows/ecosystem-ci-trigger.yml index 9be1f00104..7753b606e1 100644 --- a/.github/workflows/ecosystem-ci-trigger.yml +++ b/.github/workflows/ecosystem-ci-trigger.yml @@ -15,7 +15,6 @@ jobs: contents: read # to clone the repo steps: - name: monitor action permissions - uses: GitHubSecurityLab/actions-permissions/monitor@v1 - name: check user authorization # user needs triage permission uses: actions/github-script@v7 id: check-permissions diff --git a/.github/workflows/pkg.pr.new-comment.yml b/.github/workflows/pkg.pr.new-comment.yml index 3f1fca5a0b..8bf13bb8f1 100644 --- a/.github/workflows/pkg.pr.new-comment.yml +++ b/.github/workflows/pkg.pr.new-comment.yml @@ -14,7 +14,6 @@ jobs: name: 'Update comment' runs-on: ubuntu-latest steps: - - uses: GitHubSecurityLab/actions-permissions/monitor@v1 - name: Download artifact uses: actions/download-artifact@v4 with: diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index f9d683a2ae..b78346d883 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -17,7 +17,6 @@ jobs: name: Release runs-on: ubuntu-latest steps: - - uses: GitHubSecurityLab/actions-permissions/monitor@v1 - name: Checkout Repo uses: actions/checkout@v4 with: diff --git a/.prettierignore b/.prettierignore index 5e1d9b1aa7..9cf9a4bfe1 100644 --- a/.prettierignore +++ b/.prettierignore @@ -15,6 +15,7 @@ packages/svelte/src/internal/client/warnings.js packages/svelte/src/internal/shared/errors.js packages/svelte/src/internal/shared/warnings.js packages/svelte/src/internal/server/errors.js +packages/svelte/src/internal/server/warnings.js packages/svelte/tests/migrate/samples/*/output.svelte packages/svelte/tests/**/*.svelte packages/svelte/tests/**/_expected* diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 0653b08b76..e940252892 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -9,7 +9,7 @@ The [Open Source Guides](https://opensource.guide/) website has a collection of ## Get involved -There are many ways to contribute to Svelte, and many of them do not involve writing any code. Here's a few ideas to get started: +There are many ways to contribute to Svelte, and many of them do not involve writing any code. Here are a few ideas to get started: - Simply start using Svelte. Go through the [Getting Started](https://svelte.dev/docs#getting-started) guide. Does everything work as expected? If not, we're always looking for improvements. Let us know by [opening an issue](#reporting-new-issues). - Look through the [open issues](https://github.com/sveltejs/svelte/issues). A good starting point would be issues tagged [good first issue](https://github.com/sveltejs/svelte/issues?q=is%3Aissue+is%3Aopen+label%3A%22good+first+issue%22). Provide workarounds, ask for clarification, or suggest labels. Help [triage issues](#triaging-issues-and-pull-requests). @@ -90,9 +90,9 @@ A good test plan has the exact commands you ran and their output, provides scree #### Writing tests -All tests are located in `/test` folder. +All tests are located in the `/tests` folder. -Test samples are kept in `/test/xxx/samples` folder. +Test samples are kept in `/tests/xxx/samples` folders. #### Running tests diff --git a/documentation/docs/98-reference/.generated/client-warnings.md b/documentation/docs/98-reference/.generated/client-warnings.md index 6f1d677fe9..c95ace2229 100644 --- a/documentation/docs/98-reference/.generated/client-warnings.md +++ b/documentation/docs/98-reference/.generated/client-warnings.md @@ -312,6 +312,27 @@ Reactive `$state(...)` proxies and the values they proxy have different identiti To resolve this, ensure you're comparing values where both values were created with `$state(...)`, or neither were. Note that `$state.raw(...)` will _not_ create a state proxy. +### state_proxy_unmount + +``` +Tried to unmount a state proxy, rather than a component +``` + +`unmount` was called with a state proxy: + +```js +import { mount, unmount } from 'svelte'; +import Component from './Component.svelte'; +let target = document.body; +// ---cut--- +let component = $state(mount(Component, { target })); + +// later... +unmount(component); +``` + +Avoid using `$state` here. If `component` _does_ need to be reactive for some reason, use `$state.raw` instead. + ### svelte_boundary_reset_noop ``` diff --git a/documentation/docs/98-reference/.generated/server-errors.md b/documentation/docs/98-reference/.generated/server-errors.md index c3e8b53c31..6263032212 100644 --- a/documentation/docs/98-reference/.generated/server-errors.md +++ b/documentation/docs/98-reference/.generated/server-errors.md @@ -1,5 +1,19 @@ +### await_invalid + +``` +Encountered asynchronous work while rendering synchronously. +``` + +You (or the framework you're using) called [`render(...)`](svelte-server#render) with a component containing an `await` expression. Either `await` the result of `render` or wrap the `await` (or the component containing it) in a [``](svelte-boundary) with a `pending` snippet. + +### html_deprecated + +``` +The `html` property of server render results has been deprecated. Use `body` instead. +``` + ### lifecycle_function_unavailable ``` diff --git a/documentation/docs/98-reference/.generated/server-warnings.md b/documentation/docs/98-reference/.generated/server-warnings.md new file mode 100644 index 0000000000..26b3628be9 --- /dev/null +++ b/documentation/docs/98-reference/.generated/server-warnings.md @@ -0,0 +1,9 @@ + + +### experimental_async_ssr + +``` +Attempted to use asynchronous rendering without `experimental.async` enabled +``` + +Set `experimental.async: true` in your compiler options (usually in `svelte.config.js`) to use async server rendering. This render ran synchronously. diff --git a/documentation/docs/98-reference/.generated/shared-errors.md b/documentation/docs/98-reference/.generated/shared-errors.md index de34b3f5da..6c31aaafd0 100644 --- a/documentation/docs/98-reference/.generated/shared-errors.md +++ b/documentation/docs/98-reference/.generated/shared-errors.md @@ -1,25 +1,5 @@ -### await_outside_boundary - -``` -Cannot await outside a `` with a `pending` snippet -``` - -The `await` keyword can only appear in a `$derived(...)` or template expression, or at the top level of a component's ``); -} - -export function reset_elements() { - let old_parent = parent; - parent = null; - return () => { - parent = old_parent; - }; + renderer.head((r) => r.push(``)); } /** - * @param {Payload} payload + * @param {Renderer} renderer * @param {string} tag * @param {number} line * @param {number} column */ -export function push_element(payload, tag, line, column) { - var filename = /** @type {Component} */ (current_component).function[FILENAME]; - var child = { tag, parent, filename, line, column }; +export function push_element(renderer, tag, line, column) { + var context = /** @type {SSRContext} */ (ssr_context); + var filename = context.function[FILENAME]; + var parent = context.element; + var element = { tag, parent, filename, line, column }; - if (parent !== null) { + if (parent !== undefined) { var ancestor = parent.parent; var ancestors = [parent.tag]; @@ -71,7 +64,7 @@ export function push_element(payload, tag, line, column) { : undefined; const message = is_tag_valid_with_parent(tag, parent.tag, child_loc, parent_loc); - if (message) print_error(payload, message); + if (message) print_error(renderer, message); while (ancestor != null) { ancestors.push(ancestor.tag); @@ -80,27 +73,27 @@ export function push_element(payload, tag, line, column) { : undefined; const message = is_tag_valid_with_ancestor(tag, ancestors, child_loc, ancestor_loc); - if (message) print_error(payload, message); + if (message) print_error(renderer, message); ancestor = ancestor.parent; } } - parent = child; + set_ssr_context({ ...context, p: context, element }); } export function pop_element() { - parent = /** @type {Element} */ (parent).parent; + set_ssr_context(/** @type {SSRContext} */ (ssr_context).p); } /** - * @param {Payload} payload + * @param {Renderer} renderer */ -export function validate_snippet_args(payload) { +export function validate_snippet_args(renderer) { if ( - typeof payload !== 'object' || - // for some reason typescript consider the type of payload as never after the first instanceof - !(payload instanceof Payload || /** @type {any} */ (payload) instanceof HeadPayload) + typeof renderer !== 'object' || + // for some reason typescript consider the type of renderer as never after the first instanceof + !(renderer instanceof Renderer) ) { e.invalid_snippet_arguments(); } diff --git a/packages/svelte/src/internal/server/errors.js b/packages/svelte/src/internal/server/errors.js index 458937218f..bde49fe935 100644 --- a/packages/svelte/src/internal/server/errors.js +++ b/packages/svelte/src/internal/server/errors.js @@ -2,6 +2,30 @@ export * from '../shared/errors.js'; +/** + * Encountered asynchronous work while rendering synchronously. + * @returns {never} + */ +export function await_invalid() { + const error = new Error(`await_invalid\nEncountered asynchronous work while rendering synchronously.\nhttps://svelte.dev/e/await_invalid`); + + error.name = 'Svelte error'; + + throw error; +} + +/** + * The `html` property of server render results has been deprecated. Use `body` instead. + * @returns {never} + */ +export function html_deprecated() { + const error = new Error(`html_deprecated\nThe \`html\` property of server render results has been deprecated. Use \`body\` instead.\nhttps://svelte.dev/e/html_deprecated`); + + error.name = 'Svelte error'; + + throw error; +} + /** * `%name%(...)` is not available on the server * @param {string} name diff --git a/packages/svelte/src/internal/server/index.js b/packages/svelte/src/internal/server/index.js index 3aa44f2daa..3ff44f8030 100644 --- a/packages/svelte/src/internal/server/index.js +++ b/packages/svelte/src/internal/server/index.js @@ -1,6 +1,7 @@ -/** @import { ComponentType, SvelteComponent } from 'svelte' */ -/** @import { Component, RenderOutput } from '#server' */ +/** @import { ComponentType, SvelteComponent, Component } from 'svelte' */ +/** @import { RenderOutput } from '#server' */ /** @import { Store } from '#shared' */ +/** @import { AccumulatedContent } from './renderer.js' */ export { FILENAME, HMR } from '../../constants.js'; import { attr, clsx, to_class, to_style } from '../shared/attributes.js'; import { is_promise, noop } from '../shared/utils.js'; @@ -13,13 +14,10 @@ import { } from '../../constants.js'; 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, BLOCK_OPEN_ELSE } from './hydration.js'; import { validate_store } from '../shared/validate.js'; import { is_boolean_attribute, is_raw_text_element, is_void } from '../../utils.js'; -import { reset_elements } from './dev.js'; -import { Payload } from './payload.js'; -import { abort } from './abort-signal.js'; +import { Renderer } from './renderer.js'; // https://html.spec.whatwg.org/multipage/syntax.html#attributes-2 // https://infra.spec.whatwg.org/#noncharacter @@ -27,150 +25,96 @@ const INVALID_ATTR_NAME_CHAR_REGEX = /[\s'">/=\u{FDD0}-\u{FDEF}\u{FFFE}\u{FFFF}\u{1FFFE}\u{1FFFF}\u{2FFFE}\u{2FFFF}\u{3FFFE}\u{3FFFF}\u{4FFFE}\u{4FFFF}\u{5FFFE}\u{5FFFF}\u{6FFFE}\u{6FFFF}\u{7FFFE}\u{7FFFF}\u{8FFFE}\u{8FFFF}\u{9FFFE}\u{9FFFF}\u{AFFFE}\u{AFFFF}\u{BFFFE}\u{BFFFF}\u{CFFFE}\u{CFFFF}\u{DFFFE}\u{DFFFF}\u{EFFFE}\u{EFFFF}\u{FFFFE}\u{FFFFF}\u{10FFFE}\u{10FFFF}]/u; /** - * @param {Payload} payload + * @param {Renderer} renderer * @param {string} tag * @param {() => void} attributes_fn * @param {() => void} children_fn * @returns {void} */ -export function element(payload, tag, attributes_fn = noop, children_fn = noop) { - payload.out.push(''); +export function element(renderer, tag, attributes_fn = noop, children_fn = noop) { + renderer.push(''); if (tag) { - payload.out.push(`<${tag}`); + renderer.push(`<${tag}`); attributes_fn(); - payload.out.push(`>`); + renderer.push(`>`); if (!is_void(tag)) { children_fn(); if (!is_raw_text_element(tag)) { - payload.out.push(EMPTY_COMMENT); + renderer.push(EMPTY_COMMENT); } - payload.out.push(``); + renderer.push(``); } } - payload.out.push(''); + renderer.push(''); } -/** - * Array of `onDestroy` callbacks that should be called at the end of the server render function - * @type {Function[]} - */ -export let on_destroy = []; - /** * Only available on the server and when compiling with the `server` option. * Takes a component and returns an object with `body` and `head` properties on it, which you can use to populate the HTML when server-rendering your app. * @template {Record} Props - * @param {import('svelte').Component | ComponentType>} component + * @param {Component | ComponentType>} component * @param {{ props?: Omit; context?: Map; idPrefix?: string }} [options] * @returns {RenderOutput} */ export function render(component, options = {}) { - try { - const payload = new Payload(options.idPrefix ? options.idPrefix + '-' : ''); - - const prev_on_destroy = on_destroy; - on_destroy = []; - payload.out.push(BLOCK_OPEN); - - let reset_reset_element; - - if (DEV) { - // prevent parent/child element state being corrupted by a bad render - reset_reset_element = reset_elements(); - } - - if (options.context) { - push(); - /** @type {Component} */ (current_component).c = options.context; - } - - // @ts-expect-error - component(payload, options.props ?? {}, {}, {}); - - if (options.context) { - pop(); - } - - if (reset_reset_element) { - reset_reset_element(); - } - - payload.out.push(BLOCK_CLOSE); - for (const cleanup of on_destroy) cleanup(); - on_destroy = prev_on_destroy; - - let head = payload.head.out.join('') + payload.head.title; - - for (const { hash, code } of payload.css) { - head += ``; - } - - const body = payload.out.join(''); - - return { - head, - html: body, - body: body - }; - } finally { - abort(); - } + return Renderer.render(/** @type {Component} */ (component), options); } /** - * @param {Payload} payload - * @param {(head_payload: Payload['head']) => void} fn + * @param {Renderer} renderer + * @param {(renderer: Renderer) => Promise | void} fn * @returns {void} */ -export function head(payload, fn) { - const head_payload = payload.head; - head_payload.out.push(BLOCK_OPEN); - fn(head_payload); - head_payload.out.push(BLOCK_CLOSE); +export function head(renderer, fn) { + renderer.head((renderer) => { + renderer.push(BLOCK_OPEN); + renderer.child(fn); + renderer.push(BLOCK_CLOSE); + }); } /** - * @param {Payload} payload + * @param {Renderer} renderer * @param {boolean} is_html * @param {Record} props * @param {() => void} component * @param {boolean} dynamic * @returns {void} */ -export function css_props(payload, is_html, props, component, dynamic = false) { +export function css_props(renderer, is_html, props, component, dynamic = false) { const styles = style_object_to_string(props); if (is_html) { - payload.out.push(``); + renderer.push(``); } else { - payload.out.push(``); + renderer.push(``); } if (dynamic) { - payload.out.push(''); + renderer.push(''); } component(); if (is_html) { - payload.out.push(``); + renderer.push(``); } else { - payload.out.push(``); + renderer.push(``); } } /** * @param {Record} attrs - * @param {string | null} css_hash + * @param {string} [css_hash] * @param {Record} [classes] * @param {Record} [styles] * @param {number} [flags] * @returns {string} */ -export function spread_attributes(attrs, css_hash, classes, styles, flags = 0) { +export function attributes(attrs, css_hash, classes, styles, flags = 0) { if (styles) { attrs.style = to_style(attrs.style, styles); } @@ -360,14 +304,14 @@ export function unsubscribe_stores(store_values) { } /** - * @param {Payload} payload + * @param {Renderer} renderer * @param {Record} $$props * @param {string} name * @param {Record} slot_props * @param {null | (() => void)} fallback_fn * @returns {void} */ -export function slot(payload, $$props, name, slot_props, fallback_fn) { +export function slot(renderer, $$props, name, slot_props, fallback_fn) { var slot_fn = $$props.$$slots?.[name]; // Interop: Can use snippets to fill slots if (slot_fn === true) { @@ -375,7 +319,7 @@ export function slot(payload, $$props, name, slot_props, fallback_fn) { } if (slot_fn !== undefined) { - slot_fn(payload, slot_props); + slot_fn(renderer, slot_props); } else { fallback_fn?.(); } @@ -443,21 +387,21 @@ export function bind_props(props_parent, props_now) { /** * @template V - * @param {Payload} payload + * @param {Renderer} renderer * @param {Promise} promise * @param {null | (() => void)} pending_fn * @param {(value: V) => void} then_fn * @returns {void} */ -function await_block(payload, promise, pending_fn, then_fn) { +function await_block(renderer, promise, pending_fn, then_fn) { if (is_promise(promise)) { - payload.out.push(BLOCK_OPEN); + renderer.push(BLOCK_OPEN); promise.then(null, noop); if (pending_fn !== null) { pending_fn(); } } else if (then_fn !== null) { - payload.out.push(BLOCK_OPEN_ELSE); + renderer.push(BLOCK_OPEN_ELSE); then_fn(promise); } } @@ -499,12 +443,12 @@ export function once(get_value) { /** * Create an unique ID - * @param {Payload} payload + * @param {Renderer} renderer * @returns {string} */ -export function props_id(payload) { - const uid = payload.uid(); - payload.out.push(''); +export function props_id(renderer) { + const uid = renderer.global.uid(); + renderer.push(''); return uid; } @@ -512,12 +456,10 @@ export { attr, clsx }; export { html } from './blocks/html.js'; -export { push, pop } from './context.js'; +export { save } from './context.js'; export { push_element, pop_element, validate_snippet_args } from './dev.js'; -export { assign_payload, copy_payload } from './payload.js'; - export { snapshot } from '../shared/clone.js'; export { fallback, to_array } from '../shared/utils.js'; @@ -531,8 +473,6 @@ export { export { escape_html as escape }; -export { await_outside_boundary } from '../shared/errors.js'; - /** * @template T * @param {()=>T} fn @@ -553,33 +493,3 @@ export function derived(fn) { return updated_value; }; } - -/** - * - * @param {Payload} payload - * @param {*} value - */ -export function maybe_selected(payload, value) { - return value === payload.select_value ? ' selected' : ''; -} - -/** - * @param {Payload} payload - * @param {() => void} children - * @returns {void} - */ -export function valueless_option(payload, children) { - var i = payload.out.length; - - children(); - - var body = payload.out.slice(i).join(''); - - if (body.replace(//g, '') === payload.select_value) { - // replace '>' with ' selected>' (closing tag will be added later) - var last_item = payload.out[i - 1]; - payload.out[i - 1] = last_item.slice(0, -1) + ' selected>'; - // Remove the old items after position i and add the body as a single item - payload.out.splice(i, payload.out.length - i, body); - } -} diff --git a/packages/svelte/src/internal/server/payload.js b/packages/svelte/src/internal/server/payload.js deleted file mode 100644 index a7e40ad1db..0000000000 --- a/packages/svelte/src/internal/server/payload.js +++ /dev/null @@ -1,80 +0,0 @@ -export class HeadPayload { - /** @type {Set<{ hash: string; code: string }>} */ - css = new Set(); - /** @type {string[]} */ - out = []; - uid = () => ''; - title = ''; - - constructor( - /** @type {Set<{ hash: string; code: string }>} */ css = new Set(), - /** @type {string[]} */ out = [], - title = '', - uid = () => '' - ) { - this.css = css; - this.out = out; - this.title = title; - this.uid = uid; - } -} - -export class Payload { - /** @type {Set<{ hash: string; code: string }>} */ - css = new Set(); - /** @type {string[]} */ - out = []; - uid = () => ''; - select_value = undefined; - - head = new HeadPayload(); - - constructor(id_prefix = '') { - this.uid = props_id_generator(id_prefix); - this.head.uid = this.uid; - } -} - -/** - * Used in legacy mode to handle bindings - * @param {Payload} to_copy - * @returns {Payload} - */ -export function copy_payload({ out, css, head, uid }) { - const payload = new Payload(); - - payload.out = [...out]; - payload.css = new Set(css); - payload.uid = uid; - - payload.head = new HeadPayload(); - payload.head.out = [...head.out]; - payload.head.css = new Set(head.css); - payload.head.title = head.title; - payload.head.uid = head.uid; - - return payload; -} - -/** - * Assigns second payload to first - * @param {Payload} p1 - * @param {Payload} p2 - * @returns {void} - */ -export function assign_payload(p1, p2) { - p1.out = [...p2.out]; - p1.css = p2.css; - p1.head = p2.head; - p1.uid = p2.uid; -} - -/** - * Creates an ID generator - * @param {string} prefix - * @returns {() => string} - */ -function props_id_generator(prefix) { - let uid = 1; - return () => `${prefix}s${uid++}`; -} diff --git a/packages/svelte/src/internal/server/renderer.js b/packages/svelte/src/internal/server/renderer.js new file mode 100644 index 0000000000..6971f200a0 --- /dev/null +++ b/packages/svelte/src/internal/server/renderer.js @@ -0,0 +1,611 @@ +/** @import { Component } from 'svelte' */ +/** @import { RenderOutput, SSRContext, SyncRenderOutput } from './types.js' */ +import { async_mode_flag } from '../flags/index.js'; +import { abort } from './abort-signal.js'; +import { pop, push, set_ssr_context, ssr_context } from './context.js'; +import * as e from './errors.js'; +import * as w from './warnings.js'; +import { BLOCK_CLOSE, BLOCK_OPEN } from './hydration.js'; +import { attributes } from './index.js'; + +/** @typedef {'head' | 'body'} RendererType */ +/** @typedef {{ [key in RendererType]: string }} AccumulatedContent */ +/** + * @template T + * @typedef {T | Promise} MaybePromise + */ +/** + * @typedef {string | Renderer} RendererItem + */ + +/** + * Renderers are basically a tree of `string | Renderer`s, where each `Renderer` in the tree represents + * work that may or may not have completed. A renderer can be {@link collect}ed to aggregate the + * content from itself and all of its children, but this will throw if any of the children are + * performing asynchronous work. To asynchronously collect a renderer, just `await` it. + * + * The `string` values within a renderer are always associated with the {@link type} of that renderer. To switch types, + * call {@link child} with a different `type` argument. + */ +export class Renderer { + /** + * The contents of the renderer. + * @type {RendererItem[]} + */ + #out = []; + + /** + * Any `onDestroy` callbacks registered during execution of this renderer. + * @type {(() => void)[] | undefined} + */ + #on_destroy = undefined; + + /** + * Whether this renderer is a component body. + * @type {boolean} + */ + #is_component_body = false; + + /** + * The type of string content that this renderer is accumulating. + * @type {RendererType} + */ + type; + + /** @type {Renderer | undefined} */ + #parent; + + /** + * Asynchronous work associated with this renderer + * @type {Promise | undefined} + */ + promise = undefined; + + /** + * State which is associated with the content tree as a whole. + * It will be re-exposed, uncopied, on all children. + * @type {SSRState} + * @readonly + */ + global; + + /** + * State that is local to the branch it is declared in. + * It will be shallow-copied to all children. + * + * @type {{ select_value: string | undefined }} + */ + local; + + /** + * @param {SSRState} global + * @param {Renderer | undefined} [parent] + */ + constructor(global, parent) { + this.#parent = parent; + + this.global = global; + this.local = parent ? { ...parent.local } : { select_value: undefined }; + this.type = parent ? parent.type : 'body'; + } + + /** + * @param {(renderer: Renderer) => void} fn + */ + head(fn) { + const head = new Renderer(this.global, this); + head.type = 'head'; + + this.#out.push(head); + head.child(fn); + } + + /** + * @param {(renderer: Renderer) => void} fn + */ + async(fn) { + this.#out.push(BLOCK_OPEN); + this.child(fn); + this.#out.push(BLOCK_CLOSE); + } + + /** + * Create a child renderer. The child renderer inherits the state from the parent, + * but has its own content. + * @param {(renderer: Renderer) => MaybePromise} fn + */ + child(fn) { + const child = new Renderer(this.global, this); + this.#out.push(child); + + const parent = ssr_context; + + set_ssr_context({ + ...ssr_context, + p: parent, + c: null, + r: child + }); + + const result = fn(child); + + set_ssr_context(parent); + + if (result instanceof Promise) { + if (child.global.mode === 'sync') { + e.await_invalid(); + } + // just to avoid unhandled promise rejections -- we'll end up throwing in `collect_async` if something fails + result.catch(() => {}); + child.promise = result; + } + + return child; + } + + /** + * Create a component renderer. The component renderer inherits the state from the parent, + * but has its own content. It is treated as an ordering boundary for ondestroy callbacks. + * @param {(renderer: Renderer) => MaybePromise} fn + * @param {Function} [component_fn] + * @returns {void} + */ + component(fn, component_fn) { + push(component_fn); + const child = this.child(fn); + child.#is_component_body = true; + pop(); + } + + /** + * @param {Record} attrs + * @param {(renderer: Renderer) => void} fn + */ + select({ value, ...attrs }, fn) { + this.push(``); + this.child((renderer) => { + renderer.local.select_value = value; + fn(renderer); + }); + this.push(''); + } + + /** + * @param {Record} attrs + * @param {string | number | boolean | ((renderer: Renderer) => void)} body + */ + option(attrs, body) { + this.#out.push(` { + if ('value' in attrs) { + value = attrs.value; + } + + if (value === this.local.select_value) { + renderer.#out.push(' selected'); + } + + renderer.#out.push(`>${body}`); + + // super edge case, but may as well handle it + if (head) { + renderer.head((child) => child.push(head)); + } + }; + + if (typeof body === 'function') { + this.child((renderer) => { + const r = new Renderer(this.global, this); + body(r); + + if (this.global.mode === 'async') { + return r.#collect_content_async().then((content) => { + close(renderer, content.body.replaceAll('', ''), content); + }); + } else { + const content = r.#collect_content(); + close(renderer, content.body.replaceAll('', ''), content); + } + }); + } else { + close(this, body, { body }); + } + } + + /** + * @param {(renderer: Renderer) => void} fn + */ + title(fn) { + const path = this.get_path(); + + /** @param {string} head */ + const close = (head) => { + this.global.set_title(head, path); + }; + + this.child((renderer) => { + const r = new Renderer(renderer.global, renderer); + fn(r); + + if (renderer.global.mode === 'async') { + return r.#collect_content_async().then((content) => { + close(content.head); + }); + } else { + const content = r.#collect_content(); + close(content.head); + } + }); + } + + /** + * @param {string | (() => Promise)} content + */ + push(content) { + if (typeof content === 'function') { + this.child(async (renderer) => renderer.push(await content())); + } else { + this.#out.push(content); + } + } + + /** + * @param {() => void} fn + */ + on_destroy(fn) { + (this.#on_destroy ??= []).push(fn); + } + + /** + * @returns {number[]} + */ + get_path() { + return this.#parent ? [...this.#parent.get_path(), this.#parent.#out.indexOf(this)] : []; + } + + /** + * @deprecated this is needed for legacy component bindings + */ + copy() { + const copy = new Renderer(this.global, this.#parent); + copy.#out = this.#out.map((item) => (item instanceof Renderer ? item.copy() : item)); + copy.promise = this.promise; + return copy; + } + + /** + * @param {Renderer} other + * @deprecated this is needed for legacy component bindings + */ + subsume(other) { + if (this.global.mode !== other.global.mode) { + throw new Error( + "invariant: A renderer cannot switch modes. If you're seeing this, there's a compiler bug. File an issue!" + ); + } + + this.local = other.local; + this.#out = other.#out.map((item) => { + if (item instanceof Renderer) { + item.subsume(item); + } + return item; + }); + this.promise = other.promise; + this.type = other.type; + } + + get length() { + return this.#out.length; + } + + /** + * Only available on the server and when compiling with the `server` option. + * Takes a component and returns an object with `body` and `head` properties on it, which you can use to populate the HTML when server-rendering your app. + * @template {Record} Props + * @param {Component} component + * @param {{ props?: Omit; context?: Map; idPrefix?: string }} [options] + * @returns {RenderOutput} + */ + static render(component, options = {}) { + /** @type {AccumulatedContent | undefined} */ + let sync; + /** @type {Promise | undefined} */ + let async; + + const result = /** @type {RenderOutput} */ ({}); + // making these properties non-enumerable so that console.logging + // doesn't trigger a sync render + Object.defineProperties(result, { + html: { + get: () => { + return (sync ??= Renderer.#render(component, options)).body; + } + }, + head: { + get: () => { + return (sync ??= Renderer.#render(component, options)).head; + } + }, + body: { + get: () => { + return (sync ??= Renderer.#render(component, options)).body; + } + }, + then: { + value: + /** + * this is not type-safe, but honestly it's the best I can do right now, and it's a straightforward function. + * + * @template TResult1 + * @template [TResult2=never] + * @param { (value: SyncRenderOutput) => TResult1 } onfulfilled + * @param { (reason: unknown) => TResult2 } onrejected + */ + (onfulfilled, onrejected) => { + if (!async_mode_flag) { + w.experimental_async_ssr(); + const result = (sync ??= Renderer.#render(component, options)); + const user_result = onfulfilled({ + head: result.head, + body: result.body, + html: result.body + }); + return Promise.resolve(user_result); + } + async ??= Renderer.#render_async(component, options); + return async.then((result) => { + Object.defineProperty(result, 'html', { + // eslint-disable-next-line getter-return + get: () => { + e.html_deprecated(); + } + }); + return onfulfilled(/** @type {SyncRenderOutput} */ (result)); + }, onrejected); + } + } + }); + + return result; + } + + /** + * Collect all of the `onDestroy` callbacks regsitered during rendering. In an async context, this is only safe to call + * after awaiting `collect_async`. + * + * Child renderers are "porous" and don't affect execution order, but component body renderers + * create ordering boundaries. Within a renderer, callbacks run in order until hitting a component boundary. + * @returns {Iterable<() => void>} + */ + *#collect_on_destroy() { + for (const component of this.#traverse_components()) { + yield* component.#collect_ondestroy(); + } + } + + /** + * Performs a depth-first search of renderers, yielding the deepest components first, then additional components as we backtrack up the tree. + * @returns {Iterable} + */ + *#traverse_components() { + for (const child of this.#out) { + if (typeof child !== 'string') { + yield* child.#traverse_components(); + } + } + if (this.#is_component_body) { + yield this; + } + } + + /** + * @returns {Iterable<() => void>} + */ + *#collect_ondestroy() { + if (this.#on_destroy) { + for (const fn of this.#on_destroy) { + yield fn; + } + } + for (const child of this.#out) { + if (child instanceof Renderer && !child.#is_component_body) { + yield* child.#collect_ondestroy(); + } + } + } + + /** + * Render a component. Throws if any of the children are performing asynchronous work. + * + * @template {Record} Props + * @param {Component} component + * @param {{ props?: Omit; context?: Map; idPrefix?: string }} options + * @returns {AccumulatedContent} + */ + static #render(component, options) { + var previous_context = ssr_context; + try { + const renderer = Renderer.#open_render('sync', component, options); + + const content = renderer.#collect_content(); + return Renderer.#close_render(content, renderer); + } finally { + abort(); + set_ssr_context(previous_context); + } + } + + /** + * Render a component. + * + * @template {Record} Props + * @param {Component} component + * @param {{ props?: Omit; context?: Map; idPrefix?: string }} options + * @returns {Promise} + */ + static async #render_async(component, options) { + var previous_context = ssr_context; + try { + const renderer = Renderer.#open_render('async', component, options); + + const content = await renderer.#collect_content_async(); + return Renderer.#close_render(content, renderer); + } finally { + abort(); + set_ssr_context(previous_context); + } + } + + /** + * Collect all of the code from the `out` array and return it as a string, or a promise resolving to a string. + * @param {AccumulatedContent} content + * @returns {AccumulatedContent} + */ + #collect_content(content = { head: '', body: '' }) { + for (const item of this.#out) { + if (typeof item === 'string') { + content[this.type] += item; + } else if (item instanceof Renderer) { + item.#collect_content(content); + } + } + + return content; + } + + /** + * Collect all of the code from the `out` array and return it as a string. + * @param {AccumulatedContent} content + * @returns {Promise} + */ + async #collect_content_async(content = { head: '', body: '' }) { + await this.promise; + + // no danger to sequentially awaiting stuff in here; all of the work is already kicked off + for (const item of this.#out) { + if (typeof item === 'string') { + content[this.type] += item; + } else if (item instanceof Renderer) { + await item.#collect_content_async(content); + } + } + + return content; + } + + /** + * @template {Record} Props + * @param {'sync' | 'async'} mode + * @param {import('svelte').Component} component + * @param {{ props?: Omit; context?: Map; idPrefix?: string }} options + * @returns {Renderer} + */ + static #open_render(mode, component, options) { + const renderer = new Renderer( + new SSRState(mode, options.idPrefix ? options.idPrefix + '-' : '') + ); + + renderer.push(BLOCK_OPEN); + + if (options.context) { + push(); + /** @type {SSRContext} */ (ssr_context).c = options.context; + /** @type {SSRContext} */ (ssr_context).r = renderer; + } + + // @ts-expect-error + component(renderer, options.props ?? {}); + + if (options.context) { + pop(); + } + + renderer.push(BLOCK_CLOSE); + + return renderer; + } + + /** + * @param {AccumulatedContent} content + * @param {Renderer} renderer + */ + static #close_render(content, renderer) { + for (const cleanup of renderer.#collect_on_destroy()) { + cleanup(); + } + + let head = content.head + renderer.global.get_title(); + let body = content.body; + + for (const { hash, code } of renderer.global.css) { + head += ``; + } + + return { + head, + body + }; + } +} + +export class SSRState { + /** @readonly @type {'sync' | 'async'} */ + mode; + + /** @readonly @type {() => string} */ + uid; + + /** @readonly @type {Set<{ hash: string; code: string }>} */ + css = new Set(); + + /** @type {{ path: number[], value: string }} */ + #title = { path: [], value: '' }; + + /** + * @param {'sync' | 'async'} mode + * @param {string} [id_prefix] + */ + constructor(mode, id_prefix = '') { + this.mode = mode; + + let uid = 1; + this.uid = () => `${id_prefix}s${uid++}`; + } + + get_title() { + return this.#title.value; + } + + /** + * Performs a depth-first (lexicographic) comparison using the path. Rejects sets + * from earlier than or equal to the current value. + * @param {string} value + * @param {number[]} path + */ + set_title(value, path) { + const current = this.#title.path; + + let i = 0; + let l = Math.min(path.length, current.length); + + // skip identical prefixes - [1, 2, 3, ...] === [1, 2, 3, ...] + while (i < l && path[i] === current[i]) i += 1; + + if (path[i] === undefined) return; + + // replace title if + // - incoming path is longer - [7, 8, 9] > [7, 8] + // - incoming path is later - [7, 8, 9] > [7, 8, 8] + if (current[i] === undefined || path[i] > current[i]) { + this.#title.path = path; + this.#title.value = value; + } + } +} diff --git a/packages/svelte/src/internal/server/renderer.test.ts b/packages/svelte/src/internal/server/renderer.test.ts new file mode 100644 index 0000000000..88fbee855a --- /dev/null +++ b/packages/svelte/src/internal/server/renderer.test.ts @@ -0,0 +1,339 @@ +import { afterAll, beforeAll, describe, expect, test } from 'vitest'; +import { Renderer, SSRState } from './renderer.js'; +import type { Component } from 'svelte'; +import { disable_async_mode_flag, enable_async_mode_flag } from '../flags/index.js'; + +test('collects synchronous body content by default', () => { + const component = (renderer: Renderer) => { + renderer.push('a'); + renderer.child(($$renderer) => { + $$renderer.push('b'); + }); + renderer.push('c'); + }; + + const { head, body } = Renderer.render(component as unknown as Component); + expect(head).toBe(''); + expect(body).toBe('abc'); +}); + +test('child type switches content area (head vs body)', () => { + const component = (renderer: Renderer) => { + renderer.push('a'); + renderer.head(($$renderer) => { + $$renderer.push('T'); + }); + renderer.push('b'); + }; + + const { head, body } = Renderer.render(component as unknown as Component); + expect(head).toBe('T'); + expect(body).toBe('ab'); +}); + +test('child inherits parent type when not specified', () => { + const component = (renderer: Renderer) => { + renderer.head((renderer) => { + renderer.push(''); + renderer.child((renderer) => { + renderer.push(''); + }); + }); + }; + + const { head, body } = Renderer.render(component as unknown as Component); + expect(body).toBe(''); + expect(head).toBe(''); +}); + +test('get_path returns the path indexes to a renderer', () => { + const root = new Renderer(new SSRState('sync')); + let child_a: InstanceType | undefined; + let child_b: InstanceType | undefined; + let child_b_0: InstanceType | undefined; + + root.child(($$renderer) => { + child_a = $$renderer; + $$renderer.push('A'); + }); + root.child(($$renderer) => { + child_b = $$renderer; + $$renderer.child(($$inner) => { + child_b_0 = $$inner; + $$inner.push('B0'); + }); + $$renderer.push('B1'); + }); + + expect(child_a!.get_path()).toEqual([0]); + expect(child_b!.get_path()).toEqual([1]); + expect(child_b_0!.get_path()).toEqual([1, 0]); +}); + +test('creating an async child in a sync context throws', () => { + const component = (renderer: Renderer) => { + renderer.push('a'); + renderer.child(async ($$renderer) => { + await Promise.resolve(); + $$renderer.push('x'); + }); + }; + + expect(() => Renderer.render(component as unknown as Component).head).toThrow('await_invalid'); + expect(() => Renderer.render(component as unknown as Component).html).toThrow('await_invalid'); + expect(() => Renderer.render(component as unknown as Component).body).toThrow('await_invalid'); +}); + +test('local state is shallow-copied to children', () => { + const root = new Renderer(new SSRState('sync')); + root.local.select_value = 'A'; + let child: InstanceType | undefined; + root.child(($$renderer) => { + child = $$renderer; + }); + + expect(child!.local.select_value).toBe('A'); + child!.local.select_value = 'B'; + expect(root.local.select_value).toBe('A'); +}); + +test('subsume replaces tree content and state from other', () => { + const a = new Renderer(new SSRState('async')); + a.type = 'head'; + + a.push(''); + a.local.select_value = 'A'; + + const b = new Renderer(new SSRState('async')); + b.child(async ($$renderer) => { + await Promise.resolve(); + $$renderer.push('body'); + }); + b.global.css.add({ hash: 'h', code: 'c' }); + b.global.set_title('Title', [1]); + b.local.select_value = 'B'; + b.promise = Promise.resolve(); + + a.subsume(b); + + expect(a.type).toBe('body'); + expect(a.local.select_value).toBe('B'); + expect(a.promise).toBe(b.promise); +}); + +test('subsume refuses to switch modes', () => { + const a = new Renderer(new SSRState('sync')); + a.type = 'head'; + + a.push(''); + a.local.select_value = 'A'; + + const b = new Renderer(new SSRState('async')); + b.child(async ($$renderer) => { + await Promise.resolve(); + $$renderer.push('body'); + }); + b.global.css.add({ hash: 'h', code: 'c' }); + b.global.set_title('Title', [1]); + b.local.select_value = 'B'; + b.promise = Promise.resolve(); + + expect(() => a.subsume(b)).toThrow( + "invariant: A renderer cannot switch modes. If you're seeing this, there's a compiler bug. File an issue!" + ); +}); + +test('SSRState uid generator uses prefix', () => { + const state = new SSRState('sync', 'id-'); + expect(state.uid()).toBe('id-s1'); +}); + +test('SSRState title ordering favors later lexicographic paths', () => { + const state = new SSRState('sync'); + + state.set_title('A', [1]); + expect(state.get_title()).toBe('A'); + + // equal path -> unchanged + state.set_title('B', [1]); + expect(state.get_title()).toBe('A'); + + // earlier -> unchanged + state.set_title('C', [0, 9]); + expect(state.get_title()).toBe('A'); + + // later -> update + state.set_title('D', [2]); + expect(state.get_title()).toBe('D'); + + // longer but same prefix -> update + state.set_title('E', [2, 0]); + expect(state.get_title()).toBe('E'); + + // shorter (earlier) than current with same prefix -> unchanged + state.set_title('F', [2]); + expect(state.get_title()).toBe('E'); +}); + +test('selects an option with an explicit value', () => { + const component = (renderer: Renderer) => { + renderer.select({ value: 2 }, (renderer) => { + renderer.option({ value: 1 }, (renderer) => renderer.push('one')); + renderer.option({ value: 2 }, (renderer) => renderer.push('two')); + renderer.option({ value: 3 }, (renderer) => renderer.push('three')); + }); + }; + + const { head, body } = Renderer.render(component as unknown as Component); + expect(head).toBe(''); + expect(body).toBe( + '' + ); +}); + +test('selects an option with an implicit value', () => { + const component = (renderer: Renderer) => { + renderer.select({ value: 'two' }, (renderer) => { + renderer.option({}, (renderer) => renderer.push('one')); + renderer.option({}, (renderer) => renderer.push('two')); + renderer.option({}, (renderer) => renderer.push('three')); + }); + }; + + const { head, body } = Renderer.render(component as unknown as Component); + expect(head).toBe(''); + expect(body).toBe( + '' + ); +}); + +describe('async', () => { + beforeAll(() => { + enable_async_mode_flag(); + }); + + afterAll(() => { + disable_async_mode_flag(); + }); + + test('awaiting renderer gets async content', async () => { + const component = (renderer: Renderer) => { + renderer.push('1'); + renderer.child(async ($$renderer) => { + await Promise.resolve(); + $$renderer.push('2'); + }); + renderer.push('3'); + }; + + const result = await Renderer.render(component as unknown as Component); + expect(result.head).toBe(''); + expect(result.body).toBe('123'); + expect(() => result.html).toThrow('html_deprecated'); + }); + + test('push accepts async functions in async context', async () => { + const component = (renderer: Renderer) => { + renderer.push('a'); + renderer.push(async () => { + await Promise.resolve(); + return 'b'; + }); + renderer.push('c'); + }; + + const { head, body } = await Renderer.render(component as unknown as Component); + expect(head).toBe(''); + expect(body).toBe('abc'); + }); + + test('push handles async functions with different timing', async () => { + const component = (renderer: Renderer) => { + renderer.push(async () => { + await Promise.resolve(); + return 'fast'; + }); + renderer.push(async () => { + await new Promise((resolve) => setTimeout(resolve, 10)); + return 'slow'; + }); + renderer.push('sync'); + }; + + const { head, body } = await Renderer.render(component as unknown as Component); + expect(head).toBe(''); + expect(body).toBe('fastslowsync'); + }); + + test('push async functions work with head content type', async () => { + const component = (renderer: Renderer) => { + renderer.head(($$renderer) => { + $$renderer.push(async () => { + await Promise.resolve(); + return 'Async Title'; + }); + }); + }; + + const { head, body } = await Renderer.render(component as unknown as Component); + expect(body).toBe(''); + expect(head).toBe('Async Title'); + }); + + test('push async functions can be mixed with child renderers', async () => { + const component = (renderer: Renderer) => { + renderer.push('start-'); + renderer.push(async () => { + await Promise.resolve(); + return 'async-'; + }); + renderer.child(($$renderer) => { + $$renderer.push('child-'); + }); + renderer.push('-end'); + }; + + const { head, body } = await Renderer.render(component as unknown as Component); + expect(head).toBe(''); + expect(body).toBe('start-async-child--end'); + }); + + test('push async functions are not supported in sync context', () => { + const component = (renderer: Renderer) => { + renderer.push('a'); + renderer.push(() => Promise.resolve('b')); + }; + + expect(() => Renderer.render(component as unknown as Component).body).toThrow('await_invalid'); + expect(() => Renderer.render(component as unknown as Component).html).toThrow('await_invalid'); + expect(() => Renderer.render(component as unknown as Component).head).toThrow('await_invalid'); + }); + + test('on_destroy yields callbacks in the correct order', async () => { + const destroyed: string[] = []; + const component = (renderer: Renderer) => { + renderer.component((renderer) => { + renderer.on_destroy(() => destroyed.push('a')); + // children should not alter relative order + renderer.child(async (renderer) => { + await Promise.resolve(); + renderer.on_destroy(() => destroyed.push('b')); + renderer.on_destroy(() => destroyed.push('b*')); + }); + // but child components should + renderer.component((renderer) => { + renderer.on_destroy(() => destroyed.push('c')); + }); + renderer.child((renderer) => { + renderer.on_destroy(() => destroyed.push('d')); + }); + renderer.component((renderer) => { + renderer.on_destroy(() => destroyed.push('e')); + }); + }); + }; + + await Renderer.render(component as unknown as Component); + expect(destroyed).toEqual(['c', 'e', 'a', 'b', 'b*', 'd']); + }); +}); diff --git a/packages/svelte/src/internal/server/types.d.ts b/packages/svelte/src/internal/server/types.d.ts index 6b0fc146c4..53cefabc69 100644 --- a/packages/svelte/src/internal/server/types.d.ts +++ b/packages/svelte/src/internal/server/types.d.ts @@ -1,17 +1,20 @@ -export interface Component { +import type { Element } from './dev'; +import type { Renderer } from './renderer'; + +export interface SSRContext { /** parent */ - p: null | Component; - /** context */ + p: null | SSRContext; + /** component context */ c: null | Map; - /** ondestroy */ - d: null | Array<() => void>; - /** - * dev mode only: the component function - */ + /** renderer */ + r: null | Renderer; + /** dev mode only: the current component function */ function?: any; + /** dev mode only: the current element */ + element?: Element; } -export interface RenderOutput { +export interface SyncRenderOutput { /** HTML that goes into the `` */ head: string; /** @deprecated use `body` instead */ @@ -19,3 +22,5 @@ export interface RenderOutput { /** HTML that goes somewhere into the `` */ body: string; } + +export type RenderOutput = SyncRenderOutput & PromiseLike; diff --git a/packages/svelte/src/internal/server/warnings.js b/packages/svelte/src/internal/server/warnings.js new file mode 100644 index 0000000000..d8d9cd6d43 --- /dev/null +++ b/packages/svelte/src/internal/server/warnings.js @@ -0,0 +1,17 @@ +/* This file is generated by scripts/process-messages/index.js. Do not edit! */ + +import { DEV } from 'esm-env'; + +var bold = 'font-weight: bold'; +var normal = 'font-weight: normal'; + +/** + * Attempted to use asynchronous rendering without `experimental.async` enabled + */ +export function experimental_async_ssr() { + if (DEV) { + console.warn(`%c[svelte] experimental_async_ssr\n%cAttempted to use asynchronous rendering without \`experimental.async\` enabled\nhttps://svelte.dev/e/experimental_async_ssr`, bold, normal); + } else { + console.warn(`https://svelte.dev/e/experimental_async_ssr`); + } +} \ No newline at end of file diff --git a/packages/svelte/src/internal/shared/attributes.js b/packages/svelte/src/internal/shared/attributes.js index c8758c1d4d..4ad550e8d6 100644 --- a/packages/svelte/src/internal/shared/attributes.js +++ b/packages/svelte/src/internal/shared/attributes.js @@ -22,6 +22,10 @@ const replacements = { * @returns {string} */ export function attr(name, value, is_boolean = false) { + // attribute hidden for values other than "until-found" behaves like a boolean attribute + if (name === 'hidden' && value !== 'until-found') { + is_boolean = true; + } if (value == null || (!value && is_boolean)) return ''; const normalized = (name in replacements && replacements[name].get(value)) || value; const assignment = is_boolean ? '' : `="${escape_html(normalized, true)}"`; diff --git a/packages/svelte/src/internal/shared/errors.js b/packages/svelte/src/internal/shared/errors.js index 66685cb00b..6bcc35016a 100644 --- a/packages/svelte/src/internal/shared/errors.js +++ b/packages/svelte/src/internal/shared/errors.js @@ -2,22 +2,6 @@ import { DEV } from 'esm-env'; -/** - * Cannot await outside a `` with a `pending` snippet - * @returns {never} - */ -export function await_outside_boundary() { - if (DEV) { - const error = new Error(`await_outside_boundary\nCannot await outside a \`\` with a \`pending\` snippet\nhttps://svelte.dev/e/await_outside_boundary`); - - error.name = 'Svelte error'; - - throw error; - } else { - throw new Error(`https://svelte.dev/e/await_outside_boundary`); - } -} - /** * Cannot use `{@render children(...)}` if the parent component uses `let:` directives. Consider using a named snippet instead * @returns {never} diff --git a/packages/svelte/src/legacy/legacy-server.js b/packages/svelte/src/legacy/legacy-server.js index 60bd8c9fc6..b7d3e673bc 100644 --- a/packages/svelte/src/legacy/legacy-server.js +++ b/packages/svelte/src/legacy/legacy-server.js @@ -1,11 +1,15 @@ /** @import { SvelteComponent } from '../index.js' */ import { asClassComponent as as_class_component, createClassComponent } from './legacy-client.js'; import { render } from '../internal/server/index.js'; +import { async_mode_flag } from '../internal/flags/index.js'; +import * as w from '../internal/server/warnings.js'; // By having this as a separate entry point for server environments, we save the client bundle from having to include the server runtime export { createClassComponent }; +/** @typedef {{ head: string, html: string, css: { code: string, map: null }}} LegacyRenderResult */ + /** * Takes a Svelte 5 component and returns a Svelte 4 compatible component constructor. * @@ -21,16 +25,58 @@ export { createClassComponent }; */ export function asClassComponent(component) { const component_constructor = as_class_component(component); - /** @type {(props?: {}, opts?: { $$slots?: {}; context?: Map; }) => { html: any; css: { code: string; map: any; }; head: string; } } */ + /** @type {(props?: {}, opts?: { $$slots?: {}; context?: Map; }) => LegacyRenderResult & PromiseLike } */ const _render = (props, { context } = {}) => { // @ts-expect-error the typings are off, but this will work if the component is compiled in SSR mode const result = render(component, { props, context }); - return { - css: { code: '', map: null }, - head: result.head, - html: result.body - }; + + const munged = Object.defineProperties( + /** @type {LegacyRenderResult & PromiseLike} */ ({}), + { + css: { + value: { code: '', map: null } + }, + head: { + get: () => result.head + }, + html: { + get: () => result.body + }, + then: { + /** + * this is not type-safe, but honestly it's the best I can do right now, and it's a straightforward function. + * + * @template TResult1 + * @template [TResult2=never] + * @param { (value: LegacyRenderResult) => TResult1 } onfulfilled + * @param { (reason: unknown) => TResult2 } onrejected + */ + value: (onfulfilled, onrejected) => { + if (!async_mode_flag) { + w.experimental_async_ssr(); + const user_result = onfulfilled({ + css: munged.css, + head: munged.head, + html: munged.html + }); + return Promise.resolve(user_result); + } + + return result.then((result) => { + return onfulfilled({ + css: munged.css, + head: result.head, + html: result.body + }); + }, onrejected); + } + } + } + ); + + return munged; }; + // @ts-expect-error this is present for SSR component_constructor.render = _render; diff --git a/packages/svelte/src/utils.js b/packages/svelte/src/utils.js index f8c39253ac..f8a7e8d46d 100644 --- a/packages/svelte/src/utils.js +++ b/packages/svelte/src/utils.js @@ -154,7 +154,6 @@ const DOM_BOOLEAN_ATTRIBUTES = [ 'default', 'disabled', 'formnovalidate', - 'hidden', 'indeterminate', 'inert', 'ismap', diff --git a/packages/svelte/src/version.js b/packages/svelte/src/version.js index edb787c00c..1ebbbb95dd 100644 --- a/packages/svelte/src/version.js +++ b/packages/svelte/src/version.js @@ -4,5 +4,5 @@ * The current version, as set in package.json. * @type {string} */ -export const VERSION = '5.38.9'; +export const VERSION = '5.39.4'; export const PUBLIC_VERSION = '5'; diff --git a/packages/svelte/tests/hydration/samples/cloudflare-mirage-borking-2/_override.html b/packages/svelte/tests/hydration/samples/cloudflare-mirage-borking-2/_override.html index 2a1c323288..117d83d8eb 100644 --- a/packages/svelte/tests/hydration/samples/cloudflare-mirage-borking-2/_override.html +++ b/packages/svelte/tests/hydration/samples/cloudflare-mirage-borking-2/_override.html @@ -1 +1 @@ -

start

cond

+

start

cond

diff --git a/packages/svelte/tests/hydration/samples/if-block-mismatch-2/_expected.html b/packages/svelte/tests/hydration/samples/if-block-mismatch-2/_expected.html index 08a3809de9..72d1a4f024 100644 --- a/packages/svelte/tests/hydration/samples/if-block-mismatch-2/_expected.html +++ b/packages/svelte/tests/hydration/samples/if-block-mismatch-2/_expected.html @@ -1 +1 @@ -
hello +
hello diff --git a/packages/svelte/tests/hydration/samples/whitespace-at-block-start/_override.html b/packages/svelte/tests/hydration/samples/whitespace-at-block-start/_override.html index 90ca4ef4b8..24b6bae7b8 100644 --- a/packages/svelte/tests/hydration/samples/whitespace-at-block-start/_override.html +++ b/packages/svelte/tests/hydration/samples/whitespace-at-block-start/_override.html @@ -1,2 +1,2 @@ -

nested

\ No newline at end of file +

nested

diff --git a/packages/svelte/tests/parser-legacy/samples/each-block-destructured/output.json b/packages/svelte/tests/parser-legacy/samples/each-block-destructured/output.json index d19f5cbbfd..637da24aea 100644 --- a/packages/svelte/tests/parser-legacy/samples/each-block-destructured/output.json +++ b/packages/svelte/tests/parser-legacy/samples/each-block-destructured/output.json @@ -259,7 +259,8 @@ "kind": "let" }, "specifiers": [], - "source": null + "source": null, + "attributes": [] } ], "sourceType": "module" diff --git a/packages/svelte/tests/parser-legacy/samples/script-context-module-unquoted/output.json b/packages/svelte/tests/parser-legacy/samples/script-context-module-unquoted/output.json index 03a526f04e..64250cb302 100644 --- a/packages/svelte/tests/parser-legacy/samples/script-context-module-unquoted/output.json +++ b/packages/svelte/tests/parser-legacy/samples/script-context-module-unquoted/output.json @@ -169,7 +169,8 @@ "kind": "const" }, "specifiers": [], - "source": null + "source": null, + "attributes": [] } ], "sourceType": "module" diff --git a/packages/svelte/tests/runtime-browser/driver-ssr.js b/packages/svelte/tests/runtime-browser/driver-ssr.js index 7067e48a1f..35aaca3ad3 100644 --- a/packages/svelte/tests/runtime-browser/driver-ssr.js +++ b/packages/svelte/tests/runtime-browser/driver-ssr.js @@ -6,5 +6,9 @@ import config from '__CONFIG__'; import { render } from 'svelte/server'; export default function () { - return render(SvelteComponent, { props: config.props || {}, idPrefix: config?.id_prefix }); + const { head, body, html } = render(SvelteComponent, { + props: config.props || {}, + idPrefix: config?.id_prefix + }); + return { head, body, html }; } diff --git a/packages/svelte/tests/runtime-legacy/samples/after-render-prevents-loop/_config.js b/packages/svelte/tests/runtime-legacy/samples/after-render-prevents-loop/_config.js index 39c0ef7f53..7a0943c3f4 100644 --- a/packages/svelte/tests/runtime-legacy/samples/after-render-prevents-loop/_config.js +++ b/packages/svelte/tests/runtime-legacy/samples/after-render-prevents-loop/_config.js @@ -1,7 +1,7 @@ import { test } from '../../test'; export default test({ - skip_mode: ['server'], + skip_mode: ['server', 'async-server'], get props() { return { value: 'hello!' }; diff --git a/packages/svelte/tests/runtime-legacy/samples/after-render-triggers-update/_config.js b/packages/svelte/tests/runtime-legacy/samples/after-render-triggers-update/_config.js index 39c0ef7f53..7a0943c3f4 100644 --- a/packages/svelte/tests/runtime-legacy/samples/after-render-triggers-update/_config.js +++ b/packages/svelte/tests/runtime-legacy/samples/after-render-triggers-update/_config.js @@ -1,7 +1,7 @@ import { test } from '../../test'; export default test({ - skip_mode: ['server'], + skip_mode: ['server', 'async-server'], get props() { return { value: 'hello!' }; diff --git a/packages/svelte/tests/runtime-legacy/samples/before-render-prevents-loop/_config.js b/packages/svelte/tests/runtime-legacy/samples/before-render-prevents-loop/_config.js index 39c0ef7f53..7a0943c3f4 100644 --- a/packages/svelte/tests/runtime-legacy/samples/before-render-prevents-loop/_config.js +++ b/packages/svelte/tests/runtime-legacy/samples/before-render-prevents-loop/_config.js @@ -1,7 +1,7 @@ import { test } from '../../test'; export default test({ - skip_mode: ['server'], + skip_mode: ['server', 'async-server'], get props() { return { value: 'hello!' }; diff --git a/packages/svelte/tests/runtime-legacy/samples/binding-select-implicit-option-value/_config.js b/packages/svelte/tests/runtime-legacy/samples/binding-select-implicit-option-value/_config.js index dd88c10c71..2b866bc8ce 100644 --- a/packages/svelte/tests/runtime-legacy/samples/binding-select-implicit-option-value/_config.js +++ b/packages/svelte/tests/runtime-legacy/samples/binding-select-implicit-option-value/_config.js @@ -8,9 +8,9 @@ export default test({ ssrHtml: `

foo: 2

@@ -21,9 +21,9 @@ export default test({ target.innerHTML, `

foo: 2

@@ -47,9 +47,9 @@ export default test({ target.innerHTML, `

foo: 3

diff --git a/packages/svelte/tests/runtime-legacy/samples/binding-select-initial-value-undefined-3/_config.js b/packages/svelte/tests/runtime-legacy/samples/binding-select-initial-value-undefined-3/_config.js index 9b25c0bc58..c48298229e 100644 --- a/packages/svelte/tests/runtime-legacy/samples/binding-select-initial-value-undefined-3/_config.js +++ b/packages/svelte/tests/runtime-legacy/samples/binding-select-initial-value-undefined-3/_config.js @@ -8,9 +8,9 @@ export default test({ `, diff --git a/packages/svelte/tests/runtime-legacy/samples/binding-select-late-2/_config.js b/packages/svelte/tests/runtime-legacy/samples/binding-select-late-2/_config.js index ab2db4b07d..72e06190ba 100644 --- a/packages/svelte/tests/runtime-legacy/samples/binding-select-late-2/_config.js +++ b/packages/svelte/tests/runtime-legacy/samples/binding-select-late-2/_config.js @@ -28,9 +28,9 @@ export default test({ target.innerHTML, `

selected: two

` diff --git a/packages/svelte/tests/runtime-legacy/samples/binding-select-late-3/_config.js b/packages/svelte/tests/runtime-legacy/samples/binding-select-late-3/_config.js index 234c2a1040..72e06190ba 100644 --- a/packages/svelte/tests/runtime-legacy/samples/binding-select-late-3/_config.js +++ b/packages/svelte/tests/runtime-legacy/samples/binding-select-late-3/_config.js @@ -28,9 +28,9 @@ export default test({ target.innerHTML, `

selected: two

` diff --git a/packages/svelte/tests/runtime-legacy/samples/binding-select-late/_config.js b/packages/svelte/tests/runtime-legacy/samples/binding-select-late/_config.js index 9c3cd8765a..6c7662def3 100644 --- a/packages/svelte/tests/runtime-legacy/samples/binding-select-late/_config.js +++ b/packages/svelte/tests/runtime-legacy/samples/binding-select-late/_config.js @@ -28,9 +28,9 @@ export default test({ target.innerHTML, `

selected: two

` diff --git a/packages/svelte/tests/runtime-legacy/samples/binding-select-unmatched-2/_config.js b/packages/svelte/tests/runtime-legacy/samples/binding-select-unmatched-2/_config.js index 85d61eeeec..b0bbed9c92 100644 --- a/packages/svelte/tests/runtime-legacy/samples/binding-select-unmatched-2/_config.js +++ b/packages/svelte/tests/runtime-legacy/samples/binding-select-unmatched-2/_config.js @@ -6,9 +6,9 @@ export default test({

selected:

selected:

@@ -37,9 +37,9 @@ export default test({

selected: a

selected: a

@@ -59,9 +59,9 @@ export default test({

selected: d

selected: d

@@ -78,9 +78,9 @@ export default test({

selected: b

selected: b

@@ -100,9 +100,9 @@ export default test({

selected:

selected:

diff --git a/packages/svelte/tests/runtime-legacy/samples/binding-select-unmatched-3/_config.js b/packages/svelte/tests/runtime-legacy/samples/binding-select-unmatched-3/_config.js index c0e64521d1..d616d9bc26 100644 --- a/packages/svelte/tests/runtime-legacy/samples/binding-select-unmatched-3/_config.js +++ b/packages/svelte/tests/runtime-legacy/samples/binding-select-unmatched-3/_config.js @@ -4,7 +4,7 @@ import { ok, test } from '../../test'; export default test({ mode: ['client', 'hydrate'], - html: `

selected: a

`, + html: `

selected: a

`, async test({ assert, component, target }) { const select = target.querySelector('select'); @@ -29,7 +29,7 @@ export default test({ // model of selected value should be kept around, even if it is not in the list assert.htmlEqual( target.innerHTML, - `

selected: a

` + `

selected: a

` ); } }); diff --git a/packages/svelte/tests/runtime-legacy/samples/constructor-prefer-passed-context/_config.js b/packages/svelte/tests/runtime-legacy/samples/constructor-prefer-passed-context/_config.js index e05bb35637..7c49960afd 100644 --- a/packages/svelte/tests/runtime-legacy/samples/constructor-prefer-passed-context/_config.js +++ b/packages/svelte/tests/runtime-legacy/samples/constructor-prefer-passed-context/_config.js @@ -1,7 +1,7 @@ import { test } from '../../test'; export default test({ - skip_mode: ['server'], + skip_mode: ['server', 'async-server'], html: `
Value in child component:
diff --git a/packages/svelte/tests/runtime-legacy/shared.ts b/packages/svelte/tests/runtime-legacy/shared.ts index 05c1a982ec..7144543242 100644 --- a/packages/svelte/tests/runtime-legacy/shared.ts +++ b/packages/svelte/tests/runtime-legacy/shared.ts @@ -43,9 +43,9 @@ Promise.withResolvers = () => { export interface RuntimeTest = Record> extends BaseTest { /** Use e.g. `mode: ['client']` to indicate that this test should never run in server/hydrate modes */ - mode?: Array<'server' | 'client' | 'hydrate'>; + mode?: Array<'server' | 'async-server' | 'client' | 'hydrate'>; /** Temporarily skip specific modes, without skipping the entire test */ - skip_mode?: Array<'server' | 'client' | 'hydrate'>; + skip_mode?: Array<'server' | 'async-server' | 'client' | 'hydrate'>; /** Skip if running with process.env.NO_ASYNC */ skip_no_async?: boolean; /** Skip if running without process.env.NO_ASYNC */ @@ -83,7 +83,11 @@ export interface RuntimeTest = Record void | Promise; - test_ssr?: (args: { logs: any[]; assert: Assert }) => void | Promise; + test_ssr?: (args: { + logs: any[]; + assert: Assert; + variant: 'ssr' | 'async-ssr'; + }) => void | Promise; accessors?: boolean; immutable?: boolean; intro?: boolean; @@ -124,8 +128,8 @@ let console_warn = console.warn; let console_error = console.error; export function runtime_suite(runes: boolean) { - return suite_with_variants( - ['dom', 'hydrate', 'ssr'], + return suite_with_variants( + ['dom', 'hydrate', 'ssr', 'async-ssr'], (variant, config, test_name) => { if (!async_mode && (config.skip_no_async || test_name.startsWith('async-'))) { return true; @@ -162,6 +166,22 @@ export function runtime_suite(runes: boolean) { if (config.skip_mode?.includes('server')) return true; } + if (variant === 'async-ssr') { + if (!runes || !async_mode) return 'no-test'; + if ( + (config.mode && !config.mode.includes('async-server')) || + (!config.test_ssr && + config.html === undefined && + config.ssrHtml === undefined && + config.error === undefined && + config.runtime_error === undefined && + !config.mode?.includes('async-server')) + ) { + return 'no-test'; + } + if (config.skip_mode?.includes('async-server')) return true; + } + return false; }, (config, cwd) => { @@ -207,7 +227,7 @@ async function common_setup(cwd: string, runes: boolean | undefined, config: Run async function run_test_variant( cwd: string, config: RuntimeTest, - variant: 'dom' | 'hydrate' | 'ssr', + variant: 'dom' | 'hydrate' | 'ssr' | 'async-ssr', compileOptions: CompileOptions, runes: boolean ) { @@ -310,20 +330,26 @@ async function run_test_variant( let snapshot = undefined; - if (variant === 'hydrate' || variant === 'ssr') { + if (variant === 'hydrate' || variant === 'ssr' || variant === 'async-ssr') { config.before_test?.(); // ssr into target const SsrSvelteComponent = (await import(`${cwd}/_output/server/main.svelte.js`)).default; - const { html, head } = render(SsrSvelteComponent, { + const render_result = render(SsrSvelteComponent, { props: config.server_props ?? config.props ?? {}, idPrefix: config.id_prefix }); + const rendered = + variant === 'async-ssr' || (variant === 'hydrate' && compileOptions.experimental?.async) + ? await render_result + : render_result; + const { body, head } = rendered; - fs.writeFileSync(`${cwd}/_output/rendered.html`, html); - target.innerHTML = html; + const prefix = variant === 'async-ssr' ? 'async_' : ''; + fs.writeFileSync(`${cwd}/_output/${prefix}rendered.html`, body); + target.innerHTML = body; if (head) { - fs.writeFileSync(`${cwd}/_output/rendered_head.html`, head); + fs.writeFileSync(`${cwd}/_output/${prefix}rendered_head.html`, head); window.document.head.innerHTML = window.document.head.innerHTML + head; } @@ -338,7 +364,7 @@ async function run_test_variant( target.innerHTML = ''; } - if (variant === 'ssr') { + if (variant === 'ssr' || variant === 'async-ssr') { if (config.ssrHtml) { assert_html_equal_with_options(target.innerHTML, config.ssrHtml, { preserveComments: @@ -361,7 +387,8 @@ async function run_test_variant( ...assert, htmlEqual: assert_html_equal, htmlEqualWithOptions: assert_html_equal_with_options - } + }, + variant }); } } else { diff --git a/packages/svelte/tests/runtime-runes/samples/async-no-pending-attributes/Child.svelte b/packages/svelte/tests/runtime-runes/samples/async-no-pending-attributes/Child.svelte new file mode 100644 index 0000000000..ab87364cb7 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-no-pending-attributes/Child.svelte @@ -0,0 +1,5 @@ + + +

{thing}

diff --git a/packages/svelte/tests/runtime-runes/samples/async-no-pending-attributes/_config.js b/packages/svelte/tests/runtime-runes/samples/async-no-pending-attributes/_config.js new file mode 100644 index 0000000000..b8f8f61d96 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-no-pending-attributes/_config.js @@ -0,0 +1,24 @@ +import { tick } from 'svelte'; +import { test } from '../../test'; + +export default test({ + skip_mode: ['async-server'], + + async test({ assert, target }) { + await tick(); + + assert.htmlEqual( + target.innerHTML, + ` +

cool

+

beans

+ +

awesome

+

sauce

+ +

neato

+

burrito

+ ` + ); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/async-no-pending-attributes/main.svelte b/packages/svelte/tests/runtime-runes/samples/async-no-pending-attributes/main.svelte new file mode 100644 index 0000000000..7a8280b314 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-no-pending-attributes/main.svelte @@ -0,0 +1,12 @@ + + +

cool

+ + +

awesome

+ + +

neato

+ diff --git a/packages/svelte/tests/runtime-runes/samples/async-no-pending-await-in-block/_config.js b/packages/svelte/tests/runtime-runes/samples/async-no-pending-await-in-block/_config.js new file mode 100644 index 0000000000..35d88f6c8b --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-no-pending-await-in-block/_config.js @@ -0,0 +1,33 @@ +import { tick } from 'svelte'; +import { test } from '../../test'; + +export default test({ + mode: ['hydrate'], + + server_props: { + browser: false + }, + + ssrHtml: ` +

hello from the server

+

hello from the server

+

hello from the server

+ `, + + props: { + browser: true + }, + + async test({ assert, target }) { + await tick(); + + assert.htmlEqual( + target.innerHTML, + ` +

hello from the browser

+

hello from the browser

+

hello from the browser

+ ` + ); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/async-no-pending-await-in-block/main.svelte b/packages/svelte/tests/runtime-runes/samples/async-no-pending-await-in-block/main.svelte new file mode 100644 index 0000000000..82e6a57a18 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-no-pending-await-in-block/main.svelte @@ -0,0 +1,10 @@ + + +{#if await true} +

hello from the {browser ? 'browser' : 'server'}

+{/if} + +

hello from the {browser ? 'browser' : 'server'}

+

hello from the {browser ? 'browser' : 'server'}

diff --git a/packages/svelte/tests/runtime-runes/samples/async-no-pending-await-in-script-2/Child.svelte b/packages/svelte/tests/runtime-runes/samples/async-no-pending-await-in-script-2/Child.svelte new file mode 100644 index 0000000000..6c2cb70b1a --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-no-pending-await-in-script-2/Child.svelte @@ -0,0 +1,3 @@ + diff --git a/packages/svelte/tests/runtime-runes/samples/async-no-pending-await-in-script-2/_config.js b/packages/svelte/tests/runtime-runes/samples/async-no-pending-await-in-script-2/_config.js new file mode 100644 index 0000000000..0a92bf3bcb --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-no-pending-await-in-script-2/_config.js @@ -0,0 +1,13 @@ +import { tick } from 'svelte'; +import { test } from '../../test'; + +export default test({ + skip_mode: ['server'], + + ssrHtml: '

hello

', + + async test({ assert, target }) { + await tick(); + assert.htmlEqual(target.innerHTML, '

hello

'); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/async-no-pending-await-in-script-2/main.svelte b/packages/svelte/tests/runtime-runes/samples/async-no-pending-await-in-script-2/main.svelte new file mode 100644 index 0000000000..108655973d --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-no-pending-await-in-script-2/main.svelte @@ -0,0 +1,9 @@ + + + + +{#if true} +

hello

+{/if} diff --git a/packages/svelte/tests/runtime-runes/samples/async-no-pending-await-in-script/_config.js b/packages/svelte/tests/runtime-runes/samples/async-no-pending-await-in-script/_config.js new file mode 100644 index 0000000000..0a92bf3bcb --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-no-pending-await-in-script/_config.js @@ -0,0 +1,13 @@ +import { tick } from 'svelte'; +import { test } from '../../test'; + +export default test({ + skip_mode: ['server'], + + ssrHtml: '

hello

', + + async test({ assert, target }) { + await tick(); + assert.htmlEqual(target.innerHTML, '

hello

'); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/async-no-pending-await-in-script/main.svelte b/packages/svelte/tests/runtime-runes/samples/async-no-pending-await-in-script/main.svelte new file mode 100644 index 0000000000..aa4c9dff2b --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-no-pending-await-in-script/main.svelte @@ -0,0 +1,7 @@ + + +{#if true} +

hello

+{/if} diff --git a/packages/svelte/tests/runtime-runes/samples/async-no-pending-throws-sync/_config.js b/packages/svelte/tests/runtime-runes/samples/async-no-pending-throws-sync/_config.js new file mode 100644 index 0000000000..366477d197 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-no-pending-throws-sync/_config.js @@ -0,0 +1,7 @@ +import { test } from '../../test'; + +export default test({ + mode: ['server'], + + error: 'await_invalid' +}); diff --git a/packages/svelte/tests/runtime-runes/samples/async-no-pending-throws-sync/main.svelte b/packages/svelte/tests/runtime-runes/samples/async-no-pending-throws-sync/main.svelte new file mode 100644 index 0000000000..4b8cd7203b --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-no-pending-throws-sync/main.svelte @@ -0,0 +1 @@ +

{await Promise.resolve('hello')}

\ No newline at end of file diff --git a/packages/svelte/tests/runtime-runes/samples/async-no-pending/_config.js b/packages/svelte/tests/runtime-runes/samples/async-no-pending/_config.js new file mode 100644 index 0000000000..91b516e7c9 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-no-pending/_config.js @@ -0,0 +1,17 @@ +import { tick } from 'svelte'; +import { ok, test } from '../../test'; + +export default test({ + skip_mode: ['server'], + + ssrHtml: '

hello

', + + html: '', + + async test({ assert, target }) { + await tick(); + const p = target.querySelector('p'); + ok(p); + assert.htmlEqual(p.outerHTML, '

hello

'); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/async-no-pending/main.svelte b/packages/svelte/tests/runtime-runes/samples/async-no-pending/main.svelte new file mode 100644 index 0000000000..4b8cd7203b --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-no-pending/main.svelte @@ -0,0 +1 @@ +

{await Promise.resolve('hello')}

\ No newline at end of file diff --git a/packages/svelte/tests/runtime-runes/samples/async-ondestroy-ordering/A.svelte b/packages/svelte/tests/runtime-runes/samples/async-ondestroy-ordering/A.svelte new file mode 100644 index 0000000000..f73e368fc8 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-ondestroy-ordering/A.svelte @@ -0,0 +1,9 @@ + + +
A
+ \ No newline at end of file diff --git a/packages/svelte/tests/runtime-runes/samples/async-ondestroy-ordering/B.svelte b/packages/svelte/tests/runtime-runes/samples/async-ondestroy-ordering/B.svelte new file mode 100644 index 0000000000..8b107601c4 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-ondestroy-ordering/B.svelte @@ -0,0 +1,9 @@ + + +
B
\ No newline at end of file diff --git a/packages/svelte/tests/runtime-runes/samples/async-ondestroy-ordering/C.svelte b/packages/svelte/tests/runtime-runes/samples/async-ondestroy-ordering/C.svelte new file mode 100644 index 0000000000..8b616d6e94 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-ondestroy-ordering/C.svelte @@ -0,0 +1,5 @@ + \ No newline at end of file diff --git a/packages/svelte/tests/runtime-runes/samples/async-ondestroy-ordering/_config.js b/packages/svelte/tests/runtime-runes/samples/async-ondestroy-ordering/_config.js new file mode 100644 index 0000000000..32def88537 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-ondestroy-ordering/_config.js @@ -0,0 +1,14 @@ +import { test } from '../../test'; +import { destroyed, reset } from './destroyed.js'; + +export default test({ + mode: ['async-server'], + + before_test() { + reset(); + }, + + test_ssr({ assert }) { + assert.deepEqual(destroyed, ['C', 'A', 'B', 'B*', 'root']); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/async-ondestroy-ordering/destroyed.js b/packages/svelte/tests/runtime-runes/samples/async-ondestroy-ordering/destroyed.js new file mode 100644 index 0000000000..1afebed3bd --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-ondestroy-ordering/destroyed.js @@ -0,0 +1,4 @@ +/** @type {string[]} */ +export const destroyed = []; + +export const reset = () => (destroyed.length = 0); diff --git a/packages/svelte/tests/runtime-runes/samples/async-ondestroy-ordering/main.svelte b/packages/svelte/tests/runtime-runes/samples/async-ondestroy-ordering/main.svelte new file mode 100644 index 0000000000..8503a54681 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-ondestroy-ordering/main.svelte @@ -0,0 +1,13 @@ + + +{#if await Promise.resolve(true)} + +{/if} + \ No newline at end of file diff --git a/packages/svelte/tests/runtime-runes/samples/async-push-element-dev/_config.js b/packages/svelte/tests/runtime-runes/samples/async-push-element-dev/_config.js new file mode 100644 index 0000000000..044c6aeb9a --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-push-element-dev/_config.js @@ -0,0 +1,15 @@ +import { test } from '../../test'; + +export default test({ + mode: ['async-server'], + + compileOptions: { + // include `push_element` calls, so that we can check they + // run with the correct ssr_context + dev: true + }, + + html: ` +

hello!

+ ` +}); diff --git a/packages/svelte/tests/runtime-runes/samples/async-push-element-dev/main.svelte b/packages/svelte/tests/runtime-runes/samples/async-push-element-dev/main.svelte new file mode 100644 index 0000000000..99ce81e0c0 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-push-element-dev/main.svelte @@ -0,0 +1,3 @@ +{#if await true} +

hello!

+{/if} diff --git a/packages/svelte/tests/runtime-runes/samples/snippet-slot-let-error/_config.js b/packages/svelte/tests/runtime-runes/samples/snippet-slot-let-error/_config.js index e8adfb38f2..4d892bd140 100644 --- a/packages/svelte/tests/runtime-runes/samples/snippet-slot-let-error/_config.js +++ b/packages/svelte/tests/runtime-runes/samples/snippet-slot-let-error/_config.js @@ -1,6 +1,7 @@ import { test } from '../../test'; export default test({ + skip_mode: ['hydrate', 'server'], compileOptions: { dev: true }, diff --git a/packages/svelte/tests/server-side-rendering/samples/async-context-throws-after-await/_config.js b/packages/svelte/tests/server-side-rendering/samples/async-context-throws-after-await/_config.js new file mode 100644 index 0000000000..a1b52a2df9 --- /dev/null +++ b/packages/svelte/tests/server-side-rendering/samples/async-context-throws-after-await/_config.js @@ -0,0 +1,6 @@ +import { test } from '../../test'; + +export default test({ + mode: ['async'], + error: 'lifecycle_outside_component' +}); diff --git a/packages/svelte/tests/server-side-rendering/samples/async-context-throws-after-await/main.svelte b/packages/svelte/tests/server-side-rendering/samples/async-context-throws-after-await/main.svelte new file mode 100644 index 0000000000..8e770c214b --- /dev/null +++ b/packages/svelte/tests/server-side-rendering/samples/async-context-throws-after-await/main.svelte @@ -0,0 +1,7 @@ + diff --git a/packages/svelte/tests/server-side-rendering/samples/async-each-fallback-hoisting/_expected.html b/packages/svelte/tests/server-side-rendering/samples/async-each-fallback-hoisting/_expected.html new file mode 100644 index 0000000000..bf0d87ab1b --- /dev/null +++ b/packages/svelte/tests/server-side-rendering/samples/async-each-fallback-hoisting/_expected.html @@ -0,0 +1 @@ +4 \ No newline at end of file diff --git a/packages/svelte/tests/server-side-rendering/samples/async-each-fallback-hoisting/main.svelte b/packages/svelte/tests/server-side-rendering/samples/async-each-fallback-hoisting/main.svelte new file mode 100644 index 0000000000..e580345a2e --- /dev/null +++ b/packages/svelte/tests/server-side-rendering/samples/async-each-fallback-hoisting/main.svelte @@ -0,0 +1,5 @@ +{#each await Promise.resolve([]) as item} + {await Promise.reject('This should never be reached')} +{:else} + {await Promise.resolve(4)} +{/each} diff --git a/packages/svelte/tests/server-side-rendering/samples/async-each-hoisting/_expected.html b/packages/svelte/tests/server-side-rendering/samples/async-each-hoisting/_expected.html new file mode 100644 index 0000000000..d800886d9c --- /dev/null +++ b/packages/svelte/tests/server-side-rendering/samples/async-each-hoisting/_expected.html @@ -0,0 +1 @@ +123 \ No newline at end of file diff --git a/packages/svelte/tests/server-side-rendering/samples/async-each-hoisting/main.svelte b/packages/svelte/tests/server-side-rendering/samples/async-each-hoisting/main.svelte new file mode 100644 index 0000000000..4b6bf7eadc --- /dev/null +++ b/packages/svelte/tests/server-side-rendering/samples/async-each-hoisting/main.svelte @@ -0,0 +1,9 @@ + + +{#each await Promise.resolve([first, second, third]) as item} + {await item} +{/each} diff --git a/packages/svelte/tests/server-side-rendering/samples/async-head-multiple-title-order-preserved/A.svelte b/packages/svelte/tests/server-side-rendering/samples/async-head-multiple-title-order-preserved/A.svelte new file mode 100644 index 0000000000..5ddccbe17c --- /dev/null +++ b/packages/svelte/tests/server-side-rendering/samples/async-head-multiple-title-order-preserved/A.svelte @@ -0,0 +1,9 @@ + + + + {#if await promise} + A + {/if} + diff --git a/packages/svelte/tests/server-side-rendering/samples/async-head-multiple-title-order-preserved/B.svelte b/packages/svelte/tests/server-side-rendering/samples/async-head-multiple-title-order-preserved/B.svelte new file mode 100644 index 0000000000..95b102e680 --- /dev/null +++ b/packages/svelte/tests/server-side-rendering/samples/async-head-multiple-title-order-preserved/B.svelte @@ -0,0 +1,7 @@ + + + + {(await promise) && 'B'} + diff --git a/packages/svelte/tests/server-side-rendering/samples/async-head-multiple-title-order-preserved/_config.js b/packages/svelte/tests/server-side-rendering/samples/async-head-multiple-title-order-preserved/_config.js new file mode 100644 index 0000000000..f47bee71df --- /dev/null +++ b/packages/svelte/tests/server-side-rendering/samples/async-head-multiple-title-order-preserved/_config.js @@ -0,0 +1,3 @@ +import { test } from '../../test'; + +export default test({}); diff --git a/packages/svelte/tests/server-side-rendering/samples/async-head-multiple-title-order-preserved/_expected.html b/packages/svelte/tests/server-side-rendering/samples/async-head-multiple-title-order-preserved/_expected.html new file mode 100644 index 0000000000..e69de29bb2 diff --git a/packages/svelte/tests/server-side-rendering/samples/async-head-multiple-title-order-preserved/_expected_head.html b/packages/svelte/tests/server-side-rendering/samples/async-head-multiple-title-order-preserved/_expected_head.html new file mode 100644 index 0000000000..af5c5feba4 --- /dev/null +++ b/packages/svelte/tests/server-side-rendering/samples/async-head-multiple-title-order-preserved/_expected_head.html @@ -0,0 +1 @@ +B \ No newline at end of file diff --git a/packages/svelte/tests/server-side-rendering/samples/async-head-multiple-title-order-preserved/main.svelte b/packages/svelte/tests/server-side-rendering/samples/async-head-multiple-title-order-preserved/main.svelte new file mode 100644 index 0000000000..9b49cf1c22 --- /dev/null +++ b/packages/svelte/tests/server-side-rendering/samples/async-head-multiple-title-order-preserved/main.svelte @@ -0,0 +1,27 @@ + + + + {#if await main.promise} + Main + {/if} + +
+ diff --git a/packages/svelte/tests/server-side-rendering/samples/async-if-alternate-hoisting/_expected.html b/packages/svelte/tests/server-side-rendering/samples/async-if-alternate-hoisting/_expected.html new file mode 100644 index 0000000000..c3b0e19566 --- /dev/null +++ b/packages/svelte/tests/server-side-rendering/samples/async-if-alternate-hoisting/_expected.html @@ -0,0 +1 @@ +yes yes yes \ No newline at end of file diff --git a/packages/svelte/tests/server-side-rendering/samples/async-if-alternate-hoisting/main.svelte b/packages/svelte/tests/server-side-rendering/samples/async-if-alternate-hoisting/main.svelte new file mode 100644 index 0000000000..a97f32f034 --- /dev/null +++ b/packages/svelte/tests/server-side-rendering/samples/async-if-alternate-hoisting/main.svelte @@ -0,0 +1,5 @@ +{#if await Promise.resolve(false)} + {await Promise.reject('no no no')} +{:else} + {await Promise.resolve('yes yes yes')} +{/if} diff --git a/packages/svelte/tests/server-side-rendering/samples/async-if-hoisting/_expected.html b/packages/svelte/tests/server-side-rendering/samples/async-if-hoisting/_expected.html new file mode 100644 index 0000000000..c3b0e19566 --- /dev/null +++ b/packages/svelte/tests/server-side-rendering/samples/async-if-hoisting/_expected.html @@ -0,0 +1 @@ +yes yes yes \ No newline at end of file diff --git a/packages/svelte/tests/server-side-rendering/samples/async-if-hoisting/main.svelte b/packages/svelte/tests/server-side-rendering/samples/async-if-hoisting/main.svelte new file mode 100644 index 0000000000..4ca3462771 --- /dev/null +++ b/packages/svelte/tests/server-side-rendering/samples/async-if-hoisting/main.svelte @@ -0,0 +1,5 @@ +{#if await Promise.resolve(true)} + {await Promise.resolve('yes yes yes')} +{:else} + {await Promise.reject('no no no')} +{/if} diff --git a/packages/svelte/tests/server-side-rendering/samples/async-option-implicit-complex-value/_expected.html b/packages/svelte/tests/server-side-rendering/samples/async-option-implicit-complex-value/_expected.html new file mode 100644 index 0000000000..f2fd8e71b9 --- /dev/null +++ b/packages/svelte/tests/server-side-rendering/samples/async-option-implicit-complex-value/_expected.html @@ -0,0 +1,3 @@ + \ No newline at end of file diff --git a/packages/svelte/tests/server-side-rendering/samples/async-option-implicit-complex-value/main.svelte b/packages/svelte/tests/server-side-rendering/samples/async-option-implicit-complex-value/main.svelte new file mode 100644 index 0000000000..2ad9b64509 --- /dev/null +++ b/packages/svelte/tests/server-side-rendering/samples/async-option-implicit-complex-value/main.svelte @@ -0,0 +1,3 @@ + \ No newline at end of file diff --git a/packages/svelte/tests/server-side-rendering/samples/async-option-implicit-simple-value/_expected.html b/packages/svelte/tests/server-side-rendering/samples/async-option-implicit-simple-value/_expected.html new file mode 100644 index 0000000000..f642109218 --- /dev/null +++ b/packages/svelte/tests/server-side-rendering/samples/async-option-implicit-simple-value/_expected.html @@ -0,0 +1,3 @@ + \ No newline at end of file diff --git a/packages/svelte/tests/server-side-rendering/samples/async-option-implicit-simple-value/main.svelte b/packages/svelte/tests/server-side-rendering/samples/async-option-implicit-simple-value/main.svelte new file mode 100644 index 0000000000..a5d91424cb --- /dev/null +++ b/packages/svelte/tests/server-side-rendering/samples/async-option-implicit-simple-value/main.svelte @@ -0,0 +1,3 @@ + \ No newline at end of file diff --git a/packages/svelte/tests/server-side-rendering/samples/async-select-value-component/Option.svelte b/packages/svelte/tests/server-side-rendering/samples/async-select-value-component/Option.svelte new file mode 100644 index 0000000000..a17413bd8d --- /dev/null +++ b/packages/svelte/tests/server-side-rendering/samples/async-select-value-component/Option.svelte @@ -0,0 +1,5 @@ + + + \ No newline at end of file diff --git a/packages/svelte/tests/server-side-rendering/samples/async-select-value-component/_expected.html b/packages/svelte/tests/server-side-rendering/samples/async-select-value-component/_expected.html new file mode 100644 index 0000000000..96d1d8b233 --- /dev/null +++ b/packages/svelte/tests/server-side-rendering/samples/async-select-value-component/_expected.html @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/packages/svelte/tests/server-side-rendering/samples/async-select-value-component/main.svelte b/packages/svelte/tests/server-side-rendering/samples/async-select-value-component/main.svelte new file mode 100644 index 0000000000..aaf10c2cb3 --- /dev/null +++ b/packages/svelte/tests/server-side-rendering/samples/async-select-value-component/main.svelte @@ -0,0 +1,8 @@ + + \ No newline at end of file diff --git a/packages/svelte/tests/server-side-rendering/samples/async-select-value-implicit-value-complex/_expected.html b/packages/svelte/tests/server-side-rendering/samples/async-select-value-implicit-value-complex/_expected.html new file mode 100644 index 0000000000..de7cba8837 --- /dev/null +++ b/packages/svelte/tests/server-side-rendering/samples/async-select-value-implicit-value-complex/_expected.html @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/packages/svelte/tests/server-side-rendering/samples/async-select-value-implicit-value-complex/main.svelte b/packages/svelte/tests/server-side-rendering/samples/async-select-value-implicit-value-complex/main.svelte new file mode 100644 index 0000000000..2cc4065eb6 --- /dev/null +++ b/packages/svelte/tests/server-side-rendering/samples/async-select-value-implicit-value-complex/main.svelte @@ -0,0 +1,13 @@ + + +{#snippet option(val)}{await val}{/snippet} \ No newline at end of file diff --git a/packages/svelte/tests/server-side-rendering/samples/async-select-value-implicit-value/_expected.html b/packages/svelte/tests/server-side-rendering/samples/async-select-value-implicit-value/_expected.html new file mode 100644 index 0000000000..de7cba8837 --- /dev/null +++ b/packages/svelte/tests/server-side-rendering/samples/async-select-value-implicit-value/_expected.html @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/packages/svelte/tests/server-side-rendering/samples/async-select-value-implicit-value/main.svelte b/packages/svelte/tests/server-side-rendering/samples/async-select-value-implicit-value/main.svelte new file mode 100644 index 0000000000..8d9393cf6c --- /dev/null +++ b/packages/svelte/tests/server-side-rendering/samples/async-select-value-implicit-value/main.svelte @@ -0,0 +1,5 @@ + \ No newline at end of file diff --git a/packages/svelte/tests/server-side-rendering/samples/attribute-spread-hidden/_expected.html b/packages/svelte/tests/server-side-rendering/samples/attribute-spread-hidden/_expected.html new file mode 100644 index 0000000000..80937efaee --- /dev/null +++ b/packages/svelte/tests/server-side-rendering/samples/attribute-spread-hidden/_expected.html @@ -0,0 +1 @@ +
A
diff --git a/packages/svelte/tests/server-side-rendering/samples/attribute-spread-hidden/main.svelte b/packages/svelte/tests/server-side-rendering/samples/attribute-spread-hidden/main.svelte new file mode 100644 index 0000000000..6738b34054 --- /dev/null +++ b/packages/svelte/tests/server-side-rendering/samples/attribute-spread-hidden/main.svelte @@ -0,0 +1,3 @@ +
A
+
B
+
C
\ No newline at end of file diff --git a/packages/svelte/tests/server-side-rendering/samples/context/Child.svelte b/packages/svelte/tests/server-side-rendering/samples/context/Child.svelte new file mode 100644 index 0000000000..fab18ea195 --- /dev/null +++ b/packages/svelte/tests/server-side-rendering/samples/context/Child.svelte @@ -0,0 +1,8 @@ + + +
{getContext('key')}
diff --git a/packages/svelte/tests/server-side-rendering/samples/context/_config.js b/packages/svelte/tests/server-side-rendering/samples/context/_config.js new file mode 100644 index 0000000000..05de37a8bd --- /dev/null +++ b/packages/svelte/tests/server-side-rendering/samples/context/_config.js @@ -0,0 +1,5 @@ +import { test } from '../../test'; + +export default test({ + mode: ['async'] +}); diff --git a/packages/svelte/tests/server-side-rendering/samples/context/_expected.html b/packages/svelte/tests/server-side-rendering/samples/context/_expected.html new file mode 100644 index 0000000000..554133d8d9 --- /dev/null +++ b/packages/svelte/tests/server-side-rendering/samples/context/_expected.html @@ -0,0 +1,3 @@ +
value
+
value
+
child value
\ No newline at end of file diff --git a/packages/svelte/tests/server-side-rendering/samples/context/main.svelte b/packages/svelte/tests/server-side-rendering/samples/context/main.svelte new file mode 100644 index 0000000000..fd75a6cbae --- /dev/null +++ b/packages/svelte/tests/server-side-rendering/samples/context/main.svelte @@ -0,0 +1,11 @@ + + +
{getContext('key')}
+
{(await Promise.resolve(true)) && getContext('key')}
+ diff --git a/packages/svelte/tests/server-side-rendering/samples/head-multiple-title/_config.js b/packages/svelte/tests/server-side-rendering/samples/head-multiple-title/_config.js index 45054bbada..f47bee71df 100644 --- a/packages/svelte/tests/server-side-rendering/samples/head-multiple-title/_config.js +++ b/packages/svelte/tests/server-side-rendering/samples/head-multiple-title/_config.js @@ -1,5 +1,3 @@ import { test } from '../../test'; -export default test({ - props: { adjective: 'custom' } -}); +export default test({}); diff --git a/packages/svelte/tests/server-side-rendering/samples/invalid-nested-svelte-element/_config.js b/packages/svelte/tests/server-side-rendering/samples/invalid-nested-svelte-element/_config.js index 71edff6a68..6325ea7d0e 100644 --- a/packages/svelte/tests/server-side-rendering/samples/invalid-nested-svelte-element/_config.js +++ b/packages/svelte/tests/server-side-rendering/samples/invalid-nested-svelte-element/_config.js @@ -4,8 +4,6 @@ export default test({ compileOptions: { dev: true }, - - errors: [ + error: 'node_invalid_placement_ssr: `

` (packages/svelte/tests/server-side-rendering/samples/invalid-nested-svelte-element/main.svelte:2:1) cannot be a child of `

` (packages/svelte/tests/server-side-rendering/samples/invalid-nested-svelte-element/main.svelte:1:0)\n\nThis can cause content to shift around as the browser repairs the HTML, and will likely result in a `hydration_mismatch` warning.' - ] }); diff --git a/packages/svelte/tests/server-side-rendering/test.ts b/packages/svelte/tests/server-side-rendering/test.ts index 3e57539427..7eede332a7 100644 --- a/packages/svelte/tests/server-side-rendering/test.ts +++ b/packages/svelte/tests/server-side-rendering/test.ts @@ -9,79 +9,133 @@ import { assert } from 'vitest'; import { render } from 'svelte/server'; import { compile_directory, should_update_expected, try_read_file } from '../helpers.js'; import { assert_html_equal_with_options } from '../html_equal.js'; -import { suite, type BaseTest } from '../suite.js'; +import { suite_with_variants, type BaseTest } from '../suite.js'; import type { CompileOptions } from '#compiler'; +import { seen } from '../../src/internal/server/dev.js'; interface SSRTest extends BaseTest { + mode?: ('sync' | 'async')[]; compileOptions?: Partial; + load_compiled?: boolean; props?: Record; id_prefix?: string; withoutNormalizeHtml?: boolean; - errors?: string[]; + error?: string; } -// eslint-disable-next-line no-console -let console_error = console.error; +// TODO remove this shim when we can +// @ts-expect-error +Promise.withResolvers = () => { + let resolve; + let reject; + + const promise = new Promise((f, r) => { + resolve = f; + reject = r; + }); + + return { promise, resolve, reject }; +}; + +const { test, run } = suite_with_variants( + ['sync', 'async'], + (variant, config, test_name) => { + if (config.mode && !config.mode.includes(variant)) { + return 'no-test'; + } -const { test, run } = suite(async (config, test_dir) => { - await compile_directory(test_dir, 'server', config.compileOptions); + if (test_name.startsWith('async') && variant === 'sync') { + return 'no-test'; + } - const errors: string[] = []; + return false; + }, + async (config, test_dir) => { + const compile_options = { + experimental: { + async: true, + ...config.compileOptions?.experimental + }, + ...config.compileOptions + }; + + if (!config.load_compiled) { + await compile_directory(test_dir, 'server', compile_options); + } - console.error = (...args) => { - errors.push(...args); - }; + return compile_options; + }, + async (config, test_dir, variant, compile_options) => { + const Component = (await import(`${test_dir}/_output/server/main.svelte.js`)).default; + const expected_html = try_read_file(`${test_dir}/_expected.html`); + const is_async = variant === 'async'; - const Component = (await import(`${test_dir}/_output/server/main.svelte.js`)).default; - const expected_html = try_read_file(`${test_dir}/_expected.html`); - const rendered = render(Component, { props: config.props || {}, idPrefix: config.id_prefix }); - const { body, head } = rendered; + seen?.clear(); + + let rendered; + try { + const render_result = render(Component, { + props: config.props || {}, + idPrefix: config.id_prefix + }); + rendered = is_async ? await render_result : render_result; + } catch (error) { + if (config.error) { + assert.deepEqual((error as Error).message, config.error); + return; + } else { + throw error; + } + } - fs.writeFileSync(`${test_dir}/_output/rendered.html`, body); + const { body, head } = rendered; - if (head) { - fs.writeFileSync(`${test_dir}/_output/rendered_head.html`, head); - } + fs.writeFileSync( + `${test_dir}/_output/${is_async ? 'async_rendered.html' : 'rendered.html'}`, + body + ); - try { - assert_html_equal_with_options(body, expected_html || '', { - preserveComments: config.compileOptions?.preserveComments, - withoutNormalizeHtml: config.withoutNormalizeHtml - }); - } catch (error: any) { - if (should_update_expected()) { - fs.writeFileSync(`${test_dir}/_expected.html`, body); - console.log(`Updated ${test_dir}/_expected.html.`); - } else { - error.message += '\n' + `${test_dir}/main.svelte`; - throw error; + if (head) { + fs.writeFileSync( + `${test_dir}/_output/${is_async ? 'async_rendered_head.html' : 'rendered_head.html'}`, + head + ); } - } - if (fs.existsSync(`${test_dir}/_expected_head.html`)) { try { - assert_html_equal_with_options( - head, - fs.readFileSync(`${test_dir}/_expected_head.html`, 'utf-8'), - {} - ); + assert_html_equal_with_options(body, expected_html || '', { + preserveComments: compile_options.preserveComments, + withoutNormalizeHtml: config.withoutNormalizeHtml + }); } catch (error: any) { if (should_update_expected()) { - fs.writeFileSync(`${test_dir}/_expected_head.html`, head); - console.log(`Updated ${test_dir}/_expected_head.html.`); - error.message += '\n' + `${test_dir}/main.svelte`; + fs.writeFileSync(`${test_dir}/_expected.html`, body); + console.log(`Updated ${test_dir}/_expected.html.`); } else { + error.message += '\n' + `${test_dir}/main.svelte`; throw error; } } - } - if (errors.length > 0) { - assert.deepEqual(config.errors, errors); + if (fs.existsSync(`${test_dir}/_expected_head.html`)) { + try { + assert_html_equal_with_options( + head, + fs.readFileSync(`${test_dir}/_expected_head.html`, 'utf-8'), + {} + ); + } catch (error: any) { + if (should_update_expected()) { + fs.writeFileSync(`${test_dir}/_expected_head.html`, head); + console.log(`Updated ${test_dir}/_expected_head.html.`); + error.message += '\n' + `${test_dir}/main.svelte`; + } else { + throw error; + } + } + } } - - console.error = console_error; -}); +); export { test }; diff --git a/packages/svelte/tests/snapshot/samples/async-each-fallback-hoisting/_config.js b/packages/svelte/tests/snapshot/samples/async-each-fallback-hoisting/_config.js new file mode 100644 index 0000000000..2e30bbeb16 --- /dev/null +++ b/packages/svelte/tests/snapshot/samples/async-each-fallback-hoisting/_config.js @@ -0,0 +1,3 @@ +import { test } from '../../test'; + +export default test({ compileOptions: { experimental: { async: true } } }); diff --git a/packages/svelte/tests/snapshot/samples/async-each-fallback-hoisting/_expected/client/index.svelte.js b/packages/svelte/tests/snapshot/samples/async-each-fallback-hoisting/_expected/client/index.svelte.js new file mode 100644 index 0000000000..cf667e1624 --- /dev/null +++ b/packages/svelte/tests/snapshot/samples/async-each-fallback-hoisting/_expected/client/index.svelte.js @@ -0,0 +1,35 @@ +import 'svelte/internal/disclose-version'; +import 'svelte/internal/flags/async'; +import * as $ from 'svelte/internal/client'; + +export default function Async_each_fallback_hoisting($$anchor) { + var fragment = $.comment(); + var node = $.first_child(fragment); + + $.async(node, [() => Promise.resolve([])], (node, $$collection) => { + $.each( + node, + 16, + () => $.get($$collection), + $.index, + ($$anchor, item) => { + $.next(); + + var text = $.text(); + + $.template_effect(($0) => $.set_text(text, $0), void 0, [() => Promise.reject('This should never be reached')]); + $.append($$anchor, text); + }, + ($$anchor) => { + $.next(); + + var text_1 = $.text(); + + $.template_effect(($0) => $.set_text(text_1, $0), void 0, [() => Promise.resolve(4)]); + $.append($$anchor, text_1); + } + ); + }); + + $.append($$anchor, fragment); +} \ No newline at end of file diff --git a/packages/svelte/tests/snapshot/samples/async-each-fallback-hoisting/_expected/server/index.svelte.js b/packages/svelte/tests/snapshot/samples/async-each-fallback-hoisting/_expected/server/index.svelte.js new file mode 100644 index 0000000000..c579fda929 --- /dev/null +++ b/packages/svelte/tests/snapshot/samples/async-each-fallback-hoisting/_expected/server/index.svelte.js @@ -0,0 +1,25 @@ +import 'svelte/internal/flags/async'; +import * as $ from 'svelte/internal/server'; + +export default function Async_each_fallback_hoisting($$renderer) { + $$renderer.async(async ($$renderer) => { + const each_array = $.ensure_array_like((await $.save(Promise.resolve([])))()); + + if (each_array.length !== 0) { + $$renderer.push(''); + + for (let $$index = 0, $$length = each_array.length; $$index < $$length; $$index++) { + let item = each_array[$$index]; + + $$renderer.push(``); + $$renderer.push(async () => $.escape(await Promise.reject('This should never be reached'))); + } + } else { + $$renderer.push(''); + $$renderer.push(``); + $$renderer.push(async () => $.escape(await Promise.resolve(4))); + } + }); + + $$renderer.push(``); +} \ No newline at end of file diff --git a/packages/svelte/tests/snapshot/samples/async-each-fallback-hoisting/index.svelte b/packages/svelte/tests/snapshot/samples/async-each-fallback-hoisting/index.svelte new file mode 100644 index 0000000000..e580345a2e --- /dev/null +++ b/packages/svelte/tests/snapshot/samples/async-each-fallback-hoisting/index.svelte @@ -0,0 +1,5 @@ +{#each await Promise.resolve([]) as item} + {await Promise.reject('This should never be reached')} +{:else} + {await Promise.resolve(4)} +{/each} diff --git a/packages/svelte/tests/snapshot/samples/async-each-hoisting/_config.js b/packages/svelte/tests/snapshot/samples/async-each-hoisting/_config.js new file mode 100644 index 0000000000..2e30bbeb16 --- /dev/null +++ b/packages/svelte/tests/snapshot/samples/async-each-hoisting/_config.js @@ -0,0 +1,3 @@ +import { test } from '../../test'; + +export default test({ compileOptions: { experimental: { async: true } } }); diff --git a/packages/svelte/tests/snapshot/samples/async-each-hoisting/_expected/client/index.svelte.js b/packages/svelte/tests/snapshot/samples/async-each-hoisting/_expected/client/index.svelte.js new file mode 100644 index 0000000000..a1535d6886 --- /dev/null +++ b/packages/svelte/tests/snapshot/samples/async-each-hoisting/_expected/client/index.svelte.js @@ -0,0 +1,24 @@ +import 'svelte/internal/disclose-version'; +import 'svelte/internal/flags/async'; +import * as $ from 'svelte/internal/client'; + +export default function Async_each_hoisting($$anchor) { + const first = Promise.resolve(1); + const second = Promise.resolve(2); + const third = Promise.resolve(3); + var fragment = $.comment(); + var node = $.first_child(fragment); + + $.async(node, [() => Promise.resolve([first, second, third])], (node, $$collection) => { + $.each(node, 17, () => $.get($$collection), $.index, ($$anchor, item) => { + $.next(); + + var text = $.text(); + + $.template_effect(($0) => $.set_text(text, $0), void 0, [() => $.get(item)]); + $.append($$anchor, text); + }); + }); + + $.append($$anchor, fragment); +} \ No newline at end of file diff --git a/packages/svelte/tests/snapshot/samples/async-each-hoisting/_expected/server/index.svelte.js b/packages/svelte/tests/snapshot/samples/async-each-hoisting/_expected/server/index.svelte.js new file mode 100644 index 0000000000..e87b50e2a4 --- /dev/null +++ b/packages/svelte/tests/snapshot/samples/async-each-hoisting/_expected/server/index.svelte.js @@ -0,0 +1,23 @@ +import 'svelte/internal/flags/async'; +import * as $ from 'svelte/internal/server'; + +export default function Async_each_hoisting($$renderer) { + const first = Promise.resolve(1); + const second = Promise.resolve(2); + const third = Promise.resolve(3); + + $$renderer.push(``); + + $$renderer.async(async ($$renderer) => { + const each_array = $.ensure_array_like((await $.save(Promise.resolve([first, second, third])))()); + + for (let $$index = 0, $$length = each_array.length; $$index < $$length; $$index++) { + let item = each_array[$$index]; + + $$renderer.push(``); + $$renderer.push(async () => $.escape(await item)); + } + }); + + $$renderer.push(``); +} \ No newline at end of file diff --git a/packages/svelte/tests/snapshot/samples/async-each-hoisting/index.svelte b/packages/svelte/tests/snapshot/samples/async-each-hoisting/index.svelte new file mode 100644 index 0000000000..4b6bf7eadc --- /dev/null +++ b/packages/svelte/tests/snapshot/samples/async-each-hoisting/index.svelte @@ -0,0 +1,9 @@ + + +{#each await Promise.resolve([first, second, third]) as item} + {await item} +{/each} diff --git a/packages/svelte/tests/snapshot/samples/async-if-alternate-hoisting/_config.js b/packages/svelte/tests/snapshot/samples/async-if-alternate-hoisting/_config.js new file mode 100644 index 0000000000..2e30bbeb16 --- /dev/null +++ b/packages/svelte/tests/snapshot/samples/async-if-alternate-hoisting/_config.js @@ -0,0 +1,3 @@ +import { test } from '../../test'; + +export default test({ compileOptions: { experimental: { async: true } } }); diff --git a/packages/svelte/tests/snapshot/samples/async-if-alternate-hoisting/_expected/client/index.svelte.js b/packages/svelte/tests/snapshot/samples/async-if-alternate-hoisting/_expected/client/index.svelte.js new file mode 100644 index 0000000000..e385f5d234 --- /dev/null +++ b/packages/svelte/tests/snapshot/samples/async-if-alternate-hoisting/_expected/client/index.svelte.js @@ -0,0 +1,30 @@ +import 'svelte/internal/disclose-version'; +import 'svelte/internal/flags/async'; +import * as $ from 'svelte/internal/client'; + +export default function Async_if_alternate_hoisting($$anchor) { + var fragment = $.comment(); + var node = $.first_child(fragment); + + $.async(node, [() => Promise.resolve(false)], (node, $$condition) => { + var consequent = ($$anchor) => { + var text = $.text(); + + $.template_effect(($0) => $.set_text(text, $0), void 0, [() => Promise.reject('no no no')]); + $.append($$anchor, text); + }; + + var alternate = ($$anchor) => { + var text_1 = $.text(); + + $.template_effect(($0) => $.set_text(text_1, $0), void 0, [() => Promise.resolve('yes yes yes')]); + $.append($$anchor, text_1); + }; + + $.if(node, ($$render) => { + if ($.get($$condition)) $$render(consequent); else $$render(alternate, false); + }); + }); + + $.append($$anchor, fragment); +} \ No newline at end of file diff --git a/packages/svelte/tests/snapshot/samples/async-if-alternate-hoisting/_expected/server/index.svelte.js b/packages/svelte/tests/snapshot/samples/async-if-alternate-hoisting/_expected/server/index.svelte.js new file mode 100644 index 0000000000..df4ad80899 --- /dev/null +++ b/packages/svelte/tests/snapshot/samples/async-if-alternate-hoisting/_expected/server/index.svelte.js @@ -0,0 +1,16 @@ +import 'svelte/internal/flags/async'; +import * as $ from 'svelte/internal/server'; + +export default function Async_if_alternate_hoisting($$renderer) { + $$renderer.async(async ($$renderer) => { + if ((await $.save(Promise.resolve(false)))()) { + $$renderer.push(''); + $$renderer.push(async () => $.escape(await Promise.reject('no no no'))); + } else { + $$renderer.push(''); + $$renderer.push(async () => $.escape(await Promise.resolve('yes yes yes'))); + } + }); + + $$renderer.push(``); +} \ No newline at end of file diff --git a/packages/svelte/tests/snapshot/samples/async-if-alternate-hoisting/index.svelte b/packages/svelte/tests/snapshot/samples/async-if-alternate-hoisting/index.svelte new file mode 100644 index 0000000000..a97f32f034 --- /dev/null +++ b/packages/svelte/tests/snapshot/samples/async-if-alternate-hoisting/index.svelte @@ -0,0 +1,5 @@ +{#if await Promise.resolve(false)} + {await Promise.reject('no no no')} +{:else} + {await Promise.resolve('yes yes yes')} +{/if} diff --git a/packages/svelte/tests/snapshot/samples/async-if-hoisting/_config.js b/packages/svelte/tests/snapshot/samples/async-if-hoisting/_config.js new file mode 100644 index 0000000000..2e30bbeb16 --- /dev/null +++ b/packages/svelte/tests/snapshot/samples/async-if-hoisting/_config.js @@ -0,0 +1,3 @@ +import { test } from '../../test'; + +export default test({ compileOptions: { experimental: { async: true } } }); diff --git a/packages/svelte/tests/snapshot/samples/async-if-hoisting/_expected/client/index.svelte.js b/packages/svelte/tests/snapshot/samples/async-if-hoisting/_expected/client/index.svelte.js new file mode 100644 index 0000000000..356e8e9607 --- /dev/null +++ b/packages/svelte/tests/snapshot/samples/async-if-hoisting/_expected/client/index.svelte.js @@ -0,0 +1,30 @@ +import 'svelte/internal/disclose-version'; +import 'svelte/internal/flags/async'; +import * as $ from 'svelte/internal/client'; + +export default function Async_if_hoisting($$anchor) { + var fragment = $.comment(); + var node = $.first_child(fragment); + + $.async(node, [() => Promise.resolve(true)], (node, $$condition) => { + var consequent = ($$anchor) => { + var text = $.text(); + + $.template_effect(($0) => $.set_text(text, $0), void 0, [() => Promise.resolve('yes yes yes')]); + $.append($$anchor, text); + }; + + var alternate = ($$anchor) => { + var text_1 = $.text(); + + $.template_effect(($0) => $.set_text(text_1, $0), void 0, [() => Promise.reject('no no no')]); + $.append($$anchor, text_1); + }; + + $.if(node, ($$render) => { + if ($.get($$condition)) $$render(consequent); else $$render(alternate, false); + }); + }); + + $.append($$anchor, fragment); +} \ No newline at end of file diff --git a/packages/svelte/tests/snapshot/samples/async-if-hoisting/_expected/server/index.svelte.js b/packages/svelte/tests/snapshot/samples/async-if-hoisting/_expected/server/index.svelte.js new file mode 100644 index 0000000000..1d935f9be8 --- /dev/null +++ b/packages/svelte/tests/snapshot/samples/async-if-hoisting/_expected/server/index.svelte.js @@ -0,0 +1,16 @@ +import 'svelte/internal/flags/async'; +import * as $ from 'svelte/internal/server'; + +export default function Async_if_hoisting($$renderer) { + $$renderer.async(async ($$renderer) => { + if ((await $.save(Promise.resolve(true)))()) { + $$renderer.push(''); + $$renderer.push(async () => $.escape(await Promise.resolve('yes yes yes'))); + } else { + $$renderer.push(''); + $$renderer.push(async () => $.escape(await Promise.reject('no no no'))); + } + }); + + $$renderer.push(``); +} \ No newline at end of file diff --git a/packages/svelte/tests/snapshot/samples/async-if-hoisting/index.svelte b/packages/svelte/tests/snapshot/samples/async-if-hoisting/index.svelte new file mode 100644 index 0000000000..4ca3462771 --- /dev/null +++ b/packages/svelte/tests/snapshot/samples/async-if-hoisting/index.svelte @@ -0,0 +1,5 @@ +{#if await Promise.resolve(true)} + {await Promise.resolve('yes yes yes')} +{:else} + {await Promise.reject('no no no')} +{/if} diff --git a/packages/svelte/tests/snapshot/samples/await-block-scope/_expected/server/index.svelte.js b/packages/svelte/tests/snapshot/samples/await-block-scope/_expected/server/index.svelte.js index cc2628c852..e9bf215dcd 100644 --- a/packages/svelte/tests/snapshot/samples/await-block-scope/_expected/server/index.svelte.js +++ b/packages/svelte/tests/snapshot/samples/await-block-scope/_expected/server/index.svelte.js @@ -1,6 +1,6 @@ import * as $ from 'svelte/internal/server'; -export default function Await_block_scope($$payload) { +export default function Await_block_scope($$renderer) { let counter = { count: 0 }; const promise = Promise.resolve(counter); @@ -8,7 +8,7 @@ export default function Await_block_scope($$payload) { counter.count += 1; } - $$payload.out.push(` `); - $.await($$payload, promise, () => {}, (counter) => {}); - $$payload.out.push(` ${$.escape(counter.count)}`); + $$renderer.push(` `); + $.await($$renderer, promise, () => {}, (counter) => {}); + $$renderer.push(` ${$.escape(counter.count)}`); } \ No newline at end of file diff --git a/packages/svelte/tests/snapshot/samples/bind-component-snippet/_expected/server/index.svelte.js b/packages/svelte/tests/snapshot/samples/bind-component-snippet/_expected/server/index.svelte.js index c0db7d2fd5..2ef3a429ba 100644 --- a/packages/svelte/tests/snapshot/samples/bind-component-snippet/_expected/server/index.svelte.js +++ b/packages/svelte/tests/snapshot/samples/bind-component-snippet/_expected/server/index.svelte.js @@ -1,18 +1,18 @@ import * as $ from 'svelte/internal/server'; import TextInput from './Child.svelte'; -function snippet($$payload) { - $$payload.out.push(`Something`); +function snippet($$renderer) { + $$renderer.push(`Something`); } -export default function Bind_component_snippet($$payload) { +export default function Bind_component_snippet($$renderer) { let value = ''; const _snippet = snippet; let $$settled = true; - let $$inner_payload; + let $$inner_renderer; - function $$render_inner($$payload) { - TextInput($$payload, { + function $$render_inner($$renderer) { + TextInput($$renderer, { get value() { return value; }, @@ -23,14 +23,14 @@ export default function Bind_component_snippet($$payload) { } }); - $$payload.out.push(` value: ${$.escape(value)}`); + $$renderer.push(` value: ${$.escape(value)}`); } do { $$settled = true; - $$inner_payload = $.copy_payload($$payload); - $$render_inner($$inner_payload); + $$inner_renderer = $$renderer.copy(); + $$render_inner($$inner_renderer); } while (!$$settled); - $.assign_payload($$payload, $$inner_payload); + $$renderer.subsume($$inner_renderer); } \ No newline at end of file diff --git a/packages/svelte/tests/snapshot/samples/bind-this/_expected/server/index.svelte.js b/packages/svelte/tests/snapshot/samples/bind-this/_expected/server/index.svelte.js index 148573766f..8697d71bfa 100644 --- a/packages/svelte/tests/snapshot/samples/bind-this/_expected/server/index.svelte.js +++ b/packages/svelte/tests/snapshot/samples/bind-this/_expected/server/index.svelte.js @@ -1,5 +1,5 @@ import * as $ from 'svelte/internal/server'; -export default function Bind_this($$payload) { - Foo($$payload, {}); +export default function Bind_this($$renderer) { + Foo($$renderer, {}); } \ No newline at end of file diff --git a/packages/svelte/tests/snapshot/samples/class-state-field-constructor-assignment/_expected/server/index.svelte.js b/packages/svelte/tests/snapshot/samples/class-state-field-constructor-assignment/_expected/server/index.svelte.js index abfc264fea..57b81d86ba 100644 --- a/packages/svelte/tests/snapshot/samples/class-state-field-constructor-assignment/_expected/server/index.svelte.js +++ b/packages/svelte/tests/snapshot/samples/class-state-field-constructor-assignment/_expected/server/index.svelte.js @@ -1,38 +1,36 @@ import * as $ from 'svelte/internal/server'; -export default function Class_state_field_constructor_assignment($$payload, $$props) { - $.push(); - - class Foo { - a = 0; - #b; - #foo = $.derived(() => ({ bar: this.a * 2 })); - - get foo() { - return this.#foo(); - } - - set foo($$value) { - return this.#foo($$value); - } - - #bar = $.derived(() => ({ baz: this.foo })); - - get bar() { - return this.#bar(); - } - - set bar($$value) { - return this.#bar($$value); +export default function Class_state_field_constructor_assignment($$renderer, $$props) { + $$renderer.component(($$renderer) => { + class Foo { + a = 0; + #b; + #foo = $.derived(() => ({ bar: this.a * 2 })); + + get foo() { + return this.#foo(); + } + + set foo($$value) { + return this.#foo($$value); + } + + #bar = $.derived(() => ({ baz: this.foo })); + + get bar() { + return this.#bar(); + } + + set bar($$value) { + return this.#bar($$value); + } + + constructor() { + this.a = 1; + this.#b = 2; + this.foo.bar = 3; + this.bar = 4; + } } - - constructor() { - this.a = 1; - this.#b = 2; - this.foo.bar = 3; - this.bar = 4; - } - } - - $.pop(); + }); } \ No newline at end of file diff --git a/packages/svelte/tests/snapshot/samples/delegated-locally-declared-shadowed/_expected/server/index.svelte.js b/packages/svelte/tests/snapshot/samples/delegated-locally-declared-shadowed/_expected/server/index.svelte.js index ac3dfcd2cb..51241b92c0 100644 --- a/packages/svelte/tests/snapshot/samples/delegated-locally-declared-shadowed/_expected/server/index.svelte.js +++ b/packages/svelte/tests/snapshot/samples/delegated-locally-declared-shadowed/_expected/server/index.svelte.js @@ -1,13 +1,13 @@ import * as $ from 'svelte/internal/server'; -export default function Delegated_locally_declared_shadowed($$payload) { - const each_array = $.ensure_array_like({ length: 1 }); +export default function Delegated_locally_declared_shadowed($$renderer) { + $$renderer.push(``); - $$payload.out.push(``); + const each_array = $.ensure_array_like({ length: 1 }); for (let index = 0, $$length = each_array.length; index < $$length; index++) { - $$payload.out.push(``); + $$renderer.push(``); } - $$payload.out.push(``); + $$renderer.push(``); } \ No newline at end of file diff --git a/packages/svelte/tests/snapshot/samples/dynamic-attributes-casing/_expected/server/main.svelte.js b/packages/svelte/tests/snapshot/samples/dynamic-attributes-casing/_expected/server/main.svelte.js index 9c837d4e1d..1ff8402974 100644 --- a/packages/svelte/tests/snapshot/samples/dynamic-attributes-casing/_expected/server/main.svelte.js +++ b/packages/svelte/tests/snapshot/samples/dynamic-attributes-casing/_expected/server/main.svelte.js @@ -1,10 +1,10 @@ import * as $ from 'svelte/internal/server'; -export default function Main($$payload) { +export default function Main($$renderer) { // needs to be a snapshot test because jsdom does auto-correct the attribute casing let x = 'test'; let y = () => 'test'; - $$payload.out.push(` `); + $$renderer.push(` `); } \ No newline at end of file diff --git a/packages/svelte/tests/snapshot/samples/each-index-non-null/_expected/server/index.svelte.js b/packages/svelte/tests/snapshot/samples/each-index-non-null/_expected/server/index.svelte.js index 8fa0c5f28c..e6de07a36e 100644 --- a/packages/svelte/tests/snapshot/samples/each-index-non-null/_expected/server/index.svelte.js +++ b/packages/svelte/tests/snapshot/samples/each-index-non-null/_expected/server/index.svelte.js @@ -1,13 +1,13 @@ import * as $ from 'svelte/internal/server'; -export default function Each_index_non_null($$payload) { - const each_array = $.ensure_array_like(Array(10)); +export default function Each_index_non_null($$renderer) { + $$renderer.push(``); - $$payload.out.push(``); + const each_array = $.ensure_array_like(Array(10)); for (let i = 0, $$length = each_array.length; i < $$length; i++) { - $$payload.out.push(`

index: ${$.escape(i)}

`); + $$renderer.push(`

index: ${$.escape(i)}

`); } - $$payload.out.push(``); + $$renderer.push(``); } \ No newline at end of file diff --git a/packages/svelte/tests/snapshot/samples/each-string-template/_expected/server/index.svelte.js b/packages/svelte/tests/snapshot/samples/each-string-template/_expected/server/index.svelte.js index 6dbe8130da..e1c550e113 100644 --- a/packages/svelte/tests/snapshot/samples/each-string-template/_expected/server/index.svelte.js +++ b/packages/svelte/tests/snapshot/samples/each-string-template/_expected/server/index.svelte.js @@ -1,15 +1,15 @@ import * as $ from 'svelte/internal/server'; -export default function Each_string_template($$payload) { - const each_array = $.ensure_array_like(['foo', 'bar', 'baz']); +export default function Each_string_template($$renderer) { + $$renderer.push(``); - $$payload.out.push(``); + const each_array = $.ensure_array_like(['foo', 'bar', 'baz']); for (let $$index = 0, $$length = each_array.length; $$index < $$length; $$index++) { let thing = each_array[$$index]; - $$payload.out.push(`${$.escape(thing)}, `); + $$renderer.push(`${$.escape(thing)}, `); } - $$payload.out.push(``); + $$renderer.push(``); } \ No newline at end of file diff --git a/packages/svelte/tests/snapshot/samples/function-prop-no-getter/_expected/server/index.svelte.js b/packages/svelte/tests/snapshot/samples/function-prop-no-getter/_expected/server/index.svelte.js index ce4f09ed1d..855ae30d21 100644 --- a/packages/svelte/tests/snapshot/samples/function-prop-no-getter/_expected/server/index.svelte.js +++ b/packages/svelte/tests/snapshot/samples/function-prop-no-getter/_expected/server/index.svelte.js @@ -1,6 +1,6 @@ import * as $ from 'svelte/internal/server'; -export default function Function_prop_no_getter($$payload) { +export default function Function_prop_no_getter($$renderer) { let count = 0; function onmouseup() { @@ -9,13 +9,13 @@ export default function Function_prop_no_getter($$payload) { const plusOne = (num) => num + 1; - Button($$payload, { + Button($$renderer, { onmousedown: () => count += 1, onmouseup, onmouseenter: () => count = plusOne(count), - children: ($$payload) => { - $$payload.out.push(`clicks: ${$.escape(count)}`); + children: ($$renderer) => { + $$renderer.push(`clicks: ${$.escape(count)}`); }, $$slots: { default: true } diff --git a/packages/svelte/tests/snapshot/samples/functional-templating/_expected/server/index.svelte.js b/packages/svelte/tests/snapshot/samples/functional-templating/_expected/server/index.svelte.js index b1a0a5f9e6..a5eb404d9e 100644 --- a/packages/svelte/tests/snapshot/samples/functional-templating/_expected/server/index.svelte.js +++ b/packages/svelte/tests/snapshot/samples/functional-templating/_expected/server/index.svelte.js @@ -1,5 +1,5 @@ import * as $ from 'svelte/internal/server'; -export default function Functional_templating($$payload) { - $$payload.out.push(`

hello

child element

another child element

`); +export default function Functional_templating($$renderer) { + $$renderer.push(`

hello

child element

another child element

`); } \ No newline at end of file diff --git a/packages/svelte/tests/snapshot/samples/hello-world/_expected/server/index.svelte.js b/packages/svelte/tests/snapshot/samples/hello-world/_expected/server/index.svelte.js index 30f6d6b74a..5c1a710f2e 100644 --- a/packages/svelte/tests/snapshot/samples/hello-world/_expected/server/index.svelte.js +++ b/packages/svelte/tests/snapshot/samples/hello-world/_expected/server/index.svelte.js @@ -1,5 +1,5 @@ import * as $ from 'svelte/internal/server'; -export default function Hello_world($$payload) { - $$payload.out.push(`

hello world

`); +export default function Hello_world($$renderer) { + $$renderer.push(`

hello world

`); } \ No newline at end of file diff --git a/packages/svelte/tests/snapshot/samples/hmr/_expected/server/index.svelte.js b/packages/svelte/tests/snapshot/samples/hmr/_expected/server/index.svelte.js index ea1d12c83b..d8b9d98b03 100644 --- a/packages/svelte/tests/snapshot/samples/hmr/_expected/server/index.svelte.js +++ b/packages/svelte/tests/snapshot/samples/hmr/_expected/server/index.svelte.js @@ -1,5 +1,5 @@ import * as $ from 'svelte/internal/server'; -export default function Hmr($$payload) { - $$payload.out.push(`

hello world

`); +export default function Hmr($$renderer) { + $$renderer.push(`

hello world

`); } \ No newline at end of file diff --git a/packages/svelte/tests/snapshot/samples/imports-in-modules/_expected/server/index.svelte.js b/packages/svelte/tests/snapshot/samples/imports-in-modules/_expected/server/index.svelte.js index 2ed863d68f..2d7b9906ed 100644 --- a/packages/svelte/tests/snapshot/samples/imports-in-modules/_expected/server/index.svelte.js +++ b/packages/svelte/tests/snapshot/samples/imports-in-modules/_expected/server/index.svelte.js @@ -1,4 +1,4 @@ import * as $ from 'svelte/internal/server'; import { random } from './module.svelte'; -export default function Imports_in_modules($$payload) {} \ No newline at end of file +export default function Imports_in_modules($$renderer) {} \ No newline at end of file diff --git a/packages/svelte/tests/snapshot/samples/nullish-coallescence-omittance/_expected/server/index.svelte.js b/packages/svelte/tests/snapshot/samples/nullish-coallescence-omittance/_expected/server/index.svelte.js index 18e01b4f72..a7e580acb8 100644 --- a/packages/svelte/tests/snapshot/samples/nullish-coallescence-omittance/_expected/server/index.svelte.js +++ b/packages/svelte/tests/snapshot/samples/nullish-coallescence-omittance/_expected/server/index.svelte.js @@ -1,8 +1,8 @@ import * as $ from 'svelte/internal/server'; -export default function Nullish_coallescence_omittance($$payload) { +export default function Nullish_coallescence_omittance($$renderer) { let name = 'world'; let count = 0; - $$payload.out.push(`

Hello, world!

123

Hello, world

`); + $$renderer.push(`

Hello, world!

123

Hello, world

`); } \ No newline at end of file diff --git a/packages/svelte/tests/snapshot/samples/props-identifier/_expected/server/index.svelte.js b/packages/svelte/tests/snapshot/samples/props-identifier/_expected/server/index.svelte.js index 33a3633939..6db24ac621 100644 --- a/packages/svelte/tests/snapshot/samples/props-identifier/_expected/server/index.svelte.js +++ b/packages/svelte/tests/snapshot/samples/props-identifier/_expected/server/index.svelte.js @@ -1,16 +1,15 @@ import * as $ from 'svelte/internal/server'; -export default function Props_identifier($$payload, $$props) { - $.push(); +export default function Props_identifier($$renderer, $$props) { + $$renderer.component(($$renderer) => { + let { $$slots, $$events, ...props } = $$props; - let { $$slots, $$events, ...props } = $$props; - - props.a; - props[a]; - props.a.b; - props.a.b = true; - props.a = true; - props[a] = true; - props; - $.pop(); + props.a; + props[a]; + props.a.b; + props.a.b = true; + props.a = true; + props[a] = true; + props; + }); } \ No newline at end of file diff --git a/packages/svelte/tests/snapshot/samples/purity/_expected/server/index.svelte.js b/packages/svelte/tests/snapshot/samples/purity/_expected/server/index.svelte.js index 29b0d0d594..9f76d8ffb8 100644 --- a/packages/svelte/tests/snapshot/samples/purity/_expected/server/index.svelte.js +++ b/packages/svelte/tests/snapshot/samples/purity/_expected/server/index.svelte.js @@ -1,7 +1,7 @@ import * as $ from 'svelte/internal/server'; -export default function Purity($$payload) { - $$payload.out.push(`

0

${$.escape(location.href)}

`); - Child($$payload, { prop: encodeURIComponent('hello') }); - $$payload.out.push(``); +export default function Purity($$renderer) { + $$renderer.push(`

0

${$.escape(location.href)}

`); + Child($$renderer, { prop: encodeURIComponent('hello') }); + $$renderer.push(``); } \ No newline at end of file diff --git a/packages/svelte/tests/snapshot/samples/skip-static-subtree/_expected/server/index.svelte.js b/packages/svelte/tests/snapshot/samples/skip-static-subtree/_expected/server/index.svelte.js index bad475ec86..7a9f6193d7 100644 --- a/packages/svelte/tests/snapshot/samples/skip-static-subtree/_expected/server/index.svelte.js +++ b/packages/svelte/tests/snapshot/samples/skip-static-subtree/_expected/server/index.svelte.js @@ -1,7 +1,13 @@ import * as $ from 'svelte/internal/server'; -export default function Skip_static_subtree($$payload, $$props) { +export default function Skip_static_subtree($$renderer, $$props) { let { title, content } = $$props; - $$payload.out.push(`

${$.escape(title)}

we don't need to traverse these nodes

or

these

ones

${$.html(content)}

these

trailing

nodes

can

be

completely

ignored

`); + $$renderer.push(`

${$.escape(title)}

we don't need to traverse these nodes

or

these

ones

${$.html(content)}

these

trailing

nodes

can

be

completely

ignored

`); } \ No newline at end of file diff --git a/packages/svelte/tests/snapshot/samples/state-proxy-literal/_expected/server/index.svelte.js b/packages/svelte/tests/snapshot/samples/state-proxy-literal/_expected/server/index.svelte.js index c2736b0f43..4ab7f90c58 100644 --- a/packages/svelte/tests/snapshot/samples/state-proxy-literal/_expected/server/index.svelte.js +++ b/packages/svelte/tests/snapshot/samples/state-proxy-literal/_expected/server/index.svelte.js @@ -1,6 +1,6 @@ import * as $ from 'svelte/internal/server'; -export default function State_proxy_literal($$payload) { +export default function State_proxy_literal($$renderer) { let str = ''; let tpl = ``; @@ -11,5 +11,5 @@ export default function State_proxy_literal($$payload) { tpl = ``; } - $$payload.out.push(` `); + $$renderer.push(` `); } \ No newline at end of file diff --git a/packages/svelte/tests/snapshot/samples/svelte-element/_expected/server/index.svelte.js b/packages/svelte/tests/snapshot/samples/svelte-element/_expected/server/index.svelte.js index 4426ad1164..fc97686bb1 100644 --- a/packages/svelte/tests/snapshot/samples/svelte-element/_expected/server/index.svelte.js +++ b/packages/svelte/tests/snapshot/samples/svelte-element/_expected/server/index.svelte.js @@ -1,7 +1,7 @@ import * as $ from 'svelte/internal/server'; -export default function Svelte_element($$payload, $$props) { +export default function Svelte_element($$renderer, $$props) { let { tag = 'hr' } = $$props; - $.element($$payload, tag); + $.element($$renderer, tag); } \ No newline at end of file diff --git a/packages/svelte/tests/snapshot/samples/text-nodes-deriveds/_expected/server/index.svelte.js b/packages/svelte/tests/snapshot/samples/text-nodes-deriveds/_expected/server/index.svelte.js index f7dc586026..f886f9fbe3 100644 --- a/packages/svelte/tests/snapshot/samples/text-nodes-deriveds/_expected/server/index.svelte.js +++ b/packages/svelte/tests/snapshot/samples/text-nodes-deriveds/_expected/server/index.svelte.js @@ -1,6 +1,6 @@ import * as $ from 'svelte/internal/server'; -export default function Text_nodes_deriveds($$payload) { +export default function Text_nodes_deriveds($$renderer) { let count1 = 0; let count2 = 0; @@ -12,5 +12,5 @@ export default function Text_nodes_deriveds($$payload) { return count2; } - $$payload.out.push(`

${$.escape(text1())}${$.escape(text2())}

`); + $$renderer.push(`

${$.escape(text1())}${$.escape(text2())}

`); } \ No newline at end of file diff --git a/packages/svelte/types/index.d.ts b/packages/svelte/types/index.d.ts index 9888de59b2..2c015b5a10 100644 --- a/packages/svelte/types/index.d.ts +++ b/packages/svelte/types/index.d.ts @@ -2493,7 +2493,7 @@ declare module 'svelte/server' { } ] ): RenderOutput; - interface RenderOutput { + interface SyncRenderOutput { /** HTML that goes into the `` */ head: string; /** @deprecated use `body` instead */ @@ -2502,6 +2502,8 @@ declare module 'svelte/server' { body: string; } + type RenderOutput = SyncRenderOutput & PromiseLike; + export {}; } diff --git a/playgrounds/sandbox/Wrapper.svelte b/playgrounds/sandbox/Wrapper.svelte deleted file mode 100644 index 1fe82c1716..0000000000 --- a/playgrounds/sandbox/Wrapper.svelte +++ /dev/null @@ -1,9 +0,0 @@ - - - - - - {#snippet pending()}{/snippet} - diff --git a/playgrounds/sandbox/index.html b/playgrounds/sandbox/index.html index 639409b877..d70409ffb6 100644 --- a/playgrounds/sandbox/index.html +++ b/playgrounds/sandbox/index.html @@ -12,7 +12,7 @@