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 { 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);
}

@ -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<unknown, unknown>}
*/
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<unknown, unknown> | 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;
};
}

@ -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<string>}
*/
let parent = null;
/** @type {Set<string>} */
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);
}
/**

@ -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<RenderOutput>}
*/
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);
}
}

@ -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<unknown, unknown>;
/** 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 {

@ -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<SSRTest, 'sync' | 'async', CompileOpti
const expected_html = try_read_file(`${test_dir}/_expected.html`);
const is_async = variant === 'async';
seen?.clear();
let rendered;
try {
rendered = is_async

Loading…
Cancel
Save