From 3e898f15f05b3b82fec479559488f60ce5e23ce1 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Tue, 16 Sep 2025 09:45:14 -0400 Subject: [PATCH] chore: fix SSR context --- packages/svelte/src/index-server.js | 6 +-- .../svelte/src/internal/server/context.js | 34 ++++++++++------- packages/svelte/src/internal/server/dev.js | 37 ++++++++----------- packages/svelte/src/internal/server/index.js | 37 +++++-------------- .../svelte/src/internal/server/types.d.ts | 14 ++++--- .../tests/server-side-rendering/test.ts | 3 ++ 6 files changed, 59 insertions(+), 72 deletions(-) diff --git a/packages/svelte/src/index-server.js b/packages/svelte/src/index-server.js index 1342e502d79d..019356a272fb 100644 --- a/packages/svelte/src/index-server.js +++ b/packages/svelte/src/index-server.js @@ -1,11 +1,11 @@ -/** @import { Component } from '#server' */ -import { current_component } from './internal/server/context.js'; +/** @import { SSRContext } from '#server' */ +import { ssr_context } from './internal/server/context.js'; import { noop } from './internal/shared/utils.js'; import * as e from './internal/server/errors.js'; /** @param {() => void} fn */ export function onDestroy(fn) { - var context = /** @type {Component} */ (current_component); + var context = /** @type {SSRContext} */ (ssr_context); (context.d ??= []).push(fn); } diff --git a/packages/svelte/src/internal/server/context.js b/packages/svelte/src/internal/server/context.js index 147d8861298a..ca5d7b648f55 100644 --- a/packages/svelte/src/internal/server/context.js +++ b/packages/svelte/src/internal/server/context.js @@ -1,10 +1,15 @@ -/** @import { Component } from '#server' */ +/** @import { SSRContext } from '#server' */ import { DEV } from 'esm-env'; import { async_on_destroy, on_destroy } from './index.js'; import * as e from './errors.js'; -/** @type {Component | null} */ -export var current_component = null; +/** @type {SSRContext | null} */ +export var ssr_context = null; + +/** @param {SSRContext | null} v */ +export function set_ssr_context(v) { + ssr_context = v; +} /** * @template T @@ -47,28 +52,29 @@ export function getAllContexts() { * @returns {Map} */ function get_or_init_context_map(name) { - if (current_component === null) { + if (ssr_context === null) { e.lifecycle_outside_component(name); } - return (current_component.c ??= new Map(get_parent_context(current_component) || undefined)); + return (ssr_context.c ??= new Map(get_parent_context(ssr_context) || undefined)); } /** * @param {Function} [fn] */ export function push(fn) { - current_component = { p: current_component, c: null, d: null }; + ssr_context = { p: ssr_context, c: null, d: null }; + if (DEV) { - // component function - current_component.function = fn; + ssr_context.function = fn; + ssr_context.element = ssr_context.p?.element; } } export function pop() { - var component = /** @type {Component} */ (current_component); + var context = /** @type {SSRContext} */ (ssr_context); - var ondestroy = component.d; + var ondestroy = context.d; if (ondestroy) { on_destroy.push(...ondestroy); @@ -76,11 +82,11 @@ export function pop() { async_on_destroy.push(...ondestroy); } - current_component = component.p; + ssr_context = context.p; } /** - * @param {Component} component_context + * @param {SSRContext} component_context * @returns {Map | null} */ function get_parent_context(component_context) { @@ -107,11 +113,11 @@ function get_parent_context(component_context) { * @returns {Promise<() => T>} */ export async function save(promise) { - var previous_component = current_component; + var previous_context = ssr_context; var value = await promise; return () => { - current_component = previous_component; + ssr_context = previous_context; return value; }; } diff --git a/packages/svelte/src/internal/server/dev.js b/packages/svelte/src/internal/server/dev.js index 2d48587d5258..d91a0c34b3ad 100644 --- a/packages/svelte/src/internal/server/dev.js +++ b/packages/svelte/src/internal/server/dev.js @@ -1,30 +1,29 @@ -/** @import { Component } from '#server' */ +/** @import { SSRContext } from '#server' */ import { FILENAME } from '../../constants.js'; import { is_tag_valid_with_ancestor, is_tag_valid_with_parent } from '../../html-tree-validation.js'; -import { current_component } from './context.js'; +import { set_ssr_context, ssr_context } from './context.js'; import * as e from './errors.js'; import { Payload } from './payload.js'; +// TODO move this /** * @typedef {{ * tag: string; - * parent: null | Element; - * filename: null | string; + * parent: undefined | Element; + * filename: undefined | string; * line: number; * column: number; * }} Element */ /** - * @type {Element | null} + * This is exported so that it can be cleared between tests + * @type {Set} */ -let parent = null; - -/** @type {Set} */ -let seen; +export let seen; /** * @param {Payload} payload @@ -46,14 +45,6 @@ function print_error(payload, message) { ); } -export function reset_elements() { - let old_parent = parent; - parent = null; - return () => { - parent = old_parent; - }; -} - /** * @param {Payload} payload * @param {string} tag @@ -61,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]; @@ -89,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(ssr_context?.p ?? null); } /** diff --git a/packages/svelte/src/internal/server/index.js b/packages/svelte/src/internal/server/index.js index 8bd085747f97..e8c62116832e 100644 --- a/packages/svelte/src/internal/server/index.js +++ b/packages/svelte/src/internal/server/index.js @@ -1,5 +1,5 @@ /** @import { ComponentType, SvelteComponent } from 'svelte' */ -/** @import { Component, RenderOutput } from '#server' */ +/** @import { RenderOutput, SSRContext } from '#server' */ /** @import { Store } from '#shared' */ /** @import { AccumulatedContent } from './payload.js' */ export { FILENAME, HMR } from '../../constants.js'; @@ -14,11 +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 { 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, TreeState } from './payload.js'; import { abort } from './abort-signal.js'; @@ -69,6 +68,8 @@ export let on_destroy = []; * @returns {RenderOutput} */ export function render(component, options = {}) { + var previous_context = ssr_context; + try { const payload = new Payload( new TreeState('sync', options.idPrefix ? options.idPrefix + '-' : '') @@ -78,16 +79,9 @@ export function render(component, options = {}) { on_destroy = []; payload.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; + /** @type {SSRContext} */ (ssr_context).c = options.context; } // @ts-expect-error @@ -97,10 +91,6 @@ export function render(component, options = {}) { pop(); } - if (reset_reset_element) { - reset_reset_element(); - } - payload.push(BLOCK_CLOSE); for (const cleanup of on_destroy) cleanup(); on_destroy = prev_on_destroy; @@ -121,6 +111,7 @@ export function render(component, options = {}) { }; } finally { abort(); + set_ssr_context(previous_context); } } @@ -140,6 +131,8 @@ export let async_on_destroy = []; * @returns {Promise} */ export async function render_async(component, options = {}) { + var previous_context = ssr_context; + try { const payload = new Payload( new TreeState('async', options.idPrefix ? options.idPrefix + '-' : '') @@ -149,16 +142,9 @@ export async function render_async(component, options = {}) { async_on_destroy = []; payload.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; + /** @type {SSRContext} */ (ssr_context).c = options.context; } // @ts-expect-error @@ -168,10 +154,6 @@ export async function render_async(component, options = {}) { pop(); } - if (reset_reset_element) { - reset_reset_element(); - } - payload.push(BLOCK_CLOSE); for (const cleanup of async_on_destroy) cleanup(); async_on_destroy = prev_on_destroy; @@ -192,6 +174,7 @@ export async function render_async(component, options = {}) { }; } finally { abort(); + set_ssr_context(previous_context); } } diff --git a/packages/svelte/src/internal/server/types.d.ts b/packages/svelte/src/internal/server/types.d.ts index 6b0fc146c419..cbf2c45fba0c 100644 --- a/packages/svelte/src/internal/server/types.d.ts +++ b/packages/svelte/src/internal/server/types.d.ts @@ -1,14 +1,16 @@ -export interface Component { +import type { Element } from './dev'; + +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 - */ + /** dev mode only: the current component function */ function?: any; + /** dev mode only: the current element */ + element?: Element; } export interface RenderOutput { diff --git a/packages/svelte/tests/server-side-rendering/test.ts b/packages/svelte/tests/server-side-rendering/test.ts index 6a9a810f5dc3..3aa3d3a8d89a 100644 --- a/packages/svelte/tests/server-side-rendering/test.ts +++ b/packages/svelte/tests/server-side-rendering/test.ts @@ -11,6 +11,7 @@ import { compile_directory, should_update_expected, try_read_file } from '../hel import { assert_html_equal_with_options } from '../html_equal.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')[]; @@ -69,6 +70,8 @@ const { test, run } = suite_with_variants