init
This commit is contained in:
+13
@@ -0,0 +1,13 @@
|
||||
import { STALE_REACTION } from '#client/constants';
|
||||
|
||||
/** @type {AbortController | null} */
|
||||
let controller = null;
|
||||
|
||||
export function abort() {
|
||||
controller?.abort(STALE_REACTION);
|
||||
controller = null;
|
||||
}
|
||||
|
||||
export function getAbortSignal() {
|
||||
return (controller ??= new AbortController()).signal;
|
||||
}
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
import { DEV } from 'esm-env';
|
||||
import { hash } from '../../../utils.js';
|
||||
|
||||
/**
|
||||
* @param {string} value
|
||||
*/
|
||||
export function html(value) {
|
||||
var html = String(value ?? '');
|
||||
var open = DEV ? `<!--${hash(html)}-->` : '<!---->';
|
||||
return open + html + '<!---->';
|
||||
}
|
||||
+24
@@ -0,0 +1,24 @@
|
||||
/** @import { Snippet } from 'svelte' */
|
||||
/** @import { Renderer } from '../renderer' */
|
||||
/** @import { Getters } from '#shared' */
|
||||
|
||||
/**
|
||||
* Create a snippet programmatically
|
||||
* @template {unknown[]} Params
|
||||
* @param {(...params: Getters<Params>) => {
|
||||
* render: () => string
|
||||
* setup?: (element: Element) => void | (() => void)
|
||||
* }} fn
|
||||
* @returns {Snippet<Params>}
|
||||
*/
|
||||
export function createRawSnippet(fn) {
|
||||
// @ts-expect-error the types are a lie
|
||||
return (/** @type {Renderer} */ renderer, /** @type {Params} */ ...args) => {
|
||||
var getters = /** @type {Getters<Params>} */ (args.map((value) => () => value));
|
||||
renderer.push(
|
||||
fn(...getters)
|
||||
.render()
|
||||
.trim()
|
||||
);
|
||||
};
|
||||
}
|
||||
+122
@@ -0,0 +1,122 @@
|
||||
/** @import { SSRContext } from '#server' */
|
||||
import { DEV } from 'esm-env';
|
||||
import * as e from './errors.js';
|
||||
|
||||
/** @type {SSRContext | null} */
|
||||
export var ssr_context = null;
|
||||
|
||||
/** @param {SSRContext | null} v */
|
||||
export function set_ssr_context(v) {
|
||||
ssr_context = v;
|
||||
}
|
||||
|
||||
/**
|
||||
* @template T
|
||||
* @returns {[() => T, (context: T) => T]}
|
||||
* @since 5.40.0
|
||||
*/
|
||||
export function createContext() {
|
||||
const key = {};
|
||||
return [() => getContext(key), (context) => setContext(key, context)];
|
||||
}
|
||||
|
||||
/**
|
||||
* @template T
|
||||
* @param {any} key
|
||||
* @returns {T}
|
||||
*/
|
||||
export function getContext(key) {
|
||||
const context_map = get_or_init_context_map('getContext');
|
||||
const result = /** @type {T} */ (context_map.get(key));
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* @template T
|
||||
* @param {any} key
|
||||
* @param {T} context
|
||||
* @returns {T}
|
||||
*/
|
||||
export function setContext(key, context) {
|
||||
get_or_init_context_map('setContext').set(key, context);
|
||||
return context;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {any} key
|
||||
* @returns {boolean}
|
||||
*/
|
||||
export function hasContext(key) {
|
||||
return get_or_init_context_map('hasContext').has(key);
|
||||
}
|
||||
|
||||
/** @returns {Map<any, any>} */
|
||||
export function getAllContexts() {
|
||||
return get_or_init_context_map('getAllContexts');
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} name
|
||||
* @returns {Map<unknown, unknown>}
|
||||
*/
|
||||
function get_or_init_context_map(name) {
|
||||
if (ssr_context === null) {
|
||||
e.lifecycle_outside_component(name);
|
||||
}
|
||||
|
||||
return (ssr_context.c ??= new Map(get_parent_context(ssr_context) || undefined));
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Function} [fn]
|
||||
*/
|
||||
export function push(fn) {
|
||||
ssr_context = { p: ssr_context, c: null, r: null };
|
||||
|
||||
if (DEV) {
|
||||
ssr_context.function = fn;
|
||||
ssr_context.element = ssr_context.p?.element;
|
||||
}
|
||||
}
|
||||
|
||||
export function pop() {
|
||||
ssr_context = /** @type {SSRContext} */ (ssr_context).p;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {SSRContext} ssr_context
|
||||
* @returns {Map<unknown, unknown> | null}
|
||||
*/
|
||||
function get_parent_context(ssr_context) {
|
||||
let parent = ssr_context.p;
|
||||
|
||||
while (parent !== null) {
|
||||
const context_map = parent.c;
|
||||
if (context_map !== null) {
|
||||
return context_map;
|
||||
}
|
||||
parent = parent.p;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Wraps an `await` expression in such a way that the component context that was
|
||||
* active before the expression evaluated can be reapplied afterwards —
|
||||
* `await a + b()` becomes `(await $.save(a))() + b()`, meaning `b()` will have access
|
||||
* to the context of its component.
|
||||
* @template T
|
||||
* @param {Promise<T>} promise
|
||||
* @returns {Promise<() => T>}
|
||||
*/
|
||||
export async function save(promise) {
|
||||
var previous_context = ssr_context;
|
||||
var value = await promise;
|
||||
|
||||
return () => {
|
||||
ssr_context = previous_context;
|
||||
return value;
|
||||
};
|
||||
}
|
||||
+41
@@ -0,0 +1,41 @@
|
||||
import { BROWSER } from 'esm-env';
|
||||
|
||||
let text_encoder;
|
||||
// TODO - remove this and use global `crypto` when we drop Node 18
|
||||
let crypto;
|
||||
|
||||
/** @param {string} data */
|
||||
export async function sha256(data) {
|
||||
text_encoder ??= new TextEncoder();
|
||||
|
||||
// @ts-expect-error
|
||||
crypto ??= globalThis.crypto?.subtle?.digest
|
||||
? globalThis.crypto
|
||||
: // @ts-ignore - we don't install node types in the prod build
|
||||
(await import('node:crypto')).webcrypto;
|
||||
|
||||
const hash_buffer = await crypto.subtle.digest('SHA-256', text_encoder.encode(data));
|
||||
|
||||
return base64_encode(hash_buffer);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Uint8Array} bytes
|
||||
* @returns {string}
|
||||
*/
|
||||
export function base64_encode(bytes) {
|
||||
// Using `Buffer` is faster than iterating
|
||||
// @ts-ignore
|
||||
if (!BROWSER && globalThis.Buffer) {
|
||||
// @ts-ignore
|
||||
return globalThis.Buffer.from(bytes).toString('base64');
|
||||
}
|
||||
|
||||
let binary = '';
|
||||
|
||||
for (let i = 0; i < bytes.length; i++) {
|
||||
binary += String.fromCharCode(bytes[i]);
|
||||
}
|
||||
|
||||
return btoa(binary);
|
||||
}
|
||||
+110
@@ -0,0 +1,110 @@
|
||||
/** @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 { get_stack } from '../shared/dev.js';
|
||||
import { set_ssr_context, ssr_context } from './context.js';
|
||||
import * as e from './errors.js';
|
||||
import { Renderer } from './renderer.js';
|
||||
|
||||
// TODO move this
|
||||
/**
|
||||
* @typedef {{
|
||||
* tag: string;
|
||||
* parent: undefined | Element;
|
||||
* filename: undefined | string;
|
||||
* line: number;
|
||||
* column: number;
|
||||
* }} Element
|
||||
*/
|
||||
|
||||
/**
|
||||
* This is exported so that it can be cleared between tests
|
||||
* @type {Set<string>}
|
||||
*/
|
||||
export let seen;
|
||||
|
||||
/**
|
||||
* @param {Renderer} renderer
|
||||
* @param {string} message
|
||||
*/
|
||||
function print_error(renderer, message) {
|
||||
message =
|
||||
`node_invalid_placement_ssr: ${message}\n\n` +
|
||||
'This can cause content to shift around as the browser repairs the HTML, and will likely result in a `hydration_mismatch` warning.';
|
||||
|
||||
if ((seen ??= new Set()).has(message)) return;
|
||||
seen.add(message);
|
||||
|
||||
// eslint-disable-next-line no-console
|
||||
console.error(message);
|
||||
renderer.head((r) => r.push(`<script>console.error(${JSON.stringify(message)})</script>`));
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Renderer} renderer
|
||||
* @param {string} tag
|
||||
* @param {number} line
|
||||
* @param {number} column
|
||||
*/
|
||||
export function push_element(renderer, tag, 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 !== undefined) {
|
||||
var ancestor = parent.parent;
|
||||
var ancestors = [parent.tag];
|
||||
|
||||
const child_loc = filename ? `${filename}:${line}:${column}` : undefined;
|
||||
const parent_loc = parent.filename
|
||||
? `${parent.filename}:${parent.line}:${parent.column}`
|
||||
: undefined;
|
||||
|
||||
const message = is_tag_valid_with_parent(tag, parent.tag, child_loc, parent_loc);
|
||||
if (message) print_error(renderer, message);
|
||||
|
||||
while (ancestor != null) {
|
||||
ancestors.push(ancestor.tag);
|
||||
const ancestor_loc = ancestor.filename
|
||||
? `${ancestor.filename}:${ancestor.line}:${ancestor.column}`
|
||||
: undefined;
|
||||
|
||||
const message = is_tag_valid_with_ancestor(tag, ancestors, child_loc, ancestor_loc);
|
||||
if (message) print_error(renderer, message);
|
||||
|
||||
ancestor = ancestor.parent;
|
||||
}
|
||||
}
|
||||
|
||||
set_ssr_context({ ...context, p: context, element });
|
||||
}
|
||||
|
||||
export function pop_element() {
|
||||
set_ssr_context(/** @type {SSRContext} */ (ssr_context).p);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Renderer} renderer
|
||||
*/
|
||||
export function validate_snippet_args(renderer) {
|
||||
if (
|
||||
typeof renderer !== 'object' ||
|
||||
// for some reason typescript consider the type of renderer as never after the first instanceof
|
||||
!(renderer instanceof Renderer)
|
||||
) {
|
||||
e.invalid_snippet_arguments();
|
||||
}
|
||||
}
|
||||
|
||||
export function get_user_code_location() {
|
||||
const stack = get_stack();
|
||||
|
||||
return stack
|
||||
.filter((line) => line.trim().startsWith('at '))
|
||||
.map((line) => line.replace(/\((.*):\d+:\d+\)$/, (_, file) => `(${file})`))
|
||||
.join('\n');
|
||||
}
|
||||
+118
@@ -0,0 +1,118 @@
|
||||
/* This file is generated by scripts/process-messages/index.js. Do not edit! */
|
||||
|
||||
export * from '../shared/errors.js';
|
||||
|
||||
/**
|
||||
* The node API `AsyncLocalStorage` is not available, but is required to use async server rendering.
|
||||
* @returns {never}
|
||||
*/
|
||||
export function async_local_storage_unavailable() {
|
||||
const error = new Error(`async_local_storage_unavailable\nThe node API \`AsyncLocalStorage\` is not available, but is required to use async server rendering.\nhttps://svelte.dev/e/async_local_storage_unavailable`);
|
||||
|
||||
error.name = 'Svelte error';
|
||||
|
||||
throw error;
|
||||
}
|
||||
|
||||
/**
|
||||
* Encountered asynchronous work while rendering synchronously.
|
||||
* @returns {never}
|
||||
*/
|
||||
export function await_invalid() {
|
||||
const error = new Error(`await_invalid\nEncountered asynchronous work while rendering synchronously.\nhttps://svelte.dev/e/await_invalid`);
|
||||
|
||||
error.name = 'Svelte error';
|
||||
|
||||
throw error;
|
||||
}
|
||||
|
||||
/**
|
||||
* The `html` property of server render results has been deprecated. Use `body` instead.
|
||||
* @returns {never}
|
||||
*/
|
||||
export function html_deprecated() {
|
||||
const error = new Error(`html_deprecated\nThe \`html\` property of server render results has been deprecated. Use \`body\` instead.\nhttps://svelte.dev/e/html_deprecated`);
|
||||
|
||||
error.name = 'Svelte error';
|
||||
|
||||
throw error;
|
||||
}
|
||||
|
||||
/**
|
||||
* Attempted to set `hydratable` with key `%key%` twice with different values.
|
||||
*
|
||||
* %stack%
|
||||
* @param {string} key
|
||||
* @param {string} stack
|
||||
* @returns {never}
|
||||
*/
|
||||
export function hydratable_clobbering(key, stack) {
|
||||
const error = new Error(`hydratable_clobbering\nAttempted to set \`hydratable\` with key \`${key}\` twice with different values.
|
||||
|
||||
${stack}\nhttps://svelte.dev/e/hydratable_clobbering`);
|
||||
|
||||
error.name = 'Svelte error';
|
||||
|
||||
throw error;
|
||||
}
|
||||
|
||||
/**
|
||||
* Failed to serialize `hydratable` data for key `%key%`.
|
||||
*
|
||||
* `hydratable` can serialize anything [`uneval` from `devalue`](https://npmjs.com/package/uneval) can, plus Promises.
|
||||
*
|
||||
* Cause:
|
||||
* %stack%
|
||||
* @param {string} key
|
||||
* @param {string} stack
|
||||
* @returns {never}
|
||||
*/
|
||||
export function hydratable_serialization_failed(key, stack) {
|
||||
const error = new Error(`hydratable_serialization_failed\nFailed to serialize \`hydratable\` data for key \`${key}\`.
|
||||
|
||||
\`hydratable\` can serialize anything [\`uneval\` from \`devalue\`](https://npmjs.com/package/uneval) can, plus Promises.
|
||||
|
||||
Cause:
|
||||
${stack}\nhttps://svelte.dev/e/hydratable_serialization_failed`);
|
||||
|
||||
error.name = 'Svelte error';
|
||||
|
||||
throw error;
|
||||
}
|
||||
|
||||
/**
|
||||
* `csp.nonce` was set while `csp.hash` was `true`. These options cannot be used simultaneously.
|
||||
* @returns {never}
|
||||
*/
|
||||
export function invalid_csp() {
|
||||
const error = new Error(`invalid_csp\n\`csp.nonce\` was set while \`csp.hash\` was \`true\`. These options cannot be used simultaneously.\nhttps://svelte.dev/e/invalid_csp`);
|
||||
|
||||
error.name = 'Svelte error';
|
||||
|
||||
throw error;
|
||||
}
|
||||
|
||||
/**
|
||||
* `%name%(...)` is not available on the server
|
||||
* @param {string} name
|
||||
* @returns {never}
|
||||
*/
|
||||
export function lifecycle_function_unavailable(name) {
|
||||
const error = new Error(`lifecycle_function_unavailable\n\`${name}(...)\` is not available on the server\nhttps://svelte.dev/e/lifecycle_function_unavailable`);
|
||||
|
||||
error.name = 'Svelte error';
|
||||
|
||||
throw error;
|
||||
}
|
||||
|
||||
/**
|
||||
* Could not resolve `render` context.
|
||||
* @returns {never}
|
||||
*/
|
||||
export function server_context_required() {
|
||||
const error = new Error(`server_context_required\nCould not resolve \`render\` context.\nhttps://svelte.dev/e/server_context_required`);
|
||||
|
||||
error.name = 'Svelte error';
|
||||
|
||||
throw error;
|
||||
}
|
||||
+142
@@ -0,0 +1,142 @@
|
||||
/** @import { HydratableLookupEntry } from '#server' */
|
||||
import { async_mode_flag } from '../flags/index.js';
|
||||
import { get_render_context } from './render-context.js';
|
||||
import * as e from './errors.js';
|
||||
import * as devalue from 'devalue';
|
||||
import { get_stack } from '../shared/dev.js';
|
||||
import { DEV } from 'esm-env';
|
||||
import { get_user_code_location } from './dev.js';
|
||||
|
||||
/**
|
||||
* @template T
|
||||
* @param {string} key
|
||||
* @param {() => T} fn
|
||||
* @returns {T}
|
||||
*/
|
||||
export function hydratable(key, fn) {
|
||||
if (!async_mode_flag) {
|
||||
e.experimental_async_required('hydratable');
|
||||
}
|
||||
|
||||
const { hydratable } = get_render_context();
|
||||
|
||||
let entry = hydratable.lookup.get(key);
|
||||
|
||||
if (entry !== undefined) {
|
||||
if (DEV) {
|
||||
const comparison = compare(key, entry, encode(key, fn()));
|
||||
comparison.catch(() => {});
|
||||
hydratable.comparisons.push(comparison);
|
||||
}
|
||||
|
||||
return /** @type {T} */ (entry.value);
|
||||
}
|
||||
|
||||
const value = fn();
|
||||
|
||||
entry = encode(key, value, hydratable.unresolved_promises);
|
||||
hydratable.lookup.set(key, entry);
|
||||
|
||||
return value;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} key
|
||||
* @param {any} value
|
||||
* @param {Map<Promise<any>, string>} [unresolved]
|
||||
*/
|
||||
function encode(key, value, unresolved) {
|
||||
/** @type {HydratableLookupEntry} */
|
||||
const entry = { value, serialized: '' };
|
||||
|
||||
if (DEV) {
|
||||
entry.stack = get_user_code_location();
|
||||
}
|
||||
|
||||
let uid = 1;
|
||||
|
||||
entry.serialized = devalue.uneval(entry.value, (value, uneval) => {
|
||||
if (is_promise(value)) {
|
||||
// we serialize promises as `"${i}"`, because it's impossible for that string
|
||||
// to occur 'naturally' (since the quote marks would have to be escaped)
|
||||
// this placeholder is returned synchronously from `uneval`, which includes it in the
|
||||
// serialized string. Later (at least one microtask from now), when `p.then` runs, it'll
|
||||
// be replaced.
|
||||
const placeholder = `"${uid++}"`;
|
||||
const p = value
|
||||
.then((v) => {
|
||||
entry.serialized = entry.serialized.replace(placeholder, `r(${uneval(v)})`);
|
||||
})
|
||||
.catch((devalue_error) =>
|
||||
e.hydratable_serialization_failed(
|
||||
key,
|
||||
serialization_stack(entry.stack, devalue_error?.stack)
|
||||
)
|
||||
);
|
||||
|
||||
unresolved?.set(p, key);
|
||||
// prevent unhandled rejections from crashing the server, track which promises are still resolving when render is complete
|
||||
p.catch(() => {}).finally(() => unresolved?.delete(p));
|
||||
|
||||
(entry.promises ??= []).push(p);
|
||||
return placeholder;
|
||||
}
|
||||
});
|
||||
|
||||
return entry;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {any} value
|
||||
* @returns {value is Promise<any>}
|
||||
*/
|
||||
function is_promise(value) {
|
||||
// we use this check rather than `instanceof Promise`
|
||||
// because it works cross-realm
|
||||
return Object.prototype.toString.call(value) === '[object Promise]';
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} key
|
||||
* @param {HydratableLookupEntry} a
|
||||
* @param {HydratableLookupEntry} b
|
||||
*/
|
||||
async function compare(key, a, b) {
|
||||
// note: these need to be loops (as opposed to Promise.all) because
|
||||
// additional promises can get pushed to them while we're awaiting
|
||||
// an earlier one
|
||||
for (const p of a?.promises ?? []) {
|
||||
await p;
|
||||
}
|
||||
|
||||
for (const p of b?.promises ?? []) {
|
||||
await p;
|
||||
}
|
||||
|
||||
if (a.serialized !== b.serialized) {
|
||||
const a_stack = /** @type {string} */ (a.stack);
|
||||
const b_stack = /** @type {string} */ (b.stack);
|
||||
|
||||
const stack =
|
||||
a_stack === b_stack
|
||||
? `Occurred at:\n${a_stack}`
|
||||
: `First occurrence at:\n${a_stack}\n\nSecond occurrence at:\n${b_stack}`;
|
||||
|
||||
e.hydratable_clobbering(key, stack);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string | undefined} root_stack
|
||||
* @param {string | undefined} uneval_stack
|
||||
*/
|
||||
function serialization_stack(root_stack, uneval_stack) {
|
||||
let out = '';
|
||||
if (root_stack) {
|
||||
out += root_stack + '\n';
|
||||
}
|
||||
if (uneval_stack) {
|
||||
out += 'Caused by:\n' + uneval_stack + '\n';
|
||||
}
|
||||
return out || '<missing stack trace>';
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
import { HYDRATION_END, HYDRATION_START, HYDRATION_START_ELSE } from '../../constants.js';
|
||||
|
||||
export const BLOCK_OPEN = `<!--${HYDRATION_START}-->`;
|
||||
export const BLOCK_OPEN_ELSE = `<!--${HYDRATION_START_ELSE}-->`;
|
||||
export const BLOCK_CLOSE = `<!--${HYDRATION_END}-->`;
|
||||
export const EMPTY_COMMENT = `<!---->`;
|
||||
+490
@@ -0,0 +1,490 @@
|
||||
/** @import { ComponentType, SvelteComponent, Component } from 'svelte' */
|
||||
/** @import { Csp, RenderOutput } from '#server' */
|
||||
/** @import { Store } from '#shared' */
|
||||
export { FILENAME, HMR } from '../../constants.js';
|
||||
import { attr, clsx, to_class, to_style } from '../shared/attributes.js';
|
||||
import { is_promise, noop } from '../shared/utils.js';
|
||||
import { subscribe_to_store } from '../../store/utils.js';
|
||||
import {
|
||||
UNINITIALIZED,
|
||||
ELEMENT_PRESERVE_ATTRIBUTE_CASE,
|
||||
ELEMENT_IS_NAMESPACED,
|
||||
ELEMENT_IS_INPUT
|
||||
} from '../../constants.js';
|
||||
import { escape_html } from '../../escaping.js';
|
||||
import { DEV } from 'esm-env';
|
||||
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 { Renderer } from './renderer.js';
|
||||
import * as e from './errors.js';
|
||||
|
||||
// https://html.spec.whatwg.org/multipage/syntax.html#attributes-2
|
||||
// https://infra.spec.whatwg.org/#noncharacter
|
||||
const INVALID_ATTR_NAME_CHAR_REGEX =
|
||||
/[\s'">/=\u{FDD0}-\u{FDEF}\u{FFFE}\u{FFFF}\u{1FFFE}\u{1FFFF}\u{2FFFE}\u{2FFFF}\u{3FFFE}\u{3FFFF}\u{4FFFE}\u{4FFFF}\u{5FFFE}\u{5FFFF}\u{6FFFE}\u{6FFFF}\u{7FFFE}\u{7FFFF}\u{8FFFE}\u{8FFFF}\u{9FFFE}\u{9FFFF}\u{AFFFE}\u{AFFFF}\u{BFFFE}\u{BFFFF}\u{CFFFE}\u{CFFFF}\u{DFFFE}\u{DFFFF}\u{EFFFE}\u{EFFFF}\u{FFFFE}\u{FFFFF}\u{10FFFE}\u{10FFFF}]/u;
|
||||
|
||||
/**
|
||||
* @param {Renderer} renderer
|
||||
* @param {string} tag
|
||||
* @param {() => void} attributes_fn
|
||||
* @param {() => void} children_fn
|
||||
* @returns {void}
|
||||
*/
|
||||
export function element(renderer, tag, attributes_fn = noop, children_fn = noop) {
|
||||
renderer.push('<!---->');
|
||||
|
||||
if (tag) {
|
||||
renderer.push(`<${tag}`);
|
||||
attributes_fn();
|
||||
renderer.push(`>`);
|
||||
|
||||
if (!is_void(tag)) {
|
||||
children_fn();
|
||||
if (!is_raw_text_element(tag)) {
|
||||
renderer.push(EMPTY_COMMENT);
|
||||
}
|
||||
renderer.push(`</${tag}>`);
|
||||
}
|
||||
}
|
||||
|
||||
renderer.push('<!---->');
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 {Component<Props> | ComponentType<SvelteComponent<Props>>} component
|
||||
* @param {{ props?: Omit<Props, '$$slots' | '$$events'>; context?: Map<any, any>; idPrefix?: string; csp?: Csp }} [options]
|
||||
* @returns {RenderOutput}
|
||||
*/
|
||||
export function render(component, options = {}) {
|
||||
if (options.csp?.hash && options.csp.nonce) {
|
||||
e.invalid_csp();
|
||||
}
|
||||
return Renderer.render(/** @type {Component<Props>} */ (component), options);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} hash
|
||||
* @param {Renderer} renderer
|
||||
* @param {(renderer: Renderer) => Promise<void> | void} fn
|
||||
* @returns {void}
|
||||
*/
|
||||
export function head(hash, renderer, fn) {
|
||||
renderer.head((renderer) => {
|
||||
renderer.push(`<!--${hash}-->`);
|
||||
renderer.child(fn);
|
||||
renderer.push(EMPTY_COMMENT);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Renderer} renderer
|
||||
* @param {boolean} is_html
|
||||
* @param {Record<string, string>} props
|
||||
* @param {() => void} component
|
||||
* @param {boolean} dynamic
|
||||
* @returns {void}
|
||||
*/
|
||||
export function css_props(renderer, is_html, props, component, dynamic = false) {
|
||||
const styles = style_object_to_string(props);
|
||||
|
||||
if (is_html) {
|
||||
renderer.push(`<svelte-css-wrapper style="display: contents; ${styles}">`);
|
||||
} else {
|
||||
renderer.push(`<g style="${styles}">`);
|
||||
}
|
||||
|
||||
if (dynamic) {
|
||||
renderer.push('<!---->');
|
||||
}
|
||||
|
||||
component();
|
||||
|
||||
if (is_html) {
|
||||
renderer.push(`<!----></svelte-css-wrapper>`);
|
||||
} else {
|
||||
renderer.push(`<!----></g>`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Record<string, unknown>} attrs
|
||||
* @param {string} [css_hash]
|
||||
* @param {Record<string, boolean>} [classes]
|
||||
* @param {Record<string, string>} [styles]
|
||||
* @param {number} [flags]
|
||||
* @returns {string}
|
||||
*/
|
||||
export function attributes(attrs, css_hash, classes, styles, flags = 0) {
|
||||
if (styles) {
|
||||
attrs.style = to_style(attrs.style, styles);
|
||||
}
|
||||
|
||||
if (attrs.class) {
|
||||
attrs.class = clsx(attrs.class);
|
||||
}
|
||||
|
||||
if (css_hash || classes) {
|
||||
attrs.class = to_class(attrs.class, css_hash, classes);
|
||||
}
|
||||
|
||||
let attr_str = '';
|
||||
let name;
|
||||
|
||||
const is_html = (flags & ELEMENT_IS_NAMESPACED) === 0;
|
||||
const lowercase = (flags & ELEMENT_PRESERVE_ATTRIBUTE_CASE) === 0;
|
||||
const is_input = (flags & ELEMENT_IS_INPUT) !== 0;
|
||||
|
||||
for (name in attrs) {
|
||||
// omit functions, internal svelte properties and invalid attribute names
|
||||
if (typeof attrs[name] === 'function') continue;
|
||||
if (name[0] === '$' && name[1] === '$') continue; // faster than name.startsWith('$$')
|
||||
if (INVALID_ATTR_NAME_CHAR_REGEX.test(name)) continue;
|
||||
|
||||
var value = attrs[name];
|
||||
|
||||
if (lowercase) {
|
||||
name = name.toLowerCase();
|
||||
}
|
||||
|
||||
if (is_input) {
|
||||
if (name === 'defaultvalue' || name === 'defaultchecked') {
|
||||
name = name === 'defaultvalue' ? 'value' : 'checked';
|
||||
if (attrs[name]) continue;
|
||||
}
|
||||
}
|
||||
|
||||
attr_str += attr(name, value, is_html && is_boolean_attribute(name));
|
||||
}
|
||||
|
||||
return attr_str;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Record<string, unknown>[]} props
|
||||
* @returns {Record<string, unknown>}
|
||||
*/
|
||||
export function spread_props(props) {
|
||||
/** @type {Record<string, unknown>} */
|
||||
const merged_props = {};
|
||||
let key;
|
||||
|
||||
for (let i = 0; i < props.length; i++) {
|
||||
const obj = props[i];
|
||||
for (key in obj) {
|
||||
const desc = Object.getOwnPropertyDescriptor(obj, key);
|
||||
if (desc) {
|
||||
Object.defineProperty(merged_props, key, desc);
|
||||
} else {
|
||||
merged_props[key] = obj[key];
|
||||
}
|
||||
}
|
||||
}
|
||||
return merged_props;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {unknown} value
|
||||
* @returns {string}
|
||||
*/
|
||||
export function stringify(value) {
|
||||
return typeof value === 'string' ? value : value == null ? '' : value + '';
|
||||
}
|
||||
|
||||
/** @param {Record<string, string>} style_object */
|
||||
function style_object_to_string(style_object) {
|
||||
return Object.keys(style_object)
|
||||
.filter(/** @param {any} key */ (key) => style_object[key] != null && style_object[key] !== '')
|
||||
.map(/** @param {any} key */ (key) => `${key}: ${escape_html(style_object[key], true)};`)
|
||||
.join(' ');
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {any} value
|
||||
* @param {string | undefined} [hash]
|
||||
* @param {Record<string, boolean>} [directives]
|
||||
*/
|
||||
export function attr_class(value, hash, directives) {
|
||||
var result = to_class(value, hash, directives);
|
||||
return result ? ` class="${escape_html(result, true)}"` : '';
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {any} value
|
||||
* @param {Record<string,any>|[Record<string,any>,Record<string,any>]} [directives]
|
||||
*/
|
||||
export function attr_style(value, directives) {
|
||||
var result = to_style(value, directives);
|
||||
return result ? ` style="${escape_html(result, true)}"` : '';
|
||||
}
|
||||
|
||||
/**
|
||||
* @template V
|
||||
* @param {Record<string, [any, any, any]>} store_values
|
||||
* @param {string} store_name
|
||||
* @param {Store<V> | null | undefined} store
|
||||
* @returns {V}
|
||||
*/
|
||||
export function store_get(store_values, store_name, store) {
|
||||
if (DEV) {
|
||||
validate_store(store, store_name.slice(1));
|
||||
}
|
||||
|
||||
// it could be that someone eagerly updates the store in the instance script, so
|
||||
// we should only reuse the store value in the template
|
||||
if (store_name in store_values && store_values[store_name][0] === store) {
|
||||
return store_values[store_name][2];
|
||||
}
|
||||
|
||||
store_values[store_name]?.[1](); // if store was switched, unsubscribe from old store
|
||||
store_values[store_name] = [store, null, undefined];
|
||||
const unsub = subscribe_to_store(
|
||||
store,
|
||||
/** @param {any} v */ (v) => (store_values[store_name][2] = v)
|
||||
);
|
||||
store_values[store_name][1] = unsub;
|
||||
return store_values[store_name][2];
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the new value of a store and returns that value.
|
||||
* @template V
|
||||
* @param {Store<V>} store
|
||||
* @param {V} value
|
||||
* @returns {V}
|
||||
*/
|
||||
export function store_set(store, value) {
|
||||
store.set(value);
|
||||
return value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates a store with a new value.
|
||||
* @template V
|
||||
* @param {Record<string, [any, any, any]>} store_values
|
||||
* @param {string} store_name
|
||||
* @param {Store<V>} store
|
||||
* @param {any} expression
|
||||
*/
|
||||
export function store_mutate(store_values, store_name, store, expression) {
|
||||
store_set(store, store_get(store_values, store_name, store));
|
||||
return expression;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Record<string, [any, any, any]>} store_values
|
||||
* @param {string} store_name
|
||||
* @param {Store<number>} store
|
||||
* @param {1 | -1} [d]
|
||||
* @returns {number}
|
||||
*/
|
||||
export function update_store(store_values, store_name, store, d = 1) {
|
||||
let store_value = store_get(store_values, store_name, store);
|
||||
store.set(store_value + d);
|
||||
return store_value;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Record<string, [any, any, any]>} store_values
|
||||
* @param {string} store_name
|
||||
* @param {Store<number>} store
|
||||
* @param {1 | -1} [d]
|
||||
* @returns {number}
|
||||
*/
|
||||
export function update_store_pre(store_values, store_name, store, d = 1) {
|
||||
const value = store_get(store_values, store_name, store) + d;
|
||||
store.set(value);
|
||||
return value;
|
||||
}
|
||||
|
||||
/** @param {Record<string, [any, any, any]>} store_values */
|
||||
export function unsubscribe_stores(store_values) {
|
||||
for (const store_name in store_values) {
|
||||
store_values[store_name][1]();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Renderer} renderer
|
||||
* @param {Record<string, any>} $$props
|
||||
* @param {string} name
|
||||
* @param {Record<string, unknown>} slot_props
|
||||
* @param {null | (() => void)} fallback_fn
|
||||
* @returns {void}
|
||||
*/
|
||||
export function slot(renderer, $$props, name, slot_props, fallback_fn) {
|
||||
var slot_fn = $$props.$$slots?.[name];
|
||||
// Interop: Can use snippets to fill slots
|
||||
if (slot_fn === true) {
|
||||
slot_fn = $$props[name === 'default' ? 'children' : name];
|
||||
}
|
||||
|
||||
if (slot_fn !== undefined) {
|
||||
slot_fn(renderer, slot_props);
|
||||
} else {
|
||||
fallback_fn?.();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Record<string, unknown>} props
|
||||
* @param {string[]} rest
|
||||
* @returns {Record<string, unknown>}
|
||||
*/
|
||||
export function rest_props(props, rest) {
|
||||
/** @type {Record<string, unknown>} */
|
||||
const rest_props = {};
|
||||
let key;
|
||||
for (key in props) {
|
||||
if (!rest.includes(key)) {
|
||||
rest_props[key] = props[key];
|
||||
}
|
||||
}
|
||||
return rest_props;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Record<string, unknown>} props
|
||||
* @returns {Record<string, unknown>}
|
||||
*/
|
||||
export function sanitize_props(props) {
|
||||
const { children, $$slots, ...sanitized } = props;
|
||||
return sanitized;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Record<string, any>} props
|
||||
* @returns {Record<string, boolean>}
|
||||
*/
|
||||
export function sanitize_slots(props) {
|
||||
/** @type {Record<string, boolean>} */
|
||||
const sanitized = {};
|
||||
if (props.children) sanitized.default = true;
|
||||
for (const key in props.$$slots) {
|
||||
sanitized[key] = true;
|
||||
}
|
||||
return sanitized;
|
||||
}
|
||||
|
||||
/**
|
||||
* Legacy mode: If the prop has a fallback and is bound in the
|
||||
* parent component, propagate the fallback value upwards.
|
||||
* @param {Record<string, unknown>} props_parent
|
||||
* @param {Record<string, unknown>} props_now
|
||||
*/
|
||||
export function bind_props(props_parent, props_now) {
|
||||
for (const key in props_now) {
|
||||
const initial_value = props_parent[key];
|
||||
const value = props_now[key];
|
||||
if (
|
||||
initial_value === undefined &&
|
||||
value !== undefined &&
|
||||
Object.getOwnPropertyDescriptor(props_parent, key)?.set
|
||||
) {
|
||||
props_parent[key] = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @template V
|
||||
* @param {Renderer} renderer
|
||||
* @param {Promise<V>} promise
|
||||
* @param {null | (() => void)} pending_fn
|
||||
* @param {(value: V) => void} then_fn
|
||||
* @returns {void}
|
||||
*/
|
||||
function await_block(renderer, promise, pending_fn, then_fn) {
|
||||
if (is_promise(promise)) {
|
||||
renderer.push(BLOCK_OPEN);
|
||||
promise.then(null, noop);
|
||||
if (pending_fn !== null) {
|
||||
pending_fn();
|
||||
}
|
||||
} else if (then_fn !== null) {
|
||||
renderer.push(BLOCK_OPEN_ELSE);
|
||||
then_fn(promise);
|
||||
}
|
||||
}
|
||||
|
||||
export { await_block as await };
|
||||
|
||||
/** @param {any} array_like_or_iterator */
|
||||
export function ensure_array_like(array_like_or_iterator) {
|
||||
if (array_like_or_iterator) {
|
||||
return array_like_or_iterator.length !== undefined
|
||||
? array_like_or_iterator
|
||||
: Array.from(array_like_or_iterator);
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
/**
|
||||
* @template V
|
||||
* @param {() => V} get_value
|
||||
*/
|
||||
export function once(get_value) {
|
||||
let value = /** @type {V} */ (UNINITIALIZED);
|
||||
return () => {
|
||||
if (value === UNINITIALIZED) {
|
||||
value = get_value();
|
||||
}
|
||||
return value;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Create an unique ID
|
||||
* @param {Renderer} renderer
|
||||
* @returns {string}
|
||||
*/
|
||||
export function props_id(renderer) {
|
||||
const uid = renderer.global.uid();
|
||||
renderer.push('<!--$' + uid + '-->');
|
||||
return uid;
|
||||
}
|
||||
|
||||
export { attr, clsx };
|
||||
|
||||
export { html } from './blocks/html.js';
|
||||
|
||||
export { save } from './context.js';
|
||||
|
||||
export { push_element, pop_element, validate_snippet_args } from './dev.js';
|
||||
|
||||
export { snapshot } from '../shared/clone.js';
|
||||
|
||||
export { fallback, to_array } from '../shared/utils.js';
|
||||
|
||||
export {
|
||||
invalid_default_snippet,
|
||||
validate_dynamic_element_tag,
|
||||
validate_void_dynamic_element,
|
||||
prevent_snippet_stringification
|
||||
} from '../shared/validate.js';
|
||||
|
||||
export { escape_html as escape };
|
||||
|
||||
/**
|
||||
* @template T
|
||||
* @param {()=>T} fn
|
||||
* @returns {(new_value?: T) => (T | void)}
|
||||
*/
|
||||
export function derived(fn) {
|
||||
const get_value = once(fn);
|
||||
/**
|
||||
* @type {T | undefined}
|
||||
*/
|
||||
let updated_value;
|
||||
|
||||
return function (new_value) {
|
||||
if (arguments.length === 0) {
|
||||
return updated_value ?? get_value();
|
||||
}
|
||||
updated_value = new_value;
|
||||
return updated_value;
|
||||
};
|
||||
}
|
||||
+86
@@ -0,0 +1,86 @@
|
||||
// @ts-ignore -- we don't include node types in the production build
|
||||
/** @import { AsyncLocalStorage } from 'node:async_hooks' */
|
||||
/** @import { RenderContext } from '#server' */
|
||||
|
||||
import { deferred, noop } from '../shared/utils.js';
|
||||
import * as e from './errors.js';
|
||||
|
||||
/** @type {Promise<void> | null} */
|
||||
let current_render = null;
|
||||
|
||||
/** @type {RenderContext | null} */
|
||||
let context = null;
|
||||
|
||||
/** @returns {RenderContext} */
|
||||
export function get_render_context() {
|
||||
const store = context ?? als?.getStore();
|
||||
|
||||
if (!store) {
|
||||
e.server_context_required();
|
||||
}
|
||||
|
||||
return store;
|
||||
}
|
||||
|
||||
/**
|
||||
* @template T
|
||||
* @param {() => Promise<T>} fn
|
||||
* @returns {Promise<T>}
|
||||
*/
|
||||
export async function with_render_context(fn) {
|
||||
context = {
|
||||
hydratable: {
|
||||
lookup: new Map(),
|
||||
comparisons: [],
|
||||
unresolved_promises: new Map()
|
||||
}
|
||||
};
|
||||
|
||||
if (in_webcontainer()) {
|
||||
const { promise, resolve } = deferred();
|
||||
const previous_render = current_render;
|
||||
current_render = promise;
|
||||
await previous_render;
|
||||
return fn().finally(resolve);
|
||||
}
|
||||
|
||||
try {
|
||||
if (als === null) {
|
||||
e.async_local_storage_unavailable();
|
||||
}
|
||||
return als.run(context, fn);
|
||||
} finally {
|
||||
context = null;
|
||||
}
|
||||
}
|
||||
|
||||
/** @type {AsyncLocalStorage<RenderContext | null> | null} */
|
||||
let als = null;
|
||||
/** @type {Promise<void> | null} */
|
||||
let als_import = null;
|
||||
|
||||
/**
|
||||
*
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
export function init_render_context() {
|
||||
// It's important the right side of this assignment can run a maximum of one time
|
||||
// otherwise it's possible for a very, very well-timed race condition to assign to `als`
|
||||
// at the beginning of a render, and then another render to assign to it again, which causes
|
||||
// the first render's second half to use a new instance of `als` which doesn't have its
|
||||
// context anymore.
|
||||
// @ts-ignore -- we don't include node types in the production build
|
||||
als_import ??= import('node:async_hooks')
|
||||
.then((hooks) => {
|
||||
als = new hooks.AsyncLocalStorage();
|
||||
})
|
||||
.then(noop, noop);
|
||||
return als_import;
|
||||
}
|
||||
|
||||
// this has to be a function because rollup won't treeshake it if it's a constant
|
||||
function in_webcontainer() {
|
||||
// @ts-ignore -- this will fail when we run typecheck because we exclude node types
|
||||
// eslint-disable-next-line n/prefer-global/process
|
||||
return !!globalThis.process?.versions?.webcontainer;
|
||||
}
|
||||
+770
@@ -0,0 +1,770 @@
|
||||
/** @import { Component } from 'svelte' */
|
||||
/** @import { Csp, HydratableContext, RenderOutput, SSRContext, SyncRenderOutput, Sha256Source } from './types.js' */
|
||||
/** @import { MaybePromise } from '#shared' */
|
||||
import { async_mode_flag } from '../flags/index.js';
|
||||
import { abort } from './abort-signal.js';
|
||||
import { pop, push, set_ssr_context, ssr_context, save } from './context.js';
|
||||
import * as e from './errors.js';
|
||||
import * as w from './warnings.js';
|
||||
import { BLOCK_CLOSE, BLOCK_OPEN } from './hydration.js';
|
||||
import { attributes } from './index.js';
|
||||
import { get_render_context, with_render_context, init_render_context } from './render-context.js';
|
||||
import { sha256 } from './crypto.js';
|
||||
import * as devalue from 'devalue';
|
||||
|
||||
/** @typedef {'head' | 'body'} RendererType */
|
||||
/** @typedef {{ [key in RendererType]: string }} AccumulatedContent */
|
||||
|
||||
/**
|
||||
* @typedef {string | Renderer} RendererItem
|
||||
*/
|
||||
|
||||
/**
|
||||
* Renderers are basically a tree of `string | Renderer`s, where each `Renderer` in the tree represents
|
||||
* work that may or may not have completed. A renderer can be {@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. To asynchronously collect a renderer, just `await` it.
|
||||
*
|
||||
* The `string` values within a renderer are always associated with the {@link type} of that renderer. To switch types,
|
||||
* call {@link child} with a different `type` argument.
|
||||
*/
|
||||
export class Renderer {
|
||||
/**
|
||||
* The contents of the renderer.
|
||||
* @type {RendererItem[]}
|
||||
*/
|
||||
#out = [];
|
||||
|
||||
/**
|
||||
* Any `onDestroy` callbacks registered during execution of this renderer.
|
||||
* @type {(() => void)[] | undefined}
|
||||
*/
|
||||
#on_destroy = undefined;
|
||||
|
||||
/**
|
||||
* Whether this renderer is a component body.
|
||||
* @type {boolean}
|
||||
*/
|
||||
#is_component_body = false;
|
||||
|
||||
/**
|
||||
* The type of string content that this renderer is accumulating.
|
||||
* @type {RendererType}
|
||||
*/
|
||||
type;
|
||||
|
||||
/** @type {Renderer | undefined} */
|
||||
#parent;
|
||||
|
||||
/**
|
||||
* Asynchronous work associated with this renderer
|
||||
* @type {Promise<void> | undefined}
|
||||
*/
|
||||
promise = undefined;
|
||||
|
||||
/**
|
||||
* State which is associated with the content tree as a whole.
|
||||
* It will be re-exposed, uncopied, on all children.
|
||||
* @type {SSRState}
|
||||
* @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 {SSRState} global
|
||||
* @param {Renderer | undefined} [parent]
|
||||
*/
|
||||
constructor(global, parent) {
|
||||
this.#parent = parent;
|
||||
|
||||
this.global = global;
|
||||
this.local = parent ? { ...parent.local } : { select_value: undefined };
|
||||
this.type = parent ? parent.type : 'body';
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {(renderer: Renderer) => void} fn
|
||||
*/
|
||||
head(fn) {
|
||||
const head = new Renderer(this.global, this);
|
||||
head.type = 'head';
|
||||
|
||||
this.#out.push(head);
|
||||
head.child(fn);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Array<Promise<void>>} blockers
|
||||
* @param {(renderer: Renderer) => void} fn
|
||||
*/
|
||||
async_block(blockers, fn) {
|
||||
this.#out.push(BLOCK_OPEN);
|
||||
this.async(blockers, fn);
|
||||
this.#out.push(BLOCK_CLOSE);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Array<Promise<void>>} blockers
|
||||
* @param {(renderer: Renderer) => void} fn
|
||||
*/
|
||||
async(blockers, fn) {
|
||||
let callback = fn;
|
||||
|
||||
if (blockers.length > 0) {
|
||||
const context = ssr_context;
|
||||
|
||||
callback = (renderer) => {
|
||||
return Promise.all(blockers).then(() => {
|
||||
const previous_context = ssr_context;
|
||||
|
||||
try {
|
||||
set_ssr_context(context);
|
||||
return fn(renderer);
|
||||
} finally {
|
||||
set_ssr_context(previous_context);
|
||||
}
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
this.child(callback);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Array<() => void>} thunks
|
||||
*/
|
||||
run(thunks) {
|
||||
const context = ssr_context;
|
||||
|
||||
let promise = Promise.resolve(thunks[0]());
|
||||
const promises = [promise];
|
||||
|
||||
for (const fn of thunks.slice(1)) {
|
||||
promise = promise.then(() => {
|
||||
const previous_context = ssr_context;
|
||||
set_ssr_context(context);
|
||||
|
||||
try {
|
||||
return fn();
|
||||
} finally {
|
||||
set_ssr_context(previous_context);
|
||||
}
|
||||
});
|
||||
|
||||
promises.push(promise);
|
||||
}
|
||||
|
||||
return promises;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a child renderer. The child renderer inherits the state from the parent,
|
||||
* but has its own content.
|
||||
* @param {(renderer: Renderer) => MaybePromise<void>} fn
|
||||
*/
|
||||
child(fn) {
|
||||
const child = new Renderer(this.global, this);
|
||||
this.#out.push(child);
|
||||
|
||||
const parent = ssr_context;
|
||||
|
||||
set_ssr_context({
|
||||
...ssr_context,
|
||||
p: parent,
|
||||
c: null,
|
||||
r: child
|
||||
});
|
||||
|
||||
const result = fn(child);
|
||||
|
||||
set_ssr_context(parent);
|
||||
|
||||
if (result instanceof Promise) {
|
||||
if (child.global.mode === 'sync') {
|
||||
e.await_invalid();
|
||||
}
|
||||
// just to avoid unhandled promise rejections -- we'll end up throwing in `collect_async` if something fails
|
||||
result.catch(() => {});
|
||||
child.promise = result;
|
||||
}
|
||||
|
||||
return child;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a component renderer. The component renderer inherits the state from the parent,
|
||||
* but has its own content. It is treated as an ordering boundary for ondestroy callbacks.
|
||||
* @param {(renderer: Renderer) => MaybePromise<void>} fn
|
||||
* @param {Function} [component_fn]
|
||||
* @returns {void}
|
||||
*/
|
||||
component(fn, component_fn) {
|
||||
push(component_fn);
|
||||
const child = this.child(fn);
|
||||
child.#is_component_body = true;
|
||||
pop();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Record<string, any>} attrs
|
||||
* @param {(renderer: Renderer) => void} fn
|
||||
* @param {string | undefined} [css_hash]
|
||||
* @param {Record<string, boolean> | undefined} [classes]
|
||||
* @param {Record<string, string> | undefined} [styles]
|
||||
* @param {number | undefined} [flags]
|
||||
* @param {boolean | undefined} [is_rich]
|
||||
* @returns {void}
|
||||
*/
|
||||
select(attrs, fn, css_hash, classes, styles, flags, is_rich) {
|
||||
const { value, ...select_attrs } = attrs;
|
||||
|
||||
this.push(`<select${attributes(select_attrs, css_hash, classes, styles, flags)}>`);
|
||||
this.child((renderer) => {
|
||||
renderer.local.select_value = value;
|
||||
fn(renderer);
|
||||
});
|
||||
this.push(`${is_rich ? '<!>' : ''}</select>`);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Record<string, any>} attrs
|
||||
* @param {string | number | boolean | ((renderer: Renderer) => void)} body
|
||||
* @param {string | undefined} [css_hash]
|
||||
* @param {Record<string, boolean> | undefined} [classes]
|
||||
* @param {Record<string, string> | undefined} [styles]
|
||||
* @param {number | undefined} [flags]
|
||||
* @param {boolean | undefined} [is_rich]
|
||||
*/
|
||||
option(attrs, body, css_hash, classes, styles, flags, is_rich) {
|
||||
this.#out.push(`<option${attributes(attrs, css_hash, classes, styles, flags)}`);
|
||||
|
||||
/**
|
||||
* @param {Renderer} renderer
|
||||
* @param {any} value
|
||||
* @param {{ head?: string, body: any }} content
|
||||
*/
|
||||
const close = (renderer, value, { head, body }) => {
|
||||
if ('value' in attrs) {
|
||||
value = attrs.value;
|
||||
}
|
||||
|
||||
if (value === this.local.select_value) {
|
||||
renderer.#out.push(' selected');
|
||||
}
|
||||
|
||||
renderer.#out.push(`>${body}${is_rich ? '<!>' : ''}</option>`);
|
||||
|
||||
// super edge case, but may as well handle it
|
||||
if (head) {
|
||||
renderer.head((child) => child.push(head));
|
||||
}
|
||||
};
|
||||
|
||||
if (typeof body === 'function') {
|
||||
this.child((renderer) => {
|
||||
const r = new Renderer(this.global, this);
|
||||
body(r);
|
||||
|
||||
if (this.global.mode === 'async') {
|
||||
return r.#collect_content_async().then((content) => {
|
||||
close(renderer, content.body.replaceAll('<!---->', ''), content);
|
||||
});
|
||||
} else {
|
||||
const content = r.#collect_content();
|
||||
close(renderer, content.body.replaceAll('<!---->', ''), content);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
close(this, body, { body });
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {(renderer: Renderer) => void} fn
|
||||
*/
|
||||
title(fn) {
|
||||
const path = this.get_path();
|
||||
|
||||
/** @param {string} head */
|
||||
const close = (head) => {
|
||||
this.global.set_title(head, path);
|
||||
};
|
||||
|
||||
this.child((renderer) => {
|
||||
const r = new Renderer(renderer.global, renderer);
|
||||
fn(r);
|
||||
|
||||
if (renderer.global.mode === 'async') {
|
||||
return r.#collect_content_async().then((content) => {
|
||||
close(content.head);
|
||||
});
|
||||
} else {
|
||||
const content = r.#collect_content();
|
||||
close(content.head);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string | (() => Promise<string>)} content
|
||||
*/
|
||||
push(content) {
|
||||
if (typeof content === 'function') {
|
||||
this.child(async (renderer) => renderer.push(await content()));
|
||||
} else {
|
||||
this.#out.push(content);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {() => void} fn
|
||||
*/
|
||||
on_destroy(fn) {
|
||||
(this.#on_destroy ??= []).push(fn);
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {number[]}
|
||||
*/
|
||||
get_path() {
|
||||
return this.#parent ? [...this.#parent.get_path(), this.#parent.#out.indexOf(this)] : [];
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated this is needed for legacy component bindings
|
||||
*/
|
||||
copy() {
|
||||
const copy = new Renderer(this.global, this.#parent);
|
||||
copy.#out = this.#out.map((item) => (item instanceof Renderer ? item.copy() : item));
|
||||
copy.promise = this.promise;
|
||||
return copy;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Renderer} other
|
||||
* @deprecated this is needed for legacy component bindings
|
||||
*/
|
||||
subsume(other) {
|
||||
if (this.global.mode !== other.global.mode) {
|
||||
throw new Error(
|
||||
"invariant: A renderer cannot switch modes. If you're seeing this, there's a compiler bug. File an issue!"
|
||||
);
|
||||
}
|
||||
|
||||
this.local = other.local;
|
||||
this.#out = other.#out.map((item) => {
|
||||
if (item instanceof Renderer) {
|
||||
item.subsume(item);
|
||||
}
|
||||
return item;
|
||||
});
|
||||
this.promise = other.promise;
|
||||
this.type = other.type;
|
||||
}
|
||||
|
||||
get length() {
|
||||
return this.#out.length;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 {Component<Props>} component
|
||||
* @param {{ props?: Omit<Props, '$$slots' | '$$events'>; context?: Map<any, any>; idPrefix?: string; csp?: Csp }} [options]
|
||||
* @returns {RenderOutput}
|
||||
*/
|
||||
static render(component, options = {}) {
|
||||
/** @type {AccumulatedContent | undefined} */
|
||||
let sync;
|
||||
/** @type {Promise<AccumulatedContent & { hashes: { script: Sha256Source[] } }> | undefined} */
|
||||
let async;
|
||||
|
||||
const result = /** @type {RenderOutput} */ ({});
|
||||
// making these properties non-enumerable so that console.logging
|
||||
// doesn't trigger a sync render
|
||||
Object.defineProperties(result, {
|
||||
html: {
|
||||
get: () => {
|
||||
return (sync ??= Renderer.#render(component, options)).body;
|
||||
}
|
||||
},
|
||||
head: {
|
||||
get: () => {
|
||||
return (sync ??= Renderer.#render(component, options)).head;
|
||||
}
|
||||
},
|
||||
body: {
|
||||
get: () => {
|
||||
return (sync ??= Renderer.#render(component, options)).body;
|
||||
}
|
||||
},
|
||||
hashes: {
|
||||
value: {
|
||||
script: ''
|
||||
}
|
||||
},
|
||||
then: {
|
||||
value:
|
||||
/**
|
||||
* this is not type-safe, but honestly it's the best I can do right now, and it's a straightforward function.
|
||||
*
|
||||
* @template TResult1
|
||||
* @template [TResult2=never]
|
||||
* @param { (value: SyncRenderOutput) => TResult1 } onfulfilled
|
||||
* @param { (reason: unknown) => TResult2 } onrejected
|
||||
*/
|
||||
(onfulfilled, onrejected) => {
|
||||
if (!async_mode_flag) {
|
||||
const result = (sync ??= Renderer.#render(component, options));
|
||||
const user_result = onfulfilled({
|
||||
head: result.head,
|
||||
body: result.body,
|
||||
html: result.body,
|
||||
hashes: { script: [] }
|
||||
});
|
||||
return Promise.resolve(user_result);
|
||||
}
|
||||
async ??= init_render_context().then(() =>
|
||||
with_render_context(() => Renderer.#render_async(component, options))
|
||||
);
|
||||
return async.then((result) => {
|
||||
Object.defineProperty(result, 'html', {
|
||||
// eslint-disable-next-line getter-return
|
||||
get: () => {
|
||||
e.html_deprecated();
|
||||
}
|
||||
});
|
||||
return onfulfilled(/** @type {SyncRenderOutput} */ (result));
|
||||
}, onrejected);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Collect all of the `onDestroy` callbacks registered during rendering. In an async context, this is only safe to call
|
||||
* after awaiting `collect_async`.
|
||||
*
|
||||
* Child renderers are "porous" and don't affect execution order, but component body renderers
|
||||
* create ordering boundaries. Within a renderer, callbacks run in order until hitting a component boundary.
|
||||
* @returns {Iterable<() => void>}
|
||||
*/
|
||||
*#collect_on_destroy() {
|
||||
for (const component of this.#traverse_components()) {
|
||||
yield* component.#collect_ondestroy();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Performs a depth-first search of renderers, yielding the deepest components first, then additional components as we backtrack up the tree.
|
||||
* @returns {Iterable<Renderer>}
|
||||
*/
|
||||
*#traverse_components() {
|
||||
for (const child of this.#out) {
|
||||
if (typeof child !== 'string') {
|
||||
yield* child.#traverse_components();
|
||||
}
|
||||
}
|
||||
if (this.#is_component_body) {
|
||||
yield this;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {Iterable<() => void>}
|
||||
*/
|
||||
*#collect_ondestroy() {
|
||||
if (this.#on_destroy) {
|
||||
for (const fn of this.#on_destroy) {
|
||||
yield fn;
|
||||
}
|
||||
}
|
||||
for (const child of this.#out) {
|
||||
if (child instanceof Renderer && !child.#is_component_body) {
|
||||
yield* child.#collect_ondestroy();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Render a component. Throws if any of the children are performing asynchronous work.
|
||||
*
|
||||
* @template {Record<string, any>} Props
|
||||
* @param {Component<Props>} component
|
||||
* @param {{ props?: Omit<Props, '$$slots' | '$$events'>; context?: Map<any, any>; idPrefix?: string }} options
|
||||
* @returns {AccumulatedContent}
|
||||
*/
|
||||
static #render(component, options) {
|
||||
var previous_context = ssr_context;
|
||||
try {
|
||||
const renderer = Renderer.#open_render('sync', component, options);
|
||||
|
||||
const content = renderer.#collect_content();
|
||||
return Renderer.#close_render(content, renderer);
|
||||
} finally {
|
||||
abort();
|
||||
set_ssr_context(previous_context);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Render a component.
|
||||
*
|
||||
* @template {Record<string, any>} Props
|
||||
* @param {Component<Props>} component
|
||||
* @param {{ props?: Omit<Props, '$$slots' | '$$events'>; context?: Map<any, any>; idPrefix?: string; csp?: Csp }} options
|
||||
* @returns {Promise<AccumulatedContent & { hashes: { script: Sha256Source[] } }>}
|
||||
*/
|
||||
static async #render_async(component, options) {
|
||||
const previous_context = ssr_context;
|
||||
|
||||
try {
|
||||
const renderer = Renderer.#open_render('async', component, options);
|
||||
const content = await renderer.#collect_content_async();
|
||||
const hydratables = await renderer.#collect_hydratables();
|
||||
if (hydratables !== null) {
|
||||
content.head = hydratables + content.head;
|
||||
}
|
||||
return Renderer.#close_render(content, renderer);
|
||||
} finally {
|
||||
set_ssr_context(previous_context);
|
||||
abort();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Collect all of the code from the `out` array and return it as a string, or a promise resolving to a string.
|
||||
* @param {AccumulatedContent} content
|
||||
* @returns {AccumulatedContent}
|
||||
*/
|
||||
#collect_content(content = { head: '', body: '' }) {
|
||||
for (const item of this.#out) {
|
||||
if (typeof item === 'string') {
|
||||
content[this.type] += item;
|
||||
} else if (item instanceof Renderer) {
|
||||
item.#collect_content(content);
|
||||
}
|
||||
}
|
||||
|
||||
return content;
|
||||
}
|
||||
|
||||
/**
|
||||
* Collect all of the code from the `out` array and return it as a string.
|
||||
* @param {AccumulatedContent} content
|
||||
* @returns {Promise<AccumulatedContent>}
|
||||
*/
|
||||
async #collect_content_async(content = { head: '', body: '' }) {
|
||||
await this.promise;
|
||||
|
||||
// no danger to sequentially awaiting stuff in here; all of the work is already kicked off
|
||||
for (const item of this.#out) {
|
||||
if (typeof item === 'string') {
|
||||
content[this.type] += item;
|
||||
} else if (item instanceof Renderer) {
|
||||
await item.#collect_content_async(content);
|
||||
}
|
||||
}
|
||||
|
||||
return content;
|
||||
}
|
||||
|
||||
async #collect_hydratables() {
|
||||
const ctx = get_render_context().hydratable;
|
||||
|
||||
for (const [_, key] of ctx.unresolved_promises) {
|
||||
// this is a problem -- it means we've finished the render but we're still waiting on a promise to resolve so we can
|
||||
// serialize it, so we're blocking the response on useless content.
|
||||
w.unresolved_hydratable(key, ctx.lookup.get(key)?.stack ?? '<missing stack trace>');
|
||||
}
|
||||
|
||||
for (const comparison of ctx.comparisons) {
|
||||
// these reject if there's a mismatch
|
||||
await comparison;
|
||||
}
|
||||
|
||||
return await this.#hydratable_block(ctx);
|
||||
}
|
||||
|
||||
/**
|
||||
* @template {Record<string, any>} Props
|
||||
* @param {'sync' | 'async'} mode
|
||||
* @param {import('svelte').Component<Props>} component
|
||||
* @param {{ props?: Omit<Props, '$$slots' | '$$events'>; context?: Map<any, any>; idPrefix?: string; csp?: Csp }} options
|
||||
* @returns {Renderer}
|
||||
*/
|
||||
static #open_render(mode, component, options) {
|
||||
const renderer = new Renderer(
|
||||
new SSRState(mode, options.idPrefix ? options.idPrefix + '-' : '', options.csp)
|
||||
);
|
||||
|
||||
renderer.push(BLOCK_OPEN);
|
||||
|
||||
if (options.context) {
|
||||
push();
|
||||
/** @type {SSRContext} */ (ssr_context).c = options.context;
|
||||
/** @type {SSRContext} */ (ssr_context).r = renderer;
|
||||
}
|
||||
|
||||
// @ts-expect-error
|
||||
component(renderer, options.props ?? {});
|
||||
|
||||
if (options.context) {
|
||||
pop();
|
||||
}
|
||||
|
||||
renderer.push(BLOCK_CLOSE);
|
||||
|
||||
return renderer;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {AccumulatedContent} content
|
||||
* @param {Renderer} renderer
|
||||
* @returns {AccumulatedContent & { hashes: { script: Sha256Source[] } }}
|
||||
*/
|
||||
static #close_render(content, renderer) {
|
||||
for (const cleanup of renderer.#collect_on_destroy()) {
|
||||
cleanup();
|
||||
}
|
||||
|
||||
let head = content.head + renderer.global.get_title();
|
||||
let body = content.body;
|
||||
|
||||
for (const { hash, code } of renderer.global.css) {
|
||||
head += `<style id="${hash}">${code}</style>`;
|
||||
}
|
||||
|
||||
return {
|
||||
head,
|
||||
body,
|
||||
hashes: {
|
||||
script: renderer.global.csp.script_hashes
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {HydratableContext} ctx
|
||||
*/
|
||||
async #hydratable_block(ctx) {
|
||||
if (ctx.lookup.size === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
let entries = [];
|
||||
let has_promises = false;
|
||||
|
||||
for (const [k, v] of ctx.lookup) {
|
||||
if (v.promises) {
|
||||
has_promises = true;
|
||||
for (const p of v.promises) await p;
|
||||
}
|
||||
|
||||
entries.push(`[${devalue.uneval(k)},${v.serialized}]`);
|
||||
}
|
||||
|
||||
let prelude = `const h = (window.__svelte ??= {}).h ??= new Map();`;
|
||||
|
||||
if (has_promises) {
|
||||
prelude = `const r = (v) => Promise.resolve(v);
|
||||
${prelude}`;
|
||||
}
|
||||
|
||||
const body = `
|
||||
{
|
||||
${prelude}
|
||||
|
||||
for (const [k, v] of [
|
||||
${entries.join(',\n\t\t\t\t\t')}
|
||||
]) {
|
||||
h.set(k, v);
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
let csp_attr = '';
|
||||
if (this.global.csp.nonce) {
|
||||
csp_attr = ` nonce="${this.global.csp.nonce}"`;
|
||||
} else if (this.global.csp.hash) {
|
||||
// note to future selves: this doesn't need to be optimized with a Map<body, hash>
|
||||
// because the it's impossible for identical data to occur multiple times in a single render
|
||||
// (this would require the same hydratable key:value pair to be serialized multiple times)
|
||||
const hash = await sha256(body);
|
||||
this.global.csp.script_hashes.push(`sha256-${hash}`);
|
||||
}
|
||||
|
||||
return `\n\t\t<script${csp_attr}>${body}</script>`;
|
||||
}
|
||||
}
|
||||
|
||||
export class SSRState {
|
||||
/** @readonly @type {Csp & { script_hashes: Sha256Source[] }} */
|
||||
csp;
|
||||
|
||||
/** @readonly @type {'sync' | 'async'} */
|
||||
mode;
|
||||
|
||||
/** @readonly @type {() => string} */
|
||||
uid;
|
||||
|
||||
/** @readonly @type {Set<{ hash: string; code: string }>} */
|
||||
css = new Set();
|
||||
|
||||
/** @type {{ path: number[], value: string }} */
|
||||
#title = { path: [], value: '' };
|
||||
|
||||
/**
|
||||
* @param {'sync' | 'async'} mode
|
||||
* @param {string} id_prefix
|
||||
* @param {Csp} csp
|
||||
*/
|
||||
constructor(mode, id_prefix = '', csp = { hash: false }) {
|
||||
this.mode = mode;
|
||||
this.csp = { ...csp, script_hashes: [] };
|
||||
|
||||
let uid = 1;
|
||||
this.uid = () => `${id_prefix}s${uid++}`;
|
||||
}
|
||||
|
||||
get_title() {
|
||||
return this.#title.value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Performs a depth-first (lexicographic) comparison using the path. Rejects sets
|
||||
* from earlier than or equal to the current value.
|
||||
* @param {string} value
|
||||
* @param {number[]} path
|
||||
*/
|
||||
set_title(value, path) {
|
||||
const current = this.#title.path;
|
||||
|
||||
let i = 0;
|
||||
let l = Math.min(path.length, current.length);
|
||||
|
||||
// skip identical prefixes - [1, 2, 3, ...] === [1, 2, 3, ...]
|
||||
while (i < l && path[i] === current[i]) i += 1;
|
||||
|
||||
if (path[i] === undefined) return;
|
||||
|
||||
// replace title if
|
||||
// - incoming path is longer - [7, 8, 9] > [7, 8]
|
||||
// - incoming path is later - [7, 8, 9] > [7, 8, 8]
|
||||
if (current[i] === undefined || path[i] > current[i]) {
|
||||
this.#title.path = path;
|
||||
this.#title.value = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
+29
@@ -0,0 +1,29 @@
|
||||
/* This file is generated by scripts/process-messages/index.js. Do not edit! */
|
||||
|
||||
import { DEV } from 'esm-env';
|
||||
|
||||
var bold = 'font-weight: bold';
|
||||
var normal = 'font-weight: normal';
|
||||
|
||||
/**
|
||||
* A `hydratable` value with key `%key%` was created, but at least part of it was not used during the render.
|
||||
*
|
||||
* The `hydratable` was initialized in:
|
||||
* %stack%
|
||||
* @param {string} key
|
||||
* @param {string} stack
|
||||
*/
|
||||
export function unresolved_hydratable(key, stack) {
|
||||
if (DEV) {
|
||||
console.warn(
|
||||
`%c[svelte] unresolved_hydratable\n%cA \`hydratable\` value with key \`${key}\` was created, but at least part of it was not used during the render.
|
||||
|
||||
The \`hydratable\` was initialized in:
|
||||
${stack}\nhttps://svelte.dev/e/unresolved_hydratable`,
|
||||
bold,
|
||||
normal
|
||||
);
|
||||
} else {
|
||||
console.warn(`https://svelte.dev/e/unresolved_hydratable`);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user