From b8fd326d96c3a57feff3a4fafcd2d1651001b109 Mon Sep 17 00:00:00 2001 From: Elliott Johnson Date: Wed, 17 Sep 2025 15:49:57 -0600 Subject: [PATCH] feat: async SSR (#16748) * feat: First pass at payload * first crack * snapshots * checkpoint * fix: cloning * add test option * big dumb * today's hard work; few tests left to fix * improve * tests passing no wayyyyy yo * lots of progress, couple of failing tests around selects * meh * solve async tree stuff * fix select/option stuff * whoop, tests * simplify * feat: hoisting * fix: `$effect.pending` sends updates to incorrect boundary * changeset * stuff from upstream * feat: first hydrationgaa * remove docs * snapshots * silly fix * checkpoint * meh * ALKASJDFALSKDFJ the test passes * chore: Update a bunch of tests for hydration markers * chore: remove snippet and is_async * naming * better errors for sync-in-async * test improvements * idk man * merge local branches (#16757) * use fragment as async hoist boundary * remove async_hoist_boundary * only dewaterfall when necessary * unused * simplify/fix * de-waterfall awaits in separate elements * update snapshots * remove unnecessary wrapper * fix * fix * remove suspends_without_fallback --------- Co-authored-by: Rich Harris * Update payload.js Co-authored-by: Rich Harris * checkpoint * got the extra children to go away * just gonna go ahead and merge this as the review comments take up too much space * chore: remove hoisted_promises (#16766) * chore: remove hoisted_promises * WIP optimise promises * WIP * fix with await in prop * tweak * fix type error * Update packages/svelte/src/compiler/phases/3-transform/server/visitors/SvelteHead.js * chore: fix hydration treeshaking (#16767) * chore: fix hydration treeshaking * fix * remove await_outside_boundary error (#16762) * chore: remove unused analysis.boundary (#16763) * chore: simplify slots (#16765) * chore: simplify slots * unused * Apply suggestions from code review * chore: remove metadata.pending (#16764) * Update packages/svelte/src/compiler/phases/3-transform/server/visitors/SnippetBlock.js * put this back where it was, keep the diff small * Update packages/svelte/src/compiler/phases/types.d.ts Co-authored-by: Rich Harris * chore: remove analysis.state.title (#16771) * chore: remove analysis.state.title * unused * chore: remove is_async (#16769) * chore: remove is_async * unused * Apply suggestions from code review Co-authored-by: Rich Harris * cleanup * lint * clean up payload a bit * compiler work * run ssr on sync and async * prettier * inline capture * Update packages/svelte/src/compiler/phases/3-transform/server/visitors/EachBlock.js * chore: simplify build_template (#16780) * small tweak to aid greppability * chore: fix SSR context (#16781) * at least passing * cleanup * fix * remove push/pop from exports, not needed with payload * I think this is better but tbh not sure * async SSR * qualification * errors: * I have lost the plot * finally * ugh * tweak error codes to better align with existing conventions, such as they are * tweak messages * remove unused args * DRY out a bit * unused * unused * unused * simplify - we can enforce readonly at a type level * unused * simplify * avoid magical accessors * simplify algorithm * unused * unused * reduce indirection * TreeState -> SSRState * mark deprecated methods * grab this.local from parent directly * rename render -> fn per conventions (fn indicates 'arbitrary code') * reduce indirection * Revert "reduce indirection" This reverts commit 3ec461baad451654db6d518734aeeb7b366f7c2e. * tweak * okay works this time * no way chat, it works * fix context stuff * tweak * make it chainable * lint * clean up * lint * Update packages/svelte/src/internal/server/types.d.ts Co-authored-by: Rich Harris * sunset html for async * types * we use 'deprecated' in other messages * oops --------- Co-authored-by: Rich Harris --- .changeset/forty-insects-cheat.md | 5 + .prettierignore | 1 + .../98-reference/.generated/server-errors.md | 14 + .../.generated/server-warnings.md | 9 + .../98-reference/.generated/shared-errors.md | 20 - eslint.config.js | 1 + .../svelte/messages/server-errors/errors.md | 15 + .../messages/server-errors/lifecycle.md | 5 - .../messages/server-warnings/warnings.md | 5 + .../svelte/messages/shared-errors/errors.md | 18 - .../svelte/scripts/process-messages/index.js | 1 + .../templates/server-warnings.js | 20 + .../compiler/phases/1-parse/state/element.js | 3 +- .../src/compiler/phases/2-analyze/index.js | 15 +- .../src/compiler/phases/2-analyze/types.d.ts | 5 + .../2-analyze/visitors/AwaitExpression.js | 122 +++- .../2-analyze/visitors/CallExpression.js | 1 + .../phases/2-analyze/visitors/ConstTag.js | 6 +- .../2-analyze/visitors/RegularElement.js | 2 +- .../2-analyze/visitors/VariableDeclarator.js | 6 + .../3-transform/client/transform-client.js | 2 - .../phases/3-transform/client/types.d.ts | 8 +- .../client/visitors/AwaitExpression.js | 106 +-- .../client/visitors/CallExpression.js | 4 +- .../3-transform/client/visitors/ConstTag.js | 9 +- .../client/visitors/RegularElement.js | 41 +- .../client/visitors/VariableDeclaration.js | 14 +- .../3-transform/server/transform-server.js | 26 +- .../3-transform/server/visitors/AwaitBlock.js | 38 +- .../server/visitors/AwaitExpression.js | 22 +- .../3-transform/server/visitors/EachBlock.js | 27 +- .../3-transform/server/visitors/IfBlock.js | 22 +- .../server/visitors/RegularElement.js | 76 ++- .../server/visitors/SlotElement.js | 27 +- .../server/visitors/SvelteBoundary.js | 26 +- .../server/visitors/TitleElement.js | 4 +- .../server/visitors/shared/component.js | 60 +- .../server/visitors/shared/utils.js | 149 +++-- .../svelte/src/compiler/phases/types.d.ts | 4 +- .../svelte/src/compiler/types/template.d.ts | 2 + packages/svelte/src/compiler/utils/ast.js | 16 + .../svelte/src/compiler/utils/builders.js | 12 +- packages/svelte/src/index-server.js | 8 +- .../internal/client/dom/blocks/boundary.js | 106 +-- .../src/internal/client/reactivity/async.js | 3 +- packages/svelte/src/internal/client/render.js | 55 +- .../src/internal/server/blocks/snippet.js | 2 +- .../svelte/src/internal/server/context.js | 58 +- packages/svelte/src/internal/server/dev.js | 46 +- packages/svelte/src/internal/server/errors.js | 24 + packages/svelte/src/internal/server/index.js | 232 ++++--- .../svelte/src/internal/server/payload.js | 625 ++++++++++++++++-- .../src/internal/server/payload.test.ts | 364 ++++++++++ .../svelte/src/internal/server/types.d.ts | 23 +- .../svelte/src/internal/server/warnings.js | 17 + packages/svelte/src/internal/shared/errors.js | 16 - packages/svelte/src/legacy/legacy-server.js | 1 + .../samples/binding-input/_expected.html | 2 +- .../_override.html | 2 +- .../dynamic-text-changed/_expected.html | 2 +- .../_expected.html | 2 +- .../_expected.html | 2 +- .../_expected.html | 2 +- .../_expected.html | 2 +- .../element-attribute-added/_expected.html | 2 +- .../element-attribute-changed/_expected.html | 2 +- .../element-attribute-removed/_expected.html | 2 +- .../if-block-mismatch-2/_expected.html | 2 +- .../samples/if-block-mismatch/_expected.html | 2 +- .../_expected.html | 2 +- .../input-value-changed/_expected.html | 2 +- .../hydration/samples/noscript/_expected.html | 2 +- .../pre-first-node-newline/_expected.html | 4 +- .../_expected.html | 2 +- .../repair-mismatched-a-href/_expected.html | 2 +- .../samples/safari-borking/_override.html | 2 +- .../hydration/samples/script/_expected.html | 2 +- .../snippet-raw-hydrate/_expected.html | 2 +- .../standalone-component/_expected.html | 2 +- .../samples/standalone-snippet/_expected.html | 2 +- .../surrounding-whitespace/_expected.html | 2 +- .../surrounding-whitespace/_override.html | 2 +- .../samples/text-empty-2/_expected.html | 2 +- .../samples/text-empty/_expected.html | 2 +- .../whitespace-at-block-start/_override.html | 4 +- .../tests/runtime-browser/driver-ssr.js | 6 +- .../after-render-prevents-loop/_config.js | 2 +- .../after-render-triggers-update/_config.js | 2 +- .../before-render-prevents-loop/_config.js | 2 +- .../_config.js | 18 +- .../_config.js | 6 +- .../samples/binding-select-late-2/_config.js | 6 +- .../samples/binding-select-late-3/_config.js | 6 +- .../samples/binding-select-late/_config.js | 6 +- .../binding-select-unmatched-2/_config.js | 30 +- .../binding-select-unmatched-3/_config.js | 4 +- .../_config.js | 2 +- .../samples/textarea-content/_config.js | 4 +- .../svelte/tests/runtime-legacy/shared.ts | 53 +- .../async-no-pending-throws-sync/_config.js | 7 + .../async-no-pending-throws-sync/main.svelte | 1 + .../samples/async-no-pending/_config.js | 17 + .../samples/async-no-pending/main.svelte | 1 + .../samples/async-ondestroy-ordering/A.svelte | 9 + .../samples/async-ondestroy-ordering/B.svelte | 9 + .../samples/async-ondestroy-ordering/C.svelte | 5 + .../async-ondestroy-ordering/_config.js | 14 + .../async-ondestroy-ordering/destroyed.js | 4 + .../async-ondestroy-ordering/main.svelte | 13 + .../samples/snippet-slot-let-error/_config.js | 1 + .../_config.js | 6 + .../main.svelte | 7 + .../_expected.html | 1 + .../async-each-fallback-hoisting/main.svelte | 5 + .../async-each-hoisting/_expected.html | 1 + .../samples/async-each-hoisting/main.svelte | 9 + .../A.svelte | 9 + .../B.svelte | 7 + .../_config.js | 3 + .../_expected.html | 0 .../_expected_head.html | 1 + .../main.svelte | 27 + .../_expected.html | 1 + .../async-if-alternate-hoisting/main.svelte | 5 + .../samples/async-if-hoisting/_expected.html | 1 + .../samples/async-if-hoisting/main.svelte | 5 + .../_expected.html | 3 + .../main.svelte | 3 + .../_expected.html | 3 + .../main.svelte | 3 + .../Option.svelte | 5 + .../_expected.html | 1 + .../async-select-value-component/main.svelte | 8 + .../_expected.html | 1 + .../main.svelte | 13 + .../_expected.html | 1 + .../main.svelte | 5 + .../samples/comment-preserve/_expected.html | 2 +- .../samples/context/Child.svelte | 8 + .../samples/context/_config.js | 5 + .../samples/context/_expected.html | 3 + .../samples/context/main.svelte | 11 + .../_expected.html | 2 +- .../_expected.html | 2 + .../invalid-nested-svelte-element/_config.js | 4 +- .../tests/server-side-rendering/test.ts | 146 ++-- .../async-each-fallback-hoisting/_config.js | 3 + .../_expected/client/index.svelte.js | 35 + .../_expected/server/index.svelte.js | 25 + .../async-each-fallback-hoisting/index.svelte | 5 + .../samples/async-each-hoisting/_config.js | 3 + .../_expected/client/index.svelte.js | 24 + .../_expected/server/index.svelte.js | 23 + .../samples/async-each-hoisting/index.svelte | 9 + .../async-if-alternate-hoisting/_config.js | 3 + .../_expected/client/index.svelte.js | 30 + .../_expected/server/index.svelte.js | 16 + .../async-if-alternate-hoisting/index.svelte | 5 + .../samples/async-if-hoisting/_config.js | 3 + .../_expected/client/index.svelte.js | 30 + .../_expected/server/index.svelte.js | 16 + .../samples/async-if-hoisting/index.svelte | 5 + .../_expected/server/index.svelte.js | 4 +- .../_expected/server/index.svelte.js | 8 +- .../_expected/server/index.svelte.js | 64 +- .../_expected/server/index.svelte.js | 8 +- .../_expected/server/main.svelte.js | 2 +- .../_expected/server/index.svelte.js | 8 +- .../_expected/server/index.svelte.js | 8 +- .../_expected/server/index.svelte.js | 2 +- .../_expected/server/index.svelte.js | 2 +- .../_expected/server/index.svelte.js | 2 +- .../hmr/_expected/server/index.svelte.js | 2 +- .../_expected/server/index.svelte.js | 2 +- .../_expected/server/index.svelte.js | 21 +- .../purity/_expected/server/index.svelte.js | 4 +- .../_expected/server/index.svelte.js | 2 +- .../_expected/server/index.svelte.js | 2 +- .../_expected/server/index.svelte.js | 2 +- packages/svelte/types/index.d.ts | 4 +- playgrounds/sandbox/package.json | 3 +- 181 files changed, 2638 insertions(+), 901 deletions(-) create mode 100644 .changeset/forty-insects-cheat.md create mode 100644 documentation/docs/98-reference/.generated/server-warnings.md create mode 100644 packages/svelte/messages/server-errors/errors.md delete mode 100644 packages/svelte/messages/server-errors/lifecycle.md create mode 100644 packages/svelte/messages/server-warnings/warnings.md create mode 100644 packages/svelte/scripts/process-messages/templates/server-warnings.js create mode 100644 packages/svelte/src/internal/server/payload.test.ts create mode 100644 packages/svelte/src/internal/server/warnings.js create mode 100644 packages/svelte/tests/runtime-runes/samples/async-no-pending-throws-sync/_config.js create mode 100644 packages/svelte/tests/runtime-runes/samples/async-no-pending-throws-sync/main.svelte create mode 100644 packages/svelte/tests/runtime-runes/samples/async-no-pending/_config.js create mode 100644 packages/svelte/tests/runtime-runes/samples/async-no-pending/main.svelte create mode 100644 packages/svelte/tests/runtime-runes/samples/async-ondestroy-ordering/A.svelte create mode 100644 packages/svelte/tests/runtime-runes/samples/async-ondestroy-ordering/B.svelte create mode 100644 packages/svelte/tests/runtime-runes/samples/async-ondestroy-ordering/C.svelte create mode 100644 packages/svelte/tests/runtime-runes/samples/async-ondestroy-ordering/_config.js create mode 100644 packages/svelte/tests/runtime-runes/samples/async-ondestroy-ordering/destroyed.js create mode 100644 packages/svelte/tests/runtime-runes/samples/async-ondestroy-ordering/main.svelte create mode 100644 packages/svelte/tests/server-side-rendering/samples/async-context-throws-after-await/_config.js create mode 100644 packages/svelte/tests/server-side-rendering/samples/async-context-throws-after-await/main.svelte create mode 100644 packages/svelte/tests/server-side-rendering/samples/async-each-fallback-hoisting/_expected.html create mode 100644 packages/svelte/tests/server-side-rendering/samples/async-each-fallback-hoisting/main.svelte create mode 100644 packages/svelte/tests/server-side-rendering/samples/async-each-hoisting/_expected.html create mode 100644 packages/svelte/tests/server-side-rendering/samples/async-each-hoisting/main.svelte create mode 100644 packages/svelte/tests/server-side-rendering/samples/async-head-multiple-title-order-preserved/A.svelte create mode 100644 packages/svelte/tests/server-side-rendering/samples/async-head-multiple-title-order-preserved/B.svelte create mode 100644 packages/svelte/tests/server-side-rendering/samples/async-head-multiple-title-order-preserved/_config.js create mode 100644 packages/svelte/tests/server-side-rendering/samples/async-head-multiple-title-order-preserved/_expected.html create mode 100644 packages/svelte/tests/server-side-rendering/samples/async-head-multiple-title-order-preserved/_expected_head.html create mode 100644 packages/svelte/tests/server-side-rendering/samples/async-head-multiple-title-order-preserved/main.svelte create mode 100644 packages/svelte/tests/server-side-rendering/samples/async-if-alternate-hoisting/_expected.html create mode 100644 packages/svelte/tests/server-side-rendering/samples/async-if-alternate-hoisting/main.svelte create mode 100644 packages/svelte/tests/server-side-rendering/samples/async-if-hoisting/_expected.html create mode 100644 packages/svelte/tests/server-side-rendering/samples/async-if-hoisting/main.svelte create mode 100644 packages/svelte/tests/server-side-rendering/samples/async-option-implicit-complex-value/_expected.html create mode 100644 packages/svelte/tests/server-side-rendering/samples/async-option-implicit-complex-value/main.svelte create mode 100644 packages/svelte/tests/server-side-rendering/samples/async-option-implicit-simple-value/_expected.html create mode 100644 packages/svelte/tests/server-side-rendering/samples/async-option-implicit-simple-value/main.svelte create mode 100644 packages/svelte/tests/server-side-rendering/samples/async-select-value-component/Option.svelte create mode 100644 packages/svelte/tests/server-side-rendering/samples/async-select-value-component/_expected.html create mode 100644 packages/svelte/tests/server-side-rendering/samples/async-select-value-component/main.svelte create mode 100644 packages/svelte/tests/server-side-rendering/samples/async-select-value-implicit-value-complex/_expected.html create mode 100644 packages/svelte/tests/server-side-rendering/samples/async-select-value-implicit-value-complex/main.svelte create mode 100644 packages/svelte/tests/server-side-rendering/samples/async-select-value-implicit-value/_expected.html create mode 100644 packages/svelte/tests/server-side-rendering/samples/async-select-value-implicit-value/main.svelte create mode 100644 packages/svelte/tests/server-side-rendering/samples/context/Child.svelte create mode 100644 packages/svelte/tests/server-side-rendering/samples/context/_config.js create mode 100644 packages/svelte/tests/server-side-rendering/samples/context/_expected.html create mode 100644 packages/svelte/tests/server-side-rendering/samples/context/main.svelte create mode 100644 packages/svelte/tests/snapshot/samples/async-each-fallback-hoisting/_config.js create mode 100644 packages/svelte/tests/snapshot/samples/async-each-fallback-hoisting/_expected/client/index.svelte.js create mode 100644 packages/svelte/tests/snapshot/samples/async-each-fallback-hoisting/_expected/server/index.svelte.js create mode 100644 packages/svelte/tests/snapshot/samples/async-each-fallback-hoisting/index.svelte create mode 100644 packages/svelte/tests/snapshot/samples/async-each-hoisting/_config.js create mode 100644 packages/svelte/tests/snapshot/samples/async-each-hoisting/_expected/client/index.svelte.js create mode 100644 packages/svelte/tests/snapshot/samples/async-each-hoisting/_expected/server/index.svelte.js create mode 100644 packages/svelte/tests/snapshot/samples/async-each-hoisting/index.svelte create mode 100644 packages/svelte/tests/snapshot/samples/async-if-alternate-hoisting/_config.js create mode 100644 packages/svelte/tests/snapshot/samples/async-if-alternate-hoisting/_expected/client/index.svelte.js create mode 100644 packages/svelte/tests/snapshot/samples/async-if-alternate-hoisting/_expected/server/index.svelte.js create mode 100644 packages/svelte/tests/snapshot/samples/async-if-alternate-hoisting/index.svelte create mode 100644 packages/svelte/tests/snapshot/samples/async-if-hoisting/_config.js create mode 100644 packages/svelte/tests/snapshot/samples/async-if-hoisting/_expected/client/index.svelte.js create mode 100644 packages/svelte/tests/snapshot/samples/async-if-hoisting/_expected/server/index.svelte.js create mode 100644 packages/svelte/tests/snapshot/samples/async-if-hoisting/index.svelte diff --git a/.changeset/forty-insects-cheat.md b/.changeset/forty-insects-cheat.md new file mode 100644 index 0000000000..993a8fdb24 --- /dev/null +++ b/.changeset/forty-insects-cheat.md @@ -0,0 +1,5 @@ +--- +'svelte': minor +--- + +feat: experimental async SSR 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/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; - }; + payload.child( + (payload) => payload.push(``), + 'head' + ); } /** @@ -58,10 +52,12 @@ export function reset_elements() { * @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 }; + 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]; @@ -86,11 +82,11 @@ export function push_element(payload, tag, line, column) { } } - 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); } /** @@ -100,7 +96,7 @@ export function validate_snippet_args(payload) { 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) + !(payload instanceof Payload) ) { 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..a2cf222da6 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, SSRContext } from '#server' */ /** @import { Store } from '#shared' */ +/** @import { AccumulatedContent } from './payload.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,14 @@ import { } from '../../constants.js'; import { escape_html } from '../../escaping.js'; import { DEV } from 'esm-env'; -import { current_component, pop, push } from './context.js'; +import { ssr_context, pop, push, set_ssr_context } 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 { Payload, SSRState } from './payload.js'; import { abort } from './abort-signal.js'; +import { async_mode_flag } from '../flags/index.js'; +import * as e from './errors.js'; // https://html.spec.whatwg.org/multipage/syntax.html#attributes-2 // https://infra.spec.whatwg.org/#noncharacter @@ -34,102 +36,48 @@ const INVALID_ATTR_NAME_CHAR_REGEX = * @returns {void} */ export function element(payload, tag, attributes_fn = noop, children_fn = noop) { - payload.out.push(''); + payload.push(''); if (tag) { - payload.out.push(`<${tag}`); + payload.push(`<${tag}`); attributes_fn(); - payload.out.push(`>`); + payload.push(`>`); if (!is_void(tag)) { children_fn(); if (!is_raw_text_element(tag)) { - payload.out.push(EMPTY_COMMENT); + payload.push(EMPTY_COMMENT); } - payload.out.push(``); + payload.push(``); } } - payload.out.push(''); + payload.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 Payload.render(/** @type {Component} */ (component), options); } /** * @param {Payload} payload - * @param {(head_payload: Payload['head']) => void} fn + * @param {(payload: Payload) => 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); + payload.child((payload) => { + payload.push(BLOCK_OPEN); + payload.child(fn); + payload.push(BLOCK_CLOSE); + }, 'head'); } /** @@ -144,21 +92,21 @@ export function css_props(payload, is_html, props, component, dynamic = false) { const styles = style_object_to_string(props); if (is_html) { - payload.out.push(``); + payload.push(``); } else { - payload.out.push(``); + payload.push(``); } if (dynamic) { - payload.out.push(''); + payload.push(''); } component(); if (is_html) { - payload.out.push(``); + payload.push(``); } else { - payload.out.push(``); + payload.push(``); } } @@ -451,13 +399,13 @@ export function bind_props(props_parent, props_now) { */ function await_block(payload, promise, pending_fn, then_fn) { if (is_promise(promise)) { - payload.out.push(BLOCK_OPEN); + payload.push(BLOCK_OPEN); promise.then(null, noop); if (pending_fn !== null) { pending_fn(); } } else if (then_fn !== null) { - payload.out.push(BLOCK_OPEN_ELSE); + payload.push(BLOCK_OPEN_ELSE); then_fn(promise); } } @@ -503,8 +451,8 @@ export function once(get_value) { * @returns {string} */ export function props_id(payload) { - const uid = payload.uid(); - payload.out.push(''); + const uid = payload.global.uid(); + payload.push(''); return uid; } @@ -512,12 +460,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 +477,6 @@ export { export { escape_html as escape }; -export { await_outside_boundary } from '../shared/errors.js'; - /** * @template T * @param {()=>T} fn @@ -557,29 +501,117 @@ export function derived(fn) { /** * * @param {Payload} payload - * @param {*} value + * @param {unknown} value */ export function maybe_selected(payload, value) { - return value === payload.select_value ? ' selected' : ''; + return value === payload.local.select_value ? ' selected' : ''; } /** + * When an `option` element has no `value` attribute, we need to treat the child + * content as its `value` to determine whether we should apply the `selected` attribute. + * This has to be done at runtime, for hopefully obvious reasons. It is also complicated, + * for sad reasons. * @param {Payload} payload - * @param {() => void} children + * @param {((payload: Payload) => void | Promise)} children * @returns {void} */ export function valueless_option(payload, children) { - var i = payload.out.length; + const i = payload.length; + + // prior to children, `payload` has some combination of string/unresolved payload that ends in `