mirror of https://github.com/sveltejs/svelte
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
643 lines
17 KiB
643 lines
17 KiB
/** @import { ComponentType, SvelteComponent } from 'svelte' */
|
|
/** @import { Component, Payload, RenderOutput } from '#server' */
|
|
/** @import { Store } from '#shared' */
|
|
export { FILENAME, HMR } from '../../constants.js';
|
|
import {
|
|
ELEMENT_IS_NAMESPACED,
|
|
ELEMENT_PRESERVE_ATTRIBUTE_CASE,
|
|
UNINITIALIZED
|
|
} from '../../constants.js';
|
|
import { subscribe_to_store } from '../../store/utils.js';
|
|
import { attr } from '../shared/attributes.js';
|
|
import { is_promise, noop } from '../shared/utils.js';
|
|
|
|
import { DEV } from 'esm-env';
|
|
import { escape_html } from '../../escaping.js';
|
|
import { is_boolean_attribute, is_void } from '../../utils.js';
|
|
import { validate_store } from '../shared/validate.js';
|
|
import { current_component, pop, push } from './context.js';
|
|
import { reset_elements } from './dev.js';
|
|
import { close, empty, hydratable, open, open_else, set_hydratable } from './hydration.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;
|
|
|
|
/** List of elements that require raw contents and should not have SSR comments put in them */
|
|
const RAW_TEXT_ELEMENTS = ['textarea', 'script', 'style', 'title'];
|
|
|
|
/**
|
|
* @param {Payload} to_copy
|
|
* @returns {Payload}
|
|
*/
|
|
export function copy_payload({ out, css, head, async, current_async_level }) {
|
|
return {
|
|
out,
|
|
css: new Set(css),
|
|
head: {
|
|
title: head.title,
|
|
out: head.out
|
|
},
|
|
async: async ? [...async] : [],
|
|
current_async_level
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Assigns second payload to first
|
|
* @param {Payload} p1
|
|
* @param {Payload} p2
|
|
* @returns {void}
|
|
*/
|
|
export function assign_payload(p1, p2) {
|
|
p1.out = p2.out;
|
|
p1.head = p2.head;
|
|
}
|
|
|
|
/**
|
|
* @param {Payload} payload
|
|
* @param {string} tag
|
|
* @param {() => void} attributes_fn
|
|
* @param {() => void} children_fn
|
|
* @returns {void}
|
|
*/
|
|
export function element(payload, tag, attributes_fn = noop, children_fn = noop) {
|
|
payload.out += empty();
|
|
|
|
if (tag) {
|
|
payload.out += `<${tag} `;
|
|
attributes_fn();
|
|
payload.out += `>`;
|
|
|
|
if (!is_void(tag)) {
|
|
children_fn();
|
|
if (!RAW_TEXT_ELEMENTS.includes(tag)) {
|
|
payload.out += empty();
|
|
}
|
|
payload.out += `</${tag}>`;
|
|
}
|
|
}
|
|
|
|
payload.out += empty();
|
|
}
|
|
|
|
/**
|
|
* Array of `onDestroy` callbacks that should be called at the end of the server render function
|
|
* @type {Function[]}
|
|
*/
|
|
export let 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> }} [options]
|
|
* @returns {Payload}
|
|
*/
|
|
function render_payload(component, options = {}) {
|
|
/** @type {Payload} */
|
|
const payload = {
|
|
out: '',
|
|
css: new Set(),
|
|
head: { title: '', out: '' },
|
|
current_async_level: ''
|
|
};
|
|
|
|
const prev_on_destroy = on_destroy;
|
|
on_destroy = [];
|
|
payload.out += 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.out += close();
|
|
for (const cleanup of on_destroy) cleanup();
|
|
on_destroy = prev_on_destroy;
|
|
return payload;
|
|
}
|
|
|
|
/**
|
|
* 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> }} [options]
|
|
* @returns {RenderOutput}
|
|
*/
|
|
export function render(component, options = {}) {
|
|
const payload = render_payload(component, options);
|
|
|
|
let head = payload.head.out + payload.head.title;
|
|
|
|
for (const { hash, code } of payload.css) {
|
|
head += `<style id="${hash}">${code}</style>`;
|
|
}
|
|
|
|
return {
|
|
head,
|
|
html: payload.out,
|
|
body: payload.out
|
|
};
|
|
}
|
|
|
|
/**
|
|
* 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.
|
|
* Unlike `render` it doesn't render any hydration marker.
|
|
* @template {Record<string, any>} Props
|
|
* @param {import('svelte').Component<Props> | ComponentType<SvelteComponent<Props>>} component
|
|
* @param {{ props?: Omit<Props, '$$slots' | '$$events'>; context?: Map<any, any> }} [options]
|
|
* @returns {Promise<RenderOutput>}
|
|
*/
|
|
export async function renderStaticHTML(component, options) {
|
|
set_hydratable(false);
|
|
let payload;
|
|
try {
|
|
payload = render_payload(component, options);
|
|
if (payload.async) {
|
|
for (let async_fn of payload.async) {
|
|
await async_fn();
|
|
}
|
|
}
|
|
|
|
let head = payload.head.out + payload.head.title;
|
|
|
|
for (const { hash, code } of payload.css) {
|
|
head += `<style id="${hash}">${code}</style>`;
|
|
}
|
|
|
|
return {
|
|
head,
|
|
html: payload.out,
|
|
body: payload.out
|
|
};
|
|
} finally {
|
|
set_hydratable(true);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @param {Payload} payload
|
|
* @param {(head_payload: Payload['head']) => void} fn
|
|
* @returns {void}
|
|
*/
|
|
export function head(payload, fn) {
|
|
const head_payload = payload.head;
|
|
head_payload.out += open();
|
|
fn(head_payload);
|
|
head_payload.out += close();
|
|
}
|
|
|
|
/**
|
|
* @param {Payload} payload
|
|
* @param {boolean} is_html
|
|
* @param {Record<string, string>} props
|
|
* @param {() => void} component
|
|
* @param {boolean} dynamic
|
|
* @returns {void}
|
|
*/
|
|
export function css_props(payload, is_html, props, component, dynamic = false) {
|
|
const styles = style_object_to_string(props);
|
|
|
|
if (is_html) {
|
|
payload.out += `<svelte-css-wrapper style="display: contents; ${styles}">`;
|
|
} else {
|
|
payload.out += `<g style="${styles}">`;
|
|
}
|
|
|
|
if (dynamic) {
|
|
payload.out += empty();
|
|
}
|
|
|
|
component();
|
|
|
|
if (is_html) {
|
|
payload.out += empty() + `</svelte-css-wrapper>`;
|
|
} else {
|
|
payload.out += empty() + `</g>`;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @param {Record<string, unknown>} attrs
|
|
* @param {Record<string, string>} [classes]
|
|
* @param {Record<string, string>} [styles]
|
|
* @param {number} [flags]
|
|
* @returns {string}
|
|
*/
|
|
export function spread_attributes(attrs, classes, styles, flags = 0) {
|
|
if (styles) {
|
|
attrs.style = attrs.style
|
|
? style_object_to_string(merge_styles(/** @type {string} */ (attrs.style), styles))
|
|
: style_object_to_string(styles);
|
|
}
|
|
|
|
if (classes) {
|
|
const classlist = attrs.class ? [attrs.class] : [];
|
|
|
|
for (const key in classes) {
|
|
if (classes[key]) {
|
|
classlist.push(key);
|
|
}
|
|
}
|
|
|
|
attrs.class = classlist.join(' ');
|
|
}
|
|
|
|
let attr_str = '';
|
|
let name;
|
|
|
|
const is_html = (flags & ELEMENT_IS_NAMESPACED) === 0;
|
|
const lowercase = (flags & ELEMENT_PRESERVE_ATTRIBUTE_CASE) === 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;
|
|
|
|
if (lowercase) {
|
|
name = name.toLowerCase();
|
|
}
|
|
|
|
attr_str += attr(name, attrs[name], 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 {Record<string, string>} style_object */
|
|
export function add_styles(style_object) {
|
|
const styles = style_object_to_string(style_object);
|
|
return styles ? ` style="${styles}"` : '';
|
|
}
|
|
|
|
/**
|
|
* @param {string} attribute
|
|
* @param {Record<string, string>} styles
|
|
*/
|
|
export function merge_styles(attribute, styles) {
|
|
/** @type {Record<string, string>} */
|
|
var merged = {};
|
|
|
|
if (attribute) {
|
|
for (var declaration of attribute.split(';')) {
|
|
var i = declaration.indexOf(':');
|
|
var name = declaration.slice(0, i).trim();
|
|
var value = declaration.slice(i + 1).trim();
|
|
|
|
if (name !== '') merged[name] = value;
|
|
}
|
|
}
|
|
|
|
for (name in styles) {
|
|
merged[name] = styles[name];
|
|
}
|
|
|
|
return merged;
|
|
}
|
|
|
|
/**
|
|
* @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 {Payload} payload
|
|
* @param {Record<string, any>} $$props
|
|
* @param {string} name
|
|
* @param {Record<string, unknown>} slot_props
|
|
* @param {null | (() => void)} fallback_fn
|
|
* @returns {void}
|
|
*/
|
|
export function slot(payload, $$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(payload, 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 {Payload} $$payload
|
|
* @param {Promise<V>} promise
|
|
* @param {null | (() => void)} pending_fn
|
|
* @param {(value: V, $$payload?: Payload) => void} then_fn
|
|
* @param {(error: any, $$payload?: Payload) => void} [catch_fn]
|
|
* @returns {void}
|
|
*/
|
|
function await_block($$payload, promise, pending_fn, then_fn, catch_fn) {
|
|
if (is_promise(promise)) {
|
|
promise.then(null, noop);
|
|
if (!hydratable) {
|
|
let level = $$payload.current_async_level + ($$payload.async?.length ?? 0);
|
|
const replace_marker = '\ufff0\ufff0\ufff0' + level;
|
|
$$payload.out += replace_marker;
|
|
($$payload.async ??= []).push(async () => {
|
|
/**
|
|
* @type {Payload}
|
|
*/
|
|
const new_payload = {
|
|
css: new Set(),
|
|
current_async_level: level + '.',
|
|
head: {
|
|
out: '',
|
|
title: ''
|
|
},
|
|
out: '',
|
|
async: []
|
|
};
|
|
try {
|
|
const result = await promise;
|
|
then_fn(result, new_payload);
|
|
} catch (e) {
|
|
if (catch_fn) {
|
|
catch_fn(e, new_payload);
|
|
}
|
|
}
|
|
if ($$payload.async && new_payload.async) {
|
|
for (let async_replace of new_payload.async) {
|
|
await async_replace();
|
|
}
|
|
}
|
|
$$payload.out = $$payload.out.replace(replace_marker, new_payload.out);
|
|
$$payload.head.out = $$payload.head.out.replace(replace_marker, new_payload.head.out);
|
|
$$payload.head.title = $$payload.head.title.replace(replace_marker, new_payload.head.title);
|
|
for (let css_part of new_payload.css) {
|
|
$$payload.css.add(css_part);
|
|
}
|
|
});
|
|
} else {
|
|
if (pending_fn !== null) {
|
|
pending_fn();
|
|
}
|
|
}
|
|
} else if (then_fn !== null) {
|
|
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 [];
|
|
}
|
|
|
|
/**
|
|
* @param {any[]} args
|
|
* @param {Function} [inspect]
|
|
*/
|
|
// eslint-disable-next-line no-console
|
|
export function inspect(args, inspect = console.log) {
|
|
inspect('init', ...args);
|
|
}
|
|
|
|
/**
|
|
* @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;
|
|
};
|
|
}
|
|
|
|
export { attr, close, empty, open, open_else };
|
|
|
|
export { html } from './blocks/html.js';
|
|
|
|
export { pop, push } from './context.js';
|
|
|
|
export { pop_element, push_element } from './dev.js';
|
|
|
|
export { snapshot } from '../shared/clone.js';
|
|
|
|
export { fallback } from '../shared/utils.js';
|
|
|
|
export {
|
|
invalid_default_snippet,
|
|
validate_dynamic_element_tag,
|
|
validate_void_dynamic_element
|
|
} from '../shared/validate.js';
|
|
|
|
export { escape_html as escape };
|