From df13be8727ab16319a4f01ac39ff2ab195e953ae Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Thu, 11 Sep 2025 11:30:50 -0400 Subject: [PATCH 01/24] Version Packages (#16754) Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> --- .changeset/ninety-pandas-move.md | 5 ----- packages/svelte/CHANGELOG.md | 6 ++++++ packages/svelte/package.json | 2 +- packages/svelte/src/version.js | 2 +- 4 files changed, 8 insertions(+), 7 deletions(-) delete mode 100644 .changeset/ninety-pandas-move.md 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/packages/svelte/CHANGELOG.md b/packages/svelte/CHANGELOG.md index 4e384d5697..62f109c82f 100644 --- a/packages/svelte/CHANGELOG.md +++ b/packages/svelte/CHANGELOG.md @@ -1,5 +1,11 @@ # svelte +## 5.38.10 + +### Patch Changes + +- fix: flush effects scheduled during boundary's pending phase ([#16738](https://github.com/sveltejs/svelte/pull/16738)) + ## 5.38.9 ### Patch Changes diff --git a/packages/svelte/package.json b/packages/svelte/package.json index d445e0acf8..6c91ec6bbb 100644 --- a/packages/svelte/package.json +++ b/packages/svelte/package.json @@ -2,7 +2,7 @@ "name": "svelte", "description": "Cybernetically enhanced web apps", "license": "MIT", - "version": "5.38.9", + "version": "5.38.10", "type": "module", "types": "./types/index.d.ts", "engines": { diff --git a/packages/svelte/src/version.js b/packages/svelte/src/version.js index edb787c00c..9bfa7a5421 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.38.10'; export const PUBLIC_VERSION = '5'; From 808fbb4989d0e1687577cb4534e4d8350fb8971d Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Thu, 11 Sep 2025 11:49:14 -0400 Subject: [PATCH 02/24] chore: inline `suspend` (#16755) * chore: inline `suspend` * invert condition --- packages/svelte/src/internal/client/index.js | 2 +- .../src/internal/client/reactivity/async.js | 21 +++++++++++++++--- .../src/internal/client/reactivity/batch.js | 22 ------------------- 3 files changed, 19 insertions(+), 26 deletions(-) diff --git a/packages/svelte/src/internal/client/index.js b/packages/svelte/src/internal/client/index.js index dbff5c4599..3c5409bcfe 100644 --- a/packages/svelte/src/internal/client/index.js +++ b/packages/svelte/src/internal/client/index.js @@ -103,7 +103,7 @@ export { save, track_reactivity_loss } from './reactivity/async.js'; -export { flushSync as flush, suspend } from './reactivity/batch.js'; +export { flushSync as flush } from './reactivity/batch.js'; export { async_derived, user_derived as derived, diff --git a/packages/svelte/src/internal/client/reactivity/async.js b/packages/svelte/src/internal/client/reactivity/async.js index b7a5d5cdb7..a109a1f4d8 100644 --- a/packages/svelte/src/internal/client/reactivity/async.js +++ b/packages/svelte/src/internal/client/reactivity/async.js @@ -11,7 +11,7 @@ import { set_active_effect, set_active_reaction } from '../runtime.js'; -import { current_batch, suspend } from './batch.js'; +import { Batch, current_batch } from './batch.js'; import { async_derived, current_async_effect, @@ -178,7 +178,13 @@ export function unset_context() { * @param {() => Promise} fn */ export async function async_body(fn) { - var unsuspend = suspend(); + var boundary = get_boundary(); + var batch = /** @type {Batch} */ (current_batch); + var pending = boundary.is_pending(); + + boundary.update_pending_count(1); + if (!pending) batch.increment(); + var active = /** @type {Effect} */ (active_effect); try { @@ -188,6 +194,15 @@ export async function async_body(fn) { invoke_error_boundary(error, active); } } finally { - unsuspend(); + boundary.update_pending_count(-1); + + if (pending) { + batch.flush(); + } else { + batch.activate(); + batch.decrement(); + } + + unset_context(); } } diff --git a/packages/svelte/src/internal/client/reactivity/batch.js b/packages/svelte/src/internal/client/reactivity/batch.js index e504ae2e3f..3d234f5bba 100644 --- a/packages/svelte/src/internal/client/reactivity/batch.js +++ b/packages/svelte/src/internal/client/reactivity/batch.js @@ -653,28 +653,6 @@ export function schedule_effect(signal) { queued_root_effects.push(effect); } -export function suspend() { - var boundary = get_boundary(); - var batch = /** @type {Batch} */ (current_batch); - var pending = boundary.is_pending(); - - boundary.update_pending_count(1); - if (!pending) batch.increment(); - - return function unsuspend() { - boundary.update_pending_count(-1); - - if (!pending) { - batch.activate(); - batch.decrement(); - } else { - batch.flush(); - } - - unset_context(); - }; -} - /** * Forcibly remove all current batches, to prevent cross-talk between tests */ From a0598014d2b634566d8a62acca6b0c7601054e18 Mon Sep 17 00:00:00 2001 From: Aaron Ajose Date: Sun, 14 Sep 2025 22:09:40 +0300 Subject: [PATCH 03/24] docs: Fix some inaccuracies (#16759) * docs: Fix some inaccuracies * Apply suggestions from code review --------- Co-authored-by: Rich Harris --- CONTRIBUTING.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) 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 From 8b4e1fcb7aa321c15382652b60d430bcd0bda960 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Mon, 15 Sep 2025 10:00:45 -0400 Subject: [PATCH 04/24] chore: extract a couple of drive-by fixes from other branch (#16772) * chore: extract a couple of drive-by fixes from other branch * more --- packages/svelte/src/internal/client/dom/operations.js | 6 +++--- packages/svelte/src/internal/client/dom/template.js | 3 +++ packages/svelte/src/internal/client/reactivity/batch.js | 3 --- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/packages/svelte/src/internal/client/dom/operations.js b/packages/svelte/src/internal/client/dom/operations.js index abc29a7670..c527ca23e3 100644 --- a/packages/svelte/src/internal/client/dom/operations.js +++ b/packages/svelte/src/internal/client/dom/operations.js @@ -130,11 +130,11 @@ export function child(node, is_text) { /** * Don't mark this as side-effect-free, hydration needs to walk all nodes - * @param {DocumentFragment | TemplateNode[]} fragment - * @param {boolean} is_text + * @param {DocumentFragment | TemplateNode | TemplateNode[]} fragment + * @param {boolean} [is_text] * @returns {Node | null} */ -export function first_child(fragment, is_text) { +export function first_child(fragment, is_text = false) { if (!hydrating) { // when not hydrating, `fragment` is a `DocumentFragment` (the result of calling `open_frag`) var first = /** @type {DocumentFragment} */ (get_first_child(/** @type {Node} */ (fragment))); diff --git a/packages/svelte/src/internal/client/dom/template.js b/packages/svelte/src/internal/client/dom/template.js index 265a52262f..135ca86610 100644 --- a/packages/svelte/src/internal/client/dom/template.js +++ b/packages/svelte/src/internal/client/dom/template.js @@ -316,6 +316,9 @@ export function text(value = '') { return node; } +/** + * @returns {TemplateNode | DocumentFragment} + */ export function comment() { // we're not delegating to `template` here for performance reasons if (hydrating) { diff --git a/packages/svelte/src/internal/client/reactivity/batch.js b/packages/svelte/src/internal/client/reactivity/batch.js index 3d234f5bba..35aff7d4c9 100644 --- a/packages/svelte/src/internal/client/reactivity/batch.js +++ b/packages/svelte/src/internal/client/reactivity/batch.js @@ -10,12 +10,10 @@ import { INERT, RENDER_EFFECT, ROOT_EFFECT, - USER_EFFECT, MAYBE_DIRTY } from '#client/constants'; import { async_mode_flag } from '../../flags/index.js'; import { deferred, define_property } from '../../shared/utils.js'; -import { get_boundary } from '../dom/blocks/boundary.js'; import { active_effect, is_dirty, @@ -30,7 +28,6 @@ import { DEV } from 'esm-env'; import { invoke_error_boundary } from '../error-handling.js'; import { old_values } from './sources.js'; import { unlink_effect } from './effects.js'; -import { unset_context } from './async.js'; /** @type {Set} */ const batches = new Set(); From 8b106b94f41a87ebfff251f70d602dda70265dc9 Mon Sep 17 00:00:00 2001 From: 7nik Date: Mon, 15 Sep 2025 20:24:31 +0300 Subject: [PATCH 05/24] fix: correctly SSR hidden="until-found" (#16773) --- .changeset/pink-gifts-sell.md | 5 +++++ packages/svelte/src/internal/shared/attributes.js | 4 ++++ packages/svelte/src/utils.js | 1 - .../samples/attribute-spread-hidden-2/_expected.html | 1 + .../samples/attribute-spread-hidden-2/main.svelte | 3 +++ 5 files changed, 13 insertions(+), 1 deletion(-) create mode 100644 .changeset/pink-gifts-sell.md create mode 100644 packages/svelte/tests/runtime-runes/samples/attribute-spread-hidden-2/_expected.html create mode 100644 packages/svelte/tests/runtime-runes/samples/attribute-spread-hidden-2/main.svelte diff --git a/.changeset/pink-gifts-sell.md b/.changeset/pink-gifts-sell.md new file mode 100644 index 0000000000..f3f91ff7d9 --- /dev/null +++ b/.changeset/pink-gifts-sell.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: correctly SSR hidden="until-found" diff --git a/packages/svelte/src/internal/shared/attributes.js b/packages/svelte/src/internal/shared/attributes.js index c8758c1d4d..a96e71ff6f 100644 --- a/packages/svelte/src/internal/shared/attributes.js +++ b/packages/svelte/src/internal/shared/attributes.js @@ -23,6 +23,10 @@ const replacements = { */ export function attr(name, value, is_boolean = false) { if (value == null || (!value && is_boolean)) return ''; + // attribute hidden for values other than "until-found" behaves like a boolean attribute + if (name === 'hidden' && value !== 'until-found') { + is_boolean = true; + } const normalized = (name in replacements && replacements[name].get(value)) || value; const assignment = is_boolean ? '' : `="${escape_html(normalized, true)}"`; return ` ${name}${assignment}`; 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/tests/runtime-runes/samples/attribute-spread-hidden-2/_expected.html b/packages/svelte/tests/runtime-runes/samples/attribute-spread-hidden-2/_expected.html new file mode 100644 index 0000000000..80937efaee --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/attribute-spread-hidden-2/_expected.html @@ -0,0 +1 @@ +
A
diff --git a/packages/svelte/tests/runtime-runes/samples/attribute-spread-hidden-2/main.svelte b/packages/svelte/tests/runtime-runes/samples/attribute-spread-hidden-2/main.svelte new file mode 100644 index 0000000000..6738b34054 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/attribute-spread-hidden-2/main.svelte @@ -0,0 +1,3 @@ +
A
+
B
+
C
\ No newline at end of file From 8c982f61013a517bec2edf68a82ce1a1188fcd8f Mon Sep 17 00:00:00 2001 From: 7nik Date: Tue, 16 Sep 2025 11:45:18 +0300 Subject: [PATCH 06/24] fix: correct wrong fix, get test to actually do something (#16779) #16773 added a test with _expected.html to a wrong suit, so it was ignored, and this allowed to slip in a new bug of printing the falsy hidden attribute as hidden (shortened enabled form). That fix wasn't released yet, so no changeset. --- packages/svelte/src/internal/shared/attributes.js | 2 +- .../samples/attribute-spread-hidden}/_expected.html | 0 .../samples/attribute-spread-hidden}/main.svelte | 0 3 files changed, 1 insertion(+), 1 deletion(-) rename packages/svelte/tests/{runtime-runes/samples/attribute-spread-hidden-2 => server-side-rendering/samples/attribute-spread-hidden}/_expected.html (100%) rename packages/svelte/tests/{runtime-runes/samples/attribute-spread-hidden-2 => server-side-rendering/samples/attribute-spread-hidden}/main.svelte (100%) diff --git a/packages/svelte/src/internal/shared/attributes.js b/packages/svelte/src/internal/shared/attributes.js index a96e71ff6f..4ad550e8d6 100644 --- a/packages/svelte/src/internal/shared/attributes.js +++ b/packages/svelte/src/internal/shared/attributes.js @@ -22,11 +22,11 @@ const replacements = { * @returns {string} */ export function attr(name, value, is_boolean = false) { - if (value == null || (!value && is_boolean)) return ''; // 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)}"`; return ` ${name}${assignment}`; diff --git a/packages/svelte/tests/runtime-runes/samples/attribute-spread-hidden-2/_expected.html b/packages/svelte/tests/server-side-rendering/samples/attribute-spread-hidden/_expected.html similarity index 100% rename from packages/svelte/tests/runtime-runes/samples/attribute-spread-hidden-2/_expected.html rename to packages/svelte/tests/server-side-rendering/samples/attribute-spread-hidden/_expected.html diff --git a/packages/svelte/tests/runtime-runes/samples/attribute-spread-hidden-2/main.svelte b/packages/svelte/tests/server-side-rendering/samples/attribute-spread-hidden/main.svelte similarity index 100% rename from packages/svelte/tests/runtime-runes/samples/attribute-spread-hidden-2/main.svelte rename to packages/svelte/tests/server-side-rendering/samples/attribute-spread-hidden/main.svelte From b8fd326d96c3a57feff3a4fafcd2d1651001b109 Mon Sep 17 00:00:00 2001 From: Elliott Johnson Date: Wed, 17 Sep 2025 15:49:57 -0600 Subject: [PATCH 07/24] 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 `