chore: fix SSR context (#16781)

pull/16748/head
Rich Harris 4 weeks ago committed by GitHub
parent 41f0ff4115
commit f1ca5517db
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -1,11 +1,11 @@
/** @import { Component } from '#server' */ /** @import { SSRContext } from '#server' */
import { current_component } from './internal/server/context.js'; import { ssr_context } from './internal/server/context.js';
import { noop } from './internal/shared/utils.js'; import { noop } from './internal/shared/utils.js';
import * as e from './internal/server/errors.js'; import * as e from './internal/server/errors.js';
/** @param {() => void} fn */ /** @param {() => void} fn */
export function onDestroy(fn) { export function onDestroy(fn) {
var context = /** @type {Component} */ (current_component); var context = /** @type {SSRContext} */ (ssr_context);
(context.d ??= []).push(fn); (context.d ??= []).push(fn);
} }

@ -1,10 +1,15 @@
/** @import { Component } from '#server' */ /** @import { SSRContext } from '#server' */
import { DEV } from 'esm-env'; import { DEV } from 'esm-env';
import { async_on_destroy, on_destroy } from './index.js'; import { async_on_destroy, on_destroy } from './index.js';
import * as e from './errors.js'; import * as e from './errors.js';
/** @type {Component | null} */ /** @type {SSRContext | null} */
export var current_component = null; export var ssr_context = null;
/** @param {SSRContext | null} v */
export function set_ssr_context(v) {
ssr_context = v;
}
/** /**
* @template T * @template T
@ -47,28 +52,29 @@ export function getAllContexts() {
* @returns {Map<unknown, unknown>} * @returns {Map<unknown, unknown>}
*/ */
function get_or_init_context_map(name) { function get_or_init_context_map(name) {
if (current_component === null) { if (ssr_context === null) {
e.lifecycle_outside_component(name); 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] * @param {Function} [fn]
*/ */
export function push(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) { if (DEV) {
// component function ssr_context.function = fn;
current_component.function = fn; ssr_context.element = ssr_context.p?.element;
} }
} }
export function pop() { 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) { if (ondestroy) {
on_destroy.push(...ondestroy); on_destroy.push(...ondestroy);
@ -76,11 +82,11 @@ export function pop() {
async_on_destroy.push(...ondestroy); async_on_destroy.push(...ondestroy);
} }
current_component = component.p; ssr_context = context.p;
} }
/** /**
* @param {Component} component_context * @param {SSRContext} component_context
* @returns {Map<unknown, unknown> | null} * @returns {Map<unknown, unknown> | null}
*/ */
function get_parent_context(component_context) { function get_parent_context(component_context) {
@ -107,11 +113,11 @@ function get_parent_context(component_context) {
* @returns {Promise<() => T>} * @returns {Promise<() => T>}
*/ */
export async function save(promise) { export async function save(promise) {
var previous_component = current_component; var previous_context = ssr_context;
var value = await promise; var value = await promise;
return () => { return () => {
current_component = previous_component; ssr_context = previous_context;
return value; return value;
}; };
} }

@ -1,30 +1,29 @@
/** @import { Component } from '#server' */ /** @import { SSRContext } from '#server' */
import { FILENAME } from '../../constants.js'; import { FILENAME } from '../../constants.js';
import { import {
is_tag_valid_with_ancestor, is_tag_valid_with_ancestor,
is_tag_valid_with_parent is_tag_valid_with_parent
} from '../../html-tree-validation.js'; } 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 * as e from './errors.js';
import { Payload } from './payload.js'; import { Payload } from './payload.js';
// TODO move this
/** /**
* @typedef {{ * @typedef {{
* tag: string; * tag: string;
* parent: null | Element; * parent: undefined | Element;
* filename: null | string; * filename: undefined | string;
* line: number; * line: number;
* column: number; * column: number;
* }} Element * }} Element
*/ */
/** /**
* @type {Element | null} * This is exported so that it can be cleared between tests
* @type {Set<string>}
*/ */
let parent = null; export let seen;
/** @type {Set<string>} */
let seen;
/** /**
* @param {Payload} payload * @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 {Payload} payload
* @param {string} tag * @param {string} tag
@ -61,10 +52,12 @@ export function reset_elements() {
* @param {number} column * @param {number} column
*/ */
export function push_element(payload, tag, line, column) { export function push_element(payload, tag, line, column) {
var filename = /** @type {Component} */ (current_component).function[FILENAME]; var context = /** @type {SSRContext} */ (ssr_context);
var child = { tag, parent, filename, line, column }; 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 ancestor = parent.parent;
var ancestors = [parent.tag]; 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() { export function pop_element() {
parent = /** @type {Element} */ (parent)?.parent; set_ssr_context(ssr_context?.p ?? null);
} }
/** /**

@ -1,5 +1,5 @@
/** @import { ComponentType, SvelteComponent } from 'svelte' */ /** @import { ComponentType, SvelteComponent } from 'svelte' */
/** @import { Component, RenderOutput } from '#server' */ /** @import { RenderOutput, SSRContext } from '#server' */
/** @import { Store } from '#shared' */ /** @import { Store } from '#shared' */
/** @import { AccumulatedContent } from './payload.js' */ /** @import { AccumulatedContent } from './payload.js' */
export { FILENAME, HMR } from '../../constants.js'; export { FILENAME, HMR } from '../../constants.js';
@ -14,11 +14,10 @@ import {
} from '../../constants.js'; } from '../../constants.js';
import { escape_html } from '../../escaping.js'; import { escape_html } from '../../escaping.js';
import { DEV } from 'esm-env'; 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 { EMPTY_COMMENT, BLOCK_CLOSE, BLOCK_OPEN, BLOCK_OPEN_ELSE } from './hydration.js';
import { validate_store } from '../shared/validate.js'; import { validate_store } from '../shared/validate.js';
import { is_boolean_attribute, is_raw_text_element, is_void } from '../../utils.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 { Payload, TreeState } from './payload.js';
import { abort } from './abort-signal.js'; import { abort } from './abort-signal.js';
@ -69,6 +68,8 @@ export let on_destroy = [];
* @returns {RenderOutput} * @returns {RenderOutput}
*/ */
export function render(component, options = {}) { export function render(component, options = {}) {
var previous_context = ssr_context;
try { try {
const payload = new Payload( const payload = new Payload(
new TreeState('sync', options.idPrefix ? options.idPrefix + '-' : '') new TreeState('sync', options.idPrefix ? options.idPrefix + '-' : '')
@ -78,16 +79,9 @@ export function render(component, options = {}) {
on_destroy = []; on_destroy = [];
payload.push(BLOCK_OPEN); 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) { if (options.context) {
push(); push();
/** @type {Component} */ (current_component).c = options.context; /** @type {SSRContext} */ (ssr_context).c = options.context;
} }
// @ts-expect-error // @ts-expect-error
@ -97,10 +91,6 @@ export function render(component, options = {}) {
pop(); pop();
} }
if (reset_reset_element) {
reset_reset_element();
}
payload.push(BLOCK_CLOSE); payload.push(BLOCK_CLOSE);
for (const cleanup of on_destroy) cleanup(); for (const cleanup of on_destroy) cleanup();
on_destroy = prev_on_destroy; on_destroy = prev_on_destroy;
@ -121,6 +111,7 @@ export function render(component, options = {}) {
}; };
} finally { } finally {
abort(); abort();
set_ssr_context(previous_context);
} }
} }
@ -140,6 +131,8 @@ export let async_on_destroy = [];
* @returns {Promise<RenderOutput>} * @returns {Promise<RenderOutput>}
*/ */
export async function render_async(component, options = {}) { export async function render_async(component, options = {}) {
var previous_context = ssr_context;
try { try {
const payload = new Payload( const payload = new Payload(
new TreeState('async', options.idPrefix ? options.idPrefix + '-' : '') new TreeState('async', options.idPrefix ? options.idPrefix + '-' : '')
@ -149,16 +142,9 @@ export async function render_async(component, options = {}) {
async_on_destroy = []; async_on_destroy = [];
payload.push(BLOCK_OPEN); 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) { if (options.context) {
push(); push();
/** @type {Component} */ (current_component).c = options.context; /** @type {SSRContext} */ (ssr_context).c = options.context;
} }
// @ts-expect-error // @ts-expect-error
@ -168,10 +154,6 @@ export async function render_async(component, options = {}) {
pop(); pop();
} }
if (reset_reset_element) {
reset_reset_element();
}
payload.push(BLOCK_CLOSE); payload.push(BLOCK_CLOSE);
for (const cleanup of async_on_destroy) cleanup(); for (const cleanup of async_on_destroy) cleanup();
async_on_destroy = prev_on_destroy; async_on_destroy = prev_on_destroy;
@ -192,6 +174,7 @@ export async function render_async(component, options = {}) {
}; };
} finally { } finally {
abort(); abort();
set_ssr_context(previous_context);
} }
} }

@ -1,14 +1,16 @@
export interface Component { import type { Element } from './dev';
export interface SSRContext {
/** parent */ /** parent */
p: null | Component; p: null | SSRContext;
/** context */ /** component context */
c: null | Map<unknown, unknown>; c: null | Map<unknown, unknown>;
/** ondestroy */ /** ondestroy */
d: null | Array<() => void>; d: null | Array<() => void>;
/** /** dev mode only: the current component function */
* dev mode only: the component function
*/
function?: any; function?: any;
/** dev mode only: the current element */
element?: Element;
} }
export interface RenderOutput { export interface RenderOutput {

@ -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 { assert_html_equal_with_options } from '../html_equal.js';
import { suite_with_variants, type BaseTest } from '../suite.js'; import { suite_with_variants, type BaseTest } from '../suite.js';
import type { CompileOptions } from '#compiler'; import type { CompileOptions } from '#compiler';
import { seen } from '../../src/internal/server/dev.js';
interface SSRTest extends BaseTest { interface SSRTest extends BaseTest {
mode?: ('sync' | 'async')[]; mode?: ('sync' | 'async')[];
@ -69,6 +70,8 @@ const { test, run } = suite_with_variants<SSRTest, 'sync' | 'async', CompileOpti
const expected_html = try_read_file(`${test_dir}/_expected.html`); const expected_html = try_read_file(`${test_dir}/_expected.html`);
const is_async = variant === 'async'; const is_async = variant === 'async';
seen?.clear();
let rendered; let rendered;
try { try {
rendered = is_async rendered = is_async

Loading…
Cancel
Save