lots of progress, couple of failing tests around selects

adjust-boundary-error-message
S. Elliott Johnson 1 week ago
parent df67afefee
commit e39b4b7546

@ -57,6 +57,7 @@ jobs:
- run: pnpm install --frozen-lockfile - run: pnpm install --frozen-lockfile
- run: pnpm playwright install chromium - run: pnpm playwright install chromium
- run: pnpm test runtime-runes - run: pnpm test runtime-runes
- run: pnpm test server-side-rendering
env: env:
CI: true CI: true
SVELTE_NO_ASYNC: true SVELTE_NO_ASYNC: true

@ -240,7 +240,7 @@ export function server_component(analysis, options) {
b.call( b.call(
'$$payload.child', '$$payload.child',
b.arrow( b.arrow(
[b.object_pattern([b.init('$$payload', b.id('$$payload'))])], [b.id('$$payload')],
b.block([ b.block([
.../** @type {Statement[]} */ (instance.body), .../** @type {Statement[]} */ (instance.body),
.../** @type {Statement[]} */ (template.body) .../** @type {Statement[]} */ (template.body)
@ -304,7 +304,7 @@ export function server_component(analysis, options) {
const code = b.literal(render_stylesheet(analysis.source, analysis, options).code); const code = b.literal(render_stylesheet(analysis.source, analysis, options).code);
body.push(b.const('$$css', b.object([b.init('hash', hash), b.init('code', code)]))); body.push(b.const('$$css', b.object([b.init('hash', hash), b.init('code', code)])));
component_block.body.unshift(b.stmt(b.call('$$payload.css.add', b.id('$$css')))); component_block.body.unshift(b.stmt(b.call('$$payload.global.css.add', b.id('$$css'))));
} }
let should_inject_props = let should_inject_props =

@ -44,12 +44,12 @@ export function EachBlock(node, context) {
); );
if (node.fallback) { if (node.fallback) {
const open = b.stmt(b.call(b.member(b.id('$$payload.out'), b.id('push')), block_open)); const open = b.stmt(b.call(b.member(b.id('$$payload'), b.id('push')), block_open));
const fallback = /** @type {BlockStatement} */ (context.visit(node.fallback)); const fallback = /** @type {BlockStatement} */ (context.visit(node.fallback));
fallback.body.unshift( fallback.body.unshift(
b.stmt(b.call(b.member(b.id('$$payload.out'), b.id('push')), b.literal(BLOCK_OPEN_ELSE))) b.stmt(b.call(b.member(b.id('$$payload'), b.id('push')), b.literal(BLOCK_OPEN_ELSE)))
); );
state.template.push( state.template.push(

@ -17,12 +17,10 @@ export function IfBlock(node, context) {
? /** @type {BlockStatement} */ (context.visit(node.alternate)) ? /** @type {BlockStatement} */ (context.visit(node.alternate))
: b.block([]); : b.block([]);
consequent.body.unshift( consequent.body.unshift(b.stmt(b.call(b.member(b.id('$$payload'), b.id('push')), block_open)));
b.stmt(b.call(b.member(b.id('$$payload.out'), b.id('push')), block_open))
);
alternate.body.unshift( alternate.body.unshift(
b.stmt(b.call(b.member(b.id('$$payload.out'), b.id('push')), b.literal(BLOCK_OPEN_ELSE))) b.stmt(b.call(b.member(b.id('$$payload'), b.id('push')), b.literal(BLOCK_OPEN_ELSE)))
); );
context.state.template.push(b.if(test, consequent, alternate), block_close); context.state.template.push(b.if(test, consequent, alternate), block_close);

@ -92,7 +92,7 @@ export function RegularElement(node, context) {
b.stmt( b.stmt(
b.assignment( b.assignment(
'=', '=',
b.id('$$payload.select_value'), b.id('$$payload.local.select_value'),
b.member( b.member(
build_spread_object( build_spread_object(
node, node,
@ -113,7 +113,7 @@ export function RegularElement(node, context) {
); );
} else if (value) { } else if (value) {
select_with_value = true; select_with_value = true;
const left = b.id('$$payload.select_value'); const left = b.id('$$payload.local.select_value');
if (value.type === 'Attribute') { if (value.type === 'Attribute') {
state.template.push( state.template.push(
b.stmt(b.assignment('=', left, build_attribute_value(value.value, context))) b.stmt(b.assignment('=', left, build_attribute_value(value.value, context)))
@ -151,7 +151,11 @@ export function RegularElement(node, context) {
b.call( b.call(
'$.valueless_option', '$.valueless_option',
b.id('$$payload'), b.id('$$payload'),
b.thunk(b.block([...inner_state.init, ...build_template(inner_state.template)])) b.arrow(
[b.id('$$payload')],
b.block([...inner_state.init, ...build_template(inner_state.template)]),
context.state.analysis.has_blocking_await
)
) )
) )
); );

@ -17,7 +17,7 @@ export function SnippetBlock(node, context) {
b.call( b.call(
'$$payload.child', '$$payload.child',
b.arrow( b.arrow(
[b.object_pattern([b.init('$$payload', b.id('$$payload'))])], [b.id('$$payload')],
/** @type {BlockStatement} */ (context.visit(node.body)), /** @type {BlockStatement} */ (context.visit(node.body)),
node.metadata.has_await node.metadata.has_await
) )

@ -11,6 +11,14 @@ export function SvelteHead(node, context) {
const block = /** @type {BlockStatement} */ (context.visit(node.fragment)); const block = /** @type {BlockStatement} */ (context.visit(node.fragment));
context.state.template.push( context.state.template.push(
b.stmt(b.call('$.head', b.id('$$payload'), b.arrow([b.id('$$payload')], block))) b.stmt(
b.call(
'$.head',
b.id('$$payload'),
// same thing as elsewhere; this will create more async functions than necessary but should never be _wrong_
// because the component rendering this head block will always be async if the head block is async
b.arrow([b.id('$$payload')], block, context.state.analysis.has_blocking_await)
)
)
); );
} }

@ -13,20 +13,30 @@ export function TitleElement(node, context) {
process_children(node.fragment.nodes, { ...context, state: { ...context.state, template } }); process_children(node.fragment.nodes, { ...context, state: { ...context.state, template } });
template.push(b.literal('</title>')); template.push(b.literal('</title>'));
if (!node.metadata.has_await) { context.state.init.push(
context.state.init.push(...build_template(template, b.id('$$payload.title.value'), '=')); b.stmt(
} else { b.call(
const async_template = b.thunk( '$$payload.child',
// TODO I'm sure there is a better way to do this // this nonsense is necessary so that the write to the title is as tightly scoped to a specific location
// in the async tree as possible. This lets us use `get_path` to compare this assignment to other assignments
// so that we can overwrite earlier assignments with later ones.
b.arrow(
[b.id('$$payload')],
b.block([ b.block([
b.const('path', b.call('$$payload.get_path')),
b.let('title'), b.let('title'),
...build_template(template, b.id('title'), '='), ...build_template(template, b.id('title'), '='),
b.return(b.id('title')) b.stmt(
b.assignment(
'=',
b.id('$$payload.global.head.title'),
b.object([b.init('path', b.id('path')), b.init('value', b.id('title'))])
)
)
]), ]),
true node.metadata.has_await
); )
context.state.init.push( )
b.stmt(b.assignment('=', b.id('$$payload.title.value'), b.call(async_template))) )
); );
}
} }

@ -238,7 +238,7 @@ export function build_inline_component(node, expression, context) {
b.call( b.call(
'$$payload.child', '$$payload.child',
b.arrow( b.arrow(
[b.object_pattern([b.init('$$payload', b.id('$$payload'))])], [b.id('$$payload')],
b.block(block.body), b.block(block.body),
context.state.analysis.has_blocking_await context.state.analysis.has_blocking_await
) )

@ -99,7 +99,7 @@ function is_statement(node) {
* @param {AssignmentOperator | 'push'} operator * @param {AssignmentOperator | 'push'} operator
* @returns {Statement[]} * @returns {Statement[]}
*/ */
export function build_template(template, out = b.id('$$payload.out'), operator = 'push') { export function build_template(template, out = b.id('$$payload'), operator = 'push') {
/** @type {string[]} */ /** @type {string[]} */
let strings = []; let strings = [];
@ -264,10 +264,5 @@ export function build_getter(node, state) {
* @returns {Statement} * @returns {Statement}
*/ */
export function wrap_in_child_payload(body, async) { export function wrap_in_child_payload(body, async) {
return b.stmt( return b.stmt(b.call('$$payload.child', b.arrow([b.id('$$payload')], body, async)));
b.call(
'$$payload.child',
b.arrow([b.object_pattern([b.init('$$payload', b.id('$$payload'))])], body, async)
)
);
} }

@ -15,7 +15,7 @@ export function createRawSnippet(fn) {
// @ts-expect-error the types are a lie // @ts-expect-error the types are a lie
return (/** @type {Payload} */ payload, /** @type {Params} */ ...args) => { return (/** @type {Payload} */ payload, /** @type {Params} */ ...args) => {
var getters = /** @type {Getters<Params>} */ (args.map((value) => () => value)); var getters = /** @type {Getters<Params>} */ (args.map((value) => () => value));
payload.out.push( payload.push(
fn(...getters) fn(...getters)
.render() .render()
.trim() .trim()

@ -6,7 +6,7 @@ import {
} from '../../html-tree-validation.js'; } from '../../html-tree-validation.js';
import { current_component } from './context.js'; import { current_component } from './context.js';
import * as e from './errors.js'; import * as e from './errors.js';
import { HeadPayload, Payload } from './payload.js'; import { Payload } from './payload.js';
/** /**
* @typedef {{ * @typedef {{
@ -40,7 +40,10 @@ function print_error(payload, message) {
// eslint-disable-next-line no-console // eslint-disable-next-line no-console
console.error(message); console.error(message);
payload.head.out.push(`<script>console.error(${JSON.stringify(message)})</script>`); payload.out.push({
type: 'head',
content: `<script>console.error(${JSON.stringify(message)})</script>`
});
} }
export function reset_elements() { export function reset_elements() {
@ -100,7 +103,7 @@ export function validate_snippet_args(payload) {
if ( if (
typeof payload !== 'object' || typeof payload !== 'object' ||
// for some reason typescript consider the type of payload as never after the first instanceof // 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(); e.invalid_snippet_arguments();
} }

@ -17,7 +17,7 @@ import { EMPTY_COMMENT, BLOCK_CLOSE, BLOCK_OPEN, BLOCK_OPEN_ELSE } from './hydra
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 { reset_elements } from './dev.js';
import { Payload } from './payload.js'; import { Payload, TreeState } from './payload.js';
import { abort } from './abort-signal.js'; import { abort } from './abort-signal.js';
// https://html.spec.whatwg.org/multipage/syntax.html#attributes-2 // https://html.spec.whatwg.org/multipage/syntax.html#attributes-2
@ -33,23 +33,23 @@ const INVALID_ATTR_NAME_CHAR_REGEX =
* @returns {void} * @returns {void}
*/ */
export function element(payload, tag, attributes_fn = noop, children_fn = noop) { export function element(payload, tag, attributes_fn = noop, children_fn = noop) {
payload.out.push('<!---->'); payload.push('<!---->');
if (tag) { if (tag) {
payload.out.push(`<${tag}`); payload.push(`<${tag}`);
attributes_fn(); attributes_fn();
payload.out.push(`>`); payload.push(`>`);
if (!is_void(tag)) { if (!is_void(tag)) {
children_fn(); children_fn();
if (!is_raw_text_element(tag)) { if (!is_raw_text_element(tag)) {
payload.out.push(EMPTY_COMMENT); payload.push(EMPTY_COMMENT);
} }
payload.out.push(`</${tag}>`); payload.push(`</${tag}>`);
} }
} }
payload.out.push('<!---->'); payload.push('<!---->');
} }
/** /**
@ -68,11 +68,11 @@ export let on_destroy = [];
*/ */
export function render(component, options = {}) { export function render(component, options = {}) {
try { try {
const payload = new Payload({ id_prefix: options.idPrefix ? options.idPrefix + '-' : '' }); const payload = new Payload(new TreeState(options.idPrefix ? options.idPrefix + '-' : ''));
const prev_on_destroy = on_destroy; const prev_on_destroy = on_destroy;
on_destroy = []; on_destroy = [];
payload.out.push(BLOCK_OPEN); payload.push(BLOCK_OPEN);
let reset_reset_element; let reset_reset_element;
@ -97,24 +97,83 @@ export function render(component, options = {}) {
reset_reset_element(); reset_reset_element();
} }
payload.out.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;
let head = payload.head.collect(); let { head, body } = payload.collect();
head += payload.global.head.title.value;
if (typeof payload.head.title.value !== 'string') { for (const { hash, code } of payload.global.css) {
throw new Error( head += `<style id="${hash}">${code}</style>`;
'TODO -- should encorporate this into the collect/collect_async logic somewhere'
);
} }
head += payload.head.title.value;
for (const { hash, code } of payload.css) { return {
head += `<style id="${hash}">${code}</style>`; head,
html: body,
body: body
};
} finally {
abort();
} }
}
const body = payload.collect(); /**
* TODO THIS NEEDS TO ACTUALLY BE DONE
* Array of `onDestroy` callbacks that should be called at the end of the server render function
* @type {Function[]}
*/
export let async_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<string, any>} Props
* @param {import('svelte').Component<Props> | ComponentType<SvelteComponent<Props>>} component
* @param {{ props?: Omit<Props, '$$slots' | '$$events'>; context?: Map<any, any>; idPrefix?: string }} [options]
* @returns {Promise<RenderOutput>}
*/
export async function render_async(component, options = {}) {
try {
const payload = new Payload(new TreeState(options.idPrefix ? options.idPrefix + '-' : ''));
const prev_on_destroy = async_on_destroy;
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;
}
// @ts-expect-error
component(payload, options.props ?? {}, {}, {});
if (options.context) {
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;
let { head, body } = await payload.collect_async();
head += payload.global.head.title.value;
for (const { hash, code } of payload.global.css) {
head += `<style id="${hash}">${code}</style>`;
}
return { return {
head, head,
@ -128,13 +187,13 @@ export function render(component, options = {}) {
/** /**
* @param {Payload} payload * @param {Payload} payload
* @param {(head_payload: Payload['head']) => void} fn * @param {(payload: Payload) => Promise<void> | void} fn
* @returns {void} * @returns {void}
*/ */
export function head(payload, fn) { export function head(payload, fn) {
payload.head.out.push(BLOCK_OPEN); payload.out.push({ type: 'head', content: BLOCK_OPEN });
payload.head.child(({ $$payload }) => fn($$payload)); payload.child(fn, 'head');
payload.head.out.push(BLOCK_CLOSE); payload.out.push({ type: 'head', content: BLOCK_CLOSE });
} }
/** /**
@ -149,21 +208,21 @@ export function css_props(payload, is_html, props, component, dynamic = false) {
const styles = style_object_to_string(props); const styles = style_object_to_string(props);
if (is_html) { if (is_html) {
payload.out.push(`<svelte-css-wrapper style="display: contents; ${styles}">`); payload.push(`<svelte-css-wrapper style="display: contents; ${styles}">`);
} else { } else {
payload.out.push(`<g style="${styles}">`); payload.push(`<g style="${styles}">`);
} }
if (dynamic) { if (dynamic) {
payload.out.push('<!---->'); payload.push('<!---->');
} }
component(); component();
if (is_html) { if (is_html) {
payload.out.push(`<!----></svelte-css-wrapper>`); payload.push(`<!----></svelte-css-wrapper>`);
} else { } else {
payload.out.push(`<!----></g>`); payload.push(`<!----></g>`);
} }
} }
@ -448,13 +507,13 @@ export function bind_props(props_parent, props_now) {
*/ */
function await_block(payload, promise, pending_fn, then_fn) { function await_block(payload, promise, pending_fn, then_fn) {
if (is_promise(promise)) { if (is_promise(promise)) {
payload.out.push(BLOCK_OPEN); payload.push(BLOCK_OPEN);
promise.then(null, noop); promise.then(null, noop);
if (pending_fn !== null) { if (pending_fn !== null) {
pending_fn(); pending_fn();
} }
} else if (then_fn !== null) { } else if (then_fn !== null) {
payload.out.push(BLOCK_OPEN_ELSE); payload.push(BLOCK_OPEN_ELSE);
then_fn(promise); then_fn(promise);
} }
} }
@ -500,8 +559,8 @@ export function once(get_value) {
* @returns {string} * @returns {string}
*/ */
export function props_id(payload) { export function props_id(payload) {
const uid = payload.uid(); const uid = payload.global.uid();
payload.out.push('<!--#' + uid + '-->'); payload.push('<!--#' + uid + '-->');
return uid; return uid;
} }
@ -555,37 +614,37 @@ export function derived(fn) {
* @param {*} value * @param {*} value
*/ */
export function maybe_selected(payload, value) { export function maybe_selected(payload, value) {
return value === payload.select_value ? ' selected' : ''; return value === payload.local.select_value ? ' selected' : '';
} }
/** /**
* @param {Payload} payload * @param {Payload} payload
* @param {() => void} children * @param {(payload: Payload) => void | Promise<void>} children
* @returns {void} * @returns {void}
*/ */
export function valueless_option(payload, children) { export function valueless_option(payload, children) {
var i = payload.out.length; var i = payload.out.length;
// prior to children, `payload` has some combination of string/unresolved payload that ends in `<option ...>` // prior to children, `payload` has some combination of string/unresolved payload that ends in `<option ...>`
children(); payload.child((payload) => children(payload));
// post-children, `payload` has child content, possibly also with some number of hydration comments. // post-children, `payload` has child content, possibly also with some number of hydration comments.
// we can compact this last chunk of content to see if it matches the select value... // we can compact this last chunk of content to see if it matches the select value...
payload.compact({ payload.compact({
start: i, start: i,
fn: (body) => { fn: (content) => {
if (body.replace(/<!---->/g, '') === payload.select_value) { if (content.body.replace(/<!---->/g, '') === payload.local.select_value) {
// ...and if it does match the select value, we can compact the part of the payload representing the `<option ...>` // ...and if it does match the select value, we can compact the part of the payload representing the `<option ...>`
// to add the `selected` attribute to the end. // to add the `selected` attribute to the end.
payload.compact({ payload.compact({
start: i - 1, start: i - 1,
end: i, end: i,
fn: (body) => { fn: (content) => {
return body.slice(0, -1) + ' selected>'; return { body: content.body.slice(0, -1) + ' selected>', head: content.head };
} }
}); });
} }
return body; return content;
} }
}); });
} }

@ -1,26 +1,31 @@
// Optimization: Right now, the state from parents is copied into the children. _Technically_ we could save the state on the root /** @typedef {{ type: 'head' | 'body', content: string }} TNode */
// and simply have the children inherit that state and re-expose it through getters. This could save memory but probably isn't worth it. /** @typedef {{ [key in TNode['type']]: string }} AccumulatedContent */
/** @typedef {{ start: number, end: number, fn: (content: AccumulatedContent) => AccumulatedContent | Promise<AccumulatedContent> }} Compaction */
// TODO we test for `instanceof AsyncContentTree` in some tight loops -- we might optimize
// by giving the tree a symbol property and checking that instead if we can actually notice any impact
/** /**
* /**
* A base class for payloads. Payloads are basically a tree of `string | Payload`s, where each * A base class for payloads. Payloads are basically a tree of `string | Payload`s, where each
* `Payload` in the tree represents work that may or may not have completed. A payload can be * `Payload` in the tree represents work that may or may not have completed. A payload can be
* {@link collect}ed to aggregate the content from itself and all of its children, but this will * {@link collect}ed to aggregate the content from itself and all of its children, but this will
* throw if any of the children are performing asynchronous work. A payload can also be collected * throw if any of the children are performing asynchronous work. A payload can also be collected
* asynchronously with {@link collect_async}, which will wait for all children to complete before * asynchronously with {@link collect_async}, which will wait for all children to complete before
* collecting their contents. * collecting their contents.
*
* @template {new (parent: Partial<InstanceType<TSubclass>>) => {}} TSubclass
*/ */
class BasePayload { export class Payload {
/** /**
* This is the magical type that represents the instance type of a subclass of this type. * @type {TNode['type']}
* How does it work? idk man but it does
* @typedef {this & InstanceType<TSubclass>} Instance
*/ */
type;
/** @type {Payload | undefined} */
parent;
/** /**
* The contents of the payload. * The contents of the payload.
* @type {(string | Instance)[]} * @type {(TNode | Payload)[]}
*/ */
out = []; out = [];
@ -31,88 +36,158 @@ class BasePayload {
*/ */
promise; promise;
/**
* State which is associated with the content tree as a whole.
* It will be re-exposed, uncopied, on all children.
* @type {TreeState}
* @readonly
*/
global;
/**
* State that is local to the branch it is declared in.
* It will be shallow-copied to all children.
* @type {{ select_value: string | undefined }}
*/
local;
/**
* @param {TreeState} [global]
* @param {{ select_value: string | undefined }} [local]
* @param {Payload | undefined} [parent]
* @param {TNode['type']} [type]
*/
constructor(global = new TreeState(), local = { select_value: undefined }, parent, type) {
this.global = global;
this.local = { ...local };
this.parent = parent;
this.type = type ?? parent?.type ?? 'body';
}
/** /**
* Create a child payload. The child payload inherits the state from the parent, * Create a child payload. The child payload inherits the state from the parent,
* but has its own `out` array and `promise` property. The child payload is automatically * but has its own `out` array and `promise` property. The child payload is automatically
* inserted into the parent payload's `out` array. * inserted into the parent payload's `out` array.
* @param {(args: { $$payload: Instance }) => void | Promise<void>} render * @param {(tree: Payload) => void | Promise<void>} render
* @param {TNode['type']} [type]
* @returns {void} * @returns {void}
*/ */
child(render) { child(render, type) {
const child = this.#create_child_instance(); const child = new Payload(this.global, this.local, this, type);
this.out.push(child); this.out.push(child);
const result = render({ $$payload: child }); const result = render(child);
if (result instanceof Promise) { if (result instanceof Promise) {
child.promise = result; child.promise = result;
} }
} }
/**
* This is a convenience function that allows pushing strings, and will automatically use the configured `type` of this
* payload. It's fine to push content of a different type to the `out` array; it's just more annoying to write.
* @param {string} content
*/
push(content) {
this.out.push({ type: this.type, content });
}
/** /**
* Compact everything between `start` and `end` into a single payload, then call `fn` with the result of that payload. * Compact everything between `start` and `end` into a single payload, then call `fn` with the result of that payload.
* The compacted payload will be sync if all of the children are sync and {@link fn} is sync, otherwise it will be async. * The compacted payload will be sync if all of the children are sync and {@link fn} is sync, otherwise it will be async.
* @param {{ start: number, end?: number, fn: (value: string) => string | Promise<string> }} args * @param {{ start: number, end?: number, fn: (content: AccumulatedContent) => AccumulatedContent | Promise<AccumulatedContent> }} args
*/ */
compact({ start, end = this.out.length, fn }) { compact({ start, end = this.out.length, fn }) {
const child = this.#create_child_instance(); const child = new Payload(this.global, this.local, this);
const to_compact = this.out.splice(start, end - start, child); const to_compact = this.out.splice(start, end - start, child);
const promises = BasePayload.#collect_promises(to_compact, []); const promises = Payload.#collect_promises(to_compact, []);
/** @param {string | Promise<string>} res */ /** @param {AccumulatedContent | Promise<AccumulatedContent>} res */
const push_result = (res) => { const push_result = (res) => {
if (typeof res === 'string') { if (res instanceof Promise) {
child.out.push(res);
} else {
child.promise = res.then((resolved) => { child.promise = res.then((resolved) => {
child.out.push(resolved); Payload.#push_accumulated_content(child, resolved);
}); });
} else {
Payload.#push_accumulated_content(child, res);
} }
}; };
if (promises.length > 0) { if (promises.length > 0) {
// we have to wait for the accumulated work associated with all branches to complete,
// then we can accumulate their content to compact it.
child.promise = Promise.all(promises) child.promise = Promise.all(promises)
.then(() => fn(BasePayload.#collect_content(to_compact))) .then(() => fn(Payload.#collect_content(to_compact)))
.then(push_result); .then(push_result);
} else { } else {
push_result(fn(BasePayload.#collect_content(to_compact))); push_result(fn(Payload.#collect_content(to_compact)));
} }
} }
/**
* @returns {number[]}
*/
get_path() {
return this.parent ? [...this.parent.get_path(), this.parent.out.indexOf(this)] : [];
}
/** /**
* Waits for all child payloads to finish their blocking asynchronous work, then returns the generated content. * Waits for all child payloads to finish their blocking asynchronous work, then returns the generated content.
* @returns {Promise<string>} * @returns {Promise<AccumulatedContent>}
*/ */
async collect_async() { async collect_async() {
// TODO: Should probably use `Promise.allSettled` here just so we can report detailed errors // TODO: Should probably use `Promise.allSettled` here just so we can report detailed errors
await Promise.all(BasePayload.#collect_promises(this.out, this.promise ? [this.promise] : [])); await Promise.all(Payload.#collect_promises(this.out, this.promise ? [this.promise] : []));
return BasePayload.#collect_content(this.out); return Payload.#collect_content(this.out);
} }
/** /**
* Collect all of the code from the `out` array and return it as a string. * Collect all of the code from the `out` array and return it as a string.
* @returns {string} * @returns {AccumulatedContent}
*/ */
collect() { collect() {
const promises = BasePayload.#collect_promises(this.out, this.promise ? [this.promise] : []); const promises = Payload.#collect_promises(this.out, this.promise ? [this.promise] : []);
if (promises.length > 0) { if (promises.length > 0) {
// TODO is there a good way to report where this is? Probably by using some sort of loc or stack trace in `child` creation // TODO is there a good way to report where this is? Probably by using some sort of loc or stack trace in `child` creation
throw new Error('Encountered an asynchronous component while rendering synchronously'); throw new Error('Encountered an asynchronous component while rendering synchronously');
} }
return BasePayload.#collect_content(this.out); return Payload.#collect_content(this.out);
}
copy() {
const copy = new Payload(this.global, this.local, this.parent, this.type);
copy.out = this.out.map((item) => (item instanceof Payload ? item.copy() : item));
copy.promise = this.promise;
return copy;
}
/**
* @param {Payload} other
*/
subsume(other) {
this.global.subsume(other.global);
this.local = other.local;
this.out = other.out.map((item) => {
if (item instanceof Payload) {
item.subsume(item);
}
return item;
});
this.promise = other.promise;
this.type = other.type;
} }
/** /**
* @param {(string | Instance)[]} items * @param {(TNode | Payload)[]} items
* @param {Promise<void>[]} promises * @param {Promise<void>[]} promises
* @returns {Promise<void>[]} * @returns {Promise<void>[]}
*/ */
static #collect_promises(items, promises) { static #collect_promises(items, promises) {
for (const item of items) { for (const item of items) {
if (typeof item !== 'string') { if (item instanceof Payload) {
if (item.promise) { if (item.promise) {
promises.push(item.promise); promises.push(item.promise);
} }
BasePayload.#collect_promises(item.out, promises); Payload.#collect_promises(item.out, promises);
} }
} }
return promises; return promises;
@ -120,46 +195,41 @@ class BasePayload {
/** /**
* Collect all of the code from the `out` array and return it as a string. * Collect all of the code from the `out` array and return it as a string.
* @param {(string | Instance)[]} items * @param {(TNode | Payload)[]} items
* @returns {string} * @param {AccumulatedContent} content
* @returns {AccumulatedContent}
*/ */
static #collect_content(items) { static #collect_content(items, content = { head: '', body: '' }) {
// TODO throw in `async` mode
let content = '';
for (const item of items) { for (const item of items) {
if (typeof item === 'string') { if (item instanceof Payload) {
content += item; Payload.#collect_content(item.out, content);
} else { } else {
content += BasePayload.#collect_content(item.out); content[item.type] += item.content;
} }
} }
return content; return content;
} }
/** @returns {Instance} */ /**
#create_child_instance() { * @param {Payload} tree
// @ts-expect-error - This lets us create an instance of the subclass of this class. Type-danger is constrained by the fact that TSubclass must accept an instance of itself in its constructor. * @param {AccumulatedContent} accumulated_content
return new this.constructor(this); */
static #push_accumulated_content(tree, accumulated_content) {
for (const [type, content] of Object.entries(accumulated_content)) {
tree.out.push({ type: /** @type {TNode['type']} */ (type), content });
}
} }
} }
/** export class TreeState {
* @extends {BasePayload<typeof HeadPayload>}
*/
export class HeadPayload extends BasePayload {
/** @type {Set<{ hash: string; code: string }>} */
#css;
/** @type {() => string} */ /** @type {() => string} */
#uid; #uid;
/** /** @type {Set<{ hash: string; code: string }>} */
* This is a string or a promise so that the last write "wins" synchronously, #css;
* as opposed to writes coming in whenever it happens to during async work.
* It's boxed so that the same value is shared across all children. /** @type {TreeHeadState} */
* @type {{ value: string | Promise<string>}} #head;
*/
#title;
get css() { get css() {
return this.#css; return this.#css;
@ -169,60 +239,48 @@ export class HeadPayload extends BasePayload {
return this.#uid; return this.#uid;
} }
get title() { get head() {
return this.#title; return this.#head;
} }
/** /**
* @param {{ css?: Set<{ hash: string; code: string }>, title?: { value: string | Promise<string> }, uid?: () => string }} args * @param {string} [id_prefix]
*/ */
constructor({ css = new Set(), title = { value: '' }, uid = () => '' } = {}) { constructor(id_prefix = '') {
super(); this.#uid = props_id_generator(id_prefix);
this.#css = css; this.#css = new Set();
this.#title = title; this.#head = new TreeHeadState(this.#uid);
this.#uid = uid;
} }
copy() { copy() {
const head_payload = new HeadPayload({ const state = new TreeState();
css: new Set(this.#css), state.#css = new Set(this.#css);
title: this.title, state.#head = this.#head.copy();
uid: this.#uid state.#uid = this.#uid;
}); return state;
head_payload.promise = this.promise;
head_payload.out = [...this.out];
return head_payload;
} }
/** /**
* @param {HeadPayload} other * @param {TreeState} other
*/ */
subsume(other) { subsume(other) {
// @ts-expect-error
this.out = [...other.out];
this.promise = other.promise;
this.#css = other.#css; this.#css = other.#css;
this.#title = other.#title;
this.#uid = other.#uid; this.#uid = other.#uid;
this.#head.subsume(other.#head);
} }
} }
/** export class TreeHeadState {
* @extends {BasePayload<typeof Payload>}
*/
export class Payload extends BasePayload {
/** @type {() => string} */
#uid;
/** @type {Set<{ hash: string; code: string }>} */ /** @type {Set<{ hash: string; code: string }>} */
#css; #css = new Set();
/** @type {HeadPayload} */ /** @type {() => string} */
#head; #uid = () => '';
/** @type {string | undefined} */ /**
select_value; * @type {{ path: number[], value: string }}
*/
#title = { path: [], value: '' };
get css() { get css() {
return this.#css; return this.#css;
@ -232,51 +290,57 @@ export class Payload extends BasePayload {
return this.#uid; return this.#uid;
} }
get head() { get title() {
return this.#head; return this.#title;
}
set title(value) {
// perform a depth-first (lexicographic) comparison using the path. Reject sets
// from earlier than or equal to the current value.
const contender_path = value.path;
const current_path = this.#title.path;
const max_len = Math.max(contender_path.length, current_path.length);
for (let i = 0; i < max_len; i++) {
const contender_segment = contender_path[i];
const current_segment = current_path[i];
// contender shorter than current and all previous segments equal -> earlier
if (contender_segment === undefined) return;
// current shorter than contender and all previous segments equal -> contender is later
if (current_segment === undefined || contender_segment > current_segment) {
this.#title.path = value.path;
this.#title.value = value.value;
return;
}
if (contender_segment < current_segment) return;
// else equal -> continue
}
// paths are equal -> keep current value (do nothing)
} }
/** /**
* @param {{ id_prefix?: string, head?: HeadPayload, uid?: () => string, css?: Set<{ hash: string; code: string }>, select_value?: string | undefined }} args * @param {() => string} uid
*/ */
constructor({ constructor(uid) {
id_prefix = '',
head = new HeadPayload(),
uid = props_id_generator(id_prefix),
css = new Set(),
select_value
} = {}) {
super();
this.#uid = uid; this.#uid = uid;
this.#css = css; this.#css = new Set();
this.#head = head; this.#title = { path: [], value: '' };
this.select_value = select_value;
} }
copy() { copy() {
const payload = new Payload({ const head_state = new TreeHeadState(this.#uid);
css: new Set(this.#css), head_state.#css = new Set(this.#css);
uid: this.#uid, head_state.#title = this.title;
head: this.#head.copy() return head_state;
});
payload.select_value = this.select_value;
payload.promise = this.promise;
payload.out = [...this.out];
return payload;
} }
/** /**
* @param {Payload} other * @param {TreeHeadState} other
*/ */
subsume(other) { subsume(other) {
// @ts-expect-error
this.out = [...other.out];
this.promise = other.promise;
this.select_value = other.select_value;
this.#css = other.#css; this.#css = other.#css;
this.#title = other.#title;
this.#uid = other.#uid; this.#uid = other.#uid;
this.#head.subsume(other.#head);
} }
} }

@ -1,6 +1,6 @@
/** @import { SvelteComponent } from '../index.js' */ /** @import { SvelteComponent } from '../index.js' */
import { asClassComponent as as_class_component, createClassComponent } from './legacy-client.js'; import { asClassComponent as as_class_component, createClassComponent } from './legacy-client.js';
import { render } from '../internal/server/index.js'; import { render, render_async } from '../internal/server/index.js';
// By having this as a separate entry point for server environments, we save the client bundle from having to include the server runtime // By having this as a separate entry point for server environments, we save the client bundle from having to include the server runtime
@ -31,8 +31,21 @@ export function asClassComponent(component) {
html: result.body html: result.body
}; };
}; };
/** @type {(props?: {}, opts?: { $$slots?: {}; context?: Map<any, any>; }) => Promise<{ html: any; css: { code: string; map: any; }; head: string; }> } */
const _render_async = async (props, { context } = {}) => {
// @ts-expect-error the typings are off, but this will work if the component is compiled in SSR mode
const result = await render_async(component, { props, context });
return {
css: { code: '', map: null },
head: result.head,
html: result.body
};
};
// @ts-expect-error this is present for SSR // @ts-expect-error this is present for SSR
component_constructor.render = _render; component_constructor.render = _render;
// @ts-expect-error this is present for SSR
component_constructor.renderAsync = _render_async;
// @ts-ignore // @ts-ignore
return component_constructor; return component_constructor;

@ -27,3 +27,30 @@ export function render<
} }
] ]
): RenderOutput; ): RenderOutput;
/**
* 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.
*/
export function renderAsync<
Comp extends SvelteComponent<any> | Component<any>,
Props extends ComponentProps<Comp> = ComponentProps<Comp>
>(
...args: {} extends Props
? [
component: Comp extends SvelteComponent<any> ? ComponentType<Comp> : Comp,
options?: {
props?: Omit<Props, '$$slots' | '$$events'>;
context?: Map<any, any>;
idPrefix?: string;
}
]
: [
component: Comp extends SvelteComponent<any> ? ComponentType<Comp> : Comp,
options: {
props: Omit<Props, '$$slots' | '$$events'>;
context?: Map<any, any>;
idPrefix?: string;
}
]
): Promise<RenderOutput>;

@ -1 +1 @@
export { render } from '../internal/server/index.js'; export { render, render_async as renderAsync } from '../internal/server/index.js';

@ -0,0 +1,9 @@
<script>
let { promise } = $props();
</script>
<svelte:head>
{#if await promise}
<title>A</title>
{/if}
</svelte:head>

@ -0,0 +1,7 @@
<script>
let { promise } = $props();
</script>
<svelte:head>
<title>{(await promise) && 'B'}</title>
</svelte:head>

@ -0,0 +1,27 @@
<script>
import { tick } from 'svelte';
import A from './A.svelte';
import B from './B.svelte';
const { promise: main_promise, resolve: main_resolve } = Promise.withResolvers();
const { promise: a_promise, resolve: a_resolve } = Promise.withResolvers();
const { promise: b_promise, resolve: b_resolve } = Promise.withResolvers();
// regardless of resolution order, title should be the result of B, because it's the last-encountered
tick().then(() => {
main_resolve(true);
tick().then(() => {
b_resolve(true);
}).then(() => {
a_resolve(true);
});
})
</script>
<svelte:head>
{#if await main_promise}
<title>Main</title>
{/if}
</svelte:head>
<A promise={a_promise}/>
<B promise={b_promise}/>

@ -0,0 +1,5 @@
<script>
let props = $props();
</script>
<option {...props}>{@render props.children?.()}</option>

@ -0,0 +1 @@
<select><option value="">--Please choose an option--</option><option value="dog" selected>Dog</option><option value="cat">Cat</option></select>

@ -0,0 +1,8 @@
<script lang="ts">
import Option from './Option.svelte';
</script>
<select value={await Promise.resolve('dog')}>
<Option value="">--Please choose an option--</Option>
<Option value={await Promise.resolve('dog')}>Dog</Option>
<Option value="cat">Cat</Option>
</select>

@ -0,0 +1 @@
<select><option>--Please choose an option--</option><option selected>dog</option><option>cat</option></select>

@ -0,0 +1,13 @@
<select value={await Promise.resolve('dog')}>
<option>
{@render option("--Please choose an option--")}
</option>
<option>
{@render option(Promise.resolve('dog'))}
</option>
<option>
{@render option(Promise.resolve('cat'))}
</option>
</select>
{#snippet option(val)}{await val}{/snippet}

@ -0,0 +1 @@
<select><option>--Please choose an option--</option><option selected>dog</option><option>cat</option></select>

@ -0,0 +1,5 @@
<select value={await Promise.resolve('dog')}>
<option>--Please choose an option--</option>
<option>{await Promise.resolve('dog')}</option>
<option>cat</option>
</select>

@ -6,8 +6,13 @@
import * as fs from 'node:fs'; import * as fs from 'node:fs';
import { assert } from 'vitest'; import { assert } from 'vitest';
import { render } from 'svelte/server'; import { render, renderAsync } from 'svelte/server';
import { compile_directory, should_update_expected, try_read_file } from '../helpers.js'; import {
async_mode,
compile_directory,
should_update_expected,
try_read_file
} from '../helpers.js';
import { assert_html_equal_with_options } from '../html_equal.js'; import { assert_html_equal_with_options } from '../html_equal.js';
import { suite, type BaseTest } from '../suite.js'; import { suite, type BaseTest } from '../suite.js';
import type { CompileOptions } from '#compiler'; import type { CompileOptions } from '#compiler';
@ -25,8 +30,16 @@ interface SSRTest extends BaseTest {
let console_error = console.error; let console_error = console.error;
const { test, run } = suite<SSRTest>(async (config, test_dir) => { const { test, run } = suite<SSRTest>(async (config, test_dir) => {
const compile_options = {
experimental: {
async: async_mode,
...config.compileOptions?.experimental
},
...config.compileOptions
};
if (!config.load_compiled) { if (!config.load_compiled) {
await compile_directory(test_dir, 'server', config.compileOptions); await compile_directory(test_dir, 'server', compile_options);
} }
const errors: string[] = []; const errors: string[] = [];
@ -37,7 +50,10 @@ const { test, run } = suite<SSRTest>(async (config, test_dir) => {
const Component = (await import(`${test_dir}/_output/server/main.svelte.js`)).default; const Component = (await import(`${test_dir}/_output/server/main.svelte.js`)).default;
const expected_html = try_read_file(`${test_dir}/_expected.html`); const expected_html = try_read_file(`${test_dir}/_expected.html`);
const rendered = render(Component, { props: config.props || {}, idPrefix: config.id_prefix }); const rendered = await (compile_options.experimental.async ? renderAsync : render)(Component, {
props: config.props || {},
idPrefix: config.id_prefix
});
const { body, head } = rendered; const { body, head } = rendered;
fs.writeFileSync(`${test_dir}/_output/rendered.html`, body); fs.writeFileSync(`${test_dir}/_output/rendered.html`, body);
@ -48,7 +64,7 @@ const { test, run } = suite<SSRTest>(async (config, test_dir) => {
try { try {
assert_html_equal_with_options(body, expected_html || '', { assert_html_equal_with_options(body, expected_html || '', {
preserveComments: config.compileOptions?.preserveComments, preserveComments: compile_options.preserveComments,
withoutNormalizeHtml: config.withoutNormalizeHtml withoutNormalizeHtml: config.withoutNormalizeHtml
}); });
} catch (error: any) { } catch (error: any) {

@ -1,7 +1,7 @@
import * as $ from 'svelte/internal/server'; import * as $ from 'svelte/internal/server';
export default function Await_block_scope($$payload) { export default function Await_block_scope($$payload) {
$$payload.child(({ $$payload }) => { $$payload.child(($$payload) => {
let counter = { count: 0 }; let counter = { count: 0 };
const promise = Promise.resolve(counter); const promise = Promise.resolve(counter);
@ -9,8 +9,8 @@ export default function Await_block_scope($$payload) {
counter.count += 1; counter.count += 1;
} }
$$payload.out.push(`<button>clicks: ${$.escape(counter.count)}</button> `); $$payload.push(`<button>clicks: ${$.escape(counter.count)}</button> `);
$.await($$payload, promise, () => {}, (counter) => {}); $.await($$payload, promise, () => {}, (counter) => {});
$$payload.out.push(`<!--]--> ${$.escape(counter.count)}`); $$payload.push(`<!--]--> ${$.escape(counter.count)}`);
}); });
} }

@ -2,13 +2,13 @@ import * as $ from 'svelte/internal/server';
import TextInput from './Child.svelte'; import TextInput from './Child.svelte';
function snippet($$payload) { function snippet($$payload) {
$$payload.child(({ $$payload }) => { $$payload.child(($$payload) => {
$$payload.out.push(`<!---->Something`); $$payload.push(`<!---->Something`);
}); });
} }
export default function Bind_component_snippet($$payload) { export default function Bind_component_snippet($$payload) {
$$payload.child(({ $$payload }) => { $$payload.child(($$payload) => {
let value = ''; let value = '';
const _snippet = snippet; const _snippet = snippet;
let $$settled = true; let $$settled = true;
@ -26,7 +26,7 @@ export default function Bind_component_snippet($$payload) {
} }
}); });
$$payload.out.push(`<!----> value: ${$.escape(value)}`); $$payload.push(`<!----> value: ${$.escape(value)}`);
} }
do { do {

@ -1,7 +1,7 @@
import * as $ from 'svelte/internal/server'; import * as $ from 'svelte/internal/server';
export default function Bind_this($$payload) { export default function Bind_this($$payload) {
$$payload.child(({ $$payload }) => { $$payload.child(($$payload) => {
Foo($$payload, {}); Foo($$payload, {});
}); });
} }

@ -3,7 +3,7 @@ import * as $ from 'svelte/internal/server';
export default function Class_state_field_constructor_assignment($$payload, $$props) { export default function Class_state_field_constructor_assignment($$payload, $$props) {
$.push(); $.push();
$$payload.child(({ $$payload }) => { $$payload.child(($$payload) => {
class Foo { class Foo {
a = 0; a = 0;
#b; #b;

@ -1,15 +1,15 @@
import * as $ from 'svelte/internal/server'; import * as $ from 'svelte/internal/server';
export default function Delegated_locally_declared_shadowed($$payload) { export default function Delegated_locally_declared_shadowed($$payload) {
$$payload.child(({ $$payload }) => { $$payload.child(($$payload) => {
const each_array = $.ensure_array_like({ length: 1 }); const each_array = $.ensure_array_like({ length: 1 });
$$payload.out.push(`<!--[-->`); $$payload.push(`<!--[-->`);
for (let index = 0, $$length = each_array.length; index < $$length; index++) { for (let index = 0, $$length = each_array.length; index < $$length; index++) {
$$payload.out.push(`<button type="button"${$.attr('data-index', index)}>B</button>`); $$payload.push(`<button type="button"${$.attr('data-index', index)}>B</button>`);
} }
$$payload.out.push(`<!--]-->`); $$payload.push(`<!--]-->`);
}); });
} }

@ -1,10 +1,10 @@
import * as $ from 'svelte/internal/server'; import * as $ from 'svelte/internal/server';
export default function Main($$payload) { export default function Main($$payload) {
$$payload.child(({ $$payload }) => { $$payload.child(($$payload) => {
let x = 'test'; let x = 'test';
let y = () => 'test'; let y = () => 'test';
$$payload.out.push(`<div${$.attr('foobar', x)}></div> <svg${$.attr('viewBox', x)}></svg> <custom-element${$.attr('foobar', x)}></custom-element> <div${$.attr('foobar', y())}></div> <svg${$.attr('viewBox', y())}></svg> <custom-element${$.attr('foobar', y())}></custom-element>`); $$payload.push(`<div${$.attr('foobar', x)}></div> <svg${$.attr('viewBox', x)}></svg> <custom-element${$.attr('foobar', x)}></custom-element> <div${$.attr('foobar', y())}></div> <svg${$.attr('viewBox', y())}></svg> <custom-element${$.attr('foobar', y())}></custom-element>`);
}); });
} }

@ -1,15 +1,15 @@
import * as $ from 'svelte/internal/server'; import * as $ from 'svelte/internal/server';
export default function Each_index_non_null($$payload) { export default function Each_index_non_null($$payload) {
$$payload.child(({ $$payload }) => { $$payload.child(($$payload) => {
const each_array = $.ensure_array_like(Array(10)); const each_array = $.ensure_array_like(Array(10));
$$payload.out.push(`<!--[-->`); $$payload.push(`<!--[-->`);
for (let i = 0, $$length = each_array.length; i < $$length; i++) { for (let i = 0, $$length = each_array.length; i < $$length; i++) {
$$payload.out.push(`<p>index: ${$.escape(i)}</p>`); $$payload.push(`<p>index: ${$.escape(i)}</p>`);
} }
$$payload.out.push(`<!--]-->`); $$payload.push(`<!--]-->`);
}); });
} }

@ -1,17 +1,17 @@
import * as $ from 'svelte/internal/server'; import * as $ from 'svelte/internal/server';
export default function Each_string_template($$payload) { export default function Each_string_template($$payload) {
$$payload.child(({ $$payload }) => { $$payload.child(($$payload) => {
const each_array = $.ensure_array_like(['foo', 'bar', 'baz']); const each_array = $.ensure_array_like(['foo', 'bar', 'baz']);
$$payload.out.push(`<!--[-->`); $$payload.push(`<!--[-->`);
for (let $$index = 0, $$length = each_array.length; $$index < $$length; $$index++) { for (let $$index = 0, $$length = each_array.length; $$index < $$length; $$index++) {
let thing = each_array[$$index]; let thing = each_array[$$index];
$$payload.out.push(`<!---->${$.escape(thing)}, `); $$payload.push(`<!---->${$.escape(thing)}, `);
} }
$$payload.out.push(`<!--]-->`); $$payload.push(`<!--]-->`);
}); });
} }

@ -1,7 +1,7 @@
import * as $ from 'svelte/internal/server'; import * as $ from 'svelte/internal/server';
export default function Function_prop_no_getter($$payload) { export default function Function_prop_no_getter($$payload) {
$$payload.child(({ $$payload }) => { $$payload.child(($$payload) => {
let count = 0; let count = 0;
function onmouseup() { function onmouseup() {
@ -16,8 +16,8 @@ export default function Function_prop_no_getter($$payload) {
onmouseenter: () => count = plusOne(count), onmouseenter: () => count = plusOne(count),
children: ($$payload) => { children: ($$payload) => {
$$payload.child(({ $$payload }) => { $$payload.child(($$payload) => {
$$payload.out.push(`<!---->clicks: ${$.escape(count)}`); $$payload.push(`<!---->clicks: ${$.escape(count)}`);
}); });
}, },

@ -1,7 +1,7 @@
import * as $ from 'svelte/internal/server'; import * as $ from 'svelte/internal/server';
export default function Functional_templating($$payload) { export default function Functional_templating($$payload) {
$$payload.child(({ $$payload }) => { $$payload.child(($$payload) => {
$$payload.out.push(`<h1>hello</h1> <div class="potato"><p>child element</p> <p>another child element</p></div>`); $$payload.push(`<h1>hello</h1> <div class="potato"><p>child element</p> <p>another child element</p></div>`);
}); });
} }

@ -1,7 +1,7 @@
import * as $ from 'svelte/internal/server'; import * as $ from 'svelte/internal/server';
export default function Hello_world($$payload) { export default function Hello_world($$payload) {
$$payload.child(({ $$payload }) => { $$payload.child(($$payload) => {
$$payload.out.push(`<h1>hello world</h1>`); $$payload.push(`<h1>hello world</h1>`);
}); });
} }

@ -1,7 +1,7 @@
import * as $ from 'svelte/internal/server'; import * as $ from 'svelte/internal/server';
export default function Hmr($$payload) { export default function Hmr($$payload) {
$$payload.child(({ $$payload }) => { $$payload.child(($$payload) => {
$$payload.out.push(`<h1>hello world</h1>`); $$payload.push(`<h1>hello world</h1>`);
}); });
} }

@ -2,5 +2,5 @@ import * as $ from 'svelte/internal/server';
import { random } from './module.svelte'; import { random } from './module.svelte';
export default function Imports_in_modules($$payload) { export default function Imports_in_modules($$payload) {
$$payload.child(({ $$payload }) => {}); $$payload.child(($$payload) => {});
} }

@ -1,10 +1,10 @@
import * as $ from 'svelte/internal/server'; import * as $ from 'svelte/internal/server';
export default function Nullish_coallescence_omittance($$payload) { export default function Nullish_coallescence_omittance($$payload) {
$$payload.child(({ $$payload }) => { $$payload.child(($$payload) => {
let name = 'world'; let name = 'world';
let count = 0; let count = 0;
$$payload.out.push(`<h1>Hello, world!</h1> <b>123</b> <button>Count is ${$.escape(count)}</button> <h1>Hello, world</h1>`); $$payload.push(`<h1>Hello, world!</h1> <b>123</b> <button>Count is ${$.escape(count)}</button> <h1>Hello, world</h1>`);
}); });
} }

@ -3,7 +3,7 @@ import * as $ from 'svelte/internal/server';
export default function Props_identifier($$payload, $$props) { export default function Props_identifier($$payload, $$props) {
$.push(); $.push();
$$payload.child(({ $$payload }) => { $$payload.child(($$payload) => {
let { $$slots, $$events, ...props } = $$props; let { $$slots, $$events, ...props } = $$props;
props.a; props.a;

@ -1,9 +1,9 @@
import * as $ from 'svelte/internal/server'; import * as $ from 'svelte/internal/server';
export default function Purity($$payload) { export default function Purity($$payload) {
$$payload.child(({ $$payload }) => { $$payload.child(($$payload) => {
$$payload.out.push(`<p>0</p> <p>${$.escape(location.href)}</p> `); $$payload.push(`<p>0</p> <p>${$.escape(location.href)}</p> `);
Child($$payload, { prop: encodeURIComponent('hello') }); Child($$payload, { prop: encodeURIComponent('hello') });
$$payload.out.push(`<!---->`); $$payload.push(`<!---->`);
}); });
} }

@ -1,9 +1,9 @@
import * as $ from 'svelte/internal/server'; import * as $ from 'svelte/internal/server';
export default function Skip_static_subtree($$payload, $$props) { export default function Skip_static_subtree($$payload, $$props) {
$$payload.child(({ $$payload }) => { $$payload.child(($$payload) => {
let { title, content } = $$props; let { title, content } = $$props;
$$payload.out.push(`<header><nav><a href="/">Home</a> <a href="/away">Away</a></nav></header> <main><h1>${$.escape(title)}</h1> <div class="static"><p>we don't need to traverse these nodes</p></div> <p>or</p> <p>these</p> <p>ones</p> ${$.html(content)} <p>these</p> <p>trailing</p> <p>nodes</p> <p>can</p> <p>be</p> <p>completely</p> <p>ignored</p></main> <cant-skip><custom-elements with="attributes"></custom-elements></cant-skip> <div><input autofocus/></div> <div><source muted/></div> <select><option value="a"${$.maybe_selected($$payload, 'a')}>a</option></select> <img src="..." alt="" loading="lazy"/> <div><img src="..." alt="" loading="lazy"/></div>`); $$payload.push(`<header><nav><a href="/">Home</a> <a href="/away">Away</a></nav></header> <main><h1>${$.escape(title)}</h1> <div class="static"><p>we don't need to traverse these nodes</p></div> <p>or</p> <p>these</p> <p>ones</p> ${$.html(content)} <p>these</p> <p>trailing</p> <p>nodes</p> <p>can</p> <p>be</p> <p>completely</p> <p>ignored</p></main> <cant-skip><custom-elements with="attributes"></custom-elements></cant-skip> <div><input autofocus/></div> <div><source muted/></div> <select><option value="a"${$.maybe_selected($$payload, 'a')}>a</option></select> <img src="..." alt="" loading="lazy"/> <div><img src="..." alt="" loading="lazy"/></div>`);
}); });
} }

@ -1,7 +1,7 @@
import * as $ from 'svelte/internal/server'; import * as $ from 'svelte/internal/server';
export default function State_proxy_literal($$payload) { export default function State_proxy_literal($$payload) {
$$payload.child(({ $$payload }) => { $$payload.child(($$payload) => {
let str = ''; let str = '';
let tpl = ``; let tpl = ``;
@ -12,6 +12,6 @@ export default function State_proxy_literal($$payload) {
tpl = ``; tpl = ``;
} }
$$payload.out.push(`<input${$.attr('value', str)}/> <input${$.attr('value', tpl)}/> <button>reset</button>`); $$payload.push(`<input${$.attr('value', str)}/> <input${$.attr('value', tpl)}/> <button>reset</button>`);
}); });
} }

@ -1,7 +1,7 @@
import * as $ from 'svelte/internal/server'; import * as $ from 'svelte/internal/server';
export default function Svelte_element($$payload, $$props) { export default function Svelte_element($$payload, $$props) {
$$payload.child(({ $$payload }) => { $$payload.child(($$payload) => {
let { tag = 'hr' } = $$props; let { tag = 'hr' } = $$props;
$.element($$payload, tag); $.element($$payload, tag);

@ -1,7 +1,7 @@
import * as $ from 'svelte/internal/server'; import * as $ from 'svelte/internal/server';
export default function Text_nodes_deriveds($$payload) { export default function Text_nodes_deriveds($$payload) {
$$payload.child(({ $$payload }) => { $$payload.child(($$payload) => {
let count1 = 0; let count1 = 0;
let count2 = 0; let count2 = 0;
@ -13,6 +13,6 @@ export default function Text_nodes_deriveds($$payload) {
return count2; return count2;
} }
$$payload.out.push(`<p>${$.escape(text1())}${$.escape(text2())}</p>`); $$payload.push(`<p>${$.escape(text1())}${$.escape(text2())}</p>`);
}); });
} }

@ -2493,6 +2493,33 @@ declare module 'svelte/server' {
} }
] ]
): RenderOutput; ): RenderOutput;
/**
* 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.
*/
export function renderAsync<
Comp extends SvelteComponent<any> | Component<any>,
Props extends ComponentProps<Comp> = ComponentProps<Comp>
>(
...args: {} extends Props
? [
component: Comp extends SvelteComponent<any> ? ComponentType<Comp> : Comp,
options?: {
props?: Omit<Props, '$$slots' | '$$events'>;
context?: Map<any, any>;
idPrefix?: string;
}
]
: [
component: Comp extends SvelteComponent<any> ? ComponentType<Comp> : Comp,
options: {
props: Omit<Props, '$$slots' | '$$events'>;
context?: Map<any, any>;
idPrefix?: string;
}
]
): Promise<RenderOutput>;
interface RenderOutput { interface RenderOutput {
/** HTML that goes into the `<head>` */ /** HTML that goes into the `<head>` */
head: string; head: string;

Loading…
Cancel
Save