Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions packages/svelte/src/index-server.js
Original file line number Diff line number Diff line change
@@ -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);
}

Expand Down
34 changes: 20 additions & 14 deletions packages/svelte/src/internal/server/context.js
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -47,40 +52,41 @@ 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);
// TODO this is probably actually broken
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) {
Expand All @@ -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;
};
}
37 changes: 15 additions & 22 deletions packages/svelte/src/internal/server/dev.js
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -46,25 +45,19 @@ 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
* @param {number} line
* @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];

Expand All @@ -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);
}

/**
Expand Down
37 changes: 10 additions & 27 deletions packages/svelte/src/internal/server/index.js
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -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';

Expand Down Expand Up @@ -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 + '-' : '')
Expand All @@ -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
Expand All @@ -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;
Expand All @@ -121,6 +111,7 @@ export function render(component, options = {}) {
};
} finally {
abort();
set_ssr_context(previous_context);
}
}

Expand All @@ -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 + '-' : '')
Expand All @@ -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
Expand All @@ -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;
Expand All @@ -192,6 +174,7 @@ export async function render_async(component, options = {}) {
};
} finally {
abort();
set_ssr_context(previous_context);
}
}

Expand Down
14 changes: 8 additions & 6 deletions packages/svelte/src/internal/server/types.d.ts
Original file line number Diff line number Diff line change
@@ -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 {
Expand Down
3 changes: 3 additions & 0 deletions packages/svelte/tests/server-side-rendering/test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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')[];
Expand Down Expand Up @@ -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
Expand Down
Loading