This commit is contained in:
2026-01-22 17:13:23 -06:00
parent 02b0f053d6
commit f15f7b8e38
1728 changed files with 925278 additions and 1 deletions
+73
View File
@@ -0,0 +1,73 @@
// General flags
export const DERIVED = 1 << 1;
export const EFFECT = 1 << 2;
export const RENDER_EFFECT = 1 << 3;
/**
* An effect that does not destroy its child effects when it reruns.
* Runs as part of render effects, i.e. not eagerly as part of tree traversal or effect flushing.
*/
export const MANAGED_EFFECT = 1 << 24;
/**
* An effect that does not destroy its child effects when it reruns (like MANAGED_EFFECT).
* Runs eagerly as part of tree traversal or effect flushing.
*/
export const BLOCK_EFFECT = 1 << 4;
export const BRANCH_EFFECT = 1 << 5;
export const ROOT_EFFECT = 1 << 6;
export const BOUNDARY_EFFECT = 1 << 7;
/**
* Indicates that a reaction is connected to an effect root — either it is an effect,
* or it is a derived that is depended on by at least one effect. If a derived has
* no dependents, we can disconnect it from the graph, allowing it to either be
* GC'd or reconnected later if an effect comes to depend on it again
*/
export const CONNECTED = 1 << 9;
export const CLEAN = 1 << 10;
export const DIRTY = 1 << 11;
export const MAYBE_DIRTY = 1 << 12;
export const INERT = 1 << 13;
export const DESTROYED = 1 << 14;
// Flags exclusive to effects
/** Set once an effect that should run synchronously has run */
export const EFFECT_RAN = 1 << 15;
/**
* 'Transparent' effects do not create a transition boundary.
* This is on a block effect 99% of the time but may also be on a branch effect if its parent block effect was pruned
*/
export const EFFECT_TRANSPARENT = 1 << 16;
export const EAGER_EFFECT = 1 << 17;
export const HEAD_EFFECT = 1 << 18;
export const EFFECT_PRESERVED = 1 << 19;
export const USER_EFFECT = 1 << 20;
export const EFFECT_OFFSCREEN = 1 << 25;
// Flags exclusive to deriveds
/**
* Tells that we marked this derived and its reactions as visited during the "mark as (maybe) dirty"-phase.
* Will be lifted during execution of the derived and during checking its dirty state (both are necessary
* because a derived might be checked but not executed).
*/
export const WAS_MARKED = 1 << 15;
// Flags used for async
export const REACTION_IS_UPDATING = 1 << 21;
export const ASYNC = 1 << 22;
export const ERROR_VALUE = 1 << 23;
export const STATE_SYMBOL = Symbol('$state');
export const LEGACY_PROPS = Symbol('legacy props');
export const LOADING_ATTR_SYMBOL = Symbol('');
export const PROXY_PATH_SYMBOL = Symbol('proxy path');
/** allow users to ignore aborted signal errors if `reason.name === 'StaleReactionError` */
export const STALE_REACTION = new (class StaleReactionError extends Error {
name = 'StaleReactionError';
message = 'The reaction that called `getAbortSignal()` was re-run or destroyed';
})();
export const ELEMENT_NODE = 1;
export const TEXT_NODE = 3;
export const COMMENT_NODE = 8;
export const DOCUMENT_FRAGMENT_NODE = 11;
+258
View File
@@ -0,0 +1,258 @@
/** @import { ComponentContext, DevStackEntry, Effect } from '#client' */
import { DEV } from 'esm-env';
import * as e from './errors.js';
import { active_effect, active_reaction } from './runtime.js';
import { create_user_effect } from './reactivity/effects.js';
import { async_mode_flag, legacy_mode_flag } from '../flags/index.js';
import { FILENAME } from '../../constants.js';
import { BRANCH_EFFECT, EFFECT_RAN } from './constants.js';
/** @type {ComponentContext | null} */
export let component_context = null;
/** @param {ComponentContext | null} context */
export function set_component_context(context) {
component_context = context;
}
/** @type {DevStackEntry | null} */
export let dev_stack = null;
/** @param {DevStackEntry | null} stack */
export function set_dev_stack(stack) {
dev_stack = stack;
}
/**
* Execute a callback with a new dev stack entry
* @param {() => any} callback - Function to execute
* @param {DevStackEntry['type']} type - Type of block/component
* @param {any} component - Component function
* @param {number} line - Line number
* @param {number} column - Column number
* @param {Record<string, any>} [additional] - Any additional properties to add to the dev stack entry
* @returns {any}
*/
export function add_svelte_meta(callback, type, component, line, column, additional) {
const parent = dev_stack;
dev_stack = {
type,
file: component[FILENAME],
line,
column,
parent,
...additional
};
try {
return callback();
} finally {
dev_stack = parent;
}
}
/**
* The current component function. Different from current component context:
* ```html
* <!-- App.svelte -->
* <Foo>
* <Bar /> <!-- context == Foo.svelte, function == App.svelte -->
* </Foo>
* ```
* @type {ComponentContext['function']}
*/
export let dev_current_component_function = null;
/** @param {ComponentContext['function']} fn */
export function set_dev_current_component_function(fn) {
dev_current_component_function = fn;
}
/**
* Returns a `[get, set]` pair of functions for working with context in a type-safe way.
*
* `get` will throw an error if no parent component called `set`.
*
* @template T
* @returns {[() => T, (context: T) => T]}
* @since 5.40.0
*/
export function createContext() {
const key = {};
return [
() => {
if (!hasContext(key)) {
e.missing_context();
}
return getContext(key);
},
(context) => setContext(key, context)
];
}
/**
* Retrieves the context that belongs to the closest parent component with the specified `key`.
* Must be called during component initialisation.
*
* [`createContext`](https://svelte.dev/docs/svelte/svelte#createContext) is a type-safe alternative.
*
* @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;
}
/**
* Associates an arbitrary `context` object with the current component and the specified `key`
* and returns that object. The context is then available to children of the component
* (including slotted content) with `getContext`.
*
* Like lifecycle functions, this must be called during component initialisation.
*
* [`createContext`](https://svelte.dev/docs/svelte/svelte#createContext) is a type-safe alternative.
*
* @template T
* @param {any} key
* @param {T} context
* @returns {T}
*/
export function setContext(key, context) {
const context_map = get_or_init_context_map('setContext');
if (async_mode_flag) {
var flags = /** @type {Effect} */ (active_effect).f;
var valid =
!active_reaction &&
(flags & BRANCH_EFFECT) !== 0 &&
// pop() runs synchronously, so this indicates we're setting context after an await
!(/** @type {ComponentContext} */ (component_context).i);
if (!valid) {
e.set_context_after_init();
}
}
context_map.set(key, context);
return context;
}
/**
* Checks whether a given `key` has been set in the context of a parent component.
* Must be called during component initialisation.
*
* @param {any} key
* @returns {boolean}
*/
export function hasContext(key) {
const context_map = get_or_init_context_map('hasContext');
return context_map.has(key);
}
/**
* Retrieves the whole context map that belongs to the closest parent component.
* Must be called during component initialisation. Useful, for example, if you
* programmatically create a component and want to pass the existing context to it.
*
* @template {Map<any, any>} [T=Map<any, any>]
* @returns {T}
*/
export function getAllContexts() {
const context_map = get_or_init_context_map('getAllContexts');
return /** @type {T} */ (context_map);
}
/**
* @param {Record<string, unknown>} props
* @param {any} runes
* @param {Function} [fn]
* @returns {void}
*/
export function push(props, runes = false, fn) {
component_context = {
p: component_context,
i: false,
c: null,
e: null,
s: props,
x: null,
l: legacy_mode_flag && !runes ? { s: null, u: null, $: [] } : null
};
if (DEV) {
// component function
component_context.function = fn;
dev_current_component_function = fn;
}
}
/**
* @template {Record<string, any>} T
* @param {T} [component]
* @returns {T}
*/
export function pop(component) {
var context = /** @type {ComponentContext} */ (component_context);
var effects = context.e;
if (effects !== null) {
context.e = null;
for (var fn of effects) {
create_user_effect(fn);
}
}
if (component !== undefined) {
context.x = component;
}
context.i = true;
component_context = context.p;
if (DEV) {
dev_current_component_function = component_context?.function ?? null;
}
return component ?? /** @type {T} */ ({});
}
/** @returns {boolean} */
export function is_runes() {
return !legacy_mode_flag || (component_context !== null && component_context.l === null);
}
/**
* @param {string} name
* @returns {Map<unknown, unknown>}
*/
function get_or_init_context_map(name) {
if (component_context === null) {
e.lifecycle_outside_component(name);
}
return (component_context.c ??= new Map(get_parent_context(component_context) || undefined));
}
/**
* @param {ComponentContext} component_context
* @returns {Map<unknown, unknown> | null}
*/
function get_parent_context(component_context) {
let parent = component_context.p;
while (parent !== null) {
const context_map = parent.c;
if (context_map !== null) {
return context_map;
}
parent = parent.p;
}
return null;
}
+78
View File
@@ -0,0 +1,78 @@
import { sanitize_location } from '../../../utils.js';
import { untrack } from '../runtime.js';
import * as w from '../warnings.js';
/**
*
* @param {any} a
* @param {any} b
* @param {string} property
* @param {string} location
*/
function compare(a, b, property, location) {
if (a !== b) {
w.assignment_value_stale(property, /** @type {string} */ (sanitize_location(location)));
}
return a;
}
/**
* @param {any} object
* @param {string} property
* @param {any} value
* @param {string} location
*/
export function assign(object, property, value, location) {
return compare(
(object[property] = value),
untrack(() => object[property]),
property,
location
);
}
/**
* @param {any} object
* @param {string} property
* @param {any} value
* @param {string} location
*/
export function assign_and(object, property, value, location) {
return compare(
(object[property] &&= value),
untrack(() => object[property]),
property,
location
);
}
/**
* @param {any} object
* @param {string} property
* @param {any} value
* @param {string} location
*/
export function assign_or(object, property, value, location) {
return compare(
(object[property] ||= value),
untrack(() => object[property]),
property,
location
);
}
/**
* @param {any} object
* @param {string} property
* @param {any} value
* @param {string} location
*/
export function assign_nullish(object, property, value, location) {
return compare(
(object[property] ??= value),
untrack(() => object[property]),
property,
location
);
}
+35
View File
@@ -0,0 +1,35 @@
import { STATE_SYMBOL } from '#client/constants';
import { snapshot } from '../../shared/clone.js';
import * as w from '../warnings.js';
import { untrack } from '../runtime.js';
/**
* @param {string} method
* @param {...any} objects
*/
export function log_if_contains_state(method, ...objects) {
untrack(() => {
try {
let has_state = false;
const transformed = [];
for (const obj of objects) {
if (obj && typeof obj === 'object' && STATE_SYMBOL in obj) {
transformed.push(snapshot(obj, true));
has_state = true;
} else {
transformed.push(obj);
}
}
if (has_state) {
w.console_log_state(method);
// eslint-disable-next-line no-console
console.log('%c[snapshot]', 'color: grey', ...transformed);
}
} catch {}
});
return objects;
}
+31
View File
@@ -0,0 +1,31 @@
/** @type {Map<String, Set<HTMLStyleElement>>} */
var all_styles = new Map();
/**
* @param {String} hash
* @param {HTMLStyleElement} style
*/
export function register_style(hash, style) {
var styles = all_styles.get(hash);
if (!styles) {
styles = new Set();
all_styles.set(hash, styles);
}
styles.add(style);
}
/**
* @param {String} hash
*/
export function cleanup_styles(hash) {
var styles = all_styles.get(hash);
if (!styles) return;
for (const style of styles) {
style.remove();
}
all_styles.delete(hash);
}
+500
View File
@@ -0,0 +1,500 @@
/** @import { Derived, Effect, Value } from '#client' */
import {
BLOCK_EFFECT,
BOUNDARY_EFFECT,
BRANCH_EFFECT,
CLEAN,
CONNECTED,
DERIVED,
DIRTY,
EFFECT,
ASYNC,
DESTROYED,
INERT,
MAYBE_DIRTY,
RENDER_EFFECT,
ROOT_EFFECT,
WAS_MARKED,
MANAGED_EFFECT
} from '#client/constants';
import { snapshot } from '../../shared/clone.js';
import { untrack } from '../runtime.js';
/**
*
* @param {Effect} effect
*/
export function root(effect) {
while (effect.parent !== null) {
effect = effect.parent;
}
return effect;
}
/**
*
* @param {Effect} effect
* @param {boolean} append_effect
* @returns {string}
*/
function effect_label(effect, append_effect = false) {
const flags = effect.f;
let label = `(unknown ${append_effect ? 'effect' : ''})`;
if ((flags & ROOT_EFFECT) !== 0) {
label = 'root';
} else if ((flags & BOUNDARY_EFFECT) !== 0) {
label = 'boundary';
} else if ((flags & BLOCK_EFFECT) !== 0) {
label = 'block';
} else if ((flags & MANAGED_EFFECT) !== 0) {
label = 'managed';
} else if ((flags & ASYNC) !== 0) {
label = 'async';
} else if ((flags & BRANCH_EFFECT) !== 0) {
label = 'branch';
} else if ((flags & RENDER_EFFECT) !== 0) {
label = 'render effect';
} else if ((flags & EFFECT) !== 0) {
label = 'effect';
}
if (append_effect && !label.endsWith('effect')) {
label += ' effect';
}
return label;
}
/**
*
* @param {Effect} effect
*/
export function log_effect_tree(effect, depth = 0) {
const flags = effect.f;
const label = effect_label(effect);
let status =
(flags & CLEAN) !== 0 ? 'clean' : (flags & MAYBE_DIRTY) !== 0 ? 'maybe dirty' : 'dirty';
// eslint-disable-next-line no-console
console.group(`%c${label} (${status})`, `font-weight: ${status === 'clean' ? 'normal' : 'bold'}`);
if (depth === 0) {
const callsite = new Error().stack
?.split('\n')[2]
.replace(/\s+at (?: \w+\(?)?(.+)\)?/, (m, $1) => $1.replace(/\?[^:]+/, ''));
// eslint-disable-next-line no-console
console.log(callsite);
} else {
// eslint-disable-next-line no-console
console.groupCollapsed(`%cfn`, `font-weight: normal`);
// eslint-disable-next-line no-console
console.log(effect.fn);
// eslint-disable-next-line no-console
console.groupEnd();
}
if (effect.deps !== null) {
// eslint-disable-next-line no-console
console.groupCollapsed('%cdeps', 'font-weight: normal');
for (const dep of effect.deps) {
log_dep(dep);
}
// eslint-disable-next-line no-console
console.groupEnd();
}
if (effect.nodes) {
// eslint-disable-next-line no-console
console.log(effect.nodes.start);
if (effect.nodes.start !== effect.nodes.end) {
// eslint-disable-next-line no-console
console.log(effect.nodes.end);
}
}
let child = effect.first;
while (child !== null) {
log_effect_tree(child, depth + 1);
child = child.next;
}
// eslint-disable-next-line no-console
console.groupEnd();
}
/**
*
* @param {Value} dep
*/
function log_dep(dep) {
if ((dep.f & DERIVED) !== 0) {
const derived = /** @type {Derived} */ (dep);
// eslint-disable-next-line no-console
console.groupCollapsed(
`%c$derived %c${dep.label ?? '<unknown>'}`,
'font-weight: bold; color: CornflowerBlue',
'font-weight: normal',
untrack(() => snapshot(derived.v))
);
if (derived.deps) {
for (const d of derived.deps) {
log_dep(d);
}
}
// eslint-disable-next-line no-console
console.groupEnd();
} else {
// eslint-disable-next-line no-console
console.log(
`%c$state %c${dep.label ?? '<unknown>'}`,
'font-weight: bold; color: CornflowerBlue',
'font-weight: normal',
untrack(() => snapshot(dep.v))
);
}
}
/**
* Logs all reactions of a source or derived transitively
* @param {Derived | Value} signal
*/
export function log_reactions(signal) {
/** @type {Set<Derived | Value>} */
const visited = new Set();
/**
* Returns an array of flag names that are set on the given flags bitmask
* @param {number} flags
* @returns {string[]}
*/
function get_derived_flag_names(flags) {
/** @type {string[]} */
const names = [];
if ((flags & CLEAN) !== 0) names.push('CLEAN');
if ((flags & DIRTY) !== 0) names.push('DIRTY');
if ((flags & MAYBE_DIRTY) !== 0) names.push('MAYBE_DIRTY');
if ((flags & CONNECTED) !== 0) names.push('CONNECTED');
if ((flags & WAS_MARKED) !== 0) names.push('WAS_MARKED');
if ((flags & INERT) !== 0) names.push('INERT');
if ((flags & DESTROYED) !== 0) names.push('DESTROYED');
return names;
}
/**
* @param {Derived | Value} d
* @param {number} depth
*/
function log_derived(d, depth) {
const flags = d.f;
const flag_names = get_derived_flag_names(flags);
const flags_str = flag_names.length > 0 ? `(${flag_names.join(', ')})` : '(no flags)';
// eslint-disable-next-line no-console
console.group(
`%c${flags & DERIVED ? '$derived' : '$state'} %c${d.label ?? '<unknown>'} %c${flags_str}`,
'font-weight: bold; color: CornflowerBlue',
'font-weight: normal; color: inherit',
'font-weight: normal; color: gray'
);
// eslint-disable-next-line no-console
console.log(untrack(() => snapshot(d.v)));
if ('fn' in d) {
// eslint-disable-next-line no-console
console.log('%cfn:', 'font-weight: bold', d.fn);
}
if (d.reactions !== null && d.reactions.length > 0) {
// eslint-disable-next-line no-console
console.group('%creactions', 'font-weight: bold');
for (const reaction of d.reactions) {
if ((reaction.f & DERIVED) !== 0) {
const derived_reaction = /** @type {Derived} */ (reaction);
if (visited.has(derived_reaction)) {
// eslint-disable-next-line no-console
console.log(
`%c$derived %c${derived_reaction.label ?? '<unknown>'} %c(already seen)`,
'font-weight: bold; color: CornflowerBlue',
'font-weight: normal; color: inherit',
'font-weight: bold; color: orange'
);
} else {
visited.add(derived_reaction);
log_derived(derived_reaction, depth + 1);
}
} else {
// It's an effect
const label = effect_label(/** @type {Effect} */ (reaction), true);
const status = (flags & MAYBE_DIRTY) !== 0 ? 'maybe dirty' : 'dirty';
// Collect parent statuses
/** @type {string[]} */
const parent_statuses = [];
let show = false;
let current = /** @type {Effect} */ (reaction).parent;
while (current !== null) {
const parent_flags = current.f;
if ((parent_flags & (ROOT_EFFECT | BRANCH_EFFECT)) !== 0) {
const parent_status = (parent_flags & CLEAN) !== 0 ? 'clean' : 'not clean';
if (parent_status === 'clean' && parent_statuses.includes('not clean')) show = true;
parent_statuses.push(parent_status);
}
if (!current.parent) break;
current = current.parent;
}
// Check if reaction is reachable from root
const seen_effects = new Set();
let reachable = false;
/**
* @param {Effect | null} effect
*/
function check_reachable(effect) {
if (effect === null || reachable) return;
if (effect === reaction) {
reachable = true;
return;
}
if (effect.f & DESTROYED) return;
if (seen_effects.has(effect)) {
throw new Error('');
}
seen_effects.add(effect);
let child = effect.first;
while (child !== null) {
check_reachable(child);
child = child.next;
}
}
try {
if (current) check_reachable(current);
} catch (e) {
// eslint-disable-next-line no-console
console.log(
`%c⚠️ Circular reference detected in effect tree`,
'font-weight: bold; color: red',
seen_effects
);
}
if (!reachable) {
// eslint-disable-next-line no-console
console.log(
`%c⚠️ Effect is NOT reachable from its parent chain`,
'font-weight: bold; color: red'
);
}
const parent_status_str = show ? ` (${parent_statuses.join(', ')})` : '';
// eslint-disable-next-line no-console
console.log(
`%c${label} (${status})${parent_status_str}`,
`font-weight: bold; color: ${parent_status_str ? 'red' : 'green'}`,
reaction
);
}
}
// eslint-disable-next-line no-console
console.groupEnd();
} else {
// eslint-disable-next-line no-console
console.log('%cno reactions', 'font-style: italic; color: gray');
}
// eslint-disable-next-line no-console
console.groupEnd();
}
// eslint-disable-next-line no-console
console.group(`%cDerived Reactions Graph`, 'font-weight: bold; color: purple');
visited.add(signal);
log_derived(signal, 0);
// eslint-disable-next-line no-console
console.groupEnd();
}
/**
* Traverses an effect tree and logs branches where a non-clean branch exists below a clean branch
* @param {Effect} effect
*/
export function log_inconsistent_branches(effect) {
const root_effect = root(effect);
/**
* @typedef {{
* effect: Effect,
* status: 'clean' | 'maybe dirty' | 'dirty',
* parent_clean: boolean,
* children: BranchInfo[]
* }} BranchInfo
*/
/**
* Collects branch effects from the tree
* @param {Effect} eff
* @param {boolean} parent_clean - whether any ancestor branch is clean
* @returns {BranchInfo[]}
*/
function collect_branches(eff, parent_clean) {
/** @type {BranchInfo[]} */
const branches = [];
const flags = eff.f;
const is_branch = (flags & BRANCH_EFFECT) !== 0;
if (is_branch) {
const status =
(flags & CLEAN) !== 0 ? 'clean' : (flags & MAYBE_DIRTY) !== 0 ? 'maybe dirty' : 'dirty';
/** @type {BranchInfo[]} */
const child_branches = [];
let child = eff.first;
while (child !== null) {
child_branches.push(...collect_branches(child, status === 'clean'));
child = child.next;
}
branches.push({
effect: eff,
status,
parent_clean,
children: child_branches
});
} else {
// Not a branch, continue traversing
let child = eff.first;
while (child !== null) {
branches.push(...collect_branches(child, parent_clean));
child = child.next;
}
}
return branches;
}
/**
* Checks if a branch tree contains any inconsistencies (non-clean below clean)
* @param {BranchInfo} branch
* @param {boolean} ancestor_clean
* @returns {boolean}
*/
function has_inconsistency(branch, ancestor_clean) {
const is_inconsistent = ancestor_clean && branch.status !== 'clean';
if (is_inconsistent) return true;
const new_ancestor_clean = ancestor_clean || branch.status === 'clean';
for (const child of branch.children) {
if (has_inconsistency(child, new_ancestor_clean)) return true;
}
return false;
}
/**
* Logs a branch and its children, but only if there are inconsistencies
* @param {BranchInfo} branch
* @param {boolean} ancestor_clean
* @param {number} depth
*/
function log_branch(branch, ancestor_clean, depth) {
const is_inconsistent = ancestor_clean && branch.status !== 'clean';
const new_ancestor_clean = ancestor_clean || branch.status === 'clean';
// Only log if this branch or any descendant has an inconsistency
if (!has_inconsistency(branch, ancestor_clean) && !is_inconsistent) {
return;
}
const style = is_inconsistent
? 'font-weight: bold; color: red'
: branch.status === 'clean'
? 'font-weight: normal; color: green'
: 'font-weight: bold; color: orange';
const warning = is_inconsistent ? ' ⚠️ INCONSISTENT' : '';
// eslint-disable-next-line no-console
console.group(`%cbranch (${branch.status})${warning}`, style);
// eslint-disable-next-line no-console
console.log('%ceffect:', 'font-weight: bold', branch.effect);
if (branch.effect.fn) {
// eslint-disable-next-line no-console
console.log('%cfn:', 'font-weight: bold', branch.effect.fn);
}
if (branch.effect.deps !== null) {
// eslint-disable-next-line no-console
console.groupCollapsed('%cdeps', 'font-weight: normal');
for (const dep of branch.effect.deps) {
log_dep(dep);
}
// eslint-disable-next-line no-console
console.groupEnd();
}
if (is_inconsistent) {
log_effect_tree(branch.effect);
} else if (branch.children.length > 0) {
// eslint-disable-next-line no-console
console.group('%cchild branches', 'font-weight: bold');
for (const child of branch.children) {
log_branch(child, new_ancestor_clean, depth + 1);
}
// eslint-disable-next-line no-console
console.groupEnd();
}
// eslint-disable-next-line no-console
console.groupEnd();
}
const branches = collect_branches(root_effect, false);
// Check if there are any inconsistencies at all
let has_any_inconsistency = false;
for (const branch of branches) {
if (has_inconsistency(branch, false)) {
has_any_inconsistency = true;
break;
}
}
if (!has_any_inconsistency) {
// eslint-disable-next-line no-console
console.log('%cNo inconsistent branches found', 'font-weight: bold; color: green');
return;
}
// eslint-disable-next-line no-console
console.group(`%cInconsistent Branches (non-clean below clean)`, 'font-weight: bold; color: red');
for (const branch of branches) {
log_branch(branch, false, 0);
}
// eslint-disable-next-line no-console
console.groupEnd();
return true;
}
+63
View File
@@ -0,0 +1,63 @@
/** @import { SourceLocation } from '#client' */
import { COMMENT_NODE, DOCUMENT_FRAGMENT_NODE, ELEMENT_NODE } from '#client/constants';
import { HYDRATION_END, HYDRATION_START, HYDRATION_START_ELSE } from '../../../constants.js';
import { hydrating } from '../dom/hydration.js';
import { dev_stack } from '../context.js';
/**
* @param {any} fn
* @param {string} filename
* @param {SourceLocation[]} locations
* @returns {any}
*/
export function add_locations(fn, filename, locations) {
return (/** @type {any[]} */ ...args) => {
const dom = fn(...args);
var node = hydrating ? dom : dom.nodeType === DOCUMENT_FRAGMENT_NODE ? dom.firstChild : dom;
assign_locations(node, filename, locations);
return dom;
};
}
/**
* @param {Element} element
* @param {string} filename
* @param {SourceLocation} location
*/
function assign_location(element, filename, location) {
// @ts-expect-error
element.__svelte_meta = {
parent: dev_stack,
loc: { file: filename, line: location[0], column: location[1] }
};
if (location[2]) {
assign_locations(element.firstChild, filename, location[2]);
}
}
/**
* @param {Node | null} node
* @param {string} filename
* @param {SourceLocation[]} locations
*/
function assign_locations(node, filename, locations) {
var i = 0;
var depth = 0;
while (node && i < locations.length) {
if (hydrating && node.nodeType === COMMENT_NODE) {
var comment = /** @type {Comment} */ (node);
if (comment.data === HYDRATION_START || comment.data === HYDRATION_START_ELSE) depth += 1;
else if (comment.data[0] === HYDRATION_END) depth -= 1;
}
if (depth === 0 && node.nodeType === ELEMENT_NODE) {
assign_location(/** @type {Element} */ (node), filename, locations[i++]);
}
node = node.nextSibling;
}
}
+101
View File
@@ -0,0 +1,101 @@
import * as w from '../warnings.js';
import { get_proxied_value } from '../proxy.js';
export function init_array_prototype_warnings() {
const array_prototype = Array.prototype;
// The REPL ends up here over and over, and this prevents it from adding more and more patches
// of the same kind to the prototype, which would slow down everything over time.
// @ts-expect-error
const cleanup = Array.__svelte_cleanup;
if (cleanup) {
cleanup();
}
const { indexOf, lastIndexOf, includes } = array_prototype;
array_prototype.indexOf = function (item, from_index) {
const index = indexOf.call(this, item, from_index);
if (index === -1) {
for (let i = from_index ?? 0; i < this.length; i += 1) {
if (get_proxied_value(this[i]) === item) {
w.state_proxy_equality_mismatch('array.indexOf(...)');
break;
}
}
}
return index;
};
array_prototype.lastIndexOf = function (item, from_index) {
// we need to specify this.length - 1 because it's probably using something like
// `arguments` inside so passing undefined is different from not passing anything
const index = lastIndexOf.call(this, item, from_index ?? this.length - 1);
if (index === -1) {
for (let i = 0; i <= (from_index ?? this.length - 1); i += 1) {
if (get_proxied_value(this[i]) === item) {
w.state_proxy_equality_mismatch('array.lastIndexOf(...)');
break;
}
}
}
return index;
};
array_prototype.includes = function (item, from_index) {
const has = includes.call(this, item, from_index);
if (!has) {
for (let i = 0; i < this.length; i += 1) {
if (get_proxied_value(this[i]) === item) {
w.state_proxy_equality_mismatch('array.includes(...)');
break;
}
}
}
return has;
};
// @ts-expect-error
Array.__svelte_cleanup = () => {
array_prototype.indexOf = indexOf;
array_prototype.lastIndexOf = lastIndexOf;
array_prototype.includes = includes;
};
}
/**
* @param {any} a
* @param {any} b
* @param {boolean} equal
* @returns {boolean}
*/
export function strict_equals(a, b, equal = true) {
// try-catch needed because this tries to read properties of `a` and `b`,
// which could be disallowed for example in a secure context
try {
if ((a === b) !== (get_proxied_value(a) === get_proxied_value(b))) {
w.state_proxy_equality_mismatch(equal ? '===' : '!==');
}
} catch {}
return (a === b) === equal;
}
/**
* @param {any} a
* @param {any} b
* @param {boolean} equal
* @returns {boolean}
*/
export function equals(a, b, equal = true) {
if ((a == b) !== (get_proxied_value(a) == get_proxied_value(b))) {
w.state_proxy_equality_mismatch(equal ? '==' : '!=');
}
return (a == b) === equal;
}
+89
View File
@@ -0,0 +1,89 @@
/** @import { Effect, TemplateNode } from '#client' */
import { FILENAME, HMR } from '../../../constants.js';
import { EFFECT_TRANSPARENT } from '#client/constants';
import { hydrate_node, hydrating } from '../dom/hydration.js';
import { block, branch, destroy_effect } from '../reactivity/effects.js';
import { set, source } from '../reactivity/sources.js';
import { set_should_intro } from '../render.js';
import { get } from '../runtime.js';
/**
* @template {(anchor: Comment, props: any) => any} Component
* @param {Component} fn
*/
export function hmr(fn) {
const current = source(fn);
/**
* @param {TemplateNode} anchor
* @param {any} props
*/
function wrapper(anchor, props) {
let component = {};
let instance = {};
/** @type {Effect} */
let effect;
let ran = false;
block(() => {
if (component === (component = get(current))) {
return;
}
if (effect) {
// @ts-ignore
for (var k in instance) delete instance[k];
destroy_effect(effect);
}
effect = branch(() => {
// when the component is invalidated, replace it without transitions
if (ran) set_should_intro(false);
// preserve getters/setters
Object.defineProperties(
instance,
Object.getOwnPropertyDescriptors(
// @ts-expect-error
new.target ? new component(anchor, props) : component(anchor, props)
)
);
if (ran) set_should_intro(true);
});
}, EFFECT_TRANSPARENT);
ran = true;
if (hydrating) {
anchor = hydrate_node;
}
return instance;
}
// @ts-expect-error
wrapper[FILENAME] = fn[FILENAME];
// @ts-ignore
wrapper[HMR] = {
fn,
current,
update: (/** @type {any} */ incoming) => {
// This logic ensures that the first version of the component is the one
// whose update function and therefore block effect is preserved across updates.
// If we don't do this dance and instead just use `incoming` as the new component
// and then update, we'll create an ever-growing stack of block effects.
// Trigger the original block effect
set(wrapper[HMR].current, incoming[HMR].fn);
// Replace the incoming source with the original one
incoming[HMR].current = wrapper[HMR].current;
}
};
return wrapper;
}
+72
View File
@@ -0,0 +1,72 @@
import { UNINITIALIZED } from '../../../constants.js';
import { snapshot } from '../../shared/clone.js';
import { eager_effect, render_effect, validate_effect } from '../reactivity/effects.js';
import { untrack } from '../runtime.js';
import { get_error } from '../../shared/dev.js';
/**
* @param {() => any[]} get_value
* @param {Function} inspector
* @param {boolean} show_stack
*/
export function inspect(get_value, inspector, show_stack = false) {
validate_effect('$inspect');
let initial = true;
let error = /** @type {any} */ (UNINITIALIZED);
// Inspect effects runs synchronously so that we can capture useful
// stack traces. As a consequence, reading the value might result
// in an error (an `$inspect(object.property)` will run before the
// `{#if object}...{/if}` that contains it)
eager_effect(() => {
try {
var value = get_value();
} catch (e) {
error = e;
return;
}
var snap = snapshot(value, true, true);
untrack(() => {
if (show_stack) {
inspector(...snap);
if (!initial) {
const stack = get_error('$inspect(...)');
if (stack) {
// eslint-disable-next-line no-console
console.groupCollapsed('stack trace');
// eslint-disable-next-line no-console
console.log(stack);
// eslint-disable-next-line no-console
console.groupEnd();
}
}
} else {
inspector(initial ? 'init' : 'update', ...snap);
}
});
initial = false;
});
// If an error occurs, we store it (along with its stack trace).
// If the render effect subsequently runs, we log the error,
// but if it doesn't run it's because the `$inspect` was
// destroyed, meaning we don't need to bother
render_effect(() => {
try {
// call `get_value` so that this runs alongside the inspect effect
get_value();
} catch {
// ignore
}
if (error !== UNINITIALIZED) {
// eslint-disable-next-line no-console
console.error(error);
error = UNINITIALIZED;
}
});
}
+25
View File
@@ -0,0 +1,25 @@
import * as e from '../errors.js';
import { component_context } from '../context.js';
import { FILENAME } from '../../../constants.js';
/** @param {Function & { [FILENAME]: string }} target */
export function check_target(target) {
if (target) {
e.component_api_invalid_new(target[FILENAME] ?? 'a component', target.name);
}
}
export function legacy_api() {
const component = component_context?.function;
/** @param {string} method */
function error(method) {
e.component_api_changed(method, component[FILENAME]);
}
return {
$destroy: () => error('$destroy()'),
$on: () => error('$on(...)'),
$set: () => error('$set(...)')
};
}
+81
View File
@@ -0,0 +1,81 @@
/** @typedef {{ file: string, line: number, column: number }} Location */
import { get_descriptor } from '../../shared/utils.js';
import { LEGACY_PROPS, STATE_SYMBOL } from '#client/constants';
import { FILENAME } from '../../../constants.js';
import { component_context } from '../context.js';
import * as w from '../warnings.js';
import { sanitize_location } from '../../../utils.js';
/**
* Sets up a validator that
* - traverses the path of a prop to find out if it is allowed to be mutated
* - checks that the binding chain is not interrupted
* @param {Record<string, any>} props
*/
export function create_ownership_validator(props) {
const component = component_context?.function;
const parent = component_context?.p?.function;
return {
/**
* @param {string} prop
* @param {any[]} path
* @param {any} result
* @param {number} line
* @param {number} column
*/
mutation: (prop, path, result, line, column) => {
const name = path[0];
if (is_bound_or_unset(props, name) || !parent) {
return result;
}
/** @type {any} */
let value = props;
for (let i = 0; i < path.length - 1; i++) {
value = value[path[i]];
if (!value?.[STATE_SYMBOL]) {
return result;
}
}
const location = sanitize_location(`${component[FILENAME]}:${line}:${column}`);
w.ownership_invalid_mutation(name, location, prop, parent[FILENAME]);
return result;
},
/**
* @param {any} key
* @param {any} child_component
* @param {() => any} value
*/
binding: (key, child_component, value) => {
if (!is_bound_or_unset(props, key) && parent && value()?.[STATE_SYMBOL]) {
w.ownership_invalid_binding(
component[FILENAME],
key,
child_component[FILENAME],
parent[FILENAME]
);
}
}
};
}
/**
* @param {Record<string, any>} props
* @param {string} prop_name
*/
function is_bound_or_unset(props, prop_name) {
// Can be the case when someone does `mount(Component, props)` with `let props = $state({...})`
// or `createClassComponent(Component, props)`
const is_entry_props = STATE_SYMBOL in props || LEGACY_PROPS in props;
return (
!!get_descriptor(props, prop_name)?.set ||
(is_entry_props && prop_name in props) ||
!(prop_name in props)
);
}
+162
View File
@@ -0,0 +1,162 @@
/** @import { Derived, Reaction, Value } from '#client' */
import { UNINITIALIZED } from '../../../constants.js';
import { snapshot } from '../../shared/clone.js';
import { DERIVED, ASYNC, PROXY_PATH_SYMBOL, STATE_SYMBOL } from '#client/constants';
import { effect_tracking } from '../reactivity/effects.js';
import { active_reaction, untrack } from '../runtime.js';
/**
* @typedef {{
* traces: Error[];
* }} TraceEntry
*/
/** @type {{ reaction: Reaction | null, entries: Map<Value, TraceEntry> } | null} */
export let tracing_expressions = null;
/**
* @param {Value} signal
* @param {TraceEntry} [entry]
*/
function log_entry(signal, entry) {
const value = signal.v;
if (value === UNINITIALIZED) {
return;
}
const type = get_type(signal);
const current_reaction = /** @type {Reaction} */ (active_reaction);
const dirty = signal.wv > current_reaction.wv || current_reaction.wv === 0;
const style = dirty
? 'color: CornflowerBlue; font-weight: bold'
: 'color: grey; font-weight: normal';
// eslint-disable-next-line no-console
console.groupCollapsed(
signal.label ? `%c${type}%c ${signal.label}` : `%c${type}%c`,
style,
dirty ? 'font-weight: normal' : style,
typeof value === 'object' && value !== null && STATE_SYMBOL in value
? snapshot(value, true)
: value
);
if (type === '$derived') {
const deps = new Set(/** @type {Derived} */ (signal).deps);
for (const dep of deps) {
log_entry(dep);
}
}
if (signal.created) {
// eslint-disable-next-line no-console
console.log(signal.created);
}
if (dirty && signal.updated) {
for (const updated of signal.updated.values()) {
if (updated.error) {
// eslint-disable-next-line no-console
console.log(updated.error);
}
}
}
if (entry) {
for (var trace of entry.traces) {
// eslint-disable-next-line no-console
console.log(trace);
}
}
// eslint-disable-next-line no-console
console.groupEnd();
}
/**
* @param {Value} signal
* @returns {'$state' | '$derived' | 'store'}
*/
function get_type(signal) {
if ((signal.f & (DERIVED | ASYNC)) !== 0) return '$derived';
return signal.label?.startsWith('$') ? 'store' : '$state';
}
/**
* @template T
* @param {() => string} label
* @param {() => T} fn
*/
export function trace(label, fn) {
var previously_tracing_expressions = tracing_expressions;
try {
tracing_expressions = { entries: new Map(), reaction: active_reaction };
var start = performance.now();
var value = fn();
var time = (performance.now() - start).toFixed(2);
var prefix = untrack(label);
if (!effect_tracking()) {
// eslint-disable-next-line no-console
console.log(`${prefix} %cran outside of an effect (${time}ms)`, 'color: grey');
} else if (tracing_expressions.entries.size === 0) {
// eslint-disable-next-line no-console
console.log(`${prefix} %cno reactive dependencies (${time}ms)`, 'color: grey');
} else {
// eslint-disable-next-line no-console
console.group(`${prefix} %c(${time}ms)`, 'color: grey');
var entries = tracing_expressions.entries;
untrack(() => {
for (const [signal, traces] of entries) {
log_entry(signal, traces);
}
});
tracing_expressions = null;
// eslint-disable-next-line no-console
console.groupEnd();
}
return value;
} finally {
tracing_expressions = previously_tracing_expressions;
}
}
/**
* @param {Value} source
* @param {string} label
*/
export function tag(source, label) {
source.label = label;
tag_proxy(source.v, label);
return source;
}
/**
* @param {unknown} value
* @param {string} label
*/
export function tag_proxy(value, label) {
// @ts-expect-error
value?.[PROXY_PATH_SYMBOL]?.(label);
return value;
}
/**
* @param {unknown} value
*/
export function label(value) {
if (typeof value === 'symbol') return `Symbol(${value.description})`;
if (typeof value === 'function') return '<function>';
if (typeof value === 'object' && value) return '<object>';
return String(value);
}
+16
View File
@@ -0,0 +1,16 @@
import * as e from '../errors.js';
/**
* @param {Node} anchor
* @param {...(()=>any)[]} args
*/
export function validate_snippet_args(anchor, ...args) {
if (typeof anchor !== 'object' || !(anchor instanceof Node)) {
e.invalid_snippet_arguments();
}
for (let arg of args) {
if (typeof arg !== 'function') {
e.invalid_snippet_arguments();
}
}
}
+59
View File
@@ -0,0 +1,59 @@
/** @import { TemplateNode, Value } from '#client' */
import { flatten } from '../../reactivity/async.js';
import { Batch, current_batch } from '../../reactivity/batch.js';
import { get } from '../../runtime.js';
import {
hydrate_next,
hydrate_node,
hydrating,
set_hydrate_node,
set_hydrating,
skip_nodes
} from '../hydration.js';
import { get_boundary } from './boundary.js';
/**
* @param {TemplateNode} node
* @param {Array<Promise<void>>} blockers
* @param {Array<() => Promise<any>>} expressions
* @param {(anchor: TemplateNode, ...deriveds: Value[]) => void} fn
*/
export function async(node, blockers = [], expressions = [], fn) {
var boundary = get_boundary();
var batch = /** @type {Batch} */ (current_batch);
var blocking = boundary.is_rendered();
boundary.update_pending_count(1);
batch.increment(blocking);
var was_hydrating = hydrating;
if (was_hydrating) {
hydrate_next();
var previous_hydrate_node = hydrate_node;
var end = skip_nodes(false);
set_hydrate_node(end);
}
flatten(blockers, [], expressions, (values) => {
if (was_hydrating) {
set_hydrating(true);
set_hydrate_node(previous_hydrate_node);
}
try {
// get values eagerly to avoid creating blocks if they reject
for (const d of values) get(d);
fn(node, ...values);
} finally {
if (was_hydrating) {
set_hydrating(false);
}
boundary.update_pending_count(-1);
batch.decrement(blocking);
}
});
}
+142
View File
@@ -0,0 +1,142 @@
/** @import { Source, TemplateNode } from '#client' */
import { is_promise } from '../../../shared/utils.js';
import { block } from '../../reactivity/effects.js';
import { internal_set, mutable_source, source } from '../../reactivity/sources.js';
import {
hydrate_next,
hydrating,
skip_nodes,
set_hydrate_node,
set_hydrating
} from '../hydration.js';
import { queue_micro_task } from '../task.js';
import { HYDRATION_START_ELSE, UNINITIALIZED } from '../../../../constants.js';
import { is_runes } from '../../context.js';
import { Batch, flushSync, is_flushing_sync } from '../../reactivity/batch.js';
import { BranchManager } from './branches.js';
import { capture, unset_context } from '../../reactivity/async.js';
const PENDING = 0;
const THEN = 1;
const CATCH = 2;
/** @typedef {typeof PENDING | typeof THEN | typeof CATCH} AwaitState */
/**
* @template V
* @param {TemplateNode} node
* @param {(() => any)} get_input
* @param {null | ((anchor: Node) => void)} pending_fn
* @param {null | ((anchor: Node, value: Source<V>) => void)} then_fn
* @param {null | ((anchor: Node, error: unknown) => void)} catch_fn
* @returns {void}
*/
export function await_block(node, get_input, pending_fn, then_fn, catch_fn) {
if (hydrating) {
hydrate_next();
}
var runes = is_runes();
var v = /** @type {V} */ (UNINITIALIZED);
var value = runes ? source(v) : mutable_source(v, false, false);
var error = runes ? source(v) : mutable_source(v, false, false);
var branches = new BranchManager(node);
block(() => {
var input = get_input();
var destroyed = false;
/** Whether or not there was a hydration mismatch. Needs to be a `let` or else it isn't treeshaken out */
// @ts-ignore coercing `node` to a `Comment` causes TypeScript and Prettier to fight
let mismatch = hydrating && is_promise(input) === (node.data === HYDRATION_START_ELSE);
if (mismatch) {
// Hydration mismatch: remove everything inside the anchor and start fresh
set_hydrate_node(skip_nodes());
set_hydrating(false);
}
if (is_promise(input)) {
var restore = capture();
var resolved = false;
/**
* @param {() => void} fn
*/
const resolve = (fn) => {
if (destroyed) return;
resolved = true;
// We don't want to restore the previous batch here; {#await} blocks don't follow the async logic
// we have elsewhere, instead pending/resolve/fail states are each their own batch so to speak.
restore(false);
// Make sure we have a batch, since the branch manager expects one to exist
Batch.ensure();
if (hydrating) {
// `restore()` could set `hydrating` to `true`, which we very much
// don't want — we want to restore everything _except_ this
set_hydrating(false);
}
try {
fn();
} finally {
unset_context();
// without this, the DOM does not update until two ticks after the promise
// resolves, which is unexpected behaviour (and somewhat irksome to test)
if (!is_flushing_sync) flushSync();
}
};
input.then(
(v) => {
resolve(() => {
internal_set(value, v);
branches.ensure(THEN, then_fn && ((target) => then_fn(target, value)));
});
},
(e) => {
resolve(() => {
internal_set(error, e);
branches.ensure(THEN, catch_fn && ((target) => catch_fn(target, error)));
if (!catch_fn) {
// Rethrow the error if no catch block exists
throw error.v;
}
});
}
);
if (hydrating) {
branches.ensure(PENDING, pending_fn);
} else {
// Wait a microtask before checking if we should show the pending state as
// the promise might have resolved by then
queue_micro_task(() => {
if (!resolved) {
resolve(() => {
branches.ensure(PENDING, pending_fn);
});
}
});
}
} else {
internal_set(value, input);
branches.ensure(THEN, then_fn && ((target) => then_fn(target, value)));
}
if (mismatch) {
// continue in hydration mode
set_hydrating(true);
}
return () => {
destroyed = true;
};
});
}
+501
View File
@@ -0,0 +1,501 @@
/** @import { Effect, Source, TemplateNode, } from '#client' */
import {
BOUNDARY_EFFECT,
COMMENT_NODE,
DIRTY,
EFFECT_PRESERVED,
EFFECT_TRANSPARENT,
MAYBE_DIRTY
} from '#client/constants';
import { HYDRATION_START_ELSE } from '../../../../constants.js';
import { component_context, set_component_context } from '../../context.js';
import { handle_error, invoke_error_boundary } from '../../error-handling.js';
import {
block,
branch,
destroy_effect,
move_effect,
pause_effect
} from '../../reactivity/effects.js';
import {
active_effect,
active_reaction,
get,
set_active_effect,
set_active_reaction
} from '../../runtime.js';
import {
hydrate_next,
hydrate_node,
hydrating,
next,
skip_nodes,
set_hydrate_node
} from '../hydration.js';
import { queue_micro_task } from '../task.js';
import * as e from '../../errors.js';
import * as w from '../../warnings.js';
import { DEV } from 'esm-env';
import { Batch, schedule_effect } from '../../reactivity/batch.js';
import { internal_set, source } from '../../reactivity/sources.js';
import { tag } from '../../dev/tracing.js';
import { createSubscriber } from '../../../../reactivity/create-subscriber.js';
import { create_text } from '../operations.js';
import { defer_effect } from '../../reactivity/utils.js';
import { set_signal_status } from '../../reactivity/status.js';
/**
* @typedef {{
* onerror?: (error: unknown, reset: () => void) => void;
* failed?: (anchor: Node, error: () => unknown, reset: () => () => void) => void;
* pending?: (anchor: Node) => void;
* }} BoundaryProps
*/
var flags = EFFECT_TRANSPARENT | EFFECT_PRESERVED | BOUNDARY_EFFECT;
/**
* @param {TemplateNode} node
* @param {BoundaryProps} props
* @param {((anchor: Node) => void)} children
* @returns {void}
*/
export function boundary(node, props, children) {
new Boundary(node, props, children);
}
export class Boundary {
/** @type {Boundary | null} */
parent;
is_pending = false;
/** @type {TemplateNode} */
#anchor;
/** @type {TemplateNode | null} */
#hydrate_open = hydrating ? hydrate_node : null;
/** @type {BoundaryProps} */
#props;
/** @type {((anchor: Node) => void)} */
#children;
/** @type {Effect} */
#effect;
/** @type {Effect | null} */
#main_effect = null;
/** @type {Effect | null} */
#pending_effect = null;
/** @type {Effect | null} */
#failed_effect = null;
/** @type {DocumentFragment | null} */
#offscreen_fragment = null;
/** @type {TemplateNode | null} */
#pending_anchor = null;
#local_pending_count = 0;
#pending_count = 0;
#is_creating_fallback = false;
/** @type {Set<Effect>} */
#dirty_effects = new Set();
/** @type {Set<Effect>} */
#maybe_dirty_effects = new Set();
/**
* A source containing the number of pending async deriveds/expressions.
* Only created if `$effect.pending()` is used inside the boundary,
* otherwise updating the source results in needless `Batch.ensure()`
* calls followed by no-op flushes
* @type {Source<number> | null}
*/
#effect_pending = null;
#effect_pending_subscriber = createSubscriber(() => {
this.#effect_pending = source(this.#local_pending_count);
if (DEV) {
tag(this.#effect_pending, '$effect.pending()');
}
return () => {
this.#effect_pending = null;
};
});
/**
* @param {TemplateNode} node
* @param {BoundaryProps} props
* @param {((anchor: Node) => void)} children
*/
constructor(node, props, children) {
this.#anchor = node;
this.#props = props;
this.#children = children;
this.parent = /** @type {Effect} */ (active_effect).b;
this.is_pending = !!this.#props.pending;
this.#effect = block(() => {
/** @type {Effect} */ (active_effect).b = this;
if (hydrating) {
const comment = this.#hydrate_open;
hydrate_next();
const server_rendered_pending =
/** @type {Comment} */ (comment).nodeType === COMMENT_NODE &&
/** @type {Comment} */ (comment).data === HYDRATION_START_ELSE;
if (server_rendered_pending) {
this.#hydrate_pending_content();
} else {
this.#hydrate_resolved_content();
if (this.#pending_count === 0) {
this.is_pending = false;
}
}
} else {
var anchor = this.#get_anchor();
try {
this.#main_effect = branch(() => children(anchor));
} catch (error) {
this.error(error);
}
if (this.#pending_count > 0) {
this.#show_pending_snippet();
} else {
this.is_pending = false;
}
}
return () => {
this.#pending_anchor?.remove();
};
}, flags);
if (hydrating) {
this.#anchor = hydrate_node;
}
}
#hydrate_resolved_content() {
try {
this.#main_effect = branch(() => this.#children(this.#anchor));
} catch (error) {
this.error(error);
}
}
#hydrate_pending_content() {
const pending = this.#props.pending;
if (!pending) {
return;
}
this.#pending_effect = branch(() => pending(this.#anchor));
Batch.enqueue(() => {
var anchor = this.#get_anchor();
this.#main_effect = this.#run(() => {
Batch.ensure();
return branch(() => this.#children(anchor));
});
if (this.#pending_count > 0) {
this.#show_pending_snippet();
} else {
pause_effect(/** @type {Effect} */ (this.#pending_effect), () => {
this.#pending_effect = null;
});
this.is_pending = false;
}
});
}
#get_anchor() {
var anchor = this.#anchor;
if (this.is_pending) {
this.#pending_anchor = create_text();
this.#anchor.before(this.#pending_anchor);
anchor = this.#pending_anchor;
}
return anchor;
}
/**
* Defer an effect inside a pending boundary until the boundary resolves
* @param {Effect} effect
*/
defer_effect(effect) {
defer_effect(effect, this.#dirty_effects, this.#maybe_dirty_effects);
}
/**
* Returns `false` if the effect exists inside a boundary whose pending snippet is shown
* @returns {boolean}
*/
is_rendered() {
return !this.is_pending && (!this.parent || this.parent.is_rendered());
}
has_pending_snippet() {
return !!this.#props.pending;
}
/**
* @param {() => Effect | null} fn
*/
#run(fn) {
var previous_effect = active_effect;
var previous_reaction = active_reaction;
var previous_ctx = component_context;
set_active_effect(this.#effect);
set_active_reaction(this.#effect);
set_component_context(this.#effect.ctx);
try {
return fn();
} catch (e) {
handle_error(e);
return null;
} finally {
set_active_effect(previous_effect);
set_active_reaction(previous_reaction);
set_component_context(previous_ctx);
}
}
#show_pending_snippet() {
const pending = /** @type {(anchor: Node) => void} */ (this.#props.pending);
if (this.#main_effect !== null) {
this.#offscreen_fragment = document.createDocumentFragment();
this.#offscreen_fragment.append(/** @type {TemplateNode} */ (this.#pending_anchor));
move_effect(this.#main_effect, this.#offscreen_fragment);
}
if (this.#pending_effect === null) {
this.#pending_effect = branch(() => pending(this.#anchor));
}
}
/**
* Updates the pending count associated with the currently visible pending snippet,
* if any, such that we can replace the snippet with content once work is done
* @param {1 | -1} d
*/
#update_pending_count(d) {
if (!this.has_pending_snippet()) {
if (this.parent) {
this.parent.#update_pending_count(d);
}
// if there's no parent, we're in a scope with no pending snippet
return;
}
this.#pending_count += d;
if (this.#pending_count === 0) {
this.is_pending = false;
// any effects that were encountered and deferred during traversal
// should be rescheduled — after the next traversal (which will happen
// immediately, due to the same update that brought us here)
// the effects will be flushed
for (const e of this.#dirty_effects) {
set_signal_status(e, DIRTY);
schedule_effect(e);
}
for (const e of this.#maybe_dirty_effects) {
set_signal_status(e, MAYBE_DIRTY);
schedule_effect(e);
}
this.#dirty_effects.clear();
this.#maybe_dirty_effects.clear();
if (this.#pending_effect) {
pause_effect(this.#pending_effect, () => {
this.#pending_effect = null;
});
}
if (this.#offscreen_fragment) {
this.#anchor.before(this.#offscreen_fragment);
this.#offscreen_fragment = null;
}
}
}
/**
* Update the source that powers `$effect.pending()` inside this boundary,
* and controls when the current `pending` snippet (if any) is removed.
* Do not call from inside the class
* @param {1 | -1} d
*/
update_pending_count(d) {
this.#update_pending_count(d);
this.#local_pending_count += d;
if (this.#effect_pending) {
internal_set(this.#effect_pending, this.#local_pending_count);
}
}
get_effect_pending() {
this.#effect_pending_subscriber();
return get(/** @type {Source<number>} */ (this.#effect_pending));
}
/** @param {unknown} error */
error(error) {
var onerror = this.#props.onerror;
let failed = this.#props.failed;
// If we have nothing to capture the error, or if we hit an error while
// rendering the fallback, re-throw for another boundary to handle
if (this.#is_creating_fallback || (!onerror && !failed)) {
throw error;
}
if (this.#main_effect) {
destroy_effect(this.#main_effect);
this.#main_effect = null;
}
if (this.#pending_effect) {
destroy_effect(this.#pending_effect);
this.#pending_effect = null;
}
if (this.#failed_effect) {
destroy_effect(this.#failed_effect);
this.#failed_effect = null;
}
if (hydrating) {
set_hydrate_node(/** @type {TemplateNode} */ (this.#hydrate_open));
next();
set_hydrate_node(skip_nodes());
}
var did_reset = false;
var calling_on_error = false;
const reset = () => {
if (did_reset) {
w.svelte_boundary_reset_noop();
return;
}
did_reset = true;
if (calling_on_error) {
e.svelte_boundary_reset_onerror();
}
// If the failure happened while flushing effects, current_batch can be null
Batch.ensure();
this.#local_pending_count = 0;
if (this.#failed_effect !== null) {
pause_effect(this.#failed_effect, () => {
this.#failed_effect = null;
});
}
// we intentionally do not try to find the nearest pending boundary. If this boundary has one, we'll render it on reset
// but it would be really weird to show the parent's boundary on a child reset.
this.is_pending = this.has_pending_snippet();
this.#main_effect = this.#run(() => {
this.#is_creating_fallback = false;
return branch(() => this.#children(this.#anchor));
});
if (this.#pending_count > 0) {
this.#show_pending_snippet();
} else {
this.is_pending = false;
}
};
var previous_reaction = active_reaction;
try {
set_active_reaction(null);
calling_on_error = true;
onerror?.(error, reset);
calling_on_error = false;
} catch (error) {
invoke_error_boundary(error, this.#effect && this.#effect.parent);
} finally {
set_active_reaction(previous_reaction);
}
if (failed) {
queue_micro_task(() => {
this.#failed_effect = this.#run(() => {
Batch.ensure();
this.#is_creating_fallback = true;
try {
return branch(() => {
failed(
this.#anchor,
() => error,
() => reset
);
});
} catch (error) {
invoke_error_boundary(error, /** @type {Effect} */ (this.#effect.parent));
return null;
} finally {
this.#is_creating_fallback = false;
}
});
});
}
}
}
export function get_boundary() {
return /** @type {Boundary} */ (/** @type {Effect} */ (active_effect).b);
}
export function pending() {
if (active_effect === null) {
e.effect_pending_outside_reaction();
}
var boundary = active_effect.b;
if (boundary === null) {
return 0; // TODO eventually we will need this to be global
}
return boundary.get_effect_pending();
}
+227
View File
@@ -0,0 +1,227 @@
/** @import { Effect, TemplateNode } from '#client' */
import { Batch, current_batch } from '../../reactivity/batch.js';
import {
branch,
destroy_effect,
move_effect,
pause_effect,
resume_effect
} from '../../reactivity/effects.js';
import { hydrate_node, hydrating } from '../hydration.js';
import { create_text, should_defer_append } from '../operations.js';
/**
* @typedef {{ effect: Effect, fragment: DocumentFragment }} Branch
*/
/**
* @template Key
*/
export class BranchManager {
/** @type {TemplateNode} */
anchor;
/** @type {Map<Batch, Key>} */
#batches = new Map();
/**
* Map of keys to effects that are currently rendered in the DOM.
* These effects are visible and actively part of the document tree.
* Example:
* ```
* {#if condition}
* foo
* {:else}
* bar
* {/if}
* ```
* Can result in the entries `true->Effect` and `false->Effect`
* @type {Map<Key, Effect>}
*/
#onscreen = new Map();
/**
* Similar to #onscreen with respect to the keys, but contains branches that are not yet
* in the DOM, because their insertion is deferred.
* @type {Map<Key, Branch>}
*/
#offscreen = new Map();
/**
* Keys of effects that are currently outroing
* @type {Set<Key>}
*/
#outroing = new Set();
/**
* Whether to pause (i.e. outro) on change, or destroy immediately.
* This is necessary for `<svelte:element>`
*/
#transition = true;
/**
* @param {TemplateNode} anchor
* @param {boolean} transition
*/
constructor(anchor, transition = true) {
this.anchor = anchor;
this.#transition = transition;
}
#commit = () => {
var batch = /** @type {Batch} */ (current_batch);
// if this batch was made obsolete, bail
if (!this.#batches.has(batch)) return;
var key = /** @type {Key} */ (this.#batches.get(batch));
var onscreen = this.#onscreen.get(key);
if (onscreen) {
// effect is already in the DOM — abort any current outro
resume_effect(onscreen);
this.#outroing.delete(key);
} else {
// effect is currently offscreen. put it in the DOM
var offscreen = this.#offscreen.get(key);
if (offscreen) {
this.#onscreen.set(key, offscreen.effect);
this.#offscreen.delete(key);
// remove the anchor...
/** @type {TemplateNode} */ (offscreen.fragment.lastChild).remove();
// ...and append the fragment
this.anchor.before(offscreen.fragment);
onscreen = offscreen.effect;
}
}
for (const [b, k] of this.#batches) {
this.#batches.delete(b);
if (b === batch) {
// keep values for newer batches
break;
}
const offscreen = this.#offscreen.get(k);
if (offscreen) {
// for older batches, destroy offscreen effects
// as they will never be committed
destroy_effect(offscreen.effect);
this.#offscreen.delete(k);
}
}
// outro/destroy all onscreen effects...
for (const [k, effect] of this.#onscreen) {
// ...except the one that was just committed
// or those that are already outroing (else the transition is aborted and the effect destroyed right away)
if (k === key || this.#outroing.has(k)) continue;
const on_destroy = () => {
const keys = Array.from(this.#batches.values());
if (keys.includes(k)) {
// keep the effect offscreen, as another batch will need it
var fragment = document.createDocumentFragment();
move_effect(effect, fragment);
fragment.append(create_text()); // TODO can we avoid this?
this.#offscreen.set(k, { effect, fragment });
} else {
destroy_effect(effect);
}
this.#outroing.delete(k);
this.#onscreen.delete(k);
};
if (this.#transition || !onscreen) {
this.#outroing.add(k);
pause_effect(effect, on_destroy, false);
} else {
on_destroy();
}
}
};
/**
* @param {Batch} batch
*/
#discard = (batch) => {
this.#batches.delete(batch);
const keys = Array.from(this.#batches.values());
for (const [k, branch] of this.#offscreen) {
if (!keys.includes(k)) {
destroy_effect(branch.effect);
this.#offscreen.delete(k);
}
}
};
/**
*
* @param {any} key
* @param {null | ((target: TemplateNode) => void)} fn
*/
ensure(key, fn) {
var batch = /** @type {Batch} */ (current_batch);
var defer = should_defer_append();
if (fn && !this.#onscreen.has(key) && !this.#offscreen.has(key)) {
if (defer) {
var fragment = document.createDocumentFragment();
var target = create_text();
fragment.append(target);
this.#offscreen.set(key, {
effect: branch(() => fn(target)),
fragment
});
} else {
this.#onscreen.set(
key,
branch(() => fn(this.anchor))
);
}
}
this.#batches.set(batch, key);
if (defer) {
for (const [k, effect] of this.#onscreen) {
if (k === key) {
batch.skipped_effects.delete(effect);
} else {
batch.skipped_effects.add(effect);
}
}
for (const [k, branch] of this.#offscreen) {
if (k === key) {
batch.skipped_effects.delete(branch.effect);
} else {
batch.skipped_effects.add(branch.effect);
}
}
batch.oncommit(this.#commit);
batch.ondiscard(this.#discard);
} else {
if (hydrating) {
this.anchor = hydrate_node;
}
this.#commit();
}
}
}
+28
View File
@@ -0,0 +1,28 @@
import { render_effect } from '../../reactivity/effects.js';
import { hydrating, set_hydrate_node } from '../hydration.js';
import { get_first_child } from '../operations.js';
/**
* @param {HTMLDivElement | SVGGElement} element
* @param {() => Record<string, string>} get_styles
* @returns {void}
*/
export function css_props(element, get_styles) {
if (hydrating) {
set_hydrate_node(get_first_child(element));
}
render_effect(() => {
var styles = get_styles();
for (var key in styles) {
var value = styles[key];
if (value) {
element.style.setProperty(key, value);
} else {
element.style.removeProperty(key);
}
}
});
}
+666
View File
@@ -0,0 +1,666 @@
/** @import { EachItem, EachOutroGroup, EachState, Effect, EffectNodes, MaybeSource, Source, TemplateNode, TransitionManager, Value } from '#client' */
/** @import { Batch } from '../../reactivity/batch.js'; */
import {
EACH_INDEX_REACTIVE,
EACH_IS_ANIMATED,
EACH_IS_CONTROLLED,
EACH_ITEM_IMMUTABLE,
EACH_ITEM_REACTIVE,
HYDRATION_END,
HYDRATION_START_ELSE
} from '../../../../constants.js';
import {
hydrate_next,
hydrate_node,
hydrating,
read_hydration_instruction,
skip_nodes,
set_hydrate_node,
set_hydrating
} from '../hydration.js';
import {
clear_text_content,
create_text,
get_first_child,
get_next_sibling,
should_defer_append
} from '../operations.js';
import {
block,
branch,
destroy_effect,
pause_effect,
resume_effect
} from '../../reactivity/effects.js';
import { source, mutable_source, internal_set } from '../../reactivity/sources.js';
import { array_from, is_array } from '../../../shared/utils.js';
import { COMMENT_NODE, EFFECT_OFFSCREEN, INERT } from '#client/constants';
import { queue_micro_task } from '../task.js';
import { get } from '../../runtime.js';
import { DEV } from 'esm-env';
import { derived_safe_equal } from '../../reactivity/deriveds.js';
import { current_batch } from '../../reactivity/batch.js';
// When making substantive changes to this file, validate them with the each block stress test:
// https://svelte.dev/playground/1972b2cf46564476ad8c8c6405b23b7b
// This test also exists in this repo, as `packages/svelte/tests/manual/each-stress-test`
/**
* @param {any} _
* @param {number} i
*/
export function index(_, i) {
return i;
}
/**
* Pause multiple effects simultaneously, and coordinate their
* subsequent destruction. Used in each blocks
* @param {EachState} state
* @param {Effect[]} to_destroy
* @param {null | Node} controlled_anchor
*/
function pause_effects(state, to_destroy, controlled_anchor) {
/** @type {TransitionManager[]} */
var transitions = [];
var length = to_destroy.length;
/** @type {EachOutroGroup} */
var group;
var remaining = to_destroy.length;
for (var i = 0; i < length; i++) {
let effect = to_destroy[i];
pause_effect(
effect,
() => {
if (group) {
group.pending.delete(effect);
group.done.add(effect);
if (group.pending.size === 0) {
var groups = /** @type {Set<EachOutroGroup>} */ (state.outrogroups);
destroy_effects(array_from(group.done));
groups.delete(group);
if (groups.size === 0) {
state.outrogroups = null;
}
}
} else {
remaining -= 1;
}
},
false
);
}
if (remaining === 0) {
// If we're in a controlled each block (i.e. the block is the only child of an
// element), and we are removing all items, _and_ there are no out transitions,
// we can use the fast path — emptying the element and replacing the anchor
var fast_path = transitions.length === 0 && controlled_anchor !== null;
if (fast_path) {
var anchor = /** @type {Element} */ (controlled_anchor);
var parent_node = /** @type {Element} */ (anchor.parentNode);
clear_text_content(parent_node);
parent_node.append(anchor);
state.items.clear();
}
destroy_effects(to_destroy, !fast_path);
} else {
group = {
pending: new Set(to_destroy),
done: new Set()
};
(state.outrogroups ??= new Set()).add(group);
}
}
/**
* @param {Effect[]} to_destroy
* @param {boolean} remove_dom
*/
function destroy_effects(to_destroy, remove_dom = true) {
// TODO only destroy effects if no pending batch needs them. otherwise,
// just re-add the `EFFECT_OFFSCREEN` flag
for (var i = 0; i < to_destroy.length; i++) {
destroy_effect(to_destroy[i], remove_dom);
}
}
/** @type {TemplateNode} */
var offscreen_anchor;
/**
* @template V
* @param {Element | Comment} node The next sibling node, or the parent node if this is a 'controlled' block
* @param {number} flags
* @param {() => V[]} get_collection
* @param {(value: V, index: number) => any} get_key
* @param {(anchor: Node, item: MaybeSource<V>, index: MaybeSource<number>) => void} render_fn
* @param {null | ((anchor: Node) => void)} fallback_fn
* @returns {void}
*/
export function each(node, flags, get_collection, get_key, render_fn, fallback_fn = null) {
var anchor = node;
/** @type {Map<any, EachItem>} */
var items = new Map();
var is_controlled = (flags & EACH_IS_CONTROLLED) !== 0;
if (is_controlled) {
var parent_node = /** @type {Element} */ (node);
anchor = hydrating
? set_hydrate_node(get_first_child(parent_node))
: parent_node.appendChild(create_text());
}
if (hydrating) {
hydrate_next();
}
/** @type {Effect | null} */
var fallback = null;
// TODO: ideally we could use derived for runes mode but because of the ability
// to use a store which can be mutated, we can't do that here as mutating a store
// will still result in the collection array being the same from the store
var each_array = derived_safe_equal(() => {
var collection = get_collection();
return is_array(collection) ? collection : collection == null ? [] : array_from(collection);
});
/** @type {V[]} */
var array;
var first_run = true;
function commit() {
state.fallback = fallback;
reconcile(state, array, anchor, flags, get_key);
if (fallback !== null) {
if (array.length === 0) {
if ((fallback.f & EFFECT_OFFSCREEN) === 0) {
resume_effect(fallback);
} else {
fallback.f ^= EFFECT_OFFSCREEN;
move(fallback, null, anchor);
}
} else {
pause_effect(fallback, () => {
// TODO only null out if no pending batch needs it,
// otherwise re-add `fallback.fragment` and move the
// effect into it
fallback = null;
});
}
}
}
var effect = block(() => {
array = /** @type {V[]} */ (get(each_array));
var length = array.length;
/** `true` if there was a hydration mismatch. Needs to be a `let` or else it isn't treeshaken out */
let mismatch = false;
if (hydrating) {
var is_else = read_hydration_instruction(anchor) === HYDRATION_START_ELSE;
if (is_else !== (length === 0)) {
// hydration mismatch — remove the server-rendered DOM and start over
anchor = skip_nodes();
set_hydrate_node(anchor);
set_hydrating(false);
mismatch = true;
}
}
var keys = new Set();
var batch = /** @type {Batch} */ (current_batch);
var defer = should_defer_append();
for (var index = 0; index < length; index += 1) {
if (
hydrating &&
hydrate_node.nodeType === COMMENT_NODE &&
/** @type {Comment} */ (hydrate_node).data === HYDRATION_END
) {
// The server rendered fewer items than expected,
// so break out and continue appending non-hydrated items
anchor = /** @type {Comment} */ (hydrate_node);
mismatch = true;
set_hydrating(false);
}
var value = array[index];
var key = get_key(value, index);
var item = first_run ? null : items.get(key);
if (item) {
// update before reconciliation, to trigger any async updates
if (item.v) internal_set(item.v, value);
if (item.i) internal_set(item.i, index);
if (defer) {
batch.skipped_effects.delete(item.e);
}
} else {
item = create_item(
items,
first_run ? anchor : (offscreen_anchor ??= create_text()),
value,
key,
index,
render_fn,
flags,
get_collection
);
if (!first_run) {
item.e.f |= EFFECT_OFFSCREEN;
}
items.set(key, item);
}
keys.add(key);
}
if (length === 0 && fallback_fn && !fallback) {
if (first_run) {
fallback = branch(() => fallback_fn(anchor));
} else {
fallback = branch(() => fallback_fn((offscreen_anchor ??= create_text())));
fallback.f |= EFFECT_OFFSCREEN;
}
}
// remove excess nodes
if (hydrating && length > 0) {
set_hydrate_node(skip_nodes());
}
if (!first_run) {
if (defer) {
for (const [key, item] of items) {
if (!keys.has(key)) {
batch.skipped_effects.add(item.e);
}
}
batch.oncommit(commit);
batch.ondiscard(() => {
// TODO presumably we need to do something here?
});
} else {
commit();
}
}
if (mismatch) {
// continue in hydration mode
set_hydrating(true);
}
// When we mount the each block for the first time, the collection won't be
// connected to this effect as the effect hasn't finished running yet and its deps
// won't be assigned. However, it's possible that when reconciling the each block
// that a mutation occurred and it's made the collection MAYBE_DIRTY, so reading the
// collection again can provide consistency to the reactive graph again as the deriveds
// will now be `CLEAN`.
get(each_array);
});
/** @type {EachState} */
var state = { effect, flags, items, outrogroups: null, fallback };
first_run = false;
if (hydrating) {
anchor = hydrate_node;
}
}
/**
* Add, remove, or reorder items output by an each block as its input changes
* @template V
* @param {EachState} state
* @param {Array<V>} array
* @param {Element | Comment | Text} anchor
* @param {number} flags
* @param {(value: V, index: number) => any} get_key
* @returns {void}
*/
function reconcile(state, array, anchor, flags, get_key) {
var is_animated = (flags & EACH_IS_ANIMATED) !== 0;
var length = array.length;
var items = state.items;
var current = state.effect.first;
/** @type {undefined | Set<Effect>} */
var seen;
/** @type {Effect | null} */
var prev = null;
/** @type {undefined | Set<Effect>} */
var to_animate;
/** @type {Effect[]} */
var matched = [];
/** @type {Effect[]} */
var stashed = [];
/** @type {V} */
var value;
/** @type {any} */
var key;
/** @type {Effect | undefined} */
var effect;
/** @type {number} */
var i;
if (is_animated) {
for (i = 0; i < length; i += 1) {
value = array[i];
key = get_key(value, i);
effect = /** @type {EachItem} */ (items.get(key)).e;
// offscreen == coming in now, no animation in that case,
// else this would happen https://github.com/sveltejs/svelte/issues/17181
if ((effect.f & EFFECT_OFFSCREEN) === 0) {
effect.nodes?.a?.measure();
(to_animate ??= new Set()).add(effect);
}
}
}
for (i = 0; i < length; i += 1) {
value = array[i];
key = get_key(value, i);
effect = /** @type {EachItem} */ (items.get(key)).e;
if (state.outrogroups !== null) {
for (const group of state.outrogroups) {
group.pending.delete(effect);
group.done.delete(effect);
}
}
if ((effect.f & EFFECT_OFFSCREEN) !== 0) {
effect.f ^= EFFECT_OFFSCREEN;
if (effect === current) {
move(effect, null, anchor);
} else {
var next = prev ? prev.next : current;
if (effect === state.effect.last) {
state.effect.last = effect.prev;
}
if (effect.prev) effect.prev.next = effect.next;
if (effect.next) effect.next.prev = effect.prev;
link(state, prev, effect);
link(state, effect, next);
move(effect, next, anchor);
prev = effect;
matched = [];
stashed = [];
current = prev.next;
continue;
}
}
if ((effect.f & INERT) !== 0) {
resume_effect(effect);
if (is_animated) {
effect.nodes?.a?.unfix();
(to_animate ??= new Set()).delete(effect);
}
}
if (effect !== current) {
if (seen !== undefined && seen.has(effect)) {
if (matched.length < stashed.length) {
// more efficient to move later items to the front
var start = stashed[0];
var j;
prev = start.prev;
var a = matched[0];
var b = matched[matched.length - 1];
for (j = 0; j < matched.length; j += 1) {
move(matched[j], start, anchor);
}
for (j = 0; j < stashed.length; j += 1) {
seen.delete(stashed[j]);
}
link(state, a.prev, b.next);
link(state, prev, a);
link(state, b, start);
current = start;
prev = b;
i -= 1;
matched = [];
stashed = [];
} else {
// more efficient to move earlier items to the back
seen.delete(effect);
move(effect, current, anchor);
link(state, effect.prev, effect.next);
link(state, effect, prev === null ? state.effect.first : prev.next);
link(state, prev, effect);
prev = effect;
}
continue;
}
matched = [];
stashed = [];
while (current !== null && current !== effect) {
(seen ??= new Set()).add(current);
stashed.push(current);
current = current.next;
}
if (current === null) {
continue;
}
}
if ((effect.f & EFFECT_OFFSCREEN) === 0) {
matched.push(effect);
}
prev = effect;
current = effect.next;
}
if (state.outrogroups !== null) {
for (const group of state.outrogroups) {
if (group.pending.size === 0) {
destroy_effects(array_from(group.done));
state.outrogroups?.delete(group);
}
}
if (state.outrogroups.size === 0) {
state.outrogroups = null;
}
}
if (current !== null || seen !== undefined) {
/** @type {Effect[]} */
var to_destroy = [];
if (seen !== undefined) {
for (effect of seen) {
if ((effect.f & INERT) === 0) {
to_destroy.push(effect);
}
}
}
while (current !== null) {
// If the each block isn't inert, then inert effects are currently outroing and will be removed once the transition is finished
if ((current.f & INERT) === 0 && current !== state.fallback) {
to_destroy.push(current);
}
current = current.next;
}
var destroy_length = to_destroy.length;
if (destroy_length > 0) {
var controlled_anchor = (flags & EACH_IS_CONTROLLED) !== 0 && length === 0 ? anchor : null;
if (is_animated) {
for (i = 0; i < destroy_length; i += 1) {
to_destroy[i].nodes?.a?.measure();
}
for (i = 0; i < destroy_length; i += 1) {
to_destroy[i].nodes?.a?.fix();
}
}
pause_effects(state, to_destroy, controlled_anchor);
}
}
if (is_animated) {
queue_micro_task(() => {
if (to_animate === undefined) return;
for (effect of to_animate) {
effect.nodes?.a?.apply();
}
});
}
}
/**
* @template V
* @param {Map<any, EachItem>} items
* @param {Node} anchor
* @param {V} value
* @param {unknown} key
* @param {number} index
* @param {(anchor: Node, item: V | Source<V>, index: number | Value<number>, collection: () => V[]) => void} render_fn
* @param {number} flags
* @param {() => V[]} get_collection
* @returns {EachItem}
*/
function create_item(items, anchor, value, key, index, render_fn, flags, get_collection) {
var v =
(flags & EACH_ITEM_REACTIVE) !== 0
? (flags & EACH_ITEM_IMMUTABLE) === 0
? mutable_source(value, false, false)
: source(value)
: null;
var i = (flags & EACH_INDEX_REACTIVE) !== 0 ? source(index) : null;
if (DEV && v) {
// For tracing purposes, we need to link the source signal we create with the
// collection + index so that tracing works as intended
v.trace = () => {
// eslint-disable-next-line @typescript-eslint/no-unused-expressions
get_collection()[i?.v ?? index];
};
}
return {
v,
i,
e: branch(() => {
render_fn(anchor, v ?? value, i ?? index, get_collection);
return () => {
items.delete(key);
};
})
};
}
/**
* @param {Effect} effect
* @param {Effect | null} next
* @param {Text | Element | Comment} anchor
*/
function move(effect, next, anchor) {
if (!effect.nodes) return;
var node = effect.nodes.start;
var end = effect.nodes.end;
var dest =
next && (next.f & EFFECT_OFFSCREEN) === 0
? /** @type {EffectNodes} */ (next.nodes).start
: anchor;
while (node !== null) {
var next_node = /** @type {TemplateNode} */ (get_next_sibling(node));
dest.before(node);
if (node === end) {
return;
}
node = next_node;
}
}
/**
* @param {EachState} state
* @param {Effect | null} prev
* @param {Effect | null} next
*/
function link(state, prev, next) {
if (prev === null) {
state.effect.first = next;
} else {
prev.next = next;
}
if (next === null) {
state.effect.last = prev;
} else {
next.prev = prev;
}
}
+121
View File
@@ -0,0 +1,121 @@
/** @import { Effect, TemplateNode } from '#client' */
import { FILENAME, HYDRATION_ERROR } from '../../../../constants.js';
import { remove_effect_dom, template_effect } from '../../reactivity/effects.js';
import { hydrate_next, hydrate_node, hydrating, set_hydrate_node } from '../hydration.js';
import { create_fragment_from_html } from '../reconciler.js';
import { assign_nodes } from '../template.js';
import * as w from '../../warnings.js';
import { hash, sanitize_location } from '../../../../utils.js';
import { DEV } from 'esm-env';
import { dev_current_component_function } from '../../context.js';
import { get_first_child, get_next_sibling } from '../operations.js';
import { active_effect } from '../../runtime.js';
import { COMMENT_NODE } from '#client/constants';
/**
* @param {Element} element
* @param {string | null} server_hash
* @param {string} value
*/
function check_hash(element, server_hash, value) {
if (!server_hash || server_hash === hash(String(value ?? ''))) return;
let location;
// @ts-expect-error
const loc = element.__svelte_meta?.loc;
if (loc) {
location = `near ${loc.file}:${loc.line}:${loc.column}`;
} else if (dev_current_component_function?.[FILENAME]) {
location = `in ${dev_current_component_function[FILENAME]}`;
}
w.hydration_html_changed(sanitize_location(location));
}
/**
* @param {Element | Text | Comment} node
* @param {() => string} get_value
* @param {boolean} [svg]
* @param {boolean} [mathml]
* @param {boolean} [skip_warning]
* @returns {void}
*/
export function html(node, get_value, svg = false, mathml = false, skip_warning = false) {
var anchor = node;
var value = '';
template_effect(() => {
var effect = /** @type {Effect} */ (active_effect);
if (value === (value = get_value() ?? '')) {
if (hydrating) hydrate_next();
return;
}
if (effect.nodes !== null) {
remove_effect_dom(effect.nodes.start, /** @type {TemplateNode} */ (effect.nodes.end));
effect.nodes = null;
}
if (value === '') return;
if (hydrating) {
// We're deliberately not trying to repair mismatches between server and client,
// as it's costly and error-prone (and it's an edge case to have a mismatch anyway)
var hash = /** @type {Comment} */ (hydrate_node).data;
/** @type {TemplateNode | null} */
var next = hydrate_next();
var last = next;
while (
next !== null &&
(next.nodeType !== COMMENT_NODE || /** @type {Comment} */ (next).data !== '')
) {
last = next;
next = get_next_sibling(next);
}
if (next === null) {
w.hydration_mismatch();
throw HYDRATION_ERROR;
}
if (DEV && !skip_warning) {
check_hash(/** @type {Element} */ (next.parentNode), hash, value);
}
assign_nodes(hydrate_node, last);
anchor = set_hydrate_node(next);
return;
}
var html = value + '';
if (svg) html = `<svg>${html}</svg>`;
else if (mathml) html = `<math>${html}</math>`;
// Don't use create_fragment_with_script_from_html here because that would mean script tags are executed.
// @html is basically `.innerHTML = ...` and that doesn't execute scripts either due to security reasons.
/** @type {DocumentFragment | Element} */
var node = create_fragment_from_html(html);
if (svg || mathml) {
node = /** @type {Element} */ (get_first_child(node));
}
assign_nodes(
/** @type {TemplateNode} */ (get_first_child(node)),
/** @type {TemplateNode} */ (node.lastChild)
);
if (svg || mathml) {
while (get_first_child(node)) {
anchor.before(/** @type {TemplateNode} */ (get_first_child(node)));
}
} else {
anchor.before(node);
}
});
}
+70
View File
@@ -0,0 +1,70 @@
/** @import { TemplateNode } from '#client' */
import { EFFECT_TRANSPARENT } from '#client/constants';
import {
hydrate_next,
hydrating,
read_hydration_instruction,
skip_nodes,
set_hydrate_node,
set_hydrating
} from '../hydration.js';
import { block } from '../../reactivity/effects.js';
import { HYDRATION_START_ELSE } from '../../../../constants.js';
import { BranchManager } from './branches.js';
// TODO reinstate https://github.com/sveltejs/svelte/pull/15250
/**
* @param {TemplateNode} node
* @param {(branch: (fn: (anchor: Node) => void, flag?: boolean) => void) => void} fn
* @param {boolean} [elseif] True if this is an `{:else if ...}` block rather than an `{#if ...}`, as that affects which transitions are considered 'local'
* @returns {void}
*/
export function if_block(node, fn, elseif = false) {
if (hydrating) {
hydrate_next();
}
var branches = new BranchManager(node);
var flags = elseif ? EFFECT_TRANSPARENT : 0;
/**
* @param {boolean} condition,
* @param {null | ((anchor: Node) => void)} fn
*/
function update_branch(condition, fn) {
if (hydrating) {
const is_else = read_hydration_instruction(node) === HYDRATION_START_ELSE;
if (condition === is_else) {
// Hydration mismatch: remove everything inside the anchor and start fresh.
// This could happen with `{#if browser}...{/if}`, for example
var anchor = skip_nodes();
set_hydrate_node(anchor);
branches.anchor = anchor;
set_hydrating(false);
branches.ensure(condition, fn);
set_hydrating(true);
return;
}
}
branches.ensure(condition, fn);
}
block(() => {
var has_branch = false;
fn((fn, flag = true) => {
has_branch = true;
update_branch(flag, fn);
});
if (!has_branch) {
update_branch(false, null);
}
}, flags);
}
+33
View File
@@ -0,0 +1,33 @@
/** @import { TemplateNode } from '#client' */
import { is_runes } from '../../context.js';
import { block } from '../../reactivity/effects.js';
import { hydrate_next, hydrating } from '../hydration.js';
import { BranchManager } from './branches.js';
/**
* @template V
* @param {TemplateNode} node
* @param {() => V} get_key
* @param {(anchor: Node) => TemplateNode | void} render_fn
* @returns {void}
*/
export function key(node, get_key, render_fn) {
if (hydrating) {
hydrate_next();
}
var branches = new BranchManager(node);
var legacy = !is_runes();
block(() => {
var key = get_key();
// key blocks in Svelte <5 had stupid semantics
if (legacy && key !== null && typeof key === 'object') {
key = /** @type {V} */ ({});
}
branches.ensure(key, render_fn);
});
}
+44
View File
@@ -0,0 +1,44 @@
import { hydrate_next, hydrating } from '../hydration.js';
/**
* @param {Comment} anchor
* @param {Record<string, any>} $$props
* @param {string} name
* @param {Record<string, unknown>} slot_props
* @param {null | ((anchor: Comment) => void)} fallback_fn
*/
export function slot(anchor, $$props, name, slot_props, fallback_fn) {
if (hydrating) {
hydrate_next();
}
var slot_fn = $$props.$$slots?.[name];
// Interop: Can use snippets to fill slots
var is_interop = false;
if (slot_fn === true) {
slot_fn = $$props[name === 'default' ? 'children' : name];
is_interop = true;
}
if (slot_fn === undefined) {
if (fallback_fn !== null) {
fallback_fn(anchor);
}
} else {
slot_fn(anchor, is_interop ? () => slot_props : slot_props);
}
}
/**
* @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;
}
+103
View File
@@ -0,0 +1,103 @@
/** @import { Snippet } from 'svelte' */
/** @import { TemplateNode } from '#client' */
/** @import { Getters } from '#shared' */
import { EFFECT_TRANSPARENT, ELEMENT_NODE } from '#client/constants';
import { block, teardown } from '../../reactivity/effects.js';
import {
dev_current_component_function,
set_dev_current_component_function
} from '../../context.js';
import { hydrate_next, hydrate_node, hydrating } from '../hydration.js';
import { create_fragment_from_html } from '../reconciler.js';
import { assign_nodes } from '../template.js';
import * as w from '../../warnings.js';
import * as e from '../../errors.js';
import { DEV } from 'esm-env';
import { get_first_child, get_next_sibling } from '../operations.js';
import { prevent_snippet_stringification } from '../../../shared/validate.js';
import { BranchManager } from './branches.js';
/**
* @template {(node: TemplateNode, ...args: any[]) => void} SnippetFn
* @param {TemplateNode} node
* @param {() => SnippetFn | null | undefined} get_snippet
* @param {(() => any)[]} args
* @returns {void}
*/
export function snippet(node, get_snippet, ...args) {
var branches = new BranchManager(node);
block(() => {
const snippet = get_snippet() ?? null;
if (DEV && snippet == null) {
e.invalid_snippet();
}
branches.ensure(snippet, snippet && ((anchor) => snippet(anchor, ...args)));
}, EFFECT_TRANSPARENT);
}
/**
* In development, wrap the snippet function so that it passes validation, and so that the
* correct component context is set for ownership checks
* @param {any} component
* @param {(node: TemplateNode, ...args: any[]) => void} fn
*/
export function wrap_snippet(component, fn) {
const snippet = (/** @type {TemplateNode} */ node, /** @type {any[]} */ ...args) => {
var previous_component_function = dev_current_component_function;
set_dev_current_component_function(component);
try {
return fn(node, ...args);
} finally {
set_dev_current_component_function(previous_component_function);
}
};
prevent_snippet_stringification(snippet);
return snippet;
}
/**
* 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 {TemplateNode} */ anchor, /** @type {Getters<Params>} */ ...params) => {
var snippet = fn(...params);
/** @type {Element} */
var element;
if (hydrating) {
element = /** @type {Element} */ (hydrate_node);
hydrate_next();
} else {
var html = snippet.render().trim();
var fragment = create_fragment_from_html(html);
element = /** @type {Element} */ (get_first_child(fragment));
if (DEV && (get_next_sibling(element) !== null || element.nodeType !== ELEMENT_NODE)) {
w.invalid_raw_snippet_render();
}
anchor.before(element);
}
const result = snippet.setup?.(element);
assign_nodes(element, element);
if (typeof result === 'function') {
teardown(result);
}
};
}
+26
View File
@@ -0,0 +1,26 @@
/** @import { TemplateNode, Dom } from '#client' */
import { EFFECT_TRANSPARENT } from '#client/constants';
import { block } from '../../reactivity/effects.js';
import { hydrate_next, hydrating } from '../hydration.js';
import { BranchManager } from './branches.js';
/**
* @template P
* @template {(props: P) => void} C
* @param {TemplateNode} node
* @param {() => C} get_component
* @param {(anchor: TemplateNode, component: C) => Dom | void} render_fn
* @returns {void}
*/
export function component(node, get_component, render_fn) {
if (hydrating) {
hydrate_next();
}
var branches = new BranchManager(node);
block(() => {
var component = get_component() ?? null;
branches.ensure(component, component && ((target) => render_fn(target, component)));
}, EFFECT_TRANSPARENT);
}
+152
View File
@@ -0,0 +1,152 @@
/** @import { Effect, EffectNodes, TemplateNode } from '#client' */
import { FILENAME, NAMESPACE_SVG } from '../../../../constants.js';
import {
hydrate_next,
hydrate_node,
hydrating,
set_hydrate_node,
set_hydrating
} from '../hydration.js';
import { create_text, get_first_child } from '../operations.js';
import { block, teardown } from '../../reactivity/effects.js';
import { set_should_intro } from '../../render.js';
import { active_effect } from '../../runtime.js';
import { component_context, dev_stack } from '../../context.js';
import { DEV } from 'esm-env';
import { EFFECT_TRANSPARENT, ELEMENT_NODE } from '#client/constants';
import { assign_nodes } from '../template.js';
import { is_raw_text_element } from '../../../../utils.js';
import { BranchManager } from './branches.js';
import { set_animation_effect_override } from '../elements/transitions.js';
/**
* @param {Comment | Element} node
* @param {() => string} get_tag
* @param {boolean} is_svg
* @param {undefined | ((element: Element, anchor: Node | null) => void)} render_fn,
* @param {undefined | (() => string)} get_namespace
* @param {undefined | [number, number]} location
* @returns {void}
*/
export function element(node, get_tag, is_svg, render_fn, get_namespace, location) {
let was_hydrating = hydrating;
if (hydrating) {
hydrate_next();
}
var filename = DEV && location && component_context?.function[FILENAME];
/** @type {null | Element} */
var element = null;
if (hydrating && hydrate_node.nodeType === ELEMENT_NODE) {
element = /** @type {Element} */ (hydrate_node);
hydrate_next();
}
var anchor = /** @type {TemplateNode} */ (hydrating ? hydrate_node : node);
/**
* We track this so we can set it when changing the element, allowing any
* `animate:` directive to bind itself to the correct block
*/
var parent_effect = /** @type {Effect} */ (active_effect);
var branches = new BranchManager(anchor, false);
block(() => {
const next_tag = get_tag() || null;
var ns = get_namespace ? get_namespace() : is_svg || next_tag === 'svg' ? NAMESPACE_SVG : null;
if (next_tag === null) {
branches.ensure(null, null);
set_should_intro(true);
return;
}
branches.ensure(next_tag, (anchor) => {
if (next_tag) {
element = hydrating
? /** @type {Element} */ (element)
: ns
? document.createElementNS(ns, next_tag)
: document.createElement(next_tag);
if (DEV && location) {
// @ts-expect-error
element.__svelte_meta = {
parent: dev_stack,
loc: {
file: filename,
line: location[0],
column: location[1]
}
};
}
assign_nodes(element, element);
if (render_fn) {
if (hydrating && is_raw_text_element(next_tag)) {
// prevent hydration glitches
element.append(document.createComment(''));
}
// If hydrating, use the existing ssr comment as the anchor so that the
// inner open and close methods can pick up the existing nodes correctly
var child_anchor = hydrating
? get_first_child(element)
: element.appendChild(create_text());
if (hydrating) {
if (child_anchor === null) {
set_hydrating(false);
} else {
set_hydrate_node(child_anchor);
}
}
set_animation_effect_override(parent_effect);
// `child_anchor` is undefined if this is a void element, but we still
// need to call `render_fn` in order to run actions etc. If the element
// contains children, it's a user error (which is warned on elsewhere)
// and the DOM will be silently discarded
render_fn(element, child_anchor);
set_animation_effect_override(null);
}
// we do this after calling `render_fn` so that child effects don't override `nodes.end`
/** @type {Effect & { nodes: EffectNodes }} */ (active_effect).nodes.end = element;
anchor.before(element);
}
if (hydrating) {
set_hydrate_node(anchor);
}
});
// revert to the default state after the effect has been created
set_should_intro(true);
return () => {
if (next_tag) {
// if we're in this callback because we're re-running the effect,
// disable intros (unless no element is currently displayed)
set_should_intro(false);
}
};
}, EFFECT_TRANSPARENT);
teardown(() => {
set_should_intro(true);
});
if (was_hydrating) {
set_hydrating(true);
set_hydrate_node(anchor);
}
}
+59
View File
@@ -0,0 +1,59 @@
/** @import { TemplateNode } from '#client' */
import { hydrate_node, hydrating, set_hydrate_node, set_hydrating } from '../hydration.js';
import { create_text, get_first_child, get_next_sibling } from '../operations.js';
import { block } from '../../reactivity/effects.js';
import { COMMENT_NODE, HEAD_EFFECT } from '#client/constants';
/**
* @param {string} hash
* @param {(anchor: Node) => void} render_fn
* @returns {void}
*/
export function head(hash, render_fn) {
// The head function may be called after the first hydration pass and ssr comment nodes may still be present,
// therefore we need to skip that when we detect that we're not in hydration mode.
let previous_hydrate_node = null;
let was_hydrating = hydrating;
/** @type {Comment | Text} */
var anchor;
if (hydrating) {
previous_hydrate_node = hydrate_node;
var head_anchor = get_first_child(document.head);
// There might be multiple head blocks in our app, and they could have been
// rendered in an arbitrary order — find one corresponding to this component
while (
head_anchor !== null &&
(head_anchor.nodeType !== COMMENT_NODE || /** @type {Comment} */ (head_anchor).data !== hash)
) {
head_anchor = get_next_sibling(head_anchor);
}
// If we can't find an opening hydration marker, skip hydration (this can happen
// if a framework rendered body but not head content)
if (head_anchor === null) {
set_hydrating(false);
} else {
var start = /** @type {TemplateNode} */ (get_next_sibling(head_anchor));
head_anchor.remove(); // in case this component is repeated
set_hydrate_node(start);
}
}
if (!hydrating) {
anchor = document.head.appendChild(create_text());
}
try {
block(() => render_fn(anchor), HEAD_EFFECT);
} finally {
if (was_hydrating) {
set_hydrating(true);
set_hydrate_node(/** @type {TemplateNode} */ (previous_hydrate_node));
}
}
}
+32
View File
@@ -0,0 +1,32 @@
import { DEV } from 'esm-env';
import { register_style } from '../dev/css.js';
import { effect } from '../reactivity/effects.js';
/**
* @param {Node} anchor
* @param {{ hash: string, code: string }} css
*/
export function append_styles(anchor, css) {
// Use `queue_micro_task` to ensure `anchor` is in the DOM, otherwise getRootNode() will yield wrong results
effect(() => {
var root = anchor.getRootNode();
var target = /** @type {ShadowRoot} */ (root).host
? /** @type {ShadowRoot} */ (root)
: /** @type {Document} */ (root).head ?? /** @type {Document} */ (root.ownerDocument).head;
// Always querying the DOM is roughly the same perf as additionally checking for presence in a map first assuming
// that you'll get cache hits half of the time, so we just always query the dom for simplicity and code savings.
if (!target.querySelector('#' + css.hash)) {
const style = document.createElement('style');
style.id = css.hash;
style.textContent = css.code;
target.appendChild(style);
if (DEV) {
register_style(css.hash, style);
}
}
});
}
+43
View File
@@ -0,0 +1,43 @@
/** @import { ActionPayload } from '#client' */
import { effect, render_effect } from '../../reactivity/effects.js';
import { safe_not_equal } from '../../reactivity/equality.js';
import { deep_read_state, untrack } from '../../runtime.js';
/**
* @template P
* @param {Element} dom
* @param {(dom: Element, value?: P) => ActionPayload<P>} action
* @param {() => P} [get_value]
* @returns {void}
*/
export function action(dom, action, get_value) {
effect(() => {
var payload = untrack(() => action(dom, get_value?.()) || {});
if (get_value && payload?.update) {
var inited = false;
/** @type {P} */
var prev = /** @type {any} */ ({}); // initialize with something so it's never equal on first run
render_effect(() => {
var value = get_value();
// Action's update method is coarse-grained, i.e. when anything in the passed value changes, update.
// This works in legacy mode because of mutable_source being updated as a whole, but when using $state
// together with actions and mutation, it wouldn't notice the change without a deep read.
deep_read_state(value);
if (inited && safe_not_equal(prev, value)) {
prev = value;
/** @type {Function} */ (payload.update)(value);
}
});
inited = true;
}
if (payload?.destroy) {
return () => /** @type {Function} */ (payload.destroy)();
}
});
}
+33
View File
@@ -0,0 +1,33 @@
/** @import { Effect } from '#client' */
import { branch, effect, destroy_effect, managed } from '../../reactivity/effects.js';
// TODO in 6.0 or 7.0, when we remove legacy mode, we can simplify this by
// getting rid of the block/branch stuff and just letting the effect rip.
// see https://github.com/sveltejs/svelte/pull/15962
/**
* @param {Element} node
* @param {() => (node: Element) => void} get_fn
*/
export function attach(node, get_fn) {
/** @type {false | undefined | ((node: Element) => void)} */
var fn = undefined;
/** @type {Effect | null} */
var e;
managed(() => {
if (fn !== (fn = get_fn())) {
if (e) {
destroy_effect(e);
e = null;
}
if (fn) {
e = branch(() => {
effect(() => /** @type {(node: Element) => void} */ (fn)(node));
});
}
}
});
}
+657
View File
@@ -0,0 +1,657 @@
/** @import { Effect } from '#client' */
import { DEV } from 'esm-env';
import { hydrating, set_hydrating } from '../hydration.js';
import { get_descriptors, get_prototype_of } from '../../../shared/utils.js';
import { create_event, delegate } from './events.js';
import { add_form_reset_listener, autofocus } from './misc.js';
import * as w from '../../warnings.js';
import { LOADING_ATTR_SYMBOL } from '#client/constants';
import { queue_micro_task } from '../task.js';
import { is_capture_event, can_delegate_event, normalize_attribute } from '../../../../utils.js';
import {
active_effect,
active_reaction,
get,
set_active_effect,
set_active_reaction
} from '../../runtime.js';
import { attach } from './attachments.js';
import { clsx } from '../../../shared/attributes.js';
import { set_class } from './class.js';
import { set_style } from './style.js';
import { ATTACHMENT_KEY, NAMESPACE_HTML, UNINITIALIZED } from '../../../../constants.js';
import { branch, destroy_effect, effect, managed } from '../../reactivity/effects.js';
import { init_select, select_option } from './bindings/select.js';
import { flatten } from '../../reactivity/async.js';
export const CLASS = Symbol('class');
export const STYLE = Symbol('style');
const IS_CUSTOM_ELEMENT = Symbol('is custom element');
const IS_HTML = Symbol('is html');
/**
* The value/checked attribute in the template actually corresponds to the defaultValue property, so we need
* to remove it upon hydration to avoid a bug when someone resets the form value.
* @param {HTMLInputElement} input
* @returns {void}
*/
export function remove_input_defaults(input) {
if (!hydrating) return;
var already_removed = false;
// We try and remove the default attributes later, rather than sync during hydration.
// Doing it sync during hydration has a negative impact on performance, but deferring the
// work in an idle task alleviates this greatly. If a form reset event comes in before
// the idle callback, then we ensure the input defaults are cleared just before.
var remove_defaults = () => {
if (already_removed) return;
already_removed = true;
// Remove the attributes but preserve the values
if (input.hasAttribute('value')) {
var value = input.value;
set_attribute(input, 'value', null);
input.value = value;
}
if (input.hasAttribute('checked')) {
var checked = input.checked;
set_attribute(input, 'checked', null);
input.checked = checked;
}
};
// @ts-expect-error
input.__on_r = remove_defaults;
queue_micro_task(remove_defaults);
add_form_reset_listener();
}
/**
* @param {Element} element
* @param {any} value
*/
export function set_value(element, value) {
var attributes = get_attributes(element);
if (
attributes.value ===
(attributes.value =
// treat null and undefined the same for the initial value
value ?? undefined) ||
// @ts-expect-error
// `progress` elements always need their value set when it's `0`
(element.value === value && (value !== 0 || element.nodeName !== 'PROGRESS'))
) {
return;
}
// @ts-expect-error
element.value = value ?? '';
}
/**
* @param {Element} element
* @param {boolean} checked
*/
export function set_checked(element, checked) {
var attributes = get_attributes(element);
if (
attributes.checked ===
(attributes.checked =
// treat null and undefined the same for the initial value
checked ?? undefined)
) {
return;
}
// @ts-expect-error
element.checked = checked;
}
/**
* Sets the `selected` attribute on an `option` element.
* Not set through the property because that doesn't reflect to the DOM,
* which means it wouldn't be taken into account when a form is reset.
* @param {HTMLOptionElement} element
* @param {boolean} selected
*/
export function set_selected(element, selected) {
if (selected) {
// The selected option could've changed via user selection, and
// setting the value without this check would set it back.
if (!element.hasAttribute('selected')) {
element.setAttribute('selected', '');
}
} else {
element.removeAttribute('selected');
}
}
/**
* Applies the default checked property without influencing the current checked property.
* @param {HTMLInputElement} element
* @param {boolean} checked
*/
export function set_default_checked(element, checked) {
const existing_value = element.checked;
element.defaultChecked = checked;
element.checked = existing_value;
}
/**
* Applies the default value property without influencing the current value property.
* @param {HTMLInputElement | HTMLTextAreaElement} element
* @param {string} value
*/
export function set_default_value(element, value) {
const existing_value = element.value;
element.defaultValue = value;
element.value = existing_value;
}
/**
* @param {Element} element
* @param {string} attribute
* @param {string | null} value
* @param {boolean} [skip_warning]
*/
export function set_attribute(element, attribute, value, skip_warning) {
var attributes = get_attributes(element);
if (hydrating) {
attributes[attribute] = element.getAttribute(attribute);
if (
attribute === 'src' ||
attribute === 'srcset' ||
(attribute === 'href' && element.nodeName === 'LINK')
) {
if (!skip_warning) {
check_src_in_dev_hydration(element, attribute, value ?? '');
}
// If we reset these attributes, they would result in another network request, which we want to avoid.
// We assume they are the same between client and server as checking if they are equal is expensive
// (we can't just compare the strings as they can be different between client and server but result in the
// same url, so we would need to create hidden anchor elements to compare them)
return;
}
}
if (attributes[attribute] === (attributes[attribute] = value)) return;
if (attribute === 'loading') {
// @ts-expect-error
element[LOADING_ATTR_SYMBOL] = value;
}
if (value == null) {
element.removeAttribute(attribute);
} else if (typeof value !== 'string' && get_setters(element).includes(attribute)) {
// @ts-ignore
element[attribute] = value;
} else {
element.setAttribute(attribute, value);
}
}
/**
* @param {Element} dom
* @param {string} attribute
* @param {string} value
*/
export function set_xlink_attribute(dom, attribute, value) {
dom.setAttributeNS('http://www.w3.org/1999/xlink', attribute, value);
}
/**
* @param {HTMLElement} node
* @param {string} prop
* @param {any} value
*/
export function set_custom_element_data(node, prop, value) {
// We need to ensure that setting custom element props, which can
// invoke lifecycle methods on other custom elements, does not also
// associate those lifecycle methods with the current active reaction
// or effect
var previous_reaction = active_reaction;
var previous_effect = active_effect;
// If we're hydrating but the custom element is from Svelte, and it already scaffolded,
// then it might run block logic in hydration mode, which we have to prevent.
let was_hydrating = hydrating;
if (hydrating) {
set_hydrating(false);
}
set_active_reaction(null);
set_active_effect(null);
try {
if (
// `style` should use `set_attribute` rather than the setter
prop !== 'style' &&
// Don't compute setters for custom elements while they aren't registered yet,
// because during their upgrade/instantiation they might add more setters.
// Instead, fall back to a simple "an object, then set as property" heuristic.
(setters_cache.has(node.getAttribute('is') || node.nodeName) ||
// customElements may not be available in browser extension contexts
!customElements ||
customElements.get(node.getAttribute('is') || node.tagName.toLowerCase())
? get_setters(node).includes(prop)
: value && typeof value === 'object')
) {
// @ts-expect-error
node[prop] = value;
} else {
// We did getters etc checks already, stringify before passing to set_attribute
// to ensure it doesn't invoke the same logic again, and potentially populating
// the setters cache too early.
set_attribute(node, prop, value == null ? value : String(value));
}
} finally {
set_active_reaction(previous_reaction);
set_active_effect(previous_effect);
if (was_hydrating) {
set_hydrating(true);
}
}
}
/**
* Spreads attributes onto a DOM element, taking into account the currently set attributes
* @param {Element & ElementCSSInlineStyle} element
* @param {Record<string | symbol, any> | undefined} prev
* @param {Record<string | symbol, any>} next New attributes - this function mutates this object
* @param {string} [css_hash]
* @param {boolean} [should_remove_defaults]
* @param {boolean} [skip_warning]
* @returns {Record<string, any>}
*/
function set_attributes(
element,
prev,
next,
css_hash,
should_remove_defaults = false,
skip_warning = false
) {
if (hydrating && should_remove_defaults && element.tagName === 'INPUT') {
var input = /** @type {HTMLInputElement} */ (element);
var attribute = input.type === 'checkbox' ? 'defaultChecked' : 'defaultValue';
if (!(attribute in next)) {
remove_input_defaults(input);
}
}
var attributes = get_attributes(element);
var is_custom_element = attributes[IS_CUSTOM_ELEMENT];
var preserve_attribute_case = !attributes[IS_HTML];
// If we're hydrating but the custom element is from Svelte, and it already scaffolded,
// then it might run block logic in hydration mode, which we have to prevent.
let is_hydrating_custom_element = hydrating && is_custom_element;
if (is_hydrating_custom_element) {
set_hydrating(false);
}
var current = prev || {};
var is_option_element = element.tagName === 'OPTION';
for (var key in prev) {
if (!(key in next)) {
next[key] = null;
}
}
if (next.class) {
next.class = clsx(next.class);
} else if (css_hash || next[CLASS]) {
next.class = null; /* force call to set_class() */
}
if (next[STYLE]) {
next.style ??= null; /* force call to set_style() */
}
var setters = get_setters(element);
// since key is captured we use const
for (const key in next) {
// let instead of var because referenced in a closure
let value = next[key];
// Up here because we want to do this for the initial value, too, even if it's undefined,
// and this wouldn't be reached in case of undefined because of the equality check below
if (is_option_element && key === 'value' && value == null) {
// The <option> element is a special case because removing the value attribute means
// the value is set to the text content of the option element, and setting the value
// to null or undefined means the value is set to the string "null" or "undefined".
// To align with how we handle this case in non-spread-scenarios, this logic is needed.
// There's a super-edge-case bug here that is left in in favor of smaller code size:
// Because of the "set missing props to null" logic above, we can't differentiate
// between a missing value and an explicitly set value of null or undefined. That means
// that once set, the value attribute of an <option> element can't be removed. This is
// a very rare edge case, and removing the attribute altogether isn't possible either
// for the <option value={undefined}> case, so we're not losing any functionality here.
// @ts-ignore
element.value = element.__value = '';
current[key] = value;
continue;
}
if (key === 'class') {
var is_html = element.namespaceURI === 'http://www.w3.org/1999/xhtml';
set_class(element, is_html, value, css_hash, prev?.[CLASS], next[CLASS]);
current[key] = value;
current[CLASS] = next[CLASS];
continue;
}
if (key === 'style') {
set_style(element, value, prev?.[STYLE], next[STYLE]);
current[key] = value;
current[STYLE] = next[STYLE];
continue;
}
var prev_value = current[key];
// Skip if value is unchanged, unless it's `undefined` and the element still has the attribute
if (value === prev_value && !(value === undefined && element.hasAttribute(key))) {
continue;
}
current[key] = value;
var prefix = key[0] + key[1]; // this is faster than key.slice(0, 2)
if (prefix === '$$') continue;
if (prefix === 'on') {
/** @type {{ capture?: true }} */
const opts = {};
const event_handle_key = '$$' + key;
let event_name = key.slice(2);
var delegated = can_delegate_event(event_name);
if (is_capture_event(event_name)) {
event_name = event_name.slice(0, -7);
opts.capture = true;
}
if (!delegated && prev_value) {
// Listening to same event but different handler -> our handle function below takes care of this
// If we were to remove and add listeners in this case, it could happen that the event is "swallowed"
// (the browser seems to not know yet that a new one exists now) and doesn't reach the handler
// https://github.com/sveltejs/svelte/issues/11903
if (value != null) continue;
element.removeEventListener(event_name, current[event_handle_key], opts);
current[event_handle_key] = null;
}
if (value != null) {
if (!delegated) {
/**
* @this {any}
* @param {Event} evt
*/
function handle(evt) {
current[key].call(this, evt);
}
current[event_handle_key] = create_event(event_name, element, handle, opts);
} else {
// @ts-ignore
element[`__${event_name}`] = value;
delegate([event_name]);
}
} else if (delegated) {
// @ts-ignore
element[`__${event_name}`] = undefined;
}
} else if (key === 'style') {
// avoid using the setter
set_attribute(element, key, value);
} else if (key === 'autofocus') {
autofocus(/** @type {HTMLElement} */ (element), Boolean(value));
} else if (!is_custom_element && (key === '__value' || (key === 'value' && value != null))) {
// @ts-ignore We're not running this for custom elements because __value is actually
// how Lit stores the current value on the element, and messing with that would break things.
element.value = element.__value = value;
} else if (key === 'selected' && is_option_element) {
set_selected(/** @type {HTMLOptionElement} */ (element), value);
} else {
var name = key;
if (!preserve_attribute_case) {
name = normalize_attribute(name);
}
var is_default = name === 'defaultValue' || name === 'defaultChecked';
if (value == null && !is_custom_element && !is_default) {
attributes[key] = null;
if (name === 'value' || name === 'checked') {
// removing value/checked also removes defaultValue/defaultChecked — preserve
let input = /** @type {HTMLInputElement} */ (element);
const use_default = prev === undefined;
if (name === 'value') {
let previous = input.defaultValue;
input.removeAttribute(name);
input.defaultValue = previous;
// @ts-ignore
input.value = input.__value = use_default ? previous : null;
} else {
let previous = input.defaultChecked;
input.removeAttribute(name);
input.defaultChecked = previous;
input.checked = use_default ? previous : false;
}
} else {
element.removeAttribute(key);
}
} else if (
is_default ||
(setters.includes(name) && (is_custom_element || typeof value !== 'string'))
) {
// @ts-ignore
element[name] = value;
// remove it from attributes's cache
if (name in attributes) attributes[name] = UNINITIALIZED;
} else if (typeof value !== 'function') {
set_attribute(element, name, value, skip_warning);
}
}
}
if (is_hydrating_custom_element) {
set_hydrating(true);
}
return current;
}
/**
* @param {Element & ElementCSSInlineStyle} element
* @param {(...expressions: any) => Record<string | symbol, any>} fn
* @param {Array<() => any>} sync
* @param {Array<() => Promise<any>>} async
* @param {Array<Promise<void>>} blockers
* @param {string} [css_hash]
* @param {boolean} [should_remove_defaults]
* @param {boolean} [skip_warning]
*/
export function attribute_effect(
element,
fn,
sync = [],
async = [],
blockers = [],
css_hash,
should_remove_defaults = false,
skip_warning = false
) {
flatten(blockers, sync, async, (values) => {
/** @type {Record<string | symbol, any> | undefined} */
var prev = undefined;
/** @type {Record<symbol, Effect>} */
var effects = {};
var is_select = element.nodeName === 'SELECT';
var inited = false;
managed(() => {
var next = fn(...values.map(get));
/** @type {Record<string | symbol, any>} */
var current = set_attributes(
element,
prev,
next,
css_hash,
should_remove_defaults,
skip_warning
);
if (inited && is_select && 'value' in next) {
select_option(/** @type {HTMLSelectElement} */ (element), next.value);
}
for (let symbol of Object.getOwnPropertySymbols(effects)) {
if (!next[symbol]) destroy_effect(effects[symbol]);
}
for (let symbol of Object.getOwnPropertySymbols(next)) {
var n = next[symbol];
if (symbol.description === ATTACHMENT_KEY && (!prev || n !== prev[symbol])) {
if (effects[symbol]) destroy_effect(effects[symbol]);
effects[symbol] = branch(() => attach(element, () => n));
}
current[symbol] = n;
}
prev = current;
});
if (is_select) {
var select = /** @type {HTMLSelectElement} */ (element);
effect(() => {
select_option(select, /** @type {Record<string | symbol, any>} */ (prev).value, true);
init_select(select);
});
}
inited = true;
});
}
/**
*
* @param {Element} element
*/
function get_attributes(element) {
return /** @type {Record<string | symbol, unknown>} **/ (
// @ts-expect-error
element.__attributes ??= {
[IS_CUSTOM_ELEMENT]: element.nodeName.includes('-'),
[IS_HTML]: element.namespaceURI === NAMESPACE_HTML
}
);
}
/** @type {Map<string, string[]>} */
var setters_cache = new Map();
/** @param {Element} element */
function get_setters(element) {
var cache_key = element.getAttribute('is') || element.nodeName;
var setters = setters_cache.get(cache_key);
if (setters) return setters;
setters_cache.set(cache_key, (setters = []));
var descriptors;
var proto = element; // In the case of custom elements there might be setters on the instance
var element_proto = Element.prototype;
// Stop at Element, from there on there's only unnecessary setters we're not interested in
// Do not use contructor.name here as that's unreliable in some browser environments
while (element_proto !== proto) {
descriptors = get_descriptors(proto);
for (var key in descriptors) {
if (descriptors[key].set) {
setters.push(key);
}
}
proto = get_prototype_of(proto);
}
return setters;
}
/**
* @param {any} element
* @param {string} attribute
* @param {string} value
*/
function check_src_in_dev_hydration(element, attribute, value) {
if (!DEV) return;
if (attribute === 'srcset' && srcset_url_equal(element, value)) return;
if (src_url_equal(element.getAttribute(attribute) ?? '', value)) return;
w.hydration_attribute_changed(
attribute,
element.outerHTML.replace(element.innerHTML, element.innerHTML && '...'),
String(value)
);
}
/**
* @param {string} element_src
* @param {string} url
* @returns {boolean}
*/
function src_url_equal(element_src, url) {
if (element_src === url) return true;
return new URL(element_src, document.baseURI).href === new URL(url, document.baseURI).href;
}
/** @param {string} srcset */
function split_srcset(srcset) {
return srcset.split(',').map((src) => src.trim().split(' ').filter(Boolean));
}
/**
* @param {HTMLSourceElement | HTMLImageElement} element
* @param {string} srcset
* @returns {boolean}
*/
function srcset_url_equal(element, srcset) {
var element_urls = split_srcset(element.srcset);
var urls = split_srcset(srcset);
return (
urls.length === element_urls.length &&
urls.every(
([url, width], i) =>
width === element_urls[i][1] &&
// We need to test both ways because Vite will create an a full URL with
// `new URL(asset, import.meta.url).href` for the client when `base: './'`, and the
// relative URLs inside srcset are not automatically resolved to absolute URLs by
// browsers (in contrast to img.src). This means both SSR and DOM code could
// contain relative or absolute URLs.
(src_url_equal(element_urls[i][0], url) || src_url_equal(url, element_urls[i][0]))
)
);
}
@@ -0,0 +1,17 @@
import { listen } from './shared.js';
/**
* @param {(activeElement: Element | null) => void} update
* @returns {void}
*/
export function bind_active_element(update) {
listen(document, ['focusin', 'focusout'], (event) => {
if (event && event.type === 'focusout' && /** @type {FocusEvent} */ (event).relatedTarget) {
// The tests still pass if we remove this, because of JSDOM limitations, but it is necessary
// to avoid temporarily resetting to `document.body`
return;
}
update(document.activeElement);
});
}
+312
View File
@@ -0,0 +1,312 @@
/** @import { Batch } from '../../../reactivity/batch.js' */
import { DEV } from 'esm-env';
import { render_effect, teardown } from '../../../reactivity/effects.js';
import { listen_to_event_and_reset_event } from './shared.js';
import * as e from '../../../errors.js';
import { is } from '../../../proxy.js';
import { queue_micro_task } from '../../task.js';
import { hydrating } from '../../hydration.js';
import { tick, untrack } from '../../../runtime.js';
import { is_runes } from '../../../context.js';
import { current_batch, previous_batch } from '../../../reactivity/batch.js';
/**
* @param {HTMLInputElement} input
* @param {() => unknown} get
* @param {(value: unknown) => void} set
* @returns {void}
*/
export function bind_value(input, get, set = get) {
var batches = new WeakSet();
listen_to_event_and_reset_event(input, 'input', async (is_reset) => {
if (DEV && input.type === 'checkbox') {
// TODO should this happen in prod too?
e.bind_invalid_checkbox_value();
}
/** @type {any} */
var value = is_reset ? input.defaultValue : input.value;
value = is_numberlike_input(input) ? to_number(value) : value;
set(value);
if (current_batch !== null) {
batches.add(current_batch);
}
// Because `{#each ...}` blocks work by updating sources inside the flush,
// we need to wait a tick before checking to see if we should forcibly
// update the input and reset the selection state
await tick();
// Respect any validation in accessors
if (value !== (value = get())) {
var start = input.selectionStart;
var end = input.selectionEnd;
var length = input.value.length;
// the value is coerced on assignment
input.value = value ?? '';
// Restore selection
if (end !== null) {
var new_length = input.value.length;
// If cursor was at end and new input is longer, move cursor to new end
if (start === end && end === length && new_length > length) {
input.selectionStart = new_length;
input.selectionEnd = new_length;
} else {
input.selectionStart = start;
input.selectionEnd = Math.min(end, new_length);
}
}
}
});
if (
// If we are hydrating and the value has since changed,
// then use the updated value from the input instead.
(hydrating && input.defaultValue !== input.value) ||
// If defaultValue is set, then value == defaultValue
// TODO Svelte 6: remove input.value check and set to empty string?
(untrack(get) == null && input.value)
) {
set(is_numberlike_input(input) ? to_number(input.value) : input.value);
if (current_batch !== null) {
batches.add(current_batch);
}
}
render_effect(() => {
if (DEV && input.type === 'checkbox') {
// TODO should this happen in prod too?
e.bind_invalid_checkbox_value();
}
var value = get();
if (input === document.activeElement) {
// we need both, because in non-async mode, render effects run before previous_batch is set
var batch = /** @type {Batch} */ (previous_batch ?? current_batch);
// Never rewrite the contents of a focused input. We can get here if, for example,
// an update is deferred because of async work depending on the input:
//
// <input bind:value={query}>
// <p>{await find(query)}</p>
if (batches.has(batch)) {
return;
}
}
if (is_numberlike_input(input) && value === to_number(input.value)) {
// handles 0 vs 00 case (see https://github.com/sveltejs/svelte/issues/9959)
return;
}
if (input.type === 'date' && !value && !input.value) {
// Handles the case where a temporarily invalid date is set (while typing, for example with a leading 0 for the day)
// and prevents this state from clearing the other parts of the date input (see https://github.com/sveltejs/svelte/issues/7897)
return;
}
// don't set the value of the input if it's the same to allow
// minlength to work properly
if (value !== input.value) {
// @ts-expect-error the value is coerced on assignment
input.value = value ?? '';
}
});
}
/** @type {Set<HTMLInputElement[]>} */
const pending = new Set();
/**
* @param {HTMLInputElement[]} inputs
* @param {null | [number]} group_index
* @param {HTMLInputElement} input
* @param {() => unknown} get
* @param {(value: unknown) => void} set
* @returns {void}
*/
export function bind_group(inputs, group_index, input, get, set = get) {
var is_checkbox = input.getAttribute('type') === 'checkbox';
var binding_group = inputs;
// needs to be let or related code isn't treeshaken out if it's always false
let hydration_mismatch = false;
if (group_index !== null) {
for (var index of group_index) {
// @ts-expect-error
binding_group = binding_group[index] ??= [];
}
}
binding_group.push(input);
listen_to_event_and_reset_event(
input,
'change',
() => {
// @ts-ignore
var value = input.__value;
if (is_checkbox) {
value = get_binding_group_value(binding_group, value, input.checked);
}
set(value);
},
// TODO better default value handling
() => set(is_checkbox ? [] : null)
);
render_effect(() => {
var value = get();
// If we are hydrating and the value has since changed, then use the update value
// from the input instead.
if (hydrating && input.defaultChecked !== input.checked) {
hydration_mismatch = true;
return;
}
if (is_checkbox) {
value = value || [];
// @ts-ignore
input.checked = value.includes(input.__value);
} else {
// @ts-ignore
input.checked = is(input.__value, value);
}
});
teardown(() => {
var index = binding_group.indexOf(input);
if (index !== -1) {
binding_group.splice(index, 1);
}
});
if (!pending.has(binding_group)) {
pending.add(binding_group);
queue_micro_task(() => {
// necessary to maintain binding group order in all insertion scenarios
binding_group.sort((a, b) => (a.compareDocumentPosition(b) === 4 ? -1 : 1));
pending.delete(binding_group);
});
}
queue_micro_task(() => {
if (hydration_mismatch) {
var value;
if (is_checkbox) {
value = get_binding_group_value(binding_group, value, input.checked);
} else {
var hydration_input = binding_group.find((input) => input.checked);
// @ts-ignore
value = hydration_input?.__value;
}
set(value);
}
});
}
/**
* @param {HTMLInputElement} input
* @param {() => unknown} get
* @param {(value: unknown) => void} set
* @returns {void}
*/
export function bind_checked(input, get, set = get) {
listen_to_event_and_reset_event(input, 'change', (is_reset) => {
var value = is_reset ? input.defaultChecked : input.checked;
set(value);
});
if (
// If we are hydrating and the value has since changed,
// then use the update value from the input instead.
(hydrating && input.defaultChecked !== input.checked) ||
// If defaultChecked is set, then checked == defaultChecked
untrack(get) == null
) {
set(input.checked);
}
render_effect(() => {
var value = get();
input.checked = Boolean(value);
});
}
/**
* @template V
* @param {Array<HTMLInputElement>} group
* @param {V} __value
* @param {boolean} checked
* @returns {V[]}
*/
function get_binding_group_value(group, __value, checked) {
/** @type {Set<V>} */
var value = new Set();
for (var i = 0; i < group.length; i += 1) {
if (group[i].checked) {
// @ts-ignore
value.add(group[i].__value);
}
}
if (!checked) {
value.delete(__value);
}
return Array.from(value);
}
/**
* @param {HTMLInputElement} input
*/
function is_numberlike_input(input) {
var type = input.type;
return type === 'number' || type === 'range';
}
/**
* @param {string} value
*/
function to_number(value) {
return value === '' ? null : +value;
}
/**
* @param {HTMLInputElement} input
* @param {() => FileList | null} get
* @param {(value: FileList | null) => void} set
*/
export function bind_files(input, get, set = get) {
listen_to_event_and_reset_event(input, 'change', () => {
set(input.files);
});
if (
// If we are hydrating and the value has since changed,
// then use the updated value from the input instead.
hydrating &&
input.files
) {
set(input.files);
}
render_effect(() => {
input.files = get();
});
}
+232
View File
@@ -0,0 +1,232 @@
import { render_effect, effect, teardown } from '../../../reactivity/effects.js';
import { listen } from './shared.js';
/** @param {TimeRanges} ranges */
function time_ranges_to_array(ranges) {
var array = [];
for (var i = 0; i < ranges.length; i += 1) {
array.push({ start: ranges.start(i), end: ranges.end(i) });
}
return array;
}
/**
* @param {HTMLVideoElement | HTMLAudioElement} media
* @param {() => number | undefined} get
* @param {(value: number) => void} set
* @returns {void}
*/
export function bind_current_time(media, get, set = get) {
/** @type {number} */
var raf_id;
/** @type {number} */
var value;
// Ideally, listening to timeupdate would be enough, but it fires too infrequently for the currentTime
// binding, which is why we use a raf loop, too. We additionally still listen to timeupdate because
// the user could be scrubbing through the video using the native controls when the media is paused.
var callback = () => {
cancelAnimationFrame(raf_id);
if (!media.paused) {
raf_id = requestAnimationFrame(callback);
}
var next_value = media.currentTime;
if (value !== next_value) {
set((value = next_value));
}
};
raf_id = requestAnimationFrame(callback);
media.addEventListener('timeupdate', callback);
render_effect(() => {
var next_value = Number(get());
if (value !== next_value && !isNaN(/** @type {any} */ (next_value))) {
media.currentTime = value = next_value;
}
});
teardown(() => {
cancelAnimationFrame(raf_id);
media.removeEventListener('timeupdate', callback);
});
}
/**
* @param {HTMLVideoElement | HTMLAudioElement} media
* @param {(array: Array<{ start: number; end: number }>) => void} set
*/
export function bind_buffered(media, set) {
/** @type {{ start: number; end: number; }[]} */
var current;
// `buffered` can update without emitting any event, so we check it on various events.
// By specs, `buffered` always returns a new object, so we have to compare deeply.
listen(media, ['loadedmetadata', 'progress', 'timeupdate', 'seeking'], () => {
var ranges = media.buffered;
if (
!current ||
current.length !== ranges.length ||
current.some((range, i) => ranges.start(i) !== range.start || ranges.end(i) !== range.end)
) {
current = time_ranges_to_array(ranges);
set(current);
}
});
}
/**
* @param {HTMLVideoElement | HTMLAudioElement} media
* @param {(array: Array<{ start: number; end: number }>) => void} set
*/
export function bind_seekable(media, set) {
listen(media, ['loadedmetadata'], () => set(time_ranges_to_array(media.seekable)));
}
/**
* @param {HTMLVideoElement | HTMLAudioElement} media
* @param {(array: Array<{ start: number; end: number }>) => void} set
*/
export function bind_played(media, set) {
listen(media, ['timeupdate'], () => set(time_ranges_to_array(media.played)));
}
/**
* @param {HTMLVideoElement | HTMLAudioElement} media
* @param {(seeking: boolean) => void} set
*/
export function bind_seeking(media, set) {
listen(media, ['seeking', 'seeked'], () => set(media.seeking));
}
/**
* @param {HTMLVideoElement | HTMLAudioElement} media
* @param {(seeking: boolean) => void} set
*/
export function bind_ended(media, set) {
listen(media, ['timeupdate', 'ended'], () => set(media.ended));
}
/**
* @param {HTMLVideoElement | HTMLAudioElement} media
* @param {(ready_state: number) => void} set
*/
export function bind_ready_state(media, set) {
listen(
media,
['loadedmetadata', 'loadeddata', 'canplay', 'canplaythrough', 'playing', 'waiting', 'emptied'],
() => set(media.readyState)
);
}
/**
* @param {HTMLVideoElement | HTMLAudioElement} media
* @param {() => number | undefined} get
* @param {(playback_rate: number) => void} set
*/
export function bind_playback_rate(media, get, set = get) {
// Needs to happen after element is inserted into the dom (which is guaranteed by using effect),
// else playback will be set back to 1 by the browser
effect(() => {
var value = Number(get());
if (value !== media.playbackRate && !isNaN(value)) {
media.playbackRate = value;
}
});
// Start listening to ratechange events after the element is inserted into the dom,
// else playback will be set to 1 by the browser
effect(() => {
listen(media, ['ratechange'], () => {
set(media.playbackRate);
});
});
}
/**
* @param {HTMLVideoElement | HTMLAudioElement} media
* @param {() => boolean | undefined} get
* @param {(paused: boolean) => void} set
*/
export function bind_paused(media, get, set = get) {
var paused = get();
var update = () => {
if (paused !== media.paused) {
set((paused = media.paused));
}
};
// If someone switches the src while media is playing, the player will pause.
// Listen to the canplay event to get notified of this situation.
listen(media, ['play', 'pause', 'canplay'], update, paused == null);
// Needs to be an effect to ensure media element is mounted: else, if paused is `false` (i.e. should play right away)
// a "The play() request was interrupted by a new load request" error would be thrown because the resource isn't loaded yet.
effect(() => {
if ((paused = !!get()) !== media.paused) {
if (paused) {
media.pause();
} else {
media.play().catch(() => {
set((paused = true));
});
}
}
});
}
/**
* @param {HTMLVideoElement | HTMLAudioElement} media
* @param {() => number | undefined} get
* @param {(volume: number) => void} set
*/
export function bind_volume(media, get, set = get) {
var callback = () => {
set(media.volume);
};
if (get() == null) {
callback();
}
listen(media, ['volumechange'], callback, false);
render_effect(() => {
var value = Number(get());
if (value !== media.volume && !isNaN(value)) {
media.volume = value;
}
});
}
/**
* @param {HTMLVideoElement | HTMLAudioElement} media
* @param {() => boolean | undefined} get
* @param {(muted: boolean) => void} set
*/
export function bind_muted(media, get, set = get) {
var callback = () => {
set(media.muted);
};
if (get() == null) {
callback();
}
listen(media, ['volumechange'], callback, false);
render_effect(() => {
var value = !!get();
if (media.muted !== value) media.muted = value;
});
}
@@ -0,0 +1,11 @@
import { listen } from './shared.js';
/**
* @param {(online: boolean) => void} update
* @returns {void}
*/
export function bind_online(update) {
listen(window, ['online', 'offline'], () => {
update(navigator.onLine);
});
}
+22
View File
@@ -0,0 +1,22 @@
import { teardown } from '../../../reactivity/effects.js';
import { get_descriptor } from '../../../../shared/utils.js';
/**
* Makes an `export`ed (non-prop) variable available on the `$$props` object
* so that consumers can do `bind:x` on the component.
* @template V
* @param {Record<string, unknown>} props
* @param {string} prop
* @param {V} value
* @returns {void}
*/
export function bind_prop(props, prop, value) {
var desc = get_descriptor(props, prop);
if (desc && desc.set) {
props[prop] = value;
teardown(() => {
props[prop] = null;
});
}
}
+159
View File
@@ -0,0 +1,159 @@
import { effect, teardown } from '../../../reactivity/effects.js';
import { listen_to_event_and_reset_event } from './shared.js';
import { is } from '../../../proxy.js';
import { is_array } from '../../../../shared/utils.js';
import * as w from '../../../warnings.js';
import { Batch, current_batch, previous_batch } from '../../../reactivity/batch.js';
/**
* Selects the correct option(s) (depending on whether this is a multiple select)
* @template V
* @param {HTMLSelectElement} select
* @param {V} value
* @param {boolean} mounting
*/
export function select_option(select, value, mounting = false) {
if (select.multiple) {
// If value is null or undefined, keep the selection as is
if (value == undefined) {
return;
}
// If not an array, warn and keep the selection as is
if (!is_array(value)) {
return w.select_multiple_invalid_value();
}
// Otherwise, update the selection
for (var option of select.options) {
option.selected = value.includes(get_option_value(option));
}
return;
}
for (option of select.options) {
var option_value = get_option_value(option);
if (is(option_value, value)) {
option.selected = true;
return;
}
}
if (!mounting || value !== undefined) {
select.selectedIndex = -1; // no option should be selected
}
}
/**
* Selects the correct option(s) if `value` is given,
* and then sets up a mutation observer to sync the
* current selection to the dom when it changes. Such
* changes could for example occur when options are
* inside an `#each` block.
* @param {HTMLSelectElement} select
*/
export function init_select(select) {
var observer = new MutationObserver(() => {
// @ts-ignore
select_option(select, select.__value);
// Deliberately don't update the potential binding value,
// the model should be preserved unless explicitly changed
});
observer.observe(select, {
// Listen to option element changes
childList: true,
subtree: true, // because of <optgroup>
// Listen to option element value attribute changes
// (doesn't get notified of select value changes,
// because that property is not reflected as an attribute)
attributes: true,
attributeFilter: ['value']
});
teardown(() => {
observer.disconnect();
});
}
/**
* @param {HTMLSelectElement} select
* @param {() => unknown} get
* @param {(value: unknown) => void} set
* @returns {void}
*/
export function bind_select_value(select, get, set = get) {
var batches = new WeakSet();
var mounting = true;
listen_to_event_and_reset_event(select, 'change', (is_reset) => {
var query = is_reset ? '[selected]' : ':checked';
/** @type {unknown} */
var value;
if (select.multiple) {
value = [].map.call(select.querySelectorAll(query), get_option_value);
} else {
/** @type {HTMLOptionElement | null} */
var selected_option =
select.querySelector(query) ??
// will fall back to first non-disabled option if no option is selected
select.querySelector('option:not([disabled])');
value = selected_option && get_option_value(selected_option);
}
set(value);
if (current_batch !== null) {
batches.add(current_batch);
}
});
// Needs to be an effect, not a render_effect, so that in case of each loops the logic runs after the each block has updated
effect(() => {
var value = get();
if (select === document.activeElement) {
// we need both, because in non-async mode, render effects run before previous_batch is set
var batch = /** @type {Batch} */ (previous_batch ?? current_batch);
// Don't update the <select> if it is focused. We can get here if, for example,
// an update is deferred because of async work depending on the select:
//
// <select bind:value={selected}>...</select>
// <p>{await find(selected)}</p>
if (batches.has(batch)) {
return;
}
}
select_option(select, value, mounting);
// Mounting and value undefined -> take selection from dom
if (mounting && value === undefined) {
/** @type {HTMLOptionElement | null} */
var selected_option = select.querySelector(':checked');
if (selected_option !== null) {
value = get_option_value(selected_option);
set(value);
}
}
// @ts-ignore
select.__value = value;
mounting = false;
});
init_select(select);
}
/** @param {HTMLOptionElement} option */
function get_option_value(option) {
// __value only exists if the <option> has a value attribute
if ('__value' in option) {
return option.__value;
} else {
return option.value;
}
}
@@ -0,0 +1,76 @@
import { teardown } from '../../../reactivity/effects.js';
import {
active_effect,
active_reaction,
set_active_effect,
set_active_reaction
} from '../../../runtime.js';
import { add_form_reset_listener } from '../misc.js';
/**
* Fires the handler once immediately (unless corresponding arg is set to `false`),
* then listens to the given events until the render effect context is destroyed
* @param {EventTarget} target
* @param {Array<string>} events
* @param {(event?: Event) => void} handler
* @param {any} call_handler_immediately
*/
export function listen(target, events, handler, call_handler_immediately = true) {
if (call_handler_immediately) {
handler();
}
for (var name of events) {
target.addEventListener(name, handler);
}
teardown(() => {
for (var name of events) {
target.removeEventListener(name, handler);
}
});
}
/**
* @template T
* @param {() => T} fn
*/
export function without_reactive_context(fn) {
var previous_reaction = active_reaction;
var previous_effect = active_effect;
set_active_reaction(null);
set_active_effect(null);
try {
return fn();
} finally {
set_active_reaction(previous_reaction);
set_active_effect(previous_effect);
}
}
/**
* Listen to the given event, and then instantiate a global form reset listener if not already done,
* to notify all bindings when the form is reset
* @param {HTMLElement} element
* @param {string} event
* @param {(is_reset?: true) => void} handler
* @param {(is_reset?: true) => void} [on_reset]
*/
export function listen_to_event_and_reset_event(element, event, handler, on_reset = handler) {
element.addEventListener(event, () => without_reactive_context(handler));
// @ts-expect-error
const prev = element.__on_r;
if (prev) {
// special case for checkbox that can have multiple binds (group & checked)
// @ts-expect-error
element.__on_r = () => {
prev();
on_reset(true);
};
} else {
// @ts-expect-error
element.__on_r = () => on_reset(true);
}
add_form_reset_listener();
}
+108
View File
@@ -0,0 +1,108 @@
import { effect, teardown } from '../../../reactivity/effects.js';
import { untrack } from '../../../runtime.js';
/**
* Resize observer singleton.
* One listener per element only!
* https://groups.google.com/a/chromium.org/g/blink-dev/c/z6ienONUb5A/m/F5-VcUZtBAAJ
*/
class ResizeObserverSingleton {
/** */
#listeners = new WeakMap();
/** @type {ResizeObserver | undefined} */
#observer;
/** @type {ResizeObserverOptions} */
#options;
/** @static */
static entries = new WeakMap();
/** @param {ResizeObserverOptions} options */
constructor(options) {
this.#options = options;
}
/**
* @param {Element} element
* @param {(entry: ResizeObserverEntry) => any} listener
*/
observe(element, listener) {
var listeners = this.#listeners.get(element) || new Set();
listeners.add(listener);
this.#listeners.set(element, listeners);
this.#getObserver().observe(element, this.#options);
return () => {
var listeners = this.#listeners.get(element);
listeners.delete(listener);
if (listeners.size === 0) {
this.#listeners.delete(element);
/** @type {ResizeObserver} */ (this.#observer).unobserve(element);
}
};
}
#getObserver() {
return (
this.#observer ??
(this.#observer = new ResizeObserver(
/** @param {any} entries */ (entries) => {
for (var entry of entries) {
ResizeObserverSingleton.entries.set(entry.target, entry);
for (var listener of this.#listeners.get(entry.target) || []) {
listener(entry);
}
}
}
))
);
}
}
var resize_observer_content_box = /* @__PURE__ */ new ResizeObserverSingleton({
box: 'content-box'
});
var resize_observer_border_box = /* @__PURE__ */ new ResizeObserverSingleton({
box: 'border-box'
});
var resize_observer_device_pixel_content_box = /* @__PURE__ */ new ResizeObserverSingleton({
box: 'device-pixel-content-box'
});
/**
* @param {Element} element
* @param {'contentRect' | 'contentBoxSize' | 'borderBoxSize' | 'devicePixelContentBoxSize'} type
* @param {(entry: keyof ResizeObserverEntry) => void} set
*/
export function bind_resize_observer(element, type, set) {
var observer =
type === 'contentRect' || type === 'contentBoxSize'
? resize_observer_content_box
: type === 'borderBoxSize'
? resize_observer_border_box
: resize_observer_device_pixel_content_box;
var unsub = observer.observe(element, /** @param {any} entry */ (entry) => set(entry[type]));
teardown(unsub);
}
/**
* @param {HTMLElement} element
* @param {'clientWidth' | 'clientHeight' | 'offsetWidth' | 'offsetHeight'} type
* @param {(size: number) => void} set
*/
export function bind_element_size(element, type, set) {
var unsub = resize_observer_border_box.observe(element, () => set(element[type]));
effect(() => {
// The update could contain reads which should be ignored
untrack(() => set(element[type]));
return unsub;
});
}
+61
View File
@@ -0,0 +1,61 @@
import { STATE_SYMBOL } from '#client/constants';
import { effect, render_effect } from '../../../reactivity/effects.js';
import { untrack } from '../../../runtime.js';
import { queue_micro_task } from '../../task.js';
/**
* @param {any} bound_value
* @param {Element} element_or_component
* @returns {boolean}
*/
function is_bound_this(bound_value, element_or_component) {
return (
bound_value === element_or_component || bound_value?.[STATE_SYMBOL] === element_or_component
);
}
/**
* @param {any} element_or_component
* @param {(value: unknown, ...parts: unknown[]) => void} update
* @param {(...parts: unknown[]) => unknown} get_value
* @param {() => unknown[]} [get_parts] Set if the this binding is used inside an each block,
* returns all the parts of the each block context that are used in the expression
* @returns {void}
*/
export function bind_this(element_or_component = {}, update, get_value, get_parts) {
effect(() => {
/** @type {unknown[]} */
var old_parts;
/** @type {unknown[]} */
var parts;
render_effect(() => {
old_parts = parts;
// We only track changes to the parts, not the value itself to avoid unnecessary reruns.
parts = get_parts?.() || [];
untrack(() => {
if (element_or_component !== get_value(...parts)) {
update(element_or_component, ...parts);
// If this is an effect rerun (cause: each block context changes), then nullify the binding at
// the previous position if it isn't already taken over by a different effect.
if (old_parts && is_bound_this(get_value(...old_parts), element_or_component)) {
update(null, ...old_parts);
}
}
});
});
return () => {
// We cannot use effects in the teardown phase, we we use a microtask instead.
queue_micro_task(() => {
if (parts && is_bound_this(get_value(...parts), element_or_component)) {
update(null, ...parts);
}
});
};
});
return element_or_component;
}
@@ -0,0 +1,75 @@
import { render_effect, teardown } from '../../../reactivity/effects.js';
import { listen } from './shared.js';
/**
* @param {'innerHTML' | 'textContent' | 'innerText'} property
* @param {HTMLElement} element
* @param {() => unknown} get
* @param {(value: unknown) => void} set
* @returns {void}
*/
export function bind_content_editable(property, element, get, set = get) {
element.addEventListener('input', () => {
// @ts-ignore
set(element[property]);
});
render_effect(() => {
var value = get();
if (element[property] !== value) {
if (value == null) {
// @ts-ignore
var non_null_value = element[property];
set(non_null_value);
} else {
// @ts-ignore
element[property] = value + '';
}
}
});
}
/**
* @param {string} property
* @param {string} event_name
* @param {Element} element
* @param {(value: unknown) => void} set
* @param {() => unknown} [get]
* @returns {void}
*/
export function bind_property(property, event_name, element, set, get) {
var handler = () => {
// @ts-ignore
set(element[property]);
};
element.addEventListener(event_name, handler);
if (get) {
render_effect(() => {
// @ts-ignore
element[property] = get();
});
} else {
handler();
}
// @ts-ignore
if (element === document.body || element === window || element === document) {
teardown(() => {
element.removeEventListener(event_name, handler);
});
}
}
/**
* @param {HTMLElement} element
* @param {(value: unknown) => void} set
* @returns {void}
*/
export function bind_focused(element, set) {
listen(element, ['focus', 'blur'], () => {
set(element === document.activeElement);
});
}
@@ -0,0 +1,66 @@
import { effect, render_effect, teardown } from '../../../reactivity/effects.js';
import { listen, without_reactive_context } from './shared.js';
/**
* @param {'x' | 'y'} type
* @param {() => number} get
* @param {(value: number) => void} set
* @returns {void}
*/
export function bind_window_scroll(type, get, set = get) {
var is_scrolling_x = type === 'x';
var target_handler = () =>
without_reactive_context(() => {
scrolling = true;
clearTimeout(timeout);
timeout = setTimeout(clear, 100); // TODO use scrollend event if supported (or when supported everywhere?)
set(window[is_scrolling_x ? 'scrollX' : 'scrollY']);
});
addEventListener('scroll', target_handler, {
passive: true
});
var scrolling = false;
/** @type {ReturnType<typeof setTimeout>} */
var timeout;
var clear = () => {
scrolling = false;
};
var first = true;
render_effect(() => {
var latest_value = get();
// Don't scroll to the initial value for accessibility reasons
if (first) {
first = false;
} else if (!scrolling && latest_value != null) {
scrolling = true;
clearTimeout(timeout);
if (is_scrolling_x) {
scrollTo(latest_value, window.scrollY);
} else {
scrollTo(window.scrollX, latest_value);
}
timeout = setTimeout(clear, 100);
}
});
// Browsers don't fire the scroll event for the initial scroll position when scroll style isn't set to smooth
effect(target_handler);
teardown(() => {
removeEventListener('scroll', target_handler);
});
}
/**
* @param {'innerWidth' | 'innerHeight' | 'outerWidth' | 'outerHeight'} type
* @param {(size: number) => void} set
*/
export function bind_window_size(type, set) {
listen(window, ['resize'], () => without_reactive_context(() => set(window[type])));
}
+51
View File
@@ -0,0 +1,51 @@
import { to_class } from '../../../shared/attributes.js';
import { hydrating } from '../hydration.js';
/**
* @param {Element} dom
* @param {boolean | number} is_html
* @param {string | null} value
* @param {string} [hash]
* @param {Record<string, any>} [prev_classes]
* @param {Record<string, any>} [next_classes]
* @returns {Record<string, boolean> | undefined}
*/
export function set_class(dom, is_html, value, hash, prev_classes, next_classes) {
// @ts-expect-error need to add __className to patched prototype
var prev = dom.__className;
if (
hydrating ||
prev !== value ||
prev === undefined // for edge case of `class={undefined}`
) {
var next_class_name = to_class(value, hash, next_classes);
if (!hydrating || next_class_name !== dom.getAttribute('class')) {
// Removing the attribute when the value is only an empty string causes
// performance issues vs simply making the className an empty string. So
// we should only remove the class if the value is nullish
// and there no hash/directives :
if (next_class_name == null) {
dom.removeAttribute('class');
} else if (is_html) {
dom.className = next_class_name;
} else {
dom.setAttribute('class', next_class_name);
}
}
// @ts-expect-error need to add __className to patched prototype
dom.__className = value;
} else if (next_classes && prev_classes !== next_classes) {
for (var key in next_classes) {
var is_present = !!next_classes[key];
if (prev_classes == null || is_present !== !!prev_classes[key]) {
dom.classList.toggle(key, is_present);
}
}
}
return next_classes;
}
+338
View File
@@ -0,0 +1,338 @@
import { createClassComponent } from '../../../../legacy/legacy-client.js';
import { effect_root, render_effect } from '../../reactivity/effects.js';
import { append } from '../template.js';
import { define_property, get_descriptor, object_keys } from '../../../shared/utils.js';
/**
* @typedef {Object} CustomElementPropDefinition
* @property {string} [attribute]
* @property {boolean} [reflect]
* @property {'String'|'Boolean'|'Number'|'Array'|'Object'} [type]
*/
/** @type {any} */
let SvelteElement;
if (typeof HTMLElement === 'function') {
SvelteElement = class extends HTMLElement {
/** The Svelte component constructor */
$$ctor;
/** Slots */
$$s;
/** @type {any} The Svelte component instance */
$$c;
/** Whether or not the custom element is connected */
$$cn = false;
/** @type {Record<string, any>} Component props data */
$$d = {};
/** `true` if currently in the process of reflecting component props back to attributes */
$$r = false;
/** @type {Record<string, CustomElementPropDefinition>} Props definition (name, reflected, type etc) */
$$p_d = {};
/** @type {Record<string, EventListenerOrEventListenerObject[]>} Event listeners */
$$l = {};
/** @type {Map<EventListenerOrEventListenerObject, Function>} Event listener unsubscribe functions */
$$l_u = new Map();
/** @type {any} The managed render effect for reflecting attributes */
$$me;
/**
* @param {*} $$componentCtor
* @param {*} $$slots
* @param {*} use_shadow_dom
*/
constructor($$componentCtor, $$slots, use_shadow_dom) {
super();
this.$$ctor = $$componentCtor;
this.$$s = $$slots;
if (use_shadow_dom) {
this.attachShadow({ mode: 'open' });
}
}
/**
* @param {string} type
* @param {EventListenerOrEventListenerObject} listener
* @param {boolean | AddEventListenerOptions} [options]
*/
addEventListener(type, listener, options) {
// We can't determine upfront if the event is a custom event or not, so we have to
// listen to both. If someone uses a custom event with the same name as a regular
// browser event, this fires twice - we can't avoid that.
this.$$l[type] = this.$$l[type] || [];
this.$$l[type].push(listener);
if (this.$$c) {
const unsub = this.$$c.$on(type, listener);
this.$$l_u.set(listener, unsub);
}
super.addEventListener(type, listener, options);
}
/**
* @param {string} type
* @param {EventListenerOrEventListenerObject} listener
* @param {boolean | AddEventListenerOptions} [options]
*/
removeEventListener(type, listener, options) {
super.removeEventListener(type, listener, options);
if (this.$$c) {
const unsub = this.$$l_u.get(listener);
if (unsub) {
unsub();
this.$$l_u.delete(listener);
}
}
}
async connectedCallback() {
this.$$cn = true;
if (!this.$$c) {
// We wait one tick to let possible child slot elements be created/mounted
await Promise.resolve();
if (!this.$$cn || this.$$c) {
return;
}
/** @param {string} name */
function create_slot(name) {
/**
* @param {Element} anchor
*/
return (anchor) => {
const slot = document.createElement('slot');
if (name !== 'default') slot.name = name;
append(anchor, slot);
};
}
/** @type {Record<string, any>} */
const $$slots = {};
const existing_slots = get_custom_elements_slots(this);
for (const name of this.$$s) {
if (name in existing_slots) {
if (name === 'default' && !this.$$d.children) {
this.$$d.children = create_slot(name);
$$slots.default = true;
} else {
$$slots[name] = create_slot(name);
}
}
}
for (const attribute of this.attributes) {
// this.$$data takes precedence over this.attributes
const name = this.$$g_p(attribute.name);
if (!(name in this.$$d)) {
this.$$d[name] = get_custom_element_value(name, attribute.value, this.$$p_d, 'toProp');
}
}
// Port over props that were set programmatically before ce was initialized
for (const key in this.$$p_d) {
// @ts-expect-error
if (!(key in this.$$d) && this[key] !== undefined) {
// @ts-expect-error
this.$$d[key] = this[key]; // don't transform, these were set through JavaScript
// @ts-expect-error
delete this[key]; // remove the property that shadows the getter/setter
}
}
this.$$c = createClassComponent({
component: this.$$ctor,
target: this.shadowRoot || this,
props: {
...this.$$d,
$$slots,
$$host: this
}
});
// Reflect component props as attributes
this.$$me = effect_root(() => {
render_effect(() => {
this.$$r = true;
for (const key of object_keys(this.$$c)) {
if (!this.$$p_d[key]?.reflect) continue;
this.$$d[key] = this.$$c[key];
const attribute_value = get_custom_element_value(
key,
this.$$d[key],
this.$$p_d,
'toAttribute'
);
if (attribute_value == null) {
this.removeAttribute(this.$$p_d[key].attribute || key);
} else {
this.setAttribute(this.$$p_d[key].attribute || key, attribute_value);
}
}
this.$$r = false;
});
});
for (const type in this.$$l) {
for (const listener of this.$$l[type]) {
const unsub = this.$$c.$on(type, listener);
this.$$l_u.set(listener, unsub);
}
}
this.$$l = {};
}
}
// We don't need this when working within Svelte code, but for compatibility of people using this outside of Svelte
// and setting attributes through setAttribute etc, this is helpful
/**
* @param {string} attr
* @param {string} _oldValue
* @param {string} newValue
*/
attributeChangedCallback(attr, _oldValue, newValue) {
if (this.$$r) return;
attr = this.$$g_p(attr);
this.$$d[attr] = get_custom_element_value(attr, newValue, this.$$p_d, 'toProp');
this.$$c?.$set({ [attr]: this.$$d[attr] });
}
disconnectedCallback() {
this.$$cn = false;
// In a microtask, because this could be a move within the DOM
Promise.resolve().then(() => {
if (!this.$$cn && this.$$c) {
this.$$c.$destroy();
this.$$me();
this.$$c = undefined;
}
});
}
/**
* @param {string} attribute_name
*/
$$g_p(attribute_name) {
return (
object_keys(this.$$p_d).find(
(key) =>
this.$$p_d[key].attribute === attribute_name ||
(!this.$$p_d[key].attribute && key.toLowerCase() === attribute_name)
) || attribute_name
);
}
};
}
/**
* @param {string} prop
* @param {any} value
* @param {Record<string, CustomElementPropDefinition>} props_definition
* @param {'toAttribute' | 'toProp'} [transform]
*/
function get_custom_element_value(prop, value, props_definition, transform) {
const type = props_definition[prop]?.type;
value = type === 'Boolean' && typeof value !== 'boolean' ? value != null : value;
if (!transform || !props_definition[prop]) {
return value;
} else if (transform === 'toAttribute') {
switch (type) {
case 'Object':
case 'Array':
return value == null ? null : JSON.stringify(value);
case 'Boolean':
return value ? '' : null;
case 'Number':
return value == null ? null : value;
default:
return value;
}
} else {
switch (type) {
case 'Object':
case 'Array':
return value && JSON.parse(value);
case 'Boolean':
return value; // conversion already handled above
case 'Number':
return value != null ? +value : value;
default:
return value;
}
}
}
/**
* @param {HTMLElement} element
*/
function get_custom_elements_slots(element) {
/** @type {Record<string, true>} */
const result = {};
element.childNodes.forEach((node) => {
result[/** @type {Element} node */ (node).slot || 'default'] = true;
});
return result;
}
/**
* @internal
*
* Turn a Svelte component into a custom element.
* @param {any} Component A Svelte component function
* @param {Record<string, CustomElementPropDefinition>} props_definition The props to observe
* @param {string[]} slots The slots to create
* @param {string[]} exports Explicitly exported values, other than props
* @param {boolean} use_shadow_dom Whether to use shadow DOM
* @param {(ce: new () => HTMLElement) => new () => HTMLElement} [extend]
*/
export function create_custom_element(
Component,
props_definition,
slots,
exports,
use_shadow_dom,
extend
) {
let Class = class extends SvelteElement {
constructor() {
super(Component, slots, use_shadow_dom);
this.$$p_d = props_definition;
}
static get observedAttributes() {
return object_keys(props_definition).map((key) =>
(props_definition[key].attribute || key).toLowerCase()
);
}
};
object_keys(props_definition).forEach((prop) => {
define_property(Class.prototype, prop, {
get() {
return this.$$c && prop in this.$$c ? this.$$c[prop] : this.$$d[prop];
},
set(value) {
value = get_custom_element_value(prop, value, props_definition);
this.$$d[prop] = value;
var component = this.$$c;
if (component) {
// // If the instance has an accessor, use that instead
var setter = get_descriptor(component, prop)?.get;
if (setter) {
component[prop] = value;
} else {
component.$set({ [prop]: value });
}
}
}
});
});
exports.forEach((property) => {
define_property(Class.prototype, property, {
get() {
return this.$$c?.[property];
}
});
});
if (extend) {
// @ts-expect-error - assigning here is fine
Class = extend(Class);
}
Component.element = /** @type {any} */ Class;
return Class;
}
@@ -0,0 +1,98 @@
import { hydrating, reset, set_hydrate_node, set_hydrating } from '../hydration.js';
import { create_comment } from '../operations.js';
import { attach } from './attachments.js';
/** @type {boolean | null} */
let supported = null;
/**
* Checks if the browser supports rich HTML content inside `<option>` elements.
* Modern browsers preserve HTML elements inside options, while older browsers
* strip them during parsing, leaving only text content.
* @returns {boolean}
*/
function is_supported() {
if (supported === null) {
var select = document.createElement('select');
select.innerHTML = '<option><span>t</span></option>';
supported = /** @type {Element} */ (select.firstChild)?.firstChild?.nodeType === 1;
}
return supported;
}
/**
*
* @param {HTMLElement} element
* @param {(new_element: HTMLElement) => void} update_element
*/
export function selectedcontent(element, update_element) {
// if it's not supported no need for special logic
if (!is_supported()) return;
// we use the attach function directly just to make sure is executed when is mounted to the dom
attach(element, () => () => {
const select = element.closest('select');
if (!select) return;
const observer = new MutationObserver((entries) => {
var selected = false;
for (const entry of entries) {
if (entry.target === element) {
// the `<selectedcontent>` already changed, no need to replace it
return;
}
// if the changes doesn't include the selected `<option>` we don't need to do anything
selected ||= !!entry.target.parentElement?.closest('option')?.selected;
}
if (selected) {
// replace the `<selectedcontent>` with a clone
element.replaceWith((element = /** @type {HTMLElement} */ (element.cloneNode(true))));
update_element(element);
}
});
observer.observe(select, {
childList: true,
characterData: true,
subtree: true
});
return () => {
observer.disconnect();
};
});
}
/**
* Handles rich HTML content inside `<option>`, `<optgroup>`, or `<select>` elements with browser-specific branching.
* Modern browsers preserve HTML inside options, while older browsers strip it to text only.
*
* @param {HTMLOptionElement | HTMLOptGroupElement | HTMLSelectElement} element The element to process
* @param {() => void} rich_fn Function to process rich HTML content (modern browsers)
*/
export function customizable_select(element, rich_fn) {
var was_hydrating = hydrating;
if (!is_supported()) {
set_hydrating(false);
element.textContent = '';
element.append(create_comment(''));
}
try {
rich_fn();
} finally {
if (was_hydrating) {
if (hydrating) {
reset(element);
} else {
set_hydrating(true);
set_hydrate_node(element);
}
}
}
}
+338
View File
@@ -0,0 +1,338 @@
import { teardown } from '../../reactivity/effects.js';
import { define_property } from '../../../shared/utils.js';
import { hydrating } from '../hydration.js';
import { queue_micro_task } from '../task.js';
import { FILENAME } from '../../../../constants.js';
import * as w from '../../warnings.js';
import {
active_effect,
active_reaction,
set_active_effect,
set_active_reaction
} from '../../runtime.js';
import { without_reactive_context } from './bindings/shared.js';
/** @type {Set<string>} */
export const all_registered_events = new Set();
/** @type {Set<(events: Array<string>) => void>} */
export const root_event_handles = new Set();
/**
* SSR adds onload and onerror attributes to catch those events before the hydration.
* This function detects those cases, removes the attributes and replays the events.
* @param {HTMLElement} dom
*/
export function replay_events(dom) {
if (!hydrating) return;
dom.removeAttribute('onload');
dom.removeAttribute('onerror');
// @ts-expect-error
const event = dom.__e;
if (event !== undefined) {
// @ts-expect-error
dom.__e = undefined;
queueMicrotask(() => {
if (dom.isConnected) {
dom.dispatchEvent(event);
}
});
}
}
/**
* @param {string} event_name
* @param {EventTarget} dom
* @param {EventListener} [handler]
* @param {AddEventListenerOptions} [options]
*/
export function create_event(event_name, dom, handler, options = {}) {
/**
* @this {EventTarget}
*/
function target_handler(/** @type {Event} */ event) {
if (!options.capture) {
// Only call in the bubble phase, else delegated events would be called before the capturing events
handle_event_propagation.call(dom, event);
}
if (!event.cancelBubble) {
return without_reactive_context(() => {
return handler?.call(this, event);
});
}
}
// Chrome has a bug where pointer events don't work when attached to a DOM element that has been cloned
// with cloneNode() and the DOM element is disconnected from the document. To ensure the event works, we
// defer the attachment till after it's been appended to the document. TODO: remove this once Chrome fixes
// this bug. The same applies to wheel events and touch events.
if (
event_name.startsWith('pointer') ||
event_name.startsWith('touch') ||
event_name === 'wheel'
) {
queue_micro_task(() => {
dom.addEventListener(event_name, target_handler, options);
});
} else {
dom.addEventListener(event_name, target_handler, options);
}
return target_handler;
}
/**
* Attaches an event handler to an element and returns a function that removes the handler. Using this
* rather than `addEventListener` will preserve the correct order relative to handlers added declaratively
* (with attributes like `onclick`), which use event delegation for performance reasons
*
* @param {EventTarget} element
* @param {string} type
* @param {EventListener} handler
* @param {AddEventListenerOptions} [options]
*/
export function on(element, type, handler, options = {}) {
var target_handler = create_event(type, element, handler, options);
return () => {
element.removeEventListener(type, target_handler, options);
};
}
/**
* @param {string} event_name
* @param {Element} dom
* @param {EventListener} [handler]
* @param {boolean} [capture]
* @param {boolean} [passive]
* @returns {void}
*/
export function event(event_name, dom, handler, capture, passive) {
var options = { capture, passive };
var target_handler = create_event(event_name, dom, handler, options);
if (
dom === document.body ||
// @ts-ignore
dom === window ||
// @ts-ignore
dom === document ||
// Firefox has quirky behavior, it can happen that we still get "canplay" events when the element is already removed
dom instanceof HTMLMediaElement
) {
teardown(() => {
dom.removeEventListener(event_name, target_handler, options);
});
}
}
/**
* @param {Array<string>} events
* @returns {void}
*/
export function delegate(events) {
for (var i = 0; i < events.length; i++) {
all_registered_events.add(events[i]);
}
for (var fn of root_event_handles) {
fn(events);
}
}
// used to store the reference to the currently propagated event
// to prevent garbage collection between microtasks in Firefox
// If the event object is GCed too early, the expando __root property
// set on the event object is lost, causing the event delegation
// to process the event twice
let last_propagated_event = null;
/**
* @this {EventTarget}
* @param {Event} event
* @returns {void}
*/
export function handle_event_propagation(event) {
var handler_element = this;
var owner_document = /** @type {Node} */ (handler_element).ownerDocument;
var event_name = event.type;
var path = event.composedPath?.() || [];
var current_target = /** @type {null | Element} */ (path[0] || event.target);
last_propagated_event = event;
// composedPath contains list of nodes the event has propagated through.
// We check __root to skip all nodes below it in case this is a
// parent of the __root node, which indicates that there's nested
// mounted apps. In this case we don't want to trigger events multiple times.
var path_idx = 0;
// the `last_propagated_event === event` check is redundant, but
// without it the variable will be DCE'd and things will
// fail mysteriously in Firefox
// @ts-expect-error is added below
var handled_at = last_propagated_event === event && event.__root;
if (handled_at) {
var at_idx = path.indexOf(handled_at);
if (
at_idx !== -1 &&
(handler_element === document || handler_element === /** @type {any} */ (window))
) {
// This is the fallback document listener or a window listener, but the event was already handled
// -> ignore, but set handle_at to document/window so that we're resetting the event
// chain in case someone manually dispatches the same event object again.
// @ts-expect-error
event.__root = handler_element;
return;
}
// We're deliberately not skipping if the index is higher, because
// someone could create an event programmatically and emit it multiple times,
// in which case we want to handle the whole propagation chain properly each time.
// (this will only be a false negative if the event is dispatched multiple times and
// the fallback document listener isn't reached in between, but that's super rare)
var handler_idx = path.indexOf(handler_element);
if (handler_idx === -1) {
// handle_idx can theoretically be -1 (happened in some JSDOM testing scenarios with an event listener on the window object)
// so guard against that, too, and assume that everything was handled at this point.
return;
}
if (at_idx <= handler_idx) {
path_idx = at_idx;
}
}
current_target = /** @type {Element} */ (path[path_idx] || event.target);
// there can only be one delegated event per element, and we either already handled the current target,
// or this is the very first target in the chain which has a non-delegated listener, in which case it's safe
// to handle a possible delegated event on it later (through the root delegation listener for example).
if (current_target === handler_element) return;
// Proxy currentTarget to correct target
define_property(event, 'currentTarget', {
configurable: true,
get() {
return current_target || owner_document;
}
});
// This started because of Chromium issue https://chromestatus.com/feature/5128696823545856,
// where removal or moving of of the DOM can cause sync `blur` events to fire, which can cause logic
// to run inside the current `active_reaction`, which isn't what we want at all. However, on reflection,
// it's probably best that all event handled by Svelte have this behaviour, as we don't really want
// an event handler to run in the context of another reaction or effect.
var previous_reaction = active_reaction;
var previous_effect = active_effect;
set_active_reaction(null);
set_active_effect(null);
try {
/**
* @type {unknown}
*/
var throw_error;
/**
* @type {unknown[]}
*/
var other_errors = [];
while (current_target !== null) {
/** @type {null | Element} */
var parent_element =
current_target.assignedSlot ||
current_target.parentNode ||
/** @type {any} */ (current_target).host ||
null;
try {
// @ts-expect-error
var delegated = current_target['__' + event_name];
if (
delegated != null &&
(!(/** @type {any} */ (current_target).disabled) ||
// DOM could've been updated already by the time this is reached, so we check this as well
// -> the target could not have been disabled because it emits the event in the first place
event.target === current_target)
) {
delegated.call(current_target, event);
}
} catch (error) {
if (throw_error) {
other_errors.push(error);
} else {
throw_error = error;
}
}
if (event.cancelBubble || parent_element === handler_element || parent_element === null) {
break;
}
current_target = parent_element;
}
if (throw_error) {
for (let error of other_errors) {
// Throw the rest of the errors, one-by-one on a microtask
queueMicrotask(() => {
throw error;
});
}
throw throw_error;
}
} finally {
// @ts-expect-error is used above
event.__root = handler_element;
// @ts-ignore remove proxy on currentTarget
delete event.currentTarget;
set_active_reaction(previous_reaction);
set_active_effect(previous_effect);
}
}
/**
* In dev, warn if an event handler is not a function, as it means the
* user probably called the handler or forgot to add a `() =>`
* @param {() => (event: Event, ...args: any) => void} thunk
* @param {EventTarget} element
* @param {[Event, ...any]} args
* @param {any} component
* @param {[number, number]} [loc]
* @param {boolean} [remove_parens]
*/
export function apply(
thunk,
element,
args,
component,
loc,
has_side_effects = false,
remove_parens = false
) {
let handler;
let error;
try {
handler = thunk();
} catch (e) {
error = e;
}
if (typeof handler !== 'function' && (has_side_effects || handler != null || error)) {
const filename = component?.[FILENAME];
const location = loc ? ` at ${filename}:${loc[0]}:${loc[1]}` : ` in ${filename}`;
const phase = args[0]?.eventPhase < Event.BUBBLING_PHASE ? 'capture' : '';
const event_name = args[0]?.type + phase;
const description = `\`${event_name}\` handler${location}`;
const suggestion = remove_parens ? 'remove the trailing `()`' : 'add a leading `() =>`';
w.event_handler_invalid(description, suggestion);
if (error) {
throw error;
}
}
handler?.apply(element, args);
}
+58
View File
@@ -0,0 +1,58 @@
import { hydrating } from '../hydration.js';
import { clear_text_content, get_first_child } from '../operations.js';
import { queue_micro_task } from '../task.js';
/**
* @param {HTMLElement} dom
* @param {boolean} value
* @returns {void}
*/
export function autofocus(dom, value) {
if (value) {
const body = document.body;
dom.autofocus = true;
queue_micro_task(() => {
if (document.activeElement === body) {
dom.focus();
}
});
}
}
/**
* The child of a textarea actually corresponds to the defaultValue property, so we need
* to remove it upon hydration to avoid a bug when someone resets the form value.
* @param {HTMLTextAreaElement} dom
* @returns {void}
*/
export function remove_textarea_child(dom) {
if (hydrating && get_first_child(dom) !== null) {
clear_text_content(dom);
}
}
let listening_to_form_reset = false;
export function add_form_reset_listener() {
if (!listening_to_form_reset) {
listening_to_form_reset = true;
document.addEventListener(
'reset',
(evt) => {
// Needs to happen one tick later or else the dom properties of the form
// elements have not updated to their reset values yet
Promise.resolve().then(() => {
if (!evt.defaultPrevented) {
for (const e of /**@type {HTMLFormElement} */ (evt.target).elements) {
// @ts-expect-error
e.__on_r?.();
}
}
});
},
// In the capture phase to guarantee we get noticed of it (no possibility of stopPropagation)
{ capture: true }
);
}
}
+57
View File
@@ -0,0 +1,57 @@
import { to_style } from '../../../shared/attributes.js';
import { hydrating } from '../hydration.js';
/**
* @param {Element & ElementCSSInlineStyle} dom
* @param {Record<string, any>} prev
* @param {Record<string, any>} next
* @param {string} [priority]
*/
function update_styles(dom, prev = {}, next, priority) {
for (var key in next) {
var value = next[key];
if (prev[key] !== value) {
if (next[key] == null) {
dom.style.removeProperty(key);
} else {
dom.style.setProperty(key, value, priority);
}
}
}
}
/**
* @param {Element & ElementCSSInlineStyle} dom
* @param {string | null} value
* @param {Record<string, any> | [Record<string, any>, Record<string, any>]} [prev_styles]
* @param {Record<string, any> | [Record<string, any>, Record<string, any>]} [next_styles]
*/
export function set_style(dom, value, prev_styles, next_styles) {
// @ts-expect-error
var prev = dom.__style;
if (hydrating || prev !== value) {
var next_style_attr = to_style(value, next_styles);
if (!hydrating || next_style_attr !== dom.getAttribute('style')) {
if (next_style_attr == null) {
dom.removeAttribute('style');
} else {
dom.style.cssText = next_style_attr;
}
}
// @ts-expect-error
dom.__style = value;
} else if (next_styles) {
if (Array.isArray(next_styles)) {
update_styles(dom, prev_styles?.[0], next_styles[0]);
update_styles(dom, prev_styles?.[1], next_styles[1], 'important');
} else {
update_styles(dom, prev_styles, next_styles);
}
}
return next_styles;
}
+472
View File
@@ -0,0 +1,472 @@
/** @import { AnimateFn, Animation, AnimationConfig, EachItem, Effect, EffectNodes, TransitionFn, TransitionManager } from '#client' */
import { noop, is_function } from '../../../shared/utils.js';
import { effect } from '../../reactivity/effects.js';
import { active_effect, untrack } from '../../runtime.js';
import { loop } from '../../loop.js';
import { should_intro } from '../../render.js';
import { TRANSITION_GLOBAL, TRANSITION_IN, TRANSITION_OUT } from '../../../../constants.js';
import { BLOCK_EFFECT, EFFECT_RAN, EFFECT_TRANSPARENT } from '#client/constants';
import { queue_micro_task } from '../task.js';
import { without_reactive_context } from './bindings/shared.js';
/**
* @param {Element} element
* @param {'introstart' | 'introend' | 'outrostart' | 'outroend'} type
* @returns {void}
*/
function dispatch_event(element, type) {
without_reactive_context(() => {
element.dispatchEvent(new CustomEvent(type));
});
}
/**
* Converts a property to the camel-case format expected by Element.animate(), KeyframeEffect(), and KeyframeEffect.setKeyframes().
* @param {string} style
* @returns {string}
*/
function css_property_to_camelcase(style) {
// in compliance with spec
if (style === 'float') return 'cssFloat';
if (style === 'offset') return 'cssOffset';
// do not rename custom @properties
if (style.startsWith('--')) return style;
const parts = style.split('-');
if (parts.length === 1) return parts[0];
return (
parts[0] +
parts
.slice(1)
.map(/** @param {any} word */ (word) => word[0].toUpperCase() + word.slice(1))
.join('')
);
}
/**
* @param {string} css
* @returns {Keyframe}
*/
function css_to_keyframe(css) {
/** @type {Keyframe} */
const keyframe = {};
const parts = css.split(';');
for (const part of parts) {
const [property, value] = part.split(':');
if (!property || value === undefined) break;
const formatted_property = css_property_to_camelcase(property.trim());
keyframe[formatted_property] = value.trim();
}
return keyframe;
}
/** @param {number} t */
const linear = (t) => t;
/** @type {Effect | null} */
let animation_effect_override = null;
/** @param {Effect | null} v */
export function set_animation_effect_override(v) {
animation_effect_override = v;
}
/**
* Called inside keyed `{#each ...}` blocks (as `$.animation(...)`). This creates an animation manager
* and attaches it to the block, so that moves can be animated following reconciliation.
* @template P
* @param {Element} element
* @param {() => AnimateFn<P | undefined>} get_fn
* @param {(() => P) | null} get_params
*/
export function animation(element, get_fn, get_params) {
var effect = animation_effect_override ?? /** @type {Effect} */ (active_effect);
var nodes = /** @type {EffectNodes} */ (effect.nodes);
/** @type {DOMRect} */
var from;
/** @type {DOMRect} */
var to;
/** @type {Animation | undefined} */
var animation;
/** @type {null | { position: string, width: string, height: string, transform: string }} */
var original_styles = null;
nodes.a ??= {
element,
measure() {
from = this.element.getBoundingClientRect();
},
apply() {
animation?.abort();
to = this.element.getBoundingClientRect();
if (
from.left !== to.left ||
from.right !== to.right ||
from.top !== to.top ||
from.bottom !== to.bottom
) {
const options = get_fn()(this.element, { from, to }, get_params?.());
animation = animate(this.element, options, undefined, 1, () => {
animation?.abort();
animation = undefined;
});
}
},
fix() {
// If an animation is already running, transforming the element is likely to fail,
// because the styles applied by the animation take precedence. In the case of crossfade,
// that means the `translate(...)` of the crossfade transition overrules the `translate(...)`
// we would apply below, leading to the element jumping somewhere to the top left.
if (element.getAnimations().length) return;
// It's important to destructure these to get fixed values - the object itself has getters,
// and changing the style to 'absolute' can for example influence the width.
var { position, width, height } = getComputedStyle(element);
if (position !== 'absolute' && position !== 'fixed') {
var style = /** @type {HTMLElement | SVGElement} */ (element).style;
original_styles = {
position: style.position,
width: style.width,
height: style.height,
transform: style.transform
};
style.position = 'absolute';
style.width = width;
style.height = height;
var to = element.getBoundingClientRect();
if (from.left !== to.left || from.top !== to.top) {
var transform = `translate(${from.left - to.left}px, ${from.top - to.top}px)`;
style.transform = style.transform ? `${style.transform} ${transform}` : transform;
}
}
},
unfix() {
if (original_styles) {
var style = /** @type {HTMLElement | SVGElement} */ (element).style;
style.position = original_styles.position;
style.width = original_styles.width;
style.height = original_styles.height;
style.transform = original_styles.transform;
}
}
};
// in the case of a `<svelte:element>`, it's possible for `$.animation(...)` to be called
// when an animation manager already exists, if the tag changes. in that case, we need to
// swap out the element rather than creating a new manager, in case it happened at the same
// moment as a reconciliation
nodes.a.element = element;
}
/**
* Called inside block effects as `$.transition(...)`. This creates a transition manager and
* attaches it to the current effect — later, inside `pause_effect` and `resume_effect`, we
* use this to create `intro` and `outro` transitions.
* @template P
* @param {number} flags
* @param {HTMLElement} element
* @param {() => TransitionFn<P | undefined>} get_fn
* @param {(() => P) | null} get_params
* @returns {void}
*/
export function transition(flags, element, get_fn, get_params) {
var is_intro = (flags & TRANSITION_IN) !== 0;
var is_outro = (flags & TRANSITION_OUT) !== 0;
var is_both = is_intro && is_outro;
var is_global = (flags & TRANSITION_GLOBAL) !== 0;
/** @type {'in' | 'out' | 'both'} */
var direction = is_both ? 'both' : is_intro ? 'in' : 'out';
/** @type {AnimationConfig | ((opts: { direction: 'in' | 'out' }) => AnimationConfig) | undefined} */
var current_options;
var inert = element.inert;
/**
* The default overflow style, stashed so we can revert changes during the transition
* that are necessary to work around a Safari <18 bug
* TODO 6.0 remove this, if older versions of Safari have died out enough
*/
var overflow = element.style.overflow;
/** @type {Animation | undefined} */
var intro;
/** @type {Animation | undefined} */
var outro;
function get_options() {
return without_reactive_context(() => {
// If a transition is still ongoing, we use the existing options rather than generating
// new ones. This ensures that reversible transitions reverse smoothly, rather than
// jumping to a new spot because (for example) a different `duration` was used
return (current_options ??= get_fn()(element, get_params?.() ?? /** @type {P} */ ({}), {
direction
}));
});
}
/** @type {TransitionManager} */
var transition = {
is_global,
in() {
element.inert = inert;
if (!is_intro) {
outro?.abort();
outro?.reset?.();
return;
}
if (!is_outro) {
// if we intro then outro then intro again, we want to abort the first intro,
// if it's not a bidirectional transition
intro?.abort();
}
dispatch_event(element, 'introstart');
intro = animate(element, get_options(), outro, 1, () => {
dispatch_event(element, 'introend');
// Ensure we cancel the animation to prevent leaking
intro?.abort();
intro = current_options = undefined;
element.style.overflow = overflow;
});
},
out(fn) {
if (!is_outro) {
fn?.();
current_options = undefined;
return;
}
element.inert = true;
dispatch_event(element, 'outrostart');
outro = animate(element, get_options(), intro, 0, () => {
dispatch_event(element, 'outroend');
fn?.();
});
},
stop: () => {
intro?.abort();
outro?.abort();
}
};
var e = /** @type {Effect & { nodes: EffectNodes }} */ (active_effect);
(e.nodes.t ??= []).push(transition);
// if this is a local transition, we only want to run it if the parent (branch) effect's
// parent (block) effect is where the state change happened. we can determine that by
// looking at whether the block effect is currently initializing
if (is_intro && should_intro) {
var run = is_global;
if (!run) {
var block = /** @type {Effect | null} */ (e.parent);
// skip over transparent blocks (e.g. snippets, else-if blocks)
while (block && (block.f & EFFECT_TRANSPARENT) !== 0) {
while ((block = block.parent)) {
if ((block.f & BLOCK_EFFECT) !== 0) break;
}
}
run = !block || (block.f & EFFECT_RAN) !== 0;
}
if (run) {
effect(() => {
untrack(() => transition.in());
});
}
}
}
/**
* Animates an element, according to the provided configuration
* @param {Element} element
* @param {AnimationConfig | ((opts: { direction: 'in' | 'out' }) => AnimationConfig)} options
* @param {Animation | undefined} counterpart The corresponding intro/outro to this outro/intro
* @param {number} t2 The target `t` value — `1` for intro, `0` for outro
* @param {(() => void)} on_finish Called after successfully completing the animation
* @returns {Animation}
*/
function animate(element, options, counterpart, t2, on_finish) {
var is_intro = t2 === 1;
if (is_function(options)) {
// In the case of a deferred transition (such as `crossfade`), `option` will be
// a function rather than an `AnimationConfig`. We need to call this function
// once the DOM has been updated...
/** @type {Animation} */
var a;
var aborted = false;
queue_micro_task(() => {
if (aborted) return;
var o = options({ direction: is_intro ? 'in' : 'out' });
a = animate(element, o, counterpart, t2, on_finish);
});
// ...but we want to do so without using `async`/`await` everywhere, so
// we return a facade that allows everything to remain synchronous
return {
abort: () => {
aborted = true;
a?.abort();
},
deactivate: () => a.deactivate(),
reset: () => a.reset(),
t: () => a.t()
};
}
counterpart?.deactivate();
if (!options?.duration) {
on_finish();
return {
abort: noop,
deactivate: noop,
reset: noop,
t: () => t2
};
}
const { delay = 0, css, tick, easing = linear } = options;
var keyframes = [];
if (is_intro && counterpart === undefined) {
if (tick) {
tick(0, 1); // TODO put in nested effect, to avoid interleaved reads/writes?
}
if (css) {
var styles = css_to_keyframe(css(0, 1));
keyframes.push(styles, styles);
}
}
var get_t = () => 1 - t2;
// create a dummy animation that lasts as long as the delay (but with whatever devtools
// multiplier is in effect). in the common case that it is `0`, we keep it anyway so that
// the CSS keyframes aren't created until the DOM is updated
//
// fill forwards to prevent the element from rendering without styles applied
// see https://github.com/sveltejs/svelte/issues/14732
var animation = element.animate(keyframes, { duration: delay, fill: 'forwards' });
animation.onfinish = () => {
// remove dummy animation from the stack to prevent conflict with main animation
animation.cancel();
// for bidirectional transitions, we start from the current position,
// rather than doing a full intro/outro
var t1 = counterpart?.t() ?? 1 - t2;
counterpart?.abort();
var delta = t2 - t1;
var duration = /** @type {number} */ (options.duration) * Math.abs(delta);
var keyframes = [];
if (duration > 0) {
/**
* Whether or not the CSS includes `overflow: hidden`, in which case we need to
* add it as an inline style to work around a Safari <18 bug
* TODO 6.0 remove this, if possible
*/
var needs_overflow_hidden = false;
if (css) {
var n = Math.ceil(duration / (1000 / 60)); // `n` must be an integer, or we risk missing the `t2` value
for (var i = 0; i <= n; i += 1) {
var t = t1 + delta * easing(i / n);
var styles = css_to_keyframe(css(t, 1 - t));
keyframes.push(styles);
needs_overflow_hidden ||= styles.overflow === 'hidden';
}
}
if (needs_overflow_hidden) {
/** @type {HTMLElement} */ (element).style.overflow = 'hidden';
}
get_t = () => {
var time = /** @type {number} */ (
/** @type {globalThis.Animation} */ (animation).currentTime
);
return t1 + delta * easing(time / duration);
};
if (tick) {
loop(() => {
if (animation.playState !== 'running') return false;
var t = get_t();
tick(t, 1 - t);
return true;
});
}
}
animation = element.animate(keyframes, { duration, fill: 'forwards' });
animation.onfinish = () => {
get_t = () => t2;
tick?.(t2, 1 - t2);
on_finish();
};
};
return {
abort: () => {
if (animation) {
animation.cancel();
// This prevents memory leaks in Chromium
animation.effect = null;
// This prevents onfinish to be launched after cancel(),
// which can happen in some rare cases
// see https://github.com/sveltejs/svelte/issues/13681
animation.onfinish = noop;
}
},
deactivate: () => {
on_finish = noop;
},
reset: () => {
if (t2 === 0) {
tick?.(1, 0);
}
},
t: () => get_t()
};
}
+120
View File
@@ -0,0 +1,120 @@
/** @import { TemplateNode } from '#client' */
import { COMMENT_NODE } from '#client/constants';
import {
HYDRATION_END,
HYDRATION_ERROR,
HYDRATION_START,
HYDRATION_START_ELSE
} from '../../../constants.js';
import * as w from '../warnings.js';
import { get_next_sibling } from './operations.js';
/**
* Use this variable to guard everything related to hydration code so it can be treeshaken out
* if the user doesn't use the `hydrate` method and these code paths are therefore not needed.
*/
export let hydrating = false;
/** @param {boolean} value */
export function set_hydrating(value) {
hydrating = value;
}
/**
* The node that is currently being hydrated. This starts out as the first node inside the opening
* <!--[--> comment, and updates each time a component calls `$.child(...)` or `$.sibling(...)`.
* When entering a block (e.g. `{#if ...}`), `hydrate_node` is the block opening comment; by the
* time we leave the block it is the closing comment, which serves as the block's anchor.
* @type {TemplateNode}
*/
export let hydrate_node;
/** @param {TemplateNode | null} node */
export function set_hydrate_node(node) {
if (node === null) {
w.hydration_mismatch();
throw HYDRATION_ERROR;
}
return (hydrate_node = node);
}
export function hydrate_next() {
return set_hydrate_node(get_next_sibling(hydrate_node));
}
/** @param {TemplateNode} node */
export function reset(node) {
if (!hydrating) return;
// If the node has remaining siblings, something has gone wrong
if (get_next_sibling(hydrate_node) !== null) {
w.hydration_mismatch();
throw HYDRATION_ERROR;
}
hydrate_node = node;
}
/**
* @param {HTMLTemplateElement} template
*/
export function hydrate_template(template) {
if (hydrating) {
// @ts-expect-error TemplateNode doesn't include DocumentFragment, but it's actually fine
hydrate_node = template.content;
}
}
export function next(count = 1) {
if (hydrating) {
var i = count;
var node = hydrate_node;
while (i--) {
node = /** @type {TemplateNode} */ (get_next_sibling(node));
}
hydrate_node = node;
}
}
/**
* Skips or removes (depending on {@link remove}) all nodes starting at `hydrate_node` up until the next hydration end comment
* @param {boolean} remove
*/
export function skip_nodes(remove = true) {
var depth = 0;
var node = hydrate_node;
while (true) {
if (node.nodeType === COMMENT_NODE) {
var data = /** @type {Comment} */ (node).data;
if (data === HYDRATION_END) {
if (depth === 0) return node;
depth -= 1;
} else if (data === HYDRATION_START || data === HYDRATION_START_ELSE) {
depth += 1;
}
}
var next = /** @type {TemplateNode} */ (get_next_sibling(node));
if (remove) node.remove();
node = next;
}
}
/**
*
* @param {TemplateNode} node
*/
export function read_hydration_instruction(node) {
if (!node || node.nodeType !== COMMENT_NODE) {
w.hydration_mismatch();
throw HYDRATION_ERROR;
}
return /** @type {Comment} */ (node).data;
}
+127
View File
@@ -0,0 +1,127 @@
import { noop } from '../../../shared/utils.js';
import { user_pre_effect } from '../../reactivity/effects.js';
import { on } from '../elements/events.js';
/**
* Substitute for the `trusted` event modifier
* @deprecated
* @param {(event: Event, ...args: Array<unknown>) => void} fn
* @returns {(event: Event, ...args: unknown[]) => void}
*/
export function trusted(fn) {
return function (...args) {
var event = /** @type {Event} */ (args[0]);
if (event.isTrusted) {
// @ts-ignore
fn?.apply(this, args);
}
};
}
/**
* Substitute for the `self` event modifier
* @deprecated
* @param {(event: Event, ...args: Array<unknown>) => void} fn
* @returns {(event: Event, ...args: unknown[]) => void}
*/
export function self(fn) {
return function (...args) {
var event = /** @type {Event} */ (args[0]);
// @ts-ignore
if (event.target === this) {
// @ts-ignore
fn?.apply(this, args);
}
};
}
/**
* Substitute for the `stopPropagation` event modifier
* @deprecated
* @param {(event: Event, ...args: Array<unknown>) => void} fn
* @returns {(event: Event, ...args: unknown[]) => void}
*/
export function stopPropagation(fn) {
return function (...args) {
var event = /** @type {Event} */ (args[0]);
event.stopPropagation();
// @ts-ignore
return fn?.apply(this, args);
};
}
/**
* Substitute for the `once` event modifier
* @deprecated
* @param {(event: Event, ...args: Array<unknown>) => void} fn
* @returns {(event: Event, ...args: unknown[]) => void}
*/
export function once(fn) {
var ran = false;
return function (...args) {
if (ran) return;
ran = true;
// @ts-ignore
return fn?.apply(this, args);
};
}
/**
* Substitute for the `stopImmediatePropagation` event modifier
* @deprecated
* @param {(event: Event, ...args: Array<unknown>) => void} fn
* @returns {(event: Event, ...args: unknown[]) => void}
*/
export function stopImmediatePropagation(fn) {
return function (...args) {
var event = /** @type {Event} */ (args[0]);
event.stopImmediatePropagation();
// @ts-ignore
return fn?.apply(this, args);
};
}
/**
* Substitute for the `preventDefault` event modifier
* @deprecated
* @param {(event: Event, ...args: Array<unknown>) => void} fn
* @returns {(event: Event, ...args: unknown[]) => void}
*/
export function preventDefault(fn) {
return function (...args) {
var event = /** @type {Event} */ (args[0]);
event.preventDefault();
// @ts-ignore
return fn?.apply(this, args);
};
}
/**
* Substitute for the `passive` event modifier, implemented as an action
* @deprecated
* @param {HTMLElement} node
* @param {[event: string, handler: () => EventListener]} options
*/
export function passive(node, [event, handler]) {
user_pre_effect(() => {
return on(node, event, handler() ?? noop, {
passive: true
});
});
}
/**
* Substitute for the `nonpassive` event modifier, implemented as an action
* @deprecated
* @param {HTMLElement} node
* @param {[event: string, handler: () => EventListener]} options
*/
export function nonpassive(node, [event, handler]) {
user_pre_effect(() => {
return on(node, event, handler() ?? noop, {
passive: false
});
});
}
+82
View File
@@ -0,0 +1,82 @@
/** @import { ComponentContextLegacy } from '#client' */
import { run, run_all } from '../../../shared/utils.js';
import { component_context } from '../../context.js';
import { derived } from '../../reactivity/deriveds.js';
import { user_pre_effect, user_effect } from '../../reactivity/effects.js';
import { deep_read_state, get, untrack } from '../../runtime.js';
/**
* Legacy-mode only: Call `onMount` callbacks and set up `beforeUpdate`/`afterUpdate` effects
* @param {boolean} [immutable]
*/
export function init(immutable = false) {
const context = /** @type {ComponentContextLegacy} */ (component_context);
const callbacks = context.l.u;
if (!callbacks) return;
let props = () => deep_read_state(context.s);
if (immutable) {
let version = 0;
let prev = /** @type {Record<string, any>} */ ({});
// In legacy immutable mode, before/afterUpdate only fire if the object identity of a prop changes
const d = derived(() => {
let changed = false;
const props = context.s;
for (const key in props) {
if (props[key] !== prev[key]) {
prev[key] = props[key];
changed = true;
}
}
if (changed) version++;
return version;
});
props = () => get(d);
}
// beforeUpdate
if (callbacks.b.length) {
user_pre_effect(() => {
observe_all(context, props);
run_all(callbacks.b);
});
}
// onMount (must run before afterUpdate)
user_effect(() => {
const fns = untrack(() => callbacks.m.map(run));
return () => {
for (const fn of fns) {
if (typeof fn === 'function') {
fn();
}
}
};
});
// afterUpdate
if (callbacks.a.length) {
user_effect(() => {
observe_all(context, props);
run_all(callbacks.a);
});
}
}
/**
* Invoke the getter of all signals associated with a component
* so they can be registered to the effect this function is called in.
* @param {ComponentContextLegacy} context
* @param {(() => void)} props
*/
function observe_all(context, props) {
if (context.l.s) {
for (const signal of context.l.s) get(signal);
}
props();
}
+68
View File
@@ -0,0 +1,68 @@
import { set, source } from '../../reactivity/sources.js';
import { get } from '../../runtime.js';
import { is_array } from '../../../shared/utils.js';
/**
* Under some circumstances, imports may be reactive in legacy mode. In that case,
* they should be using `reactive_import` as part of the transformation
* @param {() => any} fn
*/
export function reactive_import(fn) {
var s = source(0);
return function () {
if (arguments.length === 1) {
set(s, get(s) + 1);
return arguments[0];
} else {
get(s);
return fn();
}
};
}
/**
* @this {any}
* @param {Record<string, unknown>} $$props
* @param {Event} event
* @returns {void}
*/
export function bubble_event($$props, event) {
var events = /** @type {Record<string, Function[] | Function>} */ ($$props.$$events)?.[
event.type
];
var callbacks = is_array(events) ? events.slice() : events == null ? [] : [events];
for (var fn of callbacks) {
// Preserve "this" context
fn.call(this, event);
}
}
/**
* Used to simulate `$on` on a component instance when `compatibility.componentApi === 4`
* @param {Record<string, any>} $$props
* @param {string} event_name
* @param {Function} event_callback
*/
export function add_legacy_event_listener($$props, event_name, event_callback) {
$$props.$$events ||= {};
$$props.$$events[event_name] ||= [];
$$props.$$events[event_name].push(event_callback);
}
/**
* Used to simulate `$set` on a component instance when `compatibility.componentApi === 4`.
* Needs component accessors so that it can call the setter of the prop. Therefore doesn't
* work for updating props in `$$props` or `$$restProps`.
* @this {Record<string, any>}
* @param {Record<string, any>} $$new_props
*/
export function update_legacy_props($$new_props) {
for (var key in $$new_props) {
if (key in this) {
this[key] = $$new_props[key];
}
}
}
+260
View File
@@ -0,0 +1,260 @@
/** @import { Effect, TemplateNode } from '#client' */
import { hydrate_node, hydrating, set_hydrate_node } from './hydration.js';
import { DEV } from 'esm-env';
import { init_array_prototype_warnings } from '../dev/equality.js';
import { get_descriptor, is_extensible } from '../../shared/utils.js';
import { active_effect } from '../runtime.js';
import { async_mode_flag } from '../../flags/index.js';
import { TEXT_NODE, EFFECT_RAN } from '#client/constants';
import { eager_block_effects } from '../reactivity/batch.js';
// export these for reference in the compiled code, making global name deduplication unnecessary
/** @type {Window} */
export var $window;
/** @type {Document} */
export var $document;
/** @type {boolean} */
export var is_firefox;
/** @type {() => Node | null} */
var first_child_getter;
/** @type {() => Node | null} */
var next_sibling_getter;
/**
* Initialize these lazily to avoid issues when using the runtime in a server context
* where these globals are not available while avoiding a separate server entry point
*/
export function init_operations() {
if ($window !== undefined) {
return;
}
$window = window;
$document = document;
is_firefox = /Firefox/.test(navigator.userAgent);
var element_prototype = Element.prototype;
var node_prototype = Node.prototype;
var text_prototype = Text.prototype;
// @ts-ignore
first_child_getter = get_descriptor(node_prototype, 'firstChild').get;
// @ts-ignore
next_sibling_getter = get_descriptor(node_prototype, 'nextSibling').get;
if (is_extensible(element_prototype)) {
// the following assignments improve perf of lookups on DOM nodes
// @ts-expect-error
element_prototype.__click = undefined;
// @ts-expect-error
element_prototype.__className = undefined;
// @ts-expect-error
element_prototype.__attributes = null;
// @ts-expect-error
element_prototype.__style = undefined;
// @ts-expect-error
element_prototype.__e = undefined;
}
if (is_extensible(text_prototype)) {
// @ts-expect-error
text_prototype.__t = undefined;
}
if (DEV) {
// @ts-expect-error
element_prototype.__svelte_meta = null;
init_array_prototype_warnings();
}
}
/**
* @param {string} value
* @returns {Text}
*/
export function create_text(value = '') {
return document.createTextNode(value);
}
/**
* @template {Node} N
* @param {N} node
*/
/*@__NO_SIDE_EFFECTS__*/
export function get_first_child(node) {
return /** @type {TemplateNode | null} */ (first_child_getter.call(node));
}
/**
* @template {Node} N
* @param {N} node
*/
/*@__NO_SIDE_EFFECTS__*/
export function get_next_sibling(node) {
return /** @type {TemplateNode | null} */ (next_sibling_getter.call(node));
}
/**
* Don't mark this as side-effect-free, hydration needs to walk all nodes
* @template {Node} N
* @param {N} node
* @param {boolean} is_text
* @returns {TemplateNode | null}
*/
export function child(node, is_text) {
if (!hydrating) {
return get_first_child(node);
}
var child = get_first_child(hydrate_node);
// Child can be null if we have an element with a single child, like `<p>{text}</p>`, where `text` is empty
if (child === null) {
child = hydrate_node.appendChild(create_text());
} else if (is_text && child.nodeType !== TEXT_NODE) {
var text = create_text();
child?.before(text);
set_hydrate_node(text);
return text;
}
set_hydrate_node(child);
return child;
}
/**
* Don't mark this as side-effect-free, hydration needs to walk all nodes
* @param {TemplateNode} node
* @param {boolean} [is_text]
* @returns {TemplateNode | null}
*/
export function first_child(node, is_text = false) {
if (!hydrating) {
var first = get_first_child(node);
// TODO prevent user comments with the empty string when preserveComments is true
if (first instanceof Comment && first.data === '') return get_next_sibling(first);
return first;
}
// if an {expression} is empty during SSR, there might be no
// text node to hydrate — we must therefore create one
if (is_text && hydrate_node?.nodeType !== TEXT_NODE) {
var text = create_text();
hydrate_node?.before(text);
set_hydrate_node(text);
return text;
}
return hydrate_node;
}
/**
* Don't mark this as side-effect-free, hydration needs to walk all nodes
* @param {TemplateNode} node
* @param {number} count
* @param {boolean} is_text
* @returns {TemplateNode | null}
*/
export function sibling(node, count = 1, is_text = false) {
let next_sibling = hydrating ? hydrate_node : node;
var last_sibling;
while (count--) {
last_sibling = next_sibling;
next_sibling = /** @type {TemplateNode} */ (get_next_sibling(next_sibling));
}
if (!hydrating) {
return next_sibling;
}
// if a sibling {expression} is empty during SSR, there might be no
// text node to hydrate — we must therefore create one
if (is_text && next_sibling?.nodeType !== TEXT_NODE) {
var text = create_text();
// If the next sibling is `null` and we're handling text then it's because
// the SSR content was empty for the text, so we need to generate a new text
// node and insert it after the last sibling
if (next_sibling === null) {
last_sibling?.after(text);
} else {
next_sibling.before(text);
}
set_hydrate_node(text);
return text;
}
set_hydrate_node(next_sibling);
return next_sibling;
}
/**
* @template {Node} N
* @param {N} node
* @returns {void}
*/
export function clear_text_content(node) {
node.textContent = '';
}
/**
* Returns `true` if we're updating the current block, for example `condition` in
* an `{#if condition}` block just changed. In this case, the branch should be
* appended (or removed) at the same time as other updates within the
* current `<svelte:boundary>`
*/
export function should_defer_append() {
if (!async_mode_flag) return false;
if (eager_block_effects !== null) return false;
var flags = /** @type {Effect} */ (active_effect).f;
return (flags & EFFECT_RAN) !== 0;
}
/**
*
* @param {string} tag
* @param {string} [namespace]
* @param {string} [is]
* @returns
*/
export function create_element(tag, namespace, is) {
let options = is ? { is } : undefined;
if (namespace) {
return document.createElementNS(namespace, tag, options);
}
return document.createElement(tag, options);
}
export function create_fragment() {
return document.createDocumentFragment();
}
/**
* @param {string} data
* @returns
*/
export function create_comment(data = '') {
return document.createComment(data);
}
/**
* @param {Element} element
* @param {string} key
* @param {string} value
* @returns
*/
export function set_attribute(element, key, value = '') {
if (key.startsWith('xlink:')) {
element.setAttributeNS('http://www.w3.org/1999/xlink', key, value);
return;
}
return element.setAttribute(key, value);
}
+6
View File
@@ -0,0 +1,6 @@
/** @param {string} html */
export function create_fragment_from_html(html) {
var elem = document.createElement('template');
elem.innerHTML = html.replaceAll('<!>', '<!---->'); // XHTML compliance
return elem.content;
}
+42
View File
@@ -0,0 +1,42 @@
import { run_all } from '../../shared/utils.js';
import { is_flushing_sync } from '../reactivity/batch.js';
/** @type {Array<() => void>} */
let micro_tasks = [];
function run_micro_tasks() {
var tasks = micro_tasks;
micro_tasks = [];
run_all(tasks);
}
/**
* @param {() => void} fn
*/
export function queue_micro_task(fn) {
if (micro_tasks.length === 0 && !is_flushing_sync) {
var tasks = micro_tasks;
queueMicrotask(() => {
// If this is false, a flushSync happened in the meantime. Do _not_ run new scheduled microtasks in that case
// as the ordering of microtasks would be broken at that point - consider this case:
// - queue_micro_task schedules microtask A to flush task X
// - synchronously after, flushSync runs, processing task X
// - synchronously after, some other microtask B is scheduled, but not through queue_micro_task but for example a Promise.resolve() in user code
// - synchronously after, queue_micro_task schedules microtask C to flush task Y
// - one tick later, microtask A now resolves, flushing task Y before microtask B, which is incorrect
// This if check prevents that race condition (that realistically will only happen in tests)
if (tasks === micro_tasks) run_micro_tasks();
});
}
micro_tasks.push(fn);
}
/**
* Synchronously run any queued tasks.
*/
export function flush_tasks() {
while (micro_tasks.length > 0) {
run_micro_tasks();
}
}
+388
View File
@@ -0,0 +1,388 @@
/** @import { Effect, EffectNodes, TemplateNode } from '#client' */
/** @import { TemplateStructure } from './types' */
import { hydrate_next, hydrate_node, hydrating, set_hydrate_node } from './hydration.js';
import {
create_text,
get_first_child,
is_firefox,
create_element,
create_fragment,
create_comment,
set_attribute
} from './operations.js';
import { create_fragment_from_html } from './reconciler.js';
import { active_effect } from '../runtime.js';
import {
NAMESPACE_MATHML,
NAMESPACE_SVG,
TEMPLATE_FRAGMENT,
TEMPLATE_USE_IMPORT_NODE,
TEMPLATE_USE_MATHML,
TEMPLATE_USE_SVG
} from '../../../constants.js';
import { COMMENT_NODE, DOCUMENT_FRAGMENT_NODE, EFFECT_RAN, TEXT_NODE } from '#client/constants';
/**
* @param {TemplateNode} start
* @param {TemplateNode | null} end
*/
export function assign_nodes(start, end) {
var effect = /** @type {Effect} */ (active_effect);
if (effect.nodes === null) {
effect.nodes = { start, end, a: null, t: null };
}
}
/**
* @param {string} content
* @param {number} flags
* @returns {() => Node | Node[]}
*/
/*#__NO_SIDE_EFFECTS__*/
export function from_html(content, flags) {
var is_fragment = (flags & TEMPLATE_FRAGMENT) !== 0;
var use_import_node = (flags & TEMPLATE_USE_IMPORT_NODE) !== 0;
/** @type {Node} */
var node;
/**
* Whether or not the first item is a text/element node. If not, we need to
* create an additional comment node to act as `effect.nodes.start`
*/
var has_start = !content.startsWith('<!>');
return () => {
if (hydrating) {
assign_nodes(hydrate_node, null);
return hydrate_node;
}
if (node === undefined) {
node = create_fragment_from_html(has_start ? content : '<!>' + content);
if (!is_fragment) node = /** @type {TemplateNode} */ (get_first_child(node));
}
var clone = /** @type {TemplateNode} */ (
use_import_node || is_firefox ? document.importNode(node, true) : node.cloneNode(true)
);
if (is_fragment) {
var start = /** @type {TemplateNode} */ (get_first_child(clone));
var end = /** @type {TemplateNode} */ (clone.lastChild);
assign_nodes(start, end);
} else {
assign_nodes(clone, clone);
}
return clone;
};
}
/**
* @param {string} content
* @param {number} flags
* @param {'svg' | 'math'} ns
* @returns {() => Node | Node[]}
*/
/*#__NO_SIDE_EFFECTS__*/
function from_namespace(content, flags, ns = 'svg') {
/**
* Whether or not the first item is a text/element node. If not, we need to
* create an additional comment node to act as `effect.nodes.start`
*/
var has_start = !content.startsWith('<!>');
var is_fragment = (flags & TEMPLATE_FRAGMENT) !== 0;
var wrapped = `<${ns}>${has_start ? content : '<!>' + content}</${ns}>`;
/** @type {Element | DocumentFragment} */
var node;
return () => {
if (hydrating) {
assign_nodes(hydrate_node, null);
return hydrate_node;
}
if (!node) {
var fragment = /** @type {DocumentFragment} */ (create_fragment_from_html(wrapped));
var root = /** @type {Element} */ (get_first_child(fragment));
if (is_fragment) {
node = document.createDocumentFragment();
while (get_first_child(root)) {
node.appendChild(/** @type {TemplateNode} */ (get_first_child(root)));
}
} else {
node = /** @type {Element} */ (get_first_child(root));
}
}
var clone = /** @type {TemplateNode} */ (node.cloneNode(true));
if (is_fragment) {
var start = /** @type {TemplateNode} */ (get_first_child(clone));
var end = /** @type {TemplateNode} */ (clone.lastChild);
assign_nodes(start, end);
} else {
assign_nodes(clone, clone);
}
return clone;
};
}
/**
* @param {string} content
* @param {number} flags
*/
/*#__NO_SIDE_EFFECTS__*/
export function from_svg(content, flags) {
return from_namespace(content, flags, 'svg');
}
/**
* @param {string} content
* @param {number} flags
*/
/*#__NO_SIDE_EFFECTS__*/
export function from_mathml(content, flags) {
return from_namespace(content, flags, 'math');
}
/**
* @param {TemplateStructure[]} structure
* @param {typeof NAMESPACE_SVG | typeof NAMESPACE_MATHML | undefined} [ns]
*/
function fragment_from_tree(structure, ns) {
var fragment = create_fragment();
for (var item of structure) {
if (typeof item === 'string') {
fragment.append(create_text(item));
continue;
}
// if `preserveComments === true`, comments are represented as `['// <data>']`
if (item === undefined || item[0][0] === '/') {
fragment.append(create_comment(item ? item[0].slice(3) : ''));
continue;
}
const [name, attributes, ...children] = item;
const namespace = name === 'svg' ? NAMESPACE_SVG : name === 'math' ? NAMESPACE_MATHML : ns;
var element = create_element(name, namespace, attributes?.is);
for (var key in attributes) {
set_attribute(element, key, attributes[key]);
}
if (children.length > 0) {
var target =
element.tagName === 'TEMPLATE'
? /** @type {HTMLTemplateElement} */ (element).content
: element;
target.append(
fragment_from_tree(children, element.tagName === 'foreignObject' ? undefined : namespace)
);
}
fragment.append(element);
}
return fragment;
}
/**
* @param {TemplateStructure[]} structure
* @param {number} flags
* @returns {() => Node | Node[]}
*/
/*#__NO_SIDE_EFFECTS__*/
export function from_tree(structure, flags) {
var is_fragment = (flags & TEMPLATE_FRAGMENT) !== 0;
var use_import_node = (flags & TEMPLATE_USE_IMPORT_NODE) !== 0;
/** @type {Node} */
var node;
return () => {
if (hydrating) {
assign_nodes(hydrate_node, null);
return hydrate_node;
}
if (node === undefined) {
const ns =
(flags & TEMPLATE_USE_SVG) !== 0
? NAMESPACE_SVG
: (flags & TEMPLATE_USE_MATHML) !== 0
? NAMESPACE_MATHML
: undefined;
node = fragment_from_tree(structure, ns);
if (!is_fragment) node = /** @type {TemplateNode} */ (get_first_child(node));
}
var clone = /** @type {TemplateNode} */ (
use_import_node || is_firefox ? document.importNode(node, true) : node.cloneNode(true)
);
if (is_fragment) {
var start = /** @type {TemplateNode} */ (get_first_child(clone));
var end = /** @type {TemplateNode} */ (clone.lastChild);
assign_nodes(start, end);
} else {
assign_nodes(clone, clone);
}
return clone;
};
}
/**
* @param {() => Element | DocumentFragment} fn
*/
export function with_script(fn) {
return () => run_scripts(fn());
}
/**
* Creating a document fragment from HTML that contains script tags will not execute
* the scripts. We need to replace the script tags with new ones so that they are executed.
* @param {Element | DocumentFragment} node
* @returns {Node | Node[]}
*/
function run_scripts(node) {
// scripts were SSR'd, in which case they will run
if (hydrating) return node;
const is_fragment = node.nodeType === DOCUMENT_FRAGMENT_NODE;
const scripts =
/** @type {HTMLElement} */ (node).tagName === 'SCRIPT'
? [/** @type {HTMLScriptElement} */ (node)]
: node.querySelectorAll('script');
const effect = /** @type {Effect & { nodes: EffectNodes }} */ (active_effect);
for (const script of scripts) {
const clone = document.createElement('script');
for (var attribute of script.attributes) {
clone.setAttribute(attribute.name, attribute.value);
}
clone.textContent = script.textContent;
// The script has changed - if it's at the edges, the effect now points at dead nodes
if (is_fragment ? node.firstChild === script : node === script) {
effect.nodes.start = clone;
}
if (is_fragment ? node.lastChild === script : node === script) {
effect.nodes.end = clone;
}
script.replaceWith(clone);
}
return node;
}
/**
* Don't mark this as side-effect-free, hydration needs to walk all nodes
* @param {any} value
*/
export function text(value = '') {
if (!hydrating) {
var t = create_text(value + '');
assign_nodes(t, t);
return t;
}
var node = hydrate_node;
if (node.nodeType !== TEXT_NODE) {
// if an {expression} is empty during SSR, we need to insert an empty text node
node.before((node = create_text()));
set_hydrate_node(node);
}
assign_nodes(node, node);
return node;
}
/**
* @returns {TemplateNode | DocumentFragment}
*/
export function comment() {
// we're not delegating to `template` here for performance reasons
if (hydrating) {
assign_nodes(hydrate_node, null);
return hydrate_node;
}
var frag = document.createDocumentFragment();
var start = document.createComment('');
var anchor = create_text();
frag.append(start, anchor);
assign_nodes(start, anchor);
return frag;
}
/**
* Assign the created (or in hydration mode, traversed) dom elements to the current block
* and insert the elements into the dom (in client mode).
* @param {Text | Comment | Element} anchor
* @param {DocumentFragment | Element} dom
*/
export function append(anchor, dom) {
if (hydrating) {
var effect = /** @type {Effect & { nodes: EffectNodes }} */ (active_effect);
// When hydrating and outer component and an inner component is async, i.e. blocked on a promise,
// then by the time the inner resolves we have already advanced to the end of the hydrated nodes
// of the parent component. Check for defined for that reason to avoid rewinding the parent's end marker.
if ((effect.f & EFFECT_RAN) === 0 || effect.nodes.end === null) {
effect.nodes.end = hydrate_node;
}
hydrate_next();
return;
}
if (anchor === null) {
// edge case — void `<svelte:element>` with content
return;
}
anchor.before(/** @type {Node} */ (dom));
}
/**
* Create (or hydrate) an unique UID for the component instance.
*/
export function props_id() {
if (
hydrating &&
hydrate_node &&
hydrate_node.nodeType === COMMENT_NODE &&
hydrate_node.textContent?.startsWith(`$`)
) {
const id = hydrate_node.textContent.substring(1);
hydrate_next();
return id;
}
// @ts-expect-error This way we ensure the id is unique even across Svelte runtimes
(window.__svelte ??= {}).uid ??= 1;
// @ts-expect-error
return `c${window.__svelte.uid++}`;
}
+116
View File
@@ -0,0 +1,116 @@
/** @import { Derived, Effect } from '#client' */
/** @import { Boundary } from './dom/blocks/boundary.js' */
import { DEV } from 'esm-env';
import { FILENAME } from '../../constants.js';
import { is_firefox } from './dom/operations.js';
import { ERROR_VALUE, BOUNDARY_EFFECT, EFFECT_RAN } from './constants.js';
import { define_property, get_descriptor } from '../shared/utils.js';
import { active_effect, active_reaction } from './runtime.js';
const adjustments = new WeakMap();
/**
* @param {unknown} error
*/
export function handle_error(error) {
var effect = active_effect;
// for unowned deriveds, don't throw until we read the value
if (effect === null) {
/** @type {Derived} */ (active_reaction).f |= ERROR_VALUE;
return error;
}
if (DEV && error instanceof Error && !adjustments.has(error)) {
adjustments.set(error, get_adjustments(error, effect));
}
if ((effect.f & EFFECT_RAN) === 0) {
// if the error occurred while creating this subtree, we let it
// bubble up until it hits a boundary that can handle it
if ((effect.f & BOUNDARY_EFFECT) === 0) {
if (DEV && !effect.parent && error instanceof Error) {
apply_adjustments(error);
}
throw error;
}
/** @type {Boundary} */ (effect.b).error(error);
} else {
// otherwise we bubble up the effect tree ourselves
invoke_error_boundary(error, effect);
}
}
/**
* @param {unknown} error
* @param {Effect | null} effect
*/
export function invoke_error_boundary(error, effect) {
while (effect !== null) {
if ((effect.f & BOUNDARY_EFFECT) !== 0) {
try {
/** @type {Boundary} */ (effect.b).error(error);
return;
} catch (e) {
error = e;
}
}
effect = effect.parent;
}
if (DEV && error instanceof Error) {
apply_adjustments(error);
}
throw error;
}
/**
* Add useful information to the error message/stack in development
* @param {Error} error
* @param {Effect} effect
*/
function get_adjustments(error, effect) {
const message_descriptor = get_descriptor(error, 'message');
// if the message was already changed and it's not configurable we can't change it
// or it will throw a different error swallowing the original error
if (message_descriptor && !message_descriptor.configurable) return;
var indent = is_firefox ? ' ' : '\t';
var component_stack = `\n${indent}in ${effect.fn?.name || '<unknown>'}`;
var context = effect.ctx;
while (context !== null) {
component_stack += `\n${indent}in ${context.function?.[FILENAME].split('/').pop()}`;
context = context.p;
}
return {
message: error.message + `\n${component_stack}\n`,
stack: error.stack
?.split('\n')
.filter((line) => !line.includes('svelte/src/internal'))
.join('\n')
};
}
/**
* @param {Error} error
*/
function apply_adjustments(error) {
const adjusted = adjustments.get(error);
if (adjusted) {
define_property(error, 'message', {
value: adjusted.message
});
define_property(error, 'stack', {
value: adjusted.stack
});
}
}
+491
View File
@@ -0,0 +1,491 @@
/* This file is generated by scripts/process-messages/index.js. Do not edit! */
import { DEV } from 'esm-env';
export * from '../shared/errors.js';
/**
* Cannot create a `$derived(...)` with an `await` expression outside of an effect tree
* @returns {never}
*/
export function async_derived_orphan() {
if (DEV) {
const error = new Error(`async_derived_orphan\nCannot create a \`$derived(...)\` with an \`await\` expression outside of an effect tree\nhttps://svelte.dev/e/async_derived_orphan`);
error.name = 'Svelte error';
throw error;
} else {
throw new Error(`https://svelte.dev/e/async_derived_orphan`);
}
}
/**
* Using `bind:value` together with a checkbox input is not allowed. Use `bind:checked` instead
* @returns {never}
*/
export function bind_invalid_checkbox_value() {
if (DEV) {
const error = new Error(`bind_invalid_checkbox_value\nUsing \`bind:value\` together with a checkbox input is not allowed. Use \`bind:checked\` instead\nhttps://svelte.dev/e/bind_invalid_checkbox_value`);
error.name = 'Svelte error';
throw error;
} else {
throw new Error(`https://svelte.dev/e/bind_invalid_checkbox_value`);
}
}
/**
* Component %component% has an export named `%key%` that a consumer component is trying to access using `bind:%key%`, which is disallowed. Instead, use `bind:this` (e.g. `<%name% bind:this={component} />`) and then access the property on the bound component instance (e.g. `component.%key%`)
* @param {string} component
* @param {string} key
* @param {string} name
* @returns {never}
*/
export function bind_invalid_export(component, key, name) {
if (DEV) {
const error = new Error(`bind_invalid_export\nComponent ${component} has an export named \`${key}\` that a consumer component is trying to access using \`bind:${key}\`, which is disallowed. Instead, use \`bind:this\` (e.g. \`<${name} bind:this={component} />\`) and then access the property on the bound component instance (e.g. \`component.${key}\`)\nhttps://svelte.dev/e/bind_invalid_export`);
error.name = 'Svelte error';
throw error;
} else {
throw new Error(`https://svelte.dev/e/bind_invalid_export`);
}
}
/**
* A component is attempting to bind to a non-bindable property `%key%` belonging to %component% (i.e. `<%name% bind:%key%={...}>`). To mark a property as bindable: `let { %key% = $bindable() } = $props()`
* @param {string} key
* @param {string} component
* @param {string} name
* @returns {never}
*/
export function bind_not_bindable(key, component, name) {
if (DEV) {
const error = new Error(`bind_not_bindable\nA component is attempting to bind to a non-bindable property \`${key}\` belonging to ${component} (i.e. \`<${name} bind:${key}={...}>\`). To mark a property as bindable: \`let { ${key} = $bindable() } = $props()\`\nhttps://svelte.dev/e/bind_not_bindable`);
error.name = 'Svelte error';
throw error;
} else {
throw new Error(`https://svelte.dev/e/bind_not_bindable`);
}
}
/**
* Calling `%method%` on a component instance (of %component%) is no longer valid in Svelte 5
* @param {string} method
* @param {string} component
* @returns {never}
*/
export function component_api_changed(method, component) {
if (DEV) {
const error = new Error(`component_api_changed\nCalling \`${method}\` on a component instance (of ${component}) is no longer valid in Svelte 5\nhttps://svelte.dev/e/component_api_changed`);
error.name = 'Svelte error';
throw error;
} else {
throw new Error(`https://svelte.dev/e/component_api_changed`);
}
}
/**
* Attempted to instantiate %component% with `new %name%`, which is no longer valid in Svelte 5. If this component is not under your control, set the `compatibility.componentApi` compiler option to `4` to keep it working.
* @param {string} component
* @param {string} name
* @returns {never}
*/
export function component_api_invalid_new(component, name) {
if (DEV) {
const error = new Error(`component_api_invalid_new\nAttempted to instantiate ${component} with \`new ${name}\`, which is no longer valid in Svelte 5. If this component is not under your control, set the \`compatibility.componentApi\` compiler option to \`4\` to keep it working.\nhttps://svelte.dev/e/component_api_invalid_new`);
error.name = 'Svelte error';
throw error;
} else {
throw new Error(`https://svelte.dev/e/component_api_invalid_new`);
}
}
/**
* A derived value cannot reference itself recursively
* @returns {never}
*/
export function derived_references_self() {
if (DEV) {
const error = new Error(`derived_references_self\nA derived value cannot reference itself recursively\nhttps://svelte.dev/e/derived_references_self`);
error.name = 'Svelte error';
throw error;
} else {
throw new Error(`https://svelte.dev/e/derived_references_self`);
}
}
/**
* Keyed each block has duplicate key `%value%` at indexes %a% and %b%
* @param {string} a
* @param {string} b
* @param {string | undefined | null} [value]
* @returns {never}
*/
export function each_key_duplicate(a, b, value) {
if (DEV) {
const error = new Error(`each_key_duplicate\n${value
? `Keyed each block has duplicate key \`${value}\` at indexes ${a} and ${b}`
: `Keyed each block has duplicate key at indexes ${a} and ${b}`}\nhttps://svelte.dev/e/each_key_duplicate`);
error.name = 'Svelte error';
throw error;
} else {
throw new Error(`https://svelte.dev/e/each_key_duplicate`);
}
}
/**
* `%rune%` cannot be used inside an effect cleanup function
* @param {string} rune
* @returns {never}
*/
export function effect_in_teardown(rune) {
if (DEV) {
const error = new Error(`effect_in_teardown\n\`${rune}\` cannot be used inside an effect cleanup function\nhttps://svelte.dev/e/effect_in_teardown`);
error.name = 'Svelte error';
throw error;
} else {
throw new Error(`https://svelte.dev/e/effect_in_teardown`);
}
}
/**
* Effect cannot be created inside a `$derived` value that was not itself created inside an effect
* @returns {never}
*/
export function effect_in_unowned_derived() {
if (DEV) {
const error = new Error(`effect_in_unowned_derived\nEffect cannot be created inside a \`$derived\` value that was not itself created inside an effect\nhttps://svelte.dev/e/effect_in_unowned_derived`);
error.name = 'Svelte error';
throw error;
} else {
throw new Error(`https://svelte.dev/e/effect_in_unowned_derived`);
}
}
/**
* `%rune%` can only be used inside an effect (e.g. during component initialisation)
* @param {string} rune
* @returns {never}
*/
export function effect_orphan(rune) {
if (DEV) {
const error = new Error(`effect_orphan\n\`${rune}\` can only be used inside an effect (e.g. during component initialisation)\nhttps://svelte.dev/e/effect_orphan`);
error.name = 'Svelte error';
throw error;
} else {
throw new Error(`https://svelte.dev/e/effect_orphan`);
}
}
/**
* `$effect.pending()` can only be called inside an effect or derived
* @returns {never}
*/
export function effect_pending_outside_reaction() {
if (DEV) {
const error = new Error(`effect_pending_outside_reaction\n\`$effect.pending()\` can only be called inside an effect or derived\nhttps://svelte.dev/e/effect_pending_outside_reaction`);
error.name = 'Svelte error';
throw error;
} else {
throw new Error(`https://svelte.dev/e/effect_pending_outside_reaction`);
}
}
/**
* Maximum update depth exceeded. This typically indicates that an effect reads and writes the same piece of state
* @returns {never}
*/
export function effect_update_depth_exceeded() {
if (DEV) {
const error = new Error(`effect_update_depth_exceeded\nMaximum update depth exceeded. This typically indicates that an effect reads and writes the same piece of state\nhttps://svelte.dev/e/effect_update_depth_exceeded`);
error.name = 'Svelte error';
throw error;
} else {
throw new Error(`https://svelte.dev/e/effect_update_depth_exceeded`);
}
}
/**
* Cannot use `flushSync` inside an effect
* @returns {never}
*/
export function flush_sync_in_effect() {
if (DEV) {
const error = new Error(`flush_sync_in_effect\nCannot use \`flushSync\` inside an effect\nhttps://svelte.dev/e/flush_sync_in_effect`);
error.name = 'Svelte error';
throw error;
} else {
throw new Error(`https://svelte.dev/e/flush_sync_in_effect`);
}
}
/**
* Cannot commit a fork that was already discarded
* @returns {never}
*/
export function fork_discarded() {
if (DEV) {
const error = new Error(`fork_discarded\nCannot commit a fork that was already discarded\nhttps://svelte.dev/e/fork_discarded`);
error.name = 'Svelte error';
throw error;
} else {
throw new Error(`https://svelte.dev/e/fork_discarded`);
}
}
/**
* Cannot create a fork inside an effect or when state changes are pending
* @returns {never}
*/
export function fork_timing() {
if (DEV) {
const error = new Error(`fork_timing\nCannot create a fork inside an effect or when state changes are pending\nhttps://svelte.dev/e/fork_timing`);
error.name = 'Svelte error';
throw error;
} else {
throw new Error(`https://svelte.dev/e/fork_timing`);
}
}
/**
* `getAbortSignal()` can only be called inside an effect or derived
* @returns {never}
*/
export function get_abort_signal_outside_reaction() {
if (DEV) {
const error = new Error(`get_abort_signal_outside_reaction\n\`getAbortSignal()\` can only be called inside an effect or derived\nhttps://svelte.dev/e/get_abort_signal_outside_reaction`);
error.name = 'Svelte error';
throw error;
} else {
throw new Error(`https://svelte.dev/e/get_abort_signal_outside_reaction`);
}
}
/**
* Expected to find a hydratable with key `%key%` during hydration, but did not.
* @param {string} key
* @returns {never}
*/
export function hydratable_missing_but_required(key) {
if (DEV) {
const error = new Error(`hydratable_missing_but_required\nExpected to find a hydratable with key \`${key}\` during hydration, but did not.\nhttps://svelte.dev/e/hydratable_missing_but_required`);
error.name = 'Svelte error';
throw error;
} else {
throw new Error(`https://svelte.dev/e/hydratable_missing_but_required`);
}
}
/**
* Failed to hydrate the application
* @returns {never}
*/
export function hydration_failed() {
if (DEV) {
const error = new Error(`hydration_failed\nFailed to hydrate the application\nhttps://svelte.dev/e/hydration_failed`);
error.name = 'Svelte error';
throw error;
} else {
throw new Error(`https://svelte.dev/e/hydration_failed`);
}
}
/**
* Could not `{@render}` snippet due to the expression being `null` or `undefined`. Consider using optional chaining `{@render snippet?.()}`
* @returns {never}
*/
export function invalid_snippet() {
if (DEV) {
const error = new Error(`invalid_snippet\nCould not \`{@render}\` snippet due to the expression being \`null\` or \`undefined\`. Consider using optional chaining \`{@render snippet?.()}\`\nhttps://svelte.dev/e/invalid_snippet`);
error.name = 'Svelte error';
throw error;
} else {
throw new Error(`https://svelte.dev/e/invalid_snippet`);
}
}
/**
* `%name%(...)` cannot be used in runes mode
* @param {string} name
* @returns {never}
*/
export function lifecycle_legacy_only(name) {
if (DEV) {
const error = new Error(`lifecycle_legacy_only\n\`${name}(...)\` cannot be used in runes mode\nhttps://svelte.dev/e/lifecycle_legacy_only`);
error.name = 'Svelte error';
throw error;
} else {
throw new Error(`https://svelte.dev/e/lifecycle_legacy_only`);
}
}
/**
* Cannot do `bind:%key%={undefined}` when `%key%` has a fallback value
* @param {string} key
* @returns {never}
*/
export function props_invalid_value(key) {
if (DEV) {
const error = new Error(`props_invalid_value\nCannot do \`bind:${key}={undefined}\` when \`${key}\` has a fallback value\nhttps://svelte.dev/e/props_invalid_value`);
error.name = 'Svelte error';
throw error;
} else {
throw new Error(`https://svelte.dev/e/props_invalid_value`);
}
}
/**
* Rest element properties of `$props()` such as `%property%` are readonly
* @param {string} property
* @returns {never}
*/
export function props_rest_readonly(property) {
if (DEV) {
const error = new Error(`props_rest_readonly\nRest element properties of \`$props()\` such as \`${property}\` are readonly\nhttps://svelte.dev/e/props_rest_readonly`);
error.name = 'Svelte error';
throw error;
} else {
throw new Error(`https://svelte.dev/e/props_rest_readonly`);
}
}
/**
* The `%rune%` rune is only available inside `.svelte` and `.svelte.js/ts` files
* @param {string} rune
* @returns {never}
*/
export function rune_outside_svelte(rune) {
if (DEV) {
const error = new Error(`rune_outside_svelte\nThe \`${rune}\` rune is only available inside \`.svelte\` and \`.svelte.js/ts\` files\nhttps://svelte.dev/e/rune_outside_svelte`);
error.name = 'Svelte error';
throw error;
} else {
throw new Error(`https://svelte.dev/e/rune_outside_svelte`);
}
}
/**
* `setContext` must be called when a component first initializes, not in a subsequent effect or after an `await` expression
* @returns {never}
*/
export function set_context_after_init() {
if (DEV) {
const error = new Error(`set_context_after_init\n\`setContext\` must be called when a component first initializes, not in a subsequent effect or after an \`await\` expression\nhttps://svelte.dev/e/set_context_after_init`);
error.name = 'Svelte error';
throw error;
} else {
throw new Error(`https://svelte.dev/e/set_context_after_init`);
}
}
/**
* Property descriptors defined on `$state` objects must contain `value` and always be `enumerable`, `configurable` and `writable`.
* @returns {never}
*/
export function state_descriptors_fixed() {
if (DEV) {
const error = new Error(`state_descriptors_fixed\nProperty descriptors defined on \`$state\` objects must contain \`value\` and always be \`enumerable\`, \`configurable\` and \`writable\`.\nhttps://svelte.dev/e/state_descriptors_fixed`);
error.name = 'Svelte error';
throw error;
} else {
throw new Error(`https://svelte.dev/e/state_descriptors_fixed`);
}
}
/**
* Cannot set prototype of `$state` object
* @returns {never}
*/
export function state_prototype_fixed() {
if (DEV) {
const error = new Error(`state_prototype_fixed\nCannot set prototype of \`$state\` object\nhttps://svelte.dev/e/state_prototype_fixed`);
error.name = 'Svelte error';
throw error;
} else {
throw new Error(`https://svelte.dev/e/state_prototype_fixed`);
}
}
/**
* Updating state inside `$derived(...)`, `$inspect(...)` or a template expression is forbidden. If the value should not be reactive, declare it without `$state`
* @returns {never}
*/
export function state_unsafe_mutation() {
if (DEV) {
const error = new Error(`state_unsafe_mutation\nUpdating state inside \`$derived(...)\`, \`$inspect(...)\` or a template expression is forbidden. If the value should not be reactive, declare it without \`$state\`\nhttps://svelte.dev/e/state_unsafe_mutation`);
error.name = 'Svelte error';
throw error;
} else {
throw new Error(`https://svelte.dev/e/state_unsafe_mutation`);
}
}
/**
* A `<svelte:boundary>` `reset` function cannot be called while an error is still being handled
* @returns {never}
*/
export function svelte_boundary_reset_onerror() {
if (DEV) {
const error = new Error(`svelte_boundary_reset_onerror\nA \`<svelte:boundary>\` \`reset\` function cannot be called while an error is still being handled\nhttps://svelte.dev/e/svelte_boundary_reset_onerror`);
error.name = 'Svelte error';
throw error;
} else {
throw new Error(`https://svelte.dev/e/svelte_boundary_reset_onerror`);
}
}
+33
View File
@@ -0,0 +1,33 @@
import { async_mode_flag } from '../flags/index.js';
import { hydrating } from './dom/hydration.js';
import * as w from './warnings.js';
import * as e from './errors.js';
import { DEV } from 'esm-env';
/**
* @template T
* @param {string} key
* @param {() => T} fn
* @returns {T}
*/
export function hydratable(key, fn) {
if (!async_mode_flag) {
e.experimental_async_required('hydratable');
}
if (hydrating) {
const store = window.__svelte?.h;
if (store?.has(key)) {
return /** @type {T} */ (store.get(key));
}
if (DEV) {
e.hydratable_missing_but_required(key);
} else {
w.hydratable_missing_but_expected(key);
}
}
return fn();
}
+183
View File
@@ -0,0 +1,183 @@
export { createAttachmentKey as attachment } from '../../attachments/index.js';
export { FILENAME, HMR, NAMESPACE_SVG } from '../../constants.js';
export { push, pop, add_svelte_meta } from './context.js';
export { assign, assign_and, assign_or, assign_nullish } from './dev/assign.js';
export { cleanup_styles } from './dev/css.js';
export { add_locations } from './dev/elements.js';
export { hmr } from './dev/hmr.js';
export { create_ownership_validator } from './dev/ownership.js';
export { check_target, legacy_api } from './dev/legacy.js';
export { trace, tag, tag_proxy } from './dev/tracing.js';
export { inspect } from './dev/inspect.js';
export { async } from './dom/blocks/async.js';
export { validate_snippet_args } from './dev/validation.js';
export { await_block as await } from './dom/blocks/await.js';
export { if_block as if } from './dom/blocks/if.js';
export { key } from './dom/blocks/key.js';
export { css_props } from './dom/blocks/css-props.js';
export { index, each } from './dom/blocks/each.js';
export { html } from './dom/blocks/html.js';
export { sanitize_slots, slot } from './dom/blocks/slot.js';
export { snippet, wrap_snippet } from './dom/blocks/snippet.js';
export { component } from './dom/blocks/svelte-component.js';
export { element } from './dom/blocks/svelte-element.js';
export { head } from './dom/blocks/svelte-head.js';
export { append_styles } from './dom/css.js';
export { action } from './dom/elements/actions.js';
export { attach } from './dom/elements/attachments.js';
export {
remove_input_defaults,
set_attribute,
attribute_effect,
set_custom_element_data,
set_xlink_attribute,
set_value,
set_checked,
set_selected,
set_default_checked,
set_default_value,
CLASS,
STYLE
} from './dom/elements/attributes.js';
export { set_class } from './dom/elements/class.js';
export { apply, event, delegate, replay_events } from './dom/elements/events.js';
export { autofocus, remove_textarea_child } from './dom/elements/misc.js';
export { customizable_select, selectedcontent } from './dom/elements/customizable-select.js';
export { set_style } from './dom/elements/style.js';
export { animation, transition } from './dom/elements/transitions.js';
export { bind_active_element } from './dom/elements/bindings/document.js';
export { bind_checked, bind_files, bind_group, bind_value } from './dom/elements/bindings/input.js';
export {
bind_buffered,
bind_current_time,
bind_ended,
bind_muted,
bind_paused,
bind_playback_rate,
bind_played,
bind_ready_state,
bind_seekable,
bind_seeking,
bind_volume
} from './dom/elements/bindings/media.js';
export { bind_online } from './dom/elements/bindings/navigator.js';
export { bind_prop } from './dom/elements/bindings/props.js';
export { bind_select_value, init_select, select_option } from './dom/elements/bindings/select.js';
export { bind_element_size, bind_resize_observer } from './dom/elements/bindings/size.js';
export { bind_this } from './dom/elements/bindings/this.js';
export {
bind_content_editable,
bind_property,
bind_focused
} from './dom/elements/bindings/universal.js';
export { bind_window_scroll, bind_window_size } from './dom/elements/bindings/window.js';
export { hydrate_template, next, reset } from './dom/hydration.js';
export {
once,
preventDefault,
self,
stopImmediatePropagation,
stopPropagation,
trusted
} from './dom/legacy/event-modifiers.js';
export { init } from './dom/legacy/lifecycle.js';
export {
add_legacy_event_listener,
bubble_event,
reactive_import,
update_legacy_props
} from './dom/legacy/misc.js';
export {
append,
comment,
from_html,
from_mathml,
from_svg,
from_tree,
text,
props_id,
with_script
} from './dom/template.js';
export {
for_await_track_reactivity_loss,
run,
save,
track_reactivity_loss,
run_after_blockers
} from './reactivity/async.js';
export { eager, flushSync as flush } from './reactivity/batch.js';
export {
async_derived,
user_derived as derived,
derived_safe_equal
} from './reactivity/deriveds.js';
export {
aborted,
effect_tracking,
effect_root,
legacy_pre_effect,
legacy_pre_effect_reset,
render_effect,
template_effect,
deferred_template_effect,
effect,
user_effect,
user_pre_effect
} from './reactivity/effects.js';
export { mutable_source, mutate, set, state, update, update_pre } from './reactivity/sources.js';
export {
prop,
rest_props,
legacy_rest_props,
spread_props,
update_pre_prop,
update_prop
} from './reactivity/props.js';
export {
invalidate_store,
store_mutate,
setup_stores,
store_get,
store_set,
store_unsub,
update_pre_store,
update_store,
mark_store_binding
} from './reactivity/store.js';
export { boundary, pending } from './dom/blocks/boundary.js';
export { invalidate_inner_signals } from './legacy.js';
export { set_text } from './render.js';
export {
get,
safe_get,
tick,
untrack,
exclude_from_object,
deep_read,
deep_read_state,
active_effect
} from './runtime.js';
export { validate_binding, validate_each_keys } from './validate.js';
export { raf } from './timing.js';
export { proxy } from './proxy.js';
export { create_custom_element } from './dom/elements/custom-element.js';
export {
child,
first_child,
sibling,
$window as window,
$document as document
} from './dom/operations.js';
export { attr, clsx } from '../shared/attributes.js';
export { snapshot } from '../shared/clone.js';
export { noop, fallback, to_array } from '../shared/utils.js';
export {
invalid_default_snippet,
validate_dynamic_element_tag,
validate_store,
validate_void_dynamic_element,
prevent_snippet_stringification
} from '../shared/validate.js';
export { strict_equals, equals } from './dev/equality.js';
export { log_if_contains_state } from './dev/console-log.js';
export { invoke_error_boundary } from './error-handling.js';
+46
View File
@@ -0,0 +1,46 @@
/** @import { Value } from '#client' */
import { internal_set } from './reactivity/sources.js';
import { untrack } from './runtime.js';
/**
* @type {Set<Value> | null}
* @deprecated
*/
export let captured_signals = null;
/**
* Capture an array of all the signals that are read when `fn` is called
* @template T
* @param {() => T} fn
*/
function capture_signals(fn) {
var previous_captured_signals = captured_signals;
try {
captured_signals = new Set();
untrack(fn);
if (previous_captured_signals !== null) {
for (var signal of captured_signals) {
previous_captured_signals.add(signal);
}
}
return captured_signals;
} finally {
captured_signals = previous_captured_signals;
}
}
/**
* Invokes a function and captures all signals that are read during the invocation,
* then invalidates them.
* @param {() => any} fn
* @deprecated
*/
export function invalidate_inner_signals(fn) {
for (var signal of capture_signals(fn)) {
internal_set(signal, signal.v);
}
}
+48
View File
@@ -0,0 +1,48 @@
/** @import { TaskCallback, Task, TaskEntry } from '#client' */
import { raf } from './timing.js';
// TODO move this into timing.js where it probably belongs
/**
* @returns {void}
*/
function run_tasks() {
// use `raf.now()` instead of the `requestAnimationFrame` callback argument, because
// otherwise things can get wonky https://github.com/sveltejs/svelte/pull/14541
const now = raf.now();
raf.tasks.forEach((task) => {
if (!task.c(now)) {
raf.tasks.delete(task);
task.f();
}
});
if (raf.tasks.size !== 0) {
raf.tick(run_tasks);
}
}
/**
* Creates a new task that runs on each raf frame
* until it returns a falsy value or is aborted
* @param {TaskCallback} callback
* @returns {Task}
*/
export function loop(callback) {
/** @type {TaskEntry} */
let task;
if (raf.tasks.size === 0) {
raf.tick(run_tasks);
}
return {
promise: new Promise((fulfill) => {
raf.tasks.add((task = { c: callback, f: fulfill }));
}),
abort() {
raf.tasks.delete(task);
}
};
}
+432
View File
@@ -0,0 +1,432 @@
/** @import { Source } from '#client' */
import { DEV } from 'esm-env';
import {
get,
active_effect,
update_version,
active_reaction,
set_update_version,
set_active_reaction
} from './runtime.js';
import {
array_prototype,
get_descriptor,
get_prototype_of,
is_array,
object_prototype
} from '../shared/utils.js';
import {
state as source,
set,
increment,
flush_eager_effects,
set_eager_effects_deferred
} from './reactivity/sources.js';
import { PROXY_PATH_SYMBOL, STATE_SYMBOL } from '#client/constants';
import { UNINITIALIZED } from '../../constants.js';
import * as e from './errors.js';
import { tag } from './dev/tracing.js';
import { get_error } from '../shared/dev.js';
import { tracing_mode_flag } from '../flags/index.js';
// TODO move all regexes into shared module?
const regex_is_valid_identifier = /^[a-zA-Z_$][a-zA-Z_$0-9]*$/;
/**
* @template T
* @param {T} value
* @returns {T}
*/
export function proxy(value) {
// if non-proxyable, or is already a proxy, return `value`
if (typeof value !== 'object' || value === null || STATE_SYMBOL in value) {
return value;
}
const prototype = get_prototype_of(value);
if (prototype !== object_prototype && prototype !== array_prototype) {
return value;
}
/** @type {Map<any, Source<any>>} */
var sources = new Map();
var is_proxied_array = is_array(value);
var version = source(0);
var stack = DEV && tracing_mode_flag ? get_error('created at') : null;
var parent_version = update_version;
/**
* Executes the proxy in the context of the reaction it was originally created in, if any
* @template T
* @param {() => T} fn
*/
var with_parent = (fn) => {
if (update_version === parent_version) {
return fn();
}
// child source is being created after the initial proxy —
// prevent it from being associated with the current reaction
var reaction = active_reaction;
var version = update_version;
set_active_reaction(null);
set_update_version(parent_version);
var result = fn();
set_active_reaction(reaction);
set_update_version(version);
return result;
};
if (is_proxied_array) {
// We need to create the length source eagerly to ensure that
// mutations to the array are properly synced with our proxy
sources.set('length', source(/** @type {any[]} */ (value).length, stack));
if (DEV) {
value = /** @type {any} */ (inspectable_array(/** @type {any[]} */ (value)));
}
}
/** Used in dev for $inspect.trace() */
var path = '';
let updating = false;
/** @param {string} new_path */
function update_path(new_path) {
if (updating) return;
updating = true;
path = new_path;
tag(version, `${path} version`);
// rename all child sources and child proxies
for (const [prop, source] of sources) {
tag(source, get_label(path, prop));
}
updating = false;
}
return new Proxy(/** @type {any} */ (value), {
defineProperty(_, prop, descriptor) {
if (
!('value' in descriptor) ||
descriptor.configurable === false ||
descriptor.enumerable === false ||
descriptor.writable === false
) {
// we disallow non-basic descriptors, because unless they are applied to the
// target object — which we avoid, so that state can be forked — we will run
// afoul of the various invariants
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Proxy/Proxy/getOwnPropertyDescriptor#invariants
e.state_descriptors_fixed();
}
var s = sources.get(prop);
if (s === undefined) {
s = with_parent(() => {
var s = source(descriptor.value, stack);
sources.set(prop, s);
if (DEV && typeof prop === 'string') {
tag(s, get_label(path, prop));
}
return s;
});
} else {
set(s, descriptor.value, true);
}
return true;
},
deleteProperty(target, prop) {
var s = sources.get(prop);
if (s === undefined) {
if (prop in target) {
const s = with_parent(() => source(UNINITIALIZED, stack));
sources.set(prop, s);
increment(version);
if (DEV) {
tag(s, get_label(path, prop));
}
}
} else {
set(s, UNINITIALIZED);
increment(version);
}
return true;
},
get(target, prop, receiver) {
if (prop === STATE_SYMBOL) {
return value;
}
if (DEV && prop === PROXY_PATH_SYMBOL) {
return update_path;
}
var s = sources.get(prop);
var exists = prop in target;
// create a source, but only if it's an own property and not a prototype property
if (s === undefined && (!exists || get_descriptor(target, prop)?.writable)) {
s = with_parent(() => {
var p = proxy(exists ? target[prop] : UNINITIALIZED);
var s = source(p, stack);
if (DEV) {
tag(s, get_label(path, prop));
}
return s;
});
sources.set(prop, s);
}
if (s !== undefined) {
var v = get(s);
return v === UNINITIALIZED ? undefined : v;
}
return Reflect.get(target, prop, receiver);
},
getOwnPropertyDescriptor(target, prop) {
var descriptor = Reflect.getOwnPropertyDescriptor(target, prop);
if (descriptor && 'value' in descriptor) {
var s = sources.get(prop);
if (s) descriptor.value = get(s);
} else if (descriptor === undefined) {
var source = sources.get(prop);
var value = source?.v;
if (source !== undefined && value !== UNINITIALIZED) {
return {
enumerable: true,
configurable: true,
value,
writable: true
};
}
}
return descriptor;
},
has(target, prop) {
if (prop === STATE_SYMBOL) {
return true;
}
var s = sources.get(prop);
var has = (s !== undefined && s.v !== UNINITIALIZED) || Reflect.has(target, prop);
if (
s !== undefined ||
(active_effect !== null && (!has || get_descriptor(target, prop)?.writable))
) {
if (s === undefined) {
s = with_parent(() => {
var p = has ? proxy(target[prop]) : UNINITIALIZED;
var s = source(p, stack);
if (DEV) {
tag(s, get_label(path, prop));
}
return s;
});
sources.set(prop, s);
}
var value = get(s);
if (value === UNINITIALIZED) {
return false;
}
}
return has;
},
set(target, prop, value, receiver) {
var s = sources.get(prop);
var has = prop in target;
// variable.length = value -> clear all signals with index >= value
if (is_proxied_array && prop === 'length') {
for (var i = value; i < /** @type {Source<number>} */ (s).v; i += 1) {
var other_s = sources.get(i + '');
if (other_s !== undefined) {
set(other_s, UNINITIALIZED);
} else if (i in target) {
// If the item exists in the original, we need to create an uninitialized source,
// else a later read of the property would result in a source being created with
// the value of the original item at that index.
other_s = with_parent(() => source(UNINITIALIZED, stack));
sources.set(i + '', other_s);
if (DEV) {
tag(other_s, get_label(path, i));
}
}
}
}
// If we haven't yet created a source for this property, we need to ensure
// we do so otherwise if we read it later, then the write won't be tracked and
// the heuristics of effects will be different vs if we had read the proxied
// object property before writing to that property.
if (s === undefined) {
if (!has || get_descriptor(target, prop)?.writable) {
s = with_parent(() => source(undefined, stack));
if (DEV) {
tag(s, get_label(path, prop));
}
set(s, proxy(value));
sources.set(prop, s);
}
} else {
has = s.v !== UNINITIALIZED;
var p = with_parent(() => proxy(value));
set(s, p);
}
var descriptor = Reflect.getOwnPropertyDescriptor(target, prop);
// Set the new value before updating any signals so that any listeners get the new value
if (descriptor?.set) {
descriptor.set.call(receiver, value);
}
if (!has) {
// If we have mutated an array directly, we might need to
// signal that length has also changed. Do it before updating metadata
// to ensure that iterating over the array as a result of a metadata update
// will not cause the length to be out of sync.
if (is_proxied_array && typeof prop === 'string') {
var ls = /** @type {Source<number>} */ (sources.get('length'));
var n = Number(prop);
if (Number.isInteger(n) && n >= ls.v) {
set(ls, n + 1);
}
}
increment(version);
}
return true;
},
ownKeys(target) {
get(version);
var own_keys = Reflect.ownKeys(target).filter((key) => {
var source = sources.get(key);
return source === undefined || source.v !== UNINITIALIZED;
});
for (var [key, source] of sources) {
if (source.v !== UNINITIALIZED && !(key in target)) {
own_keys.push(key);
}
}
return own_keys;
},
setPrototypeOf() {
e.state_prototype_fixed();
}
});
}
/**
* @param {string} path
* @param {string | symbol} prop
*/
function get_label(path, prop) {
if (typeof prop === 'symbol') return `${path}[Symbol(${prop.description ?? ''})]`;
if (regex_is_valid_identifier.test(prop)) return `${path}.${prop}`;
return /^\d+$/.test(prop) ? `${path}[${prop}]` : `${path}['${prop}']`;
}
/**
* @param {any} value
*/
export function get_proxied_value(value) {
try {
if (value !== null && typeof value === 'object' && STATE_SYMBOL in value) {
return value[STATE_SYMBOL];
}
} catch {
// the above if check can throw an error if the value in question
// is the contentWindow of an iframe on another domain, in which
// case we want to just return the value (because it's definitely
// not a proxied value) so we don't break any JavaScript interacting
// with that iframe (such as various payment companies client side
// JavaScript libraries interacting with their iframes on the same
// domain)
}
return value;
}
/**
* @param {any} a
* @param {any} b
*/
export function is(a, b) {
return Object.is(get_proxied_value(a), get_proxied_value(b));
}
const ARRAY_MUTATING_METHODS = new Set([
'copyWithin',
'fill',
'pop',
'push',
'reverse',
'shift',
'sort',
'splice',
'unshift'
]);
/**
* Wrap array mutating methods so $inspect is triggered only once and
* to prevent logging an array in intermediate state (e.g. with an empty slot)
* @param {any[]} array
*/
function inspectable_array(array) {
return new Proxy(array, {
get(target, prop, receiver) {
var value = Reflect.get(target, prop, receiver);
if (!ARRAY_MUTATING_METHODS.has(/** @type {string} */ (prop))) {
return value;
}
/**
* @this {any[]}
* @param {any[]} args
*/
return function (...args) {
set_eager_effects_deferred();
var result = value.apply(this, args);
flush_eager_effects();
return result;
};
}
});
}
+277
View File
@@ -0,0 +1,277 @@
/** @import { Effect, TemplateNode, Value } from '#client' */
import { DESTROYED, STALE_REACTION } from '#client/constants';
import { DEV } from 'esm-env';
import {
component_context,
dev_stack,
is_runes,
set_component_context,
set_dev_stack
} from '../context.js';
import { get_boundary } from '../dom/blocks/boundary.js';
import { invoke_error_boundary } from '../error-handling.js';
import {
active_effect,
active_reaction,
set_active_effect,
set_active_reaction
} from '../runtime.js';
import { Batch, current_batch } from './batch.js';
import {
async_derived,
current_async_effect,
derived,
derived_safe_equal,
set_from_async_derived
} from './deriveds.js';
import { aborted } from './effects.js';
/**
* @param {Array<Promise<void>>} blockers
* @param {Array<() => any>} sync
* @param {Array<() => Promise<any>>} async
* @param {(values: Value[]) => any} fn
*/
export function flatten(blockers, sync, async, fn) {
const d = is_runes() ? derived : derived_safe_equal;
if (async.length === 0 && blockers.length === 0) {
fn(sync.map(d));
return;
}
var batch = current_batch;
var parent = /** @type {Effect} */ (active_effect);
var restore = capture();
function run() {
Promise.all(async.map((expression) => async_derived(expression)))
.then((result) => {
restore();
try {
fn([...sync.map(d), ...result]);
} catch (error) {
// ignore errors in blocks that have already been destroyed
if ((parent.f & DESTROYED) === 0) {
invoke_error_boundary(error, parent);
}
}
batch?.deactivate();
unset_context();
})
.catch((error) => {
invoke_error_boundary(error, parent);
});
}
if (blockers.length > 0) {
Promise.all(blockers).then(() => {
restore();
try {
return run();
} finally {
batch?.deactivate();
unset_context();
}
});
} else {
run();
}
}
/**
* @param {Array<Promise<void>>} blockers
* @param {(values: Value[]) => any} fn
*/
export function run_after_blockers(blockers, fn) {
flatten(blockers, [], [], fn);
}
/**
* Captures the current effect context so that we can restore it after
* some asynchronous work has happened (so that e.g. `await a + b`
* causes `b` to be registered as a dependency).
*/
export function capture() {
var previous_effect = active_effect;
var previous_reaction = active_reaction;
var previous_component_context = component_context;
var previous_batch = current_batch;
if (DEV) {
var previous_dev_stack = dev_stack;
}
return function restore(activate_batch = true) {
set_active_effect(previous_effect);
set_active_reaction(previous_reaction);
set_component_context(previous_component_context);
if (activate_batch) previous_batch?.activate();
if (DEV) {
set_from_async_derived(null);
set_dev_stack(previous_dev_stack);
}
};
}
/**
* Wraps an `await` expression in such a way that the effect context that was
* active before the expression evaluated can be reapplied afterwards —
* `await a + b` becomes `(await $.save(a))() + b`
* @template T
* @param {Promise<T>} promise
* @returns {Promise<() => T>}
*/
export async function save(promise) {
var restore = capture();
var value = await promise;
return () => {
restore();
return value;
};
}
/**
* Reset `current_async_effect` after the `promise` resolves, so
* that we can emit `await_reactivity_loss` warnings
* @template T
* @param {Promise<T>} promise
* @returns {Promise<() => T>}
*/
export async function track_reactivity_loss(promise) {
var previous_async_effect = current_async_effect;
var value = await promise;
return () => {
set_from_async_derived(previous_async_effect);
return value;
};
}
/**
* Used in `for await` loops in DEV, so
* that we can emit `await_reactivity_loss` warnings
* after each `async_iterator` result resolves and
* after the `async_iterator` return resolves (if it runs)
* @template T
* @template TReturn
* @param {Iterable<T> | AsyncIterable<T>} iterable
* @returns {AsyncGenerator<T, TReturn | undefined>}
*/
export async function* for_await_track_reactivity_loss(iterable) {
// This is based on the algorithms described in ECMA-262:
// ForIn/OfBodyEvaluation
// https://tc39.es/ecma262/multipage/ecmascript-language-statements-and-declarations.html#sec-runtime-semantics-forin-div-ofbodyevaluation-lhs-stmt-iterator-lhskind-labelset
// AsyncIteratorClose
// https://tc39.es/ecma262/multipage/abstract-operations.html#sec-asynciteratorclose
/** @type {AsyncIterator<T, TReturn>} */
// @ts-ignore
const iterator = iterable[Symbol.asyncIterator]?.() ?? iterable[Symbol.iterator]?.();
if (iterator === undefined) {
throw new TypeError('value is not async iterable');
}
/** Whether the completion of the iterator was "normal", meaning it wasn't ended via `break` or a similar method */
let normal_completion = false;
try {
while (true) {
const { done, value } = (await track_reactivity_loss(iterator.next()))();
if (done) {
normal_completion = true;
break;
}
yield value;
}
} finally {
// If the iterator had a normal completion and `return` is defined on the iterator, call it and return the value
if (normal_completion && iterator.return !== undefined) {
// eslint-disable-next-line no-unsafe-finally
return /** @type {TReturn} */ ((await track_reactivity_loss(iterator.return()))().value);
}
}
}
export function unset_context() {
set_active_effect(null);
set_active_reaction(null);
set_component_context(null);
if (DEV) {
set_from_async_derived(null);
set_dev_stack(null);
}
}
/**
* @param {Array<() => void | Promise<void>>} thunks
*/
export function run(thunks) {
const restore = capture();
var boundary = get_boundary();
var batch = /** @type {Batch} */ (current_batch);
var blocking = boundary.is_rendered();
boundary.update_pending_count(1);
batch.increment(blocking);
var active = /** @type {Effect} */ (active_effect);
/** @type {null | { error: any }} */
var errored = null;
/** @param {any} error */
const handle_error = (error) => {
errored = { error }; // wrap in object in case a promise rejects with a falsy value
if (!aborted(active)) {
invoke_error_boundary(error, active);
}
};
var promise = Promise.resolve(thunks[0]()).catch(handle_error);
var promises = [promise];
for (const fn of thunks.slice(1)) {
promise = promise
.then(() => {
if (errored) {
throw errored.error;
}
if (aborted(active)) {
throw STALE_REACTION;
}
restore();
return fn();
})
.catch(handle_error)
.finally(() => {
unset_context();
current_batch?.deactivate();
});
promises.push(promise);
}
promise
// wait one more tick, so that template effects are
// guaranteed to run before `$effect(...)`
.then(() => Promise.resolve())
.finally(() => {
boundary.update_pending_count(-1);
batch.decrement(blocking);
});
return promises;
}
+991
View File
@@ -0,0 +1,991 @@
/** @import { Fork } from 'svelte' */
/** @import { Derived, Effect, Reaction, Source, Value } from '#client' */
/** @import { Boundary } from '../dom/blocks/boundary' */
import {
BLOCK_EFFECT,
BRANCH_EFFECT,
CLEAN,
DESTROYED,
DIRTY,
EFFECT,
ASYNC,
INERT,
RENDER_EFFECT,
ROOT_EFFECT,
MAYBE_DIRTY,
DERIVED,
BOUNDARY_EFFECT,
EAGER_EFFECT,
HEAD_EFFECT,
ERROR_VALUE,
MANAGED_EFFECT
} from '#client/constants';
import { async_mode_flag } from '../../flags/index.js';
import { deferred, define_property } from '../../shared/utils.js';
import {
active_effect,
get,
increment_write_version,
is_dirty,
is_updating_effect,
set_is_updating_effect,
update_effect
} from '../runtime.js';
import * as e from '../errors.js';
import { flush_tasks, queue_micro_task } from '../dom/task.js';
import { DEV } from 'esm-env';
import { invoke_error_boundary } from '../error-handling.js';
import { flush_eager_effects, old_values, set_eager_effects, source, update } from './sources.js';
import { eager_effect, unlink_effect } from './effects.js';
import { defer_effect } from './utils.js';
import { UNINITIALIZED } from '../../../constants.js';
import { set_signal_status } from './status.js';
/** @type {Set<Batch>} */
const batches = new Set();
/** @type {Batch | null} */
export let current_batch = null;
/**
* This is needed to avoid overwriting inputs in non-async mode
* TODO 6.0 remove this, as non-async mode will go away
* @type {Batch | null}
*/
export let previous_batch = null;
/**
* When time travelling (i.e. working in one batch, while other batches
* still have ongoing work), we ignore the real values of affected
* signals in favour of their values within the batch
* @type {Map<Value, any> | null}
*/
export let batch_values = null;
// TODO this should really be a property of `batch`
/** @type {Effect[]} */
let queued_root_effects = [];
/** @type {Effect | null} */
let last_scheduled_effect = null;
let is_flushing = false;
export let is_flushing_sync = false;
export class Batch {
committed = false;
/**
* The current values of any sources that are updated in this batch
* They keys of this map are identical to `this.#previous`
* @type {Map<Source, any>}
*/
current = new Map();
/**
* The values of any sources that are updated in this batch _before_ those updates took place.
* They keys of this map are identical to `this.#current`
* @type {Map<Source, any>}
*/
previous = new Map();
/**
* When the batch is committed (and the DOM is updated), we need to remove old branches
* and append new ones by calling the functions added inside (if/each/key/etc) blocks
* @type {Set<() => void>}
*/
#commit_callbacks = new Set();
/**
* If a fork is discarded, we need to destroy any effects that are no longer needed
* @type {Set<(batch: Batch) => void>}
*/
#discard_callbacks = new Set();
/**
* The number of async effects that are currently in flight
*/
#pending = 0;
/**
* The number of async effects that are currently in flight, _not_ inside a pending boundary
*/
#blocking_pending = 0;
/**
* A deferred that resolves when the batch is committed, used with `settled()`
* TODO replace with Promise.withResolvers once supported widely enough
* @type {{ promise: Promise<void>, resolve: (value?: any) => void, reject: (reason: unknown) => void } | null}
*/
#deferred = null;
/**
* Deferred effects (which run after async work has completed) that are DIRTY
* @type {Set<Effect>}
*/
#dirty_effects = new Set();
/**
* Deferred effects that are MAYBE_DIRTY
* @type {Set<Effect>}
*/
#maybe_dirty_effects = new Set();
/**
* A set of branches that still exist, but will be destroyed when this batch
* is committed — we skip over these during `process`
* @type {Set<Effect>}
*/
skipped_effects = new Set();
is_fork = false;
is_deferred() {
return this.is_fork || this.#blocking_pending > 0;
}
/**
*
* @param {Effect[]} root_effects
*/
process(root_effects) {
queued_root_effects = [];
previous_batch = null;
this.apply();
/** @type {Effect[]} */
var effects = [];
/** @type {Effect[]} */
var render_effects = [];
for (const root of root_effects) {
this.#traverse_effect_tree(root, effects, render_effects);
// Note: #traverse_effect_tree runs block effects eagerly, which can schedule effects,
// which means queued_root_effects now may be filled again.
// Helpful for debugging reactivity loss that has to do with branches being skipped:
// log_inconsistent_branches(root);
}
if (!this.is_fork) {
this.#resolve();
}
if (this.is_deferred()) {
this.#defer_effects(render_effects);
this.#defer_effects(effects);
} else {
// If sources are written to, then work needs to happen in a separate batch, else prior sources would be mixed with
// newly updated sources, which could lead to infinite loops when effects run over and over again.
previous_batch = this;
current_batch = null;
flush_queued_effects(render_effects);
flush_queued_effects(effects);
previous_batch = null;
this.#deferred?.resolve();
}
batch_values = null;
}
/**
* Traverse the effect tree, executing effects or stashing
* them for later execution as appropriate
* @param {Effect} root
* @param {Effect[]} effects
* @param {Effect[]} render_effects
*/
#traverse_effect_tree(root, effects, render_effects) {
root.f ^= CLEAN;
var effect = root.first;
/** @type {Effect | null} */
var pending_boundary = null;
while (effect !== null) {
var flags = effect.f;
var is_branch = (flags & (BRANCH_EFFECT | ROOT_EFFECT)) !== 0;
var is_skippable_branch = is_branch && (flags & CLEAN) !== 0;
var skip = is_skippable_branch || (flags & INERT) !== 0 || this.skipped_effects.has(effect);
// Inside a `<svelte:boundary>` with a pending snippet,
// all effects are deferred until the boundary resolves
// (except block/async effects, which run immediately)
if (
async_mode_flag &&
pending_boundary === null &&
(flags & BOUNDARY_EFFECT) !== 0 &&
effect.b?.is_pending
) {
pending_boundary = effect;
}
if (!skip && effect.fn !== null) {
if (is_branch) {
effect.f ^= CLEAN;
} else if (
pending_boundary !== null &&
(flags & (EFFECT | RENDER_EFFECT | MANAGED_EFFECT)) !== 0
) {
/** @type {Boundary} */ (pending_boundary.b).defer_effect(effect);
} else if ((flags & EFFECT) !== 0) {
effects.push(effect);
} else if (async_mode_flag && (flags & (RENDER_EFFECT | MANAGED_EFFECT)) !== 0) {
render_effects.push(effect);
} else if (is_dirty(effect)) {
if ((flags & BLOCK_EFFECT) !== 0) this.#dirty_effects.add(effect);
update_effect(effect);
}
var child = effect.first;
if (child !== null) {
effect = child;
continue;
}
}
var parent = effect.parent;
effect = effect.next;
while (effect === null && parent !== null) {
if (parent === pending_boundary) {
pending_boundary = null;
}
effect = parent.next;
parent = parent.parent;
}
}
}
/**
* @param {Effect[]} effects
*/
#defer_effects(effects) {
for (var i = 0; i < effects.length; i += 1) {
defer_effect(effects[i], this.#dirty_effects, this.#maybe_dirty_effects);
}
}
/**
* Associate a change to a given source with the current
* batch, noting its previous and current values
* @param {Source} source
* @param {any} value
*/
capture(source, value) {
if (value !== UNINITIALIZED && !this.previous.has(source)) {
this.previous.set(source, value);
}
// Don't save errors in `batch_values`, or they won't be thrown in `runtime.js#get`
if ((source.f & ERROR_VALUE) === 0) {
this.current.set(source, source.v);
batch_values?.set(source, source.v);
}
}
activate() {
current_batch = this;
this.apply();
}
deactivate() {
// If we're not the current batch, don't deactivate,
// else we could create zombie batches that are never flushed
if (current_batch !== this) return;
current_batch = null;
batch_values = null;
}
flush() {
this.activate();
if (queued_root_effects.length > 0) {
flush_effects();
if (current_batch !== null && current_batch !== this) {
// this can happen if a new batch was created during `flush_effects()`
return;
}
} else if (this.#pending === 0) {
this.process([]); // TODO this feels awkward
}
this.deactivate();
}
discard() {
for (const fn of this.#discard_callbacks) fn(this);
this.#discard_callbacks.clear();
}
#resolve() {
if (this.#blocking_pending === 0) {
// append/remove branches
for (const fn of this.#commit_callbacks) fn();
this.#commit_callbacks.clear();
}
if (this.#pending === 0) {
this.#commit();
}
}
#commit() {
// If there are other pending batches, they now need to be 'rebased' —
// in other words, we re-run block/async effects with the newly
// committed state, unless the batch in question has a more
// recent value for a given source
if (batches.size > 1) {
this.previous.clear();
var previous_batch_values = batch_values;
var is_earlier = true;
for (const batch of batches) {
if (batch === this) {
is_earlier = false;
continue;
}
/** @type {Source[]} */
const sources = [];
for (const [source, value] of this.current) {
if (batch.current.has(source)) {
if (is_earlier && value !== batch.current.get(source)) {
// bring the value up to date
batch.current.set(source, value);
} else {
// same value or later batch has more recent value,
// no need to re-run these effects
continue;
}
}
sources.push(source);
}
if (sources.length === 0) {
continue;
}
// Re-run async/block effects that depend on distinct values changed in both batches
const others = [...batch.current.keys()].filter((s) => !this.current.has(s));
if (others.length > 0) {
// Avoid running queued root effects on the wrong branch
var prev_queued_root_effects = queued_root_effects;
queued_root_effects = [];
/** @type {Set<Value>} */
const marked = new Set();
/** @type {Map<Reaction, boolean>} */
const checked = new Map();
for (const source of sources) {
mark_effects(source, others, marked, checked);
}
if (queued_root_effects.length > 0) {
current_batch = batch;
batch.apply();
for (const root of queued_root_effects) {
batch.#traverse_effect_tree(root, [], []);
}
// TODO do we need to do anything with the dummy effect arrays?
batch.deactivate();
}
queued_root_effects = prev_queued_root_effects;
}
}
current_batch = null;
batch_values = previous_batch_values;
}
this.committed = true;
batches.delete(this);
}
/**
*
* @param {boolean} blocking
*/
increment(blocking) {
this.#pending += 1;
if (blocking) this.#blocking_pending += 1;
}
/**
*
* @param {boolean} blocking
*/
decrement(blocking) {
this.#pending -= 1;
if (blocking) this.#blocking_pending -= 1;
this.revive();
}
revive() {
for (const e of this.#dirty_effects) {
this.#maybe_dirty_effects.delete(e);
set_signal_status(e, DIRTY);
schedule_effect(e);
}
for (const e of this.#maybe_dirty_effects) {
set_signal_status(e, MAYBE_DIRTY);
schedule_effect(e);
}
this.flush();
}
/** @param {() => void} fn */
oncommit(fn) {
this.#commit_callbacks.add(fn);
}
/** @param {(batch: Batch) => void} fn */
ondiscard(fn) {
this.#discard_callbacks.add(fn);
}
settled() {
return (this.#deferred ??= deferred()).promise;
}
static ensure() {
if (current_batch === null) {
const batch = (current_batch = new Batch());
batches.add(current_batch);
if (!is_flushing_sync) {
Batch.enqueue(() => {
if (current_batch !== batch) {
// a flushSync happened in the meantime
return;
}
batch.flush();
});
}
}
return current_batch;
}
/** @param {() => void} task */
static enqueue(task) {
queue_micro_task(task);
}
apply() {
if (!async_mode_flag || (!this.is_fork && batches.size === 1)) return;
// if there are multiple batches, we are 'time travelling' —
// we need to override values with the ones in this batch...
batch_values = new Map(this.current);
// ...and undo changes belonging to other batches
for (const batch of batches) {
if (batch === this) continue;
for (const [source, previous] of batch.previous) {
if (!batch_values.has(source)) {
batch_values.set(source, previous);
}
}
}
}
}
/**
* Synchronously flush any pending updates.
* Returns void if no callback is provided, otherwise returns the result of calling the callback.
* @template [T=void]
* @param {(() => T) | undefined} [fn]
* @returns {T}
*/
export function flushSync(fn) {
var was_flushing_sync = is_flushing_sync;
is_flushing_sync = true;
try {
var result;
if (fn) {
if (current_batch !== null) {
flush_effects();
}
result = fn();
}
while (true) {
flush_tasks();
if (queued_root_effects.length === 0) {
current_batch?.flush();
// we need to check again, in case we just updated an `$effect.pending()`
if (queued_root_effects.length === 0) {
// this would be reset in `flush_effects()` but since we are early returning here,
// we need to reset it here as well in case the first time there's 0 queued root effects
last_scheduled_effect = null;
return /** @type {T} */ (result);
}
}
flush_effects();
}
} finally {
is_flushing_sync = was_flushing_sync;
}
}
function flush_effects() {
var was_updating_effect = is_updating_effect;
is_flushing = true;
var source_stacks = DEV ? new Set() : null;
try {
var flush_count = 0;
set_is_updating_effect(true);
while (queued_root_effects.length > 0) {
var batch = Batch.ensure();
if (flush_count++ > 1000) {
if (DEV) {
var updates = new Map();
for (const source of batch.current.keys()) {
for (const [stack, update] of source.updated ?? []) {
var entry = updates.get(stack);
if (!entry) {
entry = { error: update.error, count: 0 };
updates.set(stack, entry);
}
entry.count += update.count;
}
}
for (const update of updates.values()) {
if (update.error) {
// eslint-disable-next-line no-console
console.error(update.error);
}
}
}
infinite_loop_guard();
}
batch.process(queued_root_effects);
old_values.clear();
if (DEV) {
for (const source of batch.current.keys()) {
/** @type {Set<Source>} */ (source_stacks).add(source);
}
}
}
} finally {
is_flushing = false;
set_is_updating_effect(was_updating_effect);
last_scheduled_effect = null;
if (DEV) {
for (const source of /** @type {Set<Source>} */ (source_stacks)) {
source.updated = null;
}
}
}
}
function infinite_loop_guard() {
try {
e.effect_update_depth_exceeded();
} catch (error) {
if (DEV) {
// stack contains no useful information, replace it
define_property(error, 'stack', { value: '' });
}
// Best effort: invoke the boundary nearest the most recent
// effect and hope that it's relevant to the infinite loop
invoke_error_boundary(error, last_scheduled_effect);
}
}
/** @type {Set<Effect> | null} */
export let eager_block_effects = null;
/**
* @param {Array<Effect>} effects
* @returns {void}
*/
function flush_queued_effects(effects) {
var length = effects.length;
if (length === 0) return;
var i = 0;
while (i < length) {
var effect = effects[i++];
if ((effect.f & (DESTROYED | INERT)) === 0 && is_dirty(effect)) {
eager_block_effects = new Set();
update_effect(effect);
// Effects with no dependencies or teardown do not get added to the effect tree.
// Deferred effects (e.g. `$effect(...)`) _are_ added to the tree because we
// don't know if we need to keep them until they are executed. Doing the check
// here (rather than in `update_effect`) allows us to skip the work for
// immediate effects.
if (effect.deps === null && effect.first === null && effect.nodes === null) {
// if there's no teardown or abort controller we completely unlink
// the effect from the graph
if (effect.teardown === null && effect.ac === null) {
// remove this effect from the graph
unlink_effect(effect);
} else {
// keep the effect in the graph, but free up some memory
effect.fn = null;
}
}
// If update_effect() has a flushSync() in it, we may have flushed another flush_queued_effects(),
// which already handled this logic and did set eager_block_effects to null.
if (eager_block_effects?.size > 0) {
old_values.clear();
for (const e of eager_block_effects) {
// Skip eager effects that have already been unmounted
if ((e.f & (DESTROYED | INERT)) !== 0) continue;
// Run effects in order from ancestor to descendant, else we could run into nullpointers
/** @type {Effect[]} */
const ordered_effects = [e];
let ancestor = e.parent;
while (ancestor !== null) {
if (eager_block_effects.has(ancestor)) {
eager_block_effects.delete(ancestor);
ordered_effects.push(ancestor);
}
ancestor = ancestor.parent;
}
for (let j = ordered_effects.length - 1; j >= 0; j--) {
const e = ordered_effects[j];
// Skip eager effects that have already been unmounted
if ((e.f & (DESTROYED | INERT)) !== 0) continue;
update_effect(e);
}
}
eager_block_effects.clear();
}
}
}
eager_block_effects = null;
}
/**
* This is similar to `mark_reactions`, but it only marks async/block effects
* depending on `value` and at least one of the other `sources`, so that
* these effects can re-run after another batch has been committed
* @param {Value} value
* @param {Source[]} sources
* @param {Set<Value>} marked
* @param {Map<Reaction, boolean>} checked
*/
function mark_effects(value, sources, marked, checked) {
if (marked.has(value)) return;
marked.add(value);
if (value.reactions !== null) {
for (const reaction of value.reactions) {
const flags = reaction.f;
if ((flags & DERIVED) !== 0) {
mark_effects(/** @type {Derived} */ (reaction), sources, marked, checked);
} else if (
(flags & (ASYNC | BLOCK_EFFECT)) !== 0 &&
(flags & DIRTY) === 0 &&
depends_on(reaction, sources, checked)
) {
set_signal_status(reaction, DIRTY);
schedule_effect(/** @type {Effect} */ (reaction));
}
}
}
}
/**
* When committing a fork, we need to trigger eager effects so that
* any `$state.eager(...)` expressions update immediately. This
* function allows us to discover them
* @param {Value} value
* @param {Set<Effect>} effects
*/
function mark_eager_effects(value, effects) {
if (value.reactions === null) return;
for (const reaction of value.reactions) {
const flags = reaction.f;
if ((flags & DERIVED) !== 0) {
mark_eager_effects(/** @type {Derived} */ (reaction), effects);
} else if ((flags & EAGER_EFFECT) !== 0) {
set_signal_status(reaction, DIRTY);
effects.add(/** @type {Effect} */ (reaction));
}
}
}
/**
* @param {Reaction} reaction
* @param {Source[]} sources
* @param {Map<Reaction, boolean>} checked
*/
function depends_on(reaction, sources, checked) {
const depends = checked.get(reaction);
if (depends !== undefined) return depends;
if (reaction.deps !== null) {
for (const dep of reaction.deps) {
if (sources.includes(dep)) {
return true;
}
if ((dep.f & DERIVED) !== 0 && depends_on(/** @type {Derived} */ (dep), sources, checked)) {
checked.set(/** @type {Derived} */ (dep), true);
return true;
}
}
}
checked.set(reaction, false);
return false;
}
/**
* @param {Effect} signal
* @returns {void}
*/
export function schedule_effect(signal) {
var effect = (last_scheduled_effect = signal);
while (effect.parent !== null) {
effect = effect.parent;
var flags = effect.f;
// if the effect is being scheduled because a parent (each/await/etc) block
// updated an internal source, bail out or we'll cause a second flush
if (
is_flushing &&
effect === active_effect &&
(flags & BLOCK_EFFECT) !== 0 &&
(flags & HEAD_EFFECT) === 0
) {
return;
}
if ((flags & (ROOT_EFFECT | BRANCH_EFFECT)) !== 0) {
if ((flags & CLEAN) === 0) return;
effect.f ^= CLEAN;
}
}
queued_root_effects.push(effect);
}
/** @type {Source<number>[]} */
let eager_versions = [];
function eager_flush() {
try {
flushSync(() => {
for (const version of eager_versions) {
update(version);
}
});
} finally {
eager_versions = [];
}
}
/**
* Implementation of `$state.eager(fn())`
* @template T
* @param {() => T} fn
* @returns {T}
*/
export function eager(fn) {
var version = source(0);
var initial = true;
var value = /** @type {T} */ (undefined);
get(version);
eager_effect(() => {
if (initial) {
// the first time this runs, we create an eager effect
// that will run eagerly whenever the expression changes
var previous_batch_values = batch_values;
try {
batch_values = null;
value = fn();
} finally {
batch_values = previous_batch_values;
}
return;
}
// the second time this effect runs, it's to schedule a
// `version` update. since this will recreate the effect,
// we don't need to evaluate the expression here
if (eager_versions.length === 0) {
queue_micro_task(eager_flush);
}
eager_versions.push(version);
});
initial = false;
return value;
}
/**
* Creates a 'fork', in which state changes are evaluated but not applied to the DOM.
* This is useful for speculatively loading data (for example) when you suspect that
* the user is about to take some action.
*
* Frameworks like SvelteKit can use this to preload data when the user touches or
* hovers over a link, making any subsequent navigation feel instantaneous.
*
* The `fn` parameter is a synchronous function that modifies some state. The
* state changes will be reverted after the fork is initialised, then reapplied
* if and when the fork is eventually committed.
*
* When it becomes clear that a fork will _not_ be committed (e.g. because the
* user navigated elsewhere), it must be discarded to avoid leaking memory.
*
* @param {() => void} fn
* @returns {Fork}
* @since 5.42
*/
export function fork(fn) {
if (!async_mode_flag) {
e.experimental_async_required('fork');
}
if (current_batch !== null) {
e.fork_timing();
}
var batch = Batch.ensure();
batch.is_fork = true;
batch_values = new Map();
var committed = false;
var settled = batch.settled();
flushSync(fn);
// revert state changes
for (var [source, value] of batch.previous) {
source.v = value;
}
// make writable deriveds dirty, so they recalculate correctly
for (source of batch.current.keys()) {
if ((source.f & DERIVED) !== 0) {
set_signal_status(source, DIRTY);
}
}
return {
commit: async () => {
if (committed) {
await settled;
return;
}
if (!batches.has(batch)) {
e.fork_discarded();
}
committed = true;
batch.is_fork = false;
// apply changes and update write versions so deriveds see the change
for (var [source, value] of batch.current) {
source.v = value;
source.wv = increment_write_version();
}
// trigger any `$state.eager(...)` expressions with the new state.
// eager effects don't get scheduled like other effects, so we
// can't just encounter them during traversal, we need to
// proactively flush them
// TODO maybe there's a better implementation?
flushSync(() => {
/** @type {Set<Effect>} */
var eager_effects = new Set();
for (var source of batch.current.keys()) {
mark_eager_effects(source, eager_effects);
}
set_eager_effects(eager_effects);
flush_eager_effects();
});
batch.revive();
await settled;
},
discard: () => {
if (!committed && batches.has(batch)) {
batches.delete(batch);
batch.discard();
}
}
};
}
/**
* Forcibly remove all current batches, to prevent cross-talk between tests
*/
export function clear() {
batches.clear();
}
+394
View File
@@ -0,0 +1,394 @@
/** @import { Derived, Effect, Source } from '#client' */
/** @import { Batch } from './batch.js'; */
import { DEV } from 'esm-env';
import {
ERROR_VALUE,
DERIVED,
DIRTY,
EFFECT_PRESERVED,
STALE_REACTION,
ASYNC,
WAS_MARKED,
DESTROYED,
CLEAN
} from '#client/constants';
import {
active_reaction,
active_effect,
update_reaction,
increment_write_version,
set_active_effect,
push_reaction_value,
is_destroying_effect
} from '../runtime.js';
import { equals, safe_equals } from './equality.js';
import * as e from '../errors.js';
import * as w from '../warnings.js';
import { async_effect, destroy_effect, effect_tracking, teardown } from './effects.js';
import { eager_effects, internal_set, set_eager_effects, source } from './sources.js';
import { get_error } from '../../shared/dev.js';
import { async_mode_flag, tracing_mode_flag } from '../../flags/index.js';
import { Boundary } from '../dom/blocks/boundary.js';
import { component_context } from '../context.js';
import { UNINITIALIZED } from '../../../constants.js';
import { batch_values, current_batch } from './batch.js';
import { unset_context } from './async.js';
import { deferred } from '../../shared/utils.js';
import { set_signal_status, update_derived_status } from './status.js';
/** @type {Effect | null} */
export let current_async_effect = null;
/** @param {Effect | null} v */
export function set_from_async_derived(v) {
current_async_effect = v;
}
export const recent_async_deriveds = new Set();
/**
* @template V
* @param {() => V} fn
* @returns {Derived<V>}
*/
/*#__NO_SIDE_EFFECTS__*/
export function derived(fn) {
var flags = DERIVED | DIRTY;
var parent_derived =
active_reaction !== null && (active_reaction.f & DERIVED) !== 0
? /** @type {Derived} */ (active_reaction)
: null;
if (active_effect !== null) {
// Since deriveds are evaluated lazily, any effects created inside them are
// created too late to ensure that the parent effect is added to the tree
active_effect.f |= EFFECT_PRESERVED;
}
/** @type {Derived<V>} */
const signal = {
ctx: component_context,
deps: null,
effects: null,
equals,
f: flags,
fn,
reactions: null,
rv: 0,
v: /** @type {V} */ (UNINITIALIZED),
wv: 0,
parent: parent_derived ?? active_effect,
ac: null
};
if (DEV && tracing_mode_flag) {
signal.created = get_error('created at');
}
return signal;
}
/**
* @template V
* @param {() => V | Promise<V>} fn
* @param {string} [label]
* @param {string} [location] If provided, print a warning if the value is not read immediately after update
* @returns {Promise<Source<V>>}
*/
/*#__NO_SIDE_EFFECTS__*/
export function async_derived(fn, label, location) {
let parent = /** @type {Effect | null} */ (active_effect);
if (parent === null) {
e.async_derived_orphan();
}
var boundary = /** @type {Boundary} */ (parent.b);
var promise = /** @type {Promise<V>} */ (/** @type {unknown} */ (undefined));
var signal = source(/** @type {V} */ (UNINITIALIZED));
if (DEV) signal.label = label;
// only suspend in async deriveds created on initialisation
var should_suspend = !active_reaction;
/** @type {Map<Batch, ReturnType<typeof deferred<V>>>} */
var deferreds = new Map();
async_effect(() => {
if (DEV) current_async_effect = active_effect;
/** @type {ReturnType<typeof deferred<V>>} */
var d = deferred();
promise = d.promise;
try {
// If this code is changed at some point, make sure to still access the then property
// of fn() to read any signals it might access, so that we track them as dependencies.
// We call `unset_context` to undo any `save` calls that happen inside `fn()`
Promise.resolve(fn())
.then(d.resolve, d.reject)
.then(() => {
if (batch === current_batch && batch.committed) {
// if the batch was rejected as stale, we need to cleanup
// after any `$.save(...)` calls inside `fn()`
batch.deactivate();
}
unset_context();
});
} catch (error) {
d.reject(error);
unset_context();
}
if (DEV) current_async_effect = null;
var batch = /** @type {Batch} */ (current_batch);
if (should_suspend) {
var blocking = boundary.is_rendered();
boundary.update_pending_count(1);
batch.increment(blocking);
deferreds.get(batch)?.reject(STALE_REACTION);
deferreds.delete(batch); // delete to ensure correct order in Map iteration below
deferreds.set(batch, d);
}
/**
* @param {any} value
* @param {unknown} error
*/
const handler = (value, error = undefined) => {
current_async_effect = null;
batch.activate();
if (error) {
if (error !== STALE_REACTION) {
signal.f |= ERROR_VALUE;
// @ts-expect-error the error is the wrong type, but we don't care
internal_set(signal, error);
}
} else {
if ((signal.f & ERROR_VALUE) !== 0) {
signal.f ^= ERROR_VALUE;
}
internal_set(signal, value);
// All prior async derived runs are now stale
for (const [b, d] of deferreds) {
deferreds.delete(b);
if (b === batch) break;
d.reject(STALE_REACTION);
}
if (DEV && location !== undefined) {
recent_async_deriveds.add(signal);
setTimeout(() => {
if (recent_async_deriveds.has(signal)) {
w.await_waterfall(/** @type {string} */ (signal.label), location);
recent_async_deriveds.delete(signal);
}
});
}
}
if (should_suspend) {
boundary.update_pending_count(-1);
batch.decrement(blocking);
}
};
d.promise.then(handler, (e) => handler(null, e || 'unknown'));
});
teardown(() => {
for (const d of deferreds.values()) {
d.reject(STALE_REACTION);
}
});
if (DEV) {
// add a flag that lets this be printed as a derived
// when using `$inspect.trace()`
signal.f |= ASYNC;
}
return new Promise((fulfil) => {
/** @param {Promise<V>} p */
function next(p) {
function go() {
if (p === promise) {
fulfil(signal);
} else {
// if the effect re-runs before the initial promise
// resolves, delay resolution until we have a value
next(promise);
}
}
p.then(go, go);
}
next(promise);
});
}
/**
* @template V
* @param {() => V} fn
* @returns {Derived<V>}
*/
/*#__NO_SIDE_EFFECTS__*/
export function user_derived(fn) {
const d = derived(fn);
if (!async_mode_flag) push_reaction_value(d);
return d;
}
/**
* @template V
* @param {() => V} fn
* @returns {Derived<V>}
*/
/*#__NO_SIDE_EFFECTS__*/
export function derived_safe_equal(fn) {
const signal = derived(fn);
signal.equals = safe_equals;
return signal;
}
/**
* @param {Derived} derived
* @returns {void}
*/
export function destroy_derived_effects(derived) {
var effects = derived.effects;
if (effects !== null) {
derived.effects = null;
for (var i = 0; i < effects.length; i += 1) {
destroy_effect(/** @type {Effect} */ (effects[i]));
}
}
}
/**
* The currently updating deriveds, used to detect infinite recursion
* in dev mode and provide a nicer error than 'too much recursion'
* @type {Derived[]}
*/
let stack = [];
/**
* @param {Derived} derived
* @returns {Effect | null}
*/
function get_derived_parent_effect(derived) {
var parent = derived.parent;
while (parent !== null) {
if ((parent.f & DERIVED) === 0) {
// The original parent effect might've been destroyed but the derived
// is used elsewhere now - do not return the destroyed effect in that case
return (parent.f & DESTROYED) === 0 ? /** @type {Effect} */ (parent) : null;
}
parent = parent.parent;
}
return null;
}
/**
* @template T
* @param {Derived} derived
* @returns {T}
*/
export function execute_derived(derived) {
var value;
var prev_active_effect = active_effect;
set_active_effect(get_derived_parent_effect(derived));
if (DEV) {
let prev_eager_effects = eager_effects;
set_eager_effects(new Set());
try {
if (stack.includes(derived)) {
e.derived_references_self();
}
stack.push(derived);
derived.f &= ~WAS_MARKED;
destroy_derived_effects(derived);
value = update_reaction(derived);
} finally {
set_active_effect(prev_active_effect);
set_eager_effects(prev_eager_effects);
stack.pop();
}
} else {
try {
derived.f &= ~WAS_MARKED;
destroy_derived_effects(derived);
value = update_reaction(derived);
} finally {
set_active_effect(prev_active_effect);
}
}
return value;
}
/**
* @param {Derived} derived
* @returns {void}
*/
export function update_derived(derived) {
var value = execute_derived(derived);
if (!derived.equals(value)) {
derived.wv = increment_write_version();
// in a fork, we don't update the underlying value, just `batch_values`.
// the underlying value will be updated when the fork is committed.
// otherwise, the next time we get here after a 'real world' state
// change, `derived.equals` may incorrectly return `true`
if (!current_batch?.is_fork || derived.deps === null) {
derived.v = value;
// deriveds without dependencies should never be recomputed
if (derived.deps === null) {
set_signal_status(derived, CLEAN);
return;
}
}
}
// don't mark derived clean if we're reading it inside a
// cleanup function, or it will cache a stale value
if (is_destroying_effect) {
return;
}
// During time traveling we don't want to reset the status so that
// traversal of the graph in the other batches still happens
if (batch_values !== null) {
// only cache the value if we're in a tracking context, otherwise we won't
// clear the cache in `mark_reactions` when dependencies are updated
if (effect_tracking() || current_batch?.is_fork) {
batch_values.set(derived, value);
}
} else {
update_derived_status(derived);
}
}
+717
View File
@@ -0,0 +1,717 @@
/** @import { ComponentContext, ComponentContextLegacy, Derived, Effect, TemplateNode, TransitionManager } from '#client' */
import {
is_dirty,
active_effect,
active_reaction,
update_effect,
get,
is_destroying_effect,
remove_reactions,
set_active_reaction,
set_is_destroying_effect,
untrack,
untracking
} from '../runtime.js';
import {
DIRTY,
BRANCH_EFFECT,
RENDER_EFFECT,
EFFECT,
DESTROYED,
INERT,
EFFECT_RAN,
BLOCK_EFFECT,
ROOT_EFFECT,
EFFECT_TRANSPARENT,
DERIVED,
CLEAN,
EAGER_EFFECT,
HEAD_EFFECT,
MAYBE_DIRTY,
EFFECT_PRESERVED,
STALE_REACTION,
USER_EFFECT,
ASYNC,
CONNECTED,
MANAGED_EFFECT
} from '#client/constants';
import * as e from '../errors.js';
import { DEV } from 'esm-env';
import { define_property } from '../../shared/utils.js';
import { get_next_sibling } from '../dom/operations.js';
import { component_context, dev_current_component_function, dev_stack } from '../context.js';
import { Batch, current_batch, schedule_effect } from './batch.js';
import { flatten } from './async.js';
import { without_reactive_context } from '../dom/elements/bindings/shared.js';
import { set_signal_status } from './status.js';
/**
* @param {'$effect' | '$effect.pre' | '$inspect'} rune
*/
export function validate_effect(rune) {
if (active_effect === null) {
if (active_reaction === null) {
e.effect_orphan(rune);
}
e.effect_in_unowned_derived();
}
if (is_destroying_effect) {
e.effect_in_teardown(rune);
}
}
/**
* @param {Effect} effect
* @param {Effect} parent_effect
*/
function push_effect(effect, parent_effect) {
var parent_last = parent_effect.last;
if (parent_last === null) {
parent_effect.last = parent_effect.first = effect;
} else {
parent_last.next = effect;
effect.prev = parent_last;
parent_effect.last = effect;
}
}
/**
* @param {number} type
* @param {null | (() => void | (() => void))} fn
* @param {boolean} sync
* @returns {Effect}
*/
function create_effect(type, fn, sync) {
var parent = active_effect;
if (DEV) {
// Ensure the parent is never an inspect effect
while (parent !== null && (parent.f & EAGER_EFFECT) !== 0) {
parent = parent.parent;
}
}
if (parent !== null && (parent.f & INERT) !== 0) {
type |= INERT;
}
/** @type {Effect} */
var effect = {
ctx: component_context,
deps: null,
nodes: null,
f: type | DIRTY | CONNECTED,
first: null,
fn,
last: null,
next: null,
parent,
b: parent && parent.b,
prev: null,
teardown: null,
wv: 0,
ac: null
};
if (DEV) {
effect.component_function = dev_current_component_function;
}
if (sync) {
try {
update_effect(effect);
effect.f |= EFFECT_RAN;
} catch (e) {
destroy_effect(effect);
throw e;
}
} else if (fn !== null) {
schedule_effect(effect);
}
/** @type {Effect | null} */
var e = effect;
// if an effect has already ran and doesn't need to be kept in the tree
// (because it won't re-run, has no DOM, and has no teardown etc)
// then we skip it and go to its child (if any)
if (
sync &&
e.deps === null &&
e.teardown === null &&
e.nodes === null &&
e.first === e.last && // either `null`, or a singular child
(e.f & EFFECT_PRESERVED) === 0
) {
e = e.first;
if ((type & BLOCK_EFFECT) !== 0 && (type & EFFECT_TRANSPARENT) !== 0 && e !== null) {
e.f |= EFFECT_TRANSPARENT;
}
}
if (e !== null) {
e.parent = parent;
if (parent !== null) {
push_effect(e, parent);
}
// if we're in a derived, add the effect there too
if (
active_reaction !== null &&
(active_reaction.f & DERIVED) !== 0 &&
(type & ROOT_EFFECT) === 0
) {
var derived = /** @type {Derived} */ (active_reaction);
(derived.effects ??= []).push(e);
}
}
return effect;
}
/**
* Internal representation of `$effect.tracking()`
* @returns {boolean}
*/
export function effect_tracking() {
return active_reaction !== null && !untracking;
}
/**
* @param {() => void} fn
*/
export function teardown(fn) {
const effect = create_effect(RENDER_EFFECT, null, false);
set_signal_status(effect, CLEAN);
effect.teardown = fn;
return effect;
}
/**
* Internal representation of `$effect(...)`
* @param {() => void | (() => void)} fn
*/
export function user_effect(fn) {
validate_effect('$effect');
if (DEV) {
define_property(fn, 'name', {
value: '$effect'
});
}
// Non-nested `$effect(...)` in a component should be deferred
// until the component is mounted
var flags = /** @type {Effect} */ (active_effect).f;
var defer = !active_reaction && (flags & BRANCH_EFFECT) !== 0 && (flags & EFFECT_RAN) === 0;
if (defer) {
// Top-level `$effect(...)` in an unmounted component — defer until mount
var context = /** @type {ComponentContext} */ (component_context);
(context.e ??= []).push(fn);
} else {
// Everything else — create immediately
return create_user_effect(fn);
}
}
/**
* @param {() => void | (() => void)} fn
*/
export function create_user_effect(fn) {
return create_effect(EFFECT | USER_EFFECT, fn, false);
}
/**
* Internal representation of `$effect.pre(...)`
* @param {() => void | (() => void)} fn
* @returns {Effect}
*/
export function user_pre_effect(fn) {
validate_effect('$effect.pre');
if (DEV) {
define_property(fn, 'name', {
value: '$effect.pre'
});
}
return create_effect(RENDER_EFFECT | USER_EFFECT, fn, true);
}
/** @param {() => void | (() => void)} fn */
export function eager_effect(fn) {
return create_effect(EAGER_EFFECT, fn, true);
}
/**
* Internal representation of `$effect.root(...)`
* @param {() => void | (() => void)} fn
* @returns {() => void}
*/
export function effect_root(fn) {
Batch.ensure();
const effect = create_effect(ROOT_EFFECT | EFFECT_PRESERVED, fn, true);
return () => {
destroy_effect(effect);
};
}
/**
* An effect root whose children can transition out
* @param {() => void} fn
* @returns {(options?: { outro?: boolean }) => Promise<void>}
*/
export function component_root(fn) {
Batch.ensure();
const effect = create_effect(ROOT_EFFECT | EFFECT_PRESERVED, fn, true);
return (options = {}) => {
return new Promise((fulfil) => {
if (options.outro) {
pause_effect(effect, () => {
destroy_effect(effect);
fulfil(undefined);
});
} else {
destroy_effect(effect);
fulfil(undefined);
}
});
};
}
/**
* @param {() => void | (() => void)} fn
* @returns {Effect}
*/
export function effect(fn) {
return create_effect(EFFECT, fn, false);
}
/**
* Internal representation of `$: ..`
* @param {() => any} deps
* @param {() => void | (() => void)} fn
*/
export function legacy_pre_effect(deps, fn) {
var context = /** @type {ComponentContextLegacy} */ (component_context);
/** @type {{ effect: null | Effect, ran: boolean, deps: () => any }} */
var token = { effect: null, ran: false, deps };
context.l.$.push(token);
token.effect = render_effect(() => {
deps();
// If this legacy pre effect has already run before the end of the reset, then
// bail out to emulate the same behavior.
if (token.ran) return;
token.ran = true;
untrack(fn);
});
}
export function legacy_pre_effect_reset() {
var context = /** @type {ComponentContextLegacy} */ (component_context);
render_effect(() => {
// Run dirty `$:` statements
for (var token of context.l.$) {
token.deps();
var effect = token.effect;
// If the effect is CLEAN, then make it MAYBE_DIRTY. This ensures we traverse through
// the effects dependencies and correctly ensure each dependency is up-to-date.
if ((effect.f & CLEAN) !== 0 && effect.deps !== null) {
set_signal_status(effect, MAYBE_DIRTY);
}
if (is_dirty(effect)) {
update_effect(effect);
}
token.ran = false;
}
});
}
/**
* @param {() => void | (() => void)} fn
* @returns {Effect}
*/
export function async_effect(fn) {
return create_effect(ASYNC | EFFECT_PRESERVED, fn, true);
}
/**
* @param {() => void | (() => void)} fn
* @returns {Effect}
*/
export function render_effect(fn, flags = 0) {
return create_effect(RENDER_EFFECT | flags, fn, true);
}
/**
* @param {(...expressions: any) => void | (() => void)} fn
* @param {Array<() => any>} sync
* @param {Array<() => Promise<any>>} async
* @param {Array<Promise<void>>} blockers
*/
export function template_effect(fn, sync = [], async = [], blockers = []) {
flatten(blockers, sync, async, (values) => {
create_effect(RENDER_EFFECT, () => fn(...values.map(get)), true);
});
}
/**
* Like `template_effect`, but with an effect which is deferred until the batch commits
* @param {(...expressions: any) => void | (() => void)} fn
* @param {Array<() => any>} sync
* @param {Array<() => Promise<any>>} async
* @param {Array<Promise<void>>} blockers
*/
export function deferred_template_effect(fn, sync = [], async = [], blockers = []) {
var batch = /** @type {Batch} */ (current_batch);
var is_async = async.length > 0 || blockers.length > 0;
if (is_async) batch.increment(true);
flatten(blockers, sync, async, (values) => {
create_effect(EFFECT, () => fn(...values.map(get)), false);
if (is_async) batch.decrement(true);
});
}
/**
* @param {(() => void)} fn
* @param {number} flags
*/
export function block(fn, flags = 0) {
var effect = create_effect(BLOCK_EFFECT | flags, fn, true);
if (DEV) {
effect.dev_stack = dev_stack;
}
return effect;
}
/**
* @param {(() => void)} fn
* @param {number} flags
*/
export function managed(fn, flags = 0) {
var effect = create_effect(MANAGED_EFFECT | flags, fn, true);
if (DEV) {
effect.dev_stack = dev_stack;
}
return effect;
}
/**
* @param {(() => void)} fn
*/
export function branch(fn) {
return create_effect(BRANCH_EFFECT | EFFECT_PRESERVED, fn, true);
}
/**
* @param {Effect} effect
*/
export function execute_effect_teardown(effect) {
var teardown = effect.teardown;
if (teardown !== null) {
const previously_destroying_effect = is_destroying_effect;
const previous_reaction = active_reaction;
set_is_destroying_effect(true);
set_active_reaction(null);
try {
teardown.call(null);
} finally {
set_is_destroying_effect(previously_destroying_effect);
set_active_reaction(previous_reaction);
}
}
}
/**
* @param {Effect} signal
* @param {boolean} remove_dom
* @returns {void}
*/
export function destroy_effect_children(signal, remove_dom = false) {
var effect = signal.first;
signal.first = signal.last = null;
while (effect !== null) {
const controller = effect.ac;
if (controller !== null) {
without_reactive_context(() => {
controller.abort(STALE_REACTION);
});
}
var next = effect.next;
if ((effect.f & ROOT_EFFECT) !== 0) {
// this is now an independent root
effect.parent = null;
} else {
destroy_effect(effect, remove_dom);
}
effect = next;
}
}
/**
* @param {Effect} signal
* @returns {void}
*/
export function destroy_block_effect_children(signal) {
var effect = signal.first;
while (effect !== null) {
var next = effect.next;
if ((effect.f & BRANCH_EFFECT) === 0) {
destroy_effect(effect);
}
effect = next;
}
}
/**
* @param {Effect} effect
* @param {boolean} [remove_dom]
* @returns {void}
*/
export function destroy_effect(effect, remove_dom = true) {
var removed = false;
if (
(remove_dom || (effect.f & HEAD_EFFECT) !== 0) &&
effect.nodes !== null &&
effect.nodes.end !== null
) {
remove_effect_dom(effect.nodes.start, /** @type {TemplateNode} */ (effect.nodes.end));
removed = true;
}
destroy_effect_children(effect, remove_dom && !removed);
remove_reactions(effect, 0);
set_signal_status(effect, DESTROYED);
var transitions = effect.nodes && effect.nodes.t;
if (transitions !== null) {
for (const transition of transitions) {
transition.stop();
}
}
execute_effect_teardown(effect);
var parent = effect.parent;
// If the parent doesn't have any children, then skip this work altogether
if (parent !== null && parent.first !== null) {
unlink_effect(effect);
}
if (DEV) {
effect.component_function = null;
}
// `first` and `child` are nulled out in destroy_effect_children
// we don't null out `parent` so that error propagation can work correctly
effect.next =
effect.prev =
effect.teardown =
effect.ctx =
effect.deps =
effect.fn =
effect.nodes =
effect.ac =
null;
}
/**
*
* @param {TemplateNode | null} node
* @param {TemplateNode} end
*/
export function remove_effect_dom(node, end) {
while (node !== null) {
/** @type {TemplateNode | null} */
var next = node === end ? null : get_next_sibling(node);
node.remove();
node = next;
}
}
/**
* Detach an effect from the effect tree, freeing up memory and
* reducing the amount of work that happens on subsequent traversals
* @param {Effect} effect
*/
export function unlink_effect(effect) {
var parent = effect.parent;
var prev = effect.prev;
var next = effect.next;
if (prev !== null) prev.next = next;
if (next !== null) next.prev = prev;
if (parent !== null) {
if (parent.first === effect) parent.first = next;
if (parent.last === effect) parent.last = prev;
}
}
/**
* When a block effect is removed, we don't immediately destroy it or yank it
* out of the DOM, because it might have transitions. Instead, we 'pause' it.
* It stays around (in memory, and in the DOM) until outro transitions have
* completed, and if the state change is reversed then we _resume_ it.
* A paused effect does not update, and the DOM subtree becomes inert.
* @param {Effect} effect
* @param {() => void} [callback]
* @param {boolean} [destroy]
*/
export function pause_effect(effect, callback, destroy = true) {
/** @type {TransitionManager[]} */
var transitions = [];
pause_children(effect, transitions, true);
var fn = () => {
if (destroy) destroy_effect(effect);
if (callback) callback();
};
var remaining = transitions.length;
if (remaining > 0) {
var check = () => --remaining || fn();
for (var transition of transitions) {
transition.out(check);
}
} else {
fn();
}
}
/**
* @param {Effect} effect
* @param {TransitionManager[]} transitions
* @param {boolean} local
*/
function pause_children(effect, transitions, local) {
if ((effect.f & INERT) !== 0) return;
effect.f ^= INERT;
var t = effect.nodes && effect.nodes.t;
if (t !== null) {
for (const transition of t) {
if (transition.is_global || local) {
transitions.push(transition);
}
}
}
var child = effect.first;
while (child !== null) {
var sibling = child.next;
var transparent =
(child.f & EFFECT_TRANSPARENT) !== 0 ||
// If this is a branch effect without a block effect parent,
// it means the parent block effect was pruned. In that case,
// transparency information was transferred to the branch effect.
((child.f & BRANCH_EFFECT) !== 0 && (effect.f & BLOCK_EFFECT) !== 0);
// TODO we don't need to call pause_children recursively with a linked list in place
// it's slightly more involved though as we have to account for `transparent` changing
// through the tree.
pause_children(child, transitions, transparent ? local : false);
child = sibling;
}
}
/**
* The opposite of `pause_effect`. We call this if (for example)
* `x` becomes falsy then truthy: `{#if x}...{/if}`
* @param {Effect} effect
*/
export function resume_effect(effect) {
resume_children(effect, true);
}
/**
* @param {Effect} effect
* @param {boolean} local
*/
function resume_children(effect, local) {
if ((effect.f & INERT) === 0) return;
effect.f ^= INERT;
// If a dependency of this effect changed while it was paused,
// schedule the effect to update. we don't use `is_dirty`
// here because we don't want to eagerly recompute a derived like
// `{#if foo}{foo.bar()}{/if}` if `foo` is now `undefined
if ((effect.f & CLEAN) === 0) {
set_signal_status(effect, DIRTY);
schedule_effect(effect);
}
var child = effect.first;
while (child !== null) {
var sibling = child.next;
var transparent = (child.f & EFFECT_TRANSPARENT) !== 0 || (child.f & BRANCH_EFFECT) !== 0;
// TODO we don't need to call resume_children recursively with a linked list in place
// it's slightly more involved though as we have to account for `transparent` changing
// through the tree.
resume_children(child, transparent ? local : false);
child = sibling;
}
var t = effect.nodes && effect.nodes.t;
if (t !== null) {
for (const transition of t) {
if (transition.is_global || local) {
transition.in();
}
}
}
}
export function aborted(effect = /** @type {Effect} */ (active_effect)) {
return (effect.f & DESTROYED) !== 0;
}
/**
* @param {Effect} effect
* @param {DocumentFragment} fragment
*/
export function move_effect(effect, fragment) {
if (!effect.nodes) return;
/** @type {TemplateNode | null} */
var node = effect.nodes.start;
var end = effect.nodes.end;
while (node !== null) {
/** @type {TemplateNode | null} */
var next = node === end ? null : get_next_sibling(node);
fragment.append(node);
node = next;
}
}
+31
View File
@@ -0,0 +1,31 @@
/** @import { Equals } from '#client' */
/** @type {Equals} */
export function equals(value) {
return value === this.v;
}
/**
* @param {unknown} a
* @param {unknown} b
* @returns {boolean}
*/
export function safe_not_equal(a, b) {
return a != a
? b == b
: a !== b || (a !== null && typeof a === 'object') || typeof a === 'function';
}
/**
* @param {unknown} a
* @param {unknown} b
* @returns {boolean}
*/
export function not_equal(a, b) {
return a !== b;
}
/** @type {Equals} */
export function safe_equals(value) {
return !safe_not_equal(value, this.v);
}
+430
View File
@@ -0,0 +1,430 @@
/** @import { Effect, Source } from './types.js' */
import { DEV } from 'esm-env';
import {
PROPS_IS_BINDABLE,
PROPS_IS_IMMUTABLE,
PROPS_IS_LAZY_INITIAL,
PROPS_IS_RUNES,
PROPS_IS_UPDATED
} from '../../../constants.js';
import { get_descriptor, is_function } from '../../shared/utils.js';
import { set, source, update } from './sources.js';
import { derived, derived_safe_equal } from './deriveds.js';
import {
active_effect,
get,
is_destroying_effect,
set_active_effect,
untrack
} from '../runtime.js';
import * as e from '../errors.js';
import { DESTROYED, LEGACY_PROPS, STATE_SYMBOL } from '#client/constants';
import { proxy } from '../proxy.js';
import { capture_store_binding } from './store.js';
import { legacy_mode_flag } from '../../flags/index.js';
/**
* @param {((value?: number) => number)} fn
* @param {1 | -1} [d]
* @returns {number}
*/
export function update_prop(fn, d = 1) {
const value = fn();
fn(value + d);
return value;
}
/**
* @param {((value?: number) => number)} fn
* @param {1 | -1} [d]
* @returns {number}
*/
export function update_pre_prop(fn, d = 1) {
const value = fn() + d;
fn(value);
return value;
}
/**
* The proxy handler for rest props (i.e. `const { x, ...rest } = $props()`).
* Is passed the full `$$props` object and excludes the named props.
* @type {ProxyHandler<{ props: Record<string | symbol, unknown>, exclude: Array<string | symbol>, name?: string }>}}
*/
const rest_props_handler = {
get(target, key) {
if (target.exclude.includes(key)) return;
return target.props[key];
},
set(target, key) {
if (DEV) {
// TODO should this happen in prod too?
e.props_rest_readonly(`${target.name}.${String(key)}`);
}
return false;
},
getOwnPropertyDescriptor(target, key) {
if (target.exclude.includes(key)) return;
if (key in target.props) {
return {
enumerable: true,
configurable: true,
value: target.props[key]
};
}
},
has(target, key) {
if (target.exclude.includes(key)) return false;
return key in target.props;
},
ownKeys(target) {
return Reflect.ownKeys(target.props).filter((key) => !target.exclude.includes(key));
}
};
/**
* @param {Record<string, unknown>} props
* @param {string[]} exclude
* @param {string} [name]
* @returns {Record<string, unknown>}
*/
/*#__NO_SIDE_EFFECTS__*/
export function rest_props(props, exclude, name) {
return new Proxy(
DEV ? { props, exclude, name, other: {}, to_proxy: [] } : { props, exclude },
rest_props_handler
);
}
/**
* The proxy handler for legacy $$restProps and $$props
* @type {ProxyHandler<{ props: Record<string | symbol, unknown>, exclude: Array<string | symbol>, special: Record<string | symbol, (v?: unknown) => unknown>, version: Source<number>, parent_effect: Effect }>}}
*/
const legacy_rest_props_handler = {
get(target, key) {
if (target.exclude.includes(key)) return;
get(target.version);
return key in target.special ? target.special[key]() : target.props[key];
},
set(target, key, value) {
if (!(key in target.special)) {
var previous_effect = active_effect;
try {
set_active_effect(target.parent_effect);
// Handle props that can temporarily get out of sync with the parent
/** @type {Record<string, (v?: unknown) => unknown>} */
target.special[key] = prop(
{
get [key]() {
return target.props[key];
}
},
/** @type {string} */ (key),
PROPS_IS_UPDATED
);
} finally {
set_active_effect(previous_effect);
}
}
target.special[key](value);
update(target.version); // $$props is coarse-grained: when $$props.x is updated, usages of $$props.y etc are also rerun
return true;
},
getOwnPropertyDescriptor(target, key) {
if (target.exclude.includes(key)) return;
if (key in target.props) {
return {
enumerable: true,
configurable: true,
value: target.props[key]
};
}
},
deleteProperty(target, key) {
// Svelte 4 allowed for deletions on $$restProps
if (target.exclude.includes(key)) return true;
target.exclude.push(key);
update(target.version);
return true;
},
has(target, key) {
if (target.exclude.includes(key)) return false;
return key in target.props;
},
ownKeys(target) {
return Reflect.ownKeys(target.props).filter((key) => !target.exclude.includes(key));
}
};
/**
* @param {Record<string, unknown>} props
* @param {string[]} exclude
* @returns {Record<string, unknown>}
*/
export function legacy_rest_props(props, exclude) {
return new Proxy(
{
props,
exclude,
special: {},
version: source(0),
// TODO this is only necessary because we need to track component
// destruction inside `prop`, because of `bind:this`, but it
// seems likely that we can simplify `bind:this` instead
parent_effect: /** @type {Effect} */ (active_effect)
},
legacy_rest_props_handler
);
}
/**
* The proxy handler for spread props. Handles the incoming array of props
* that looks like `() => { dynamic: props }, { static: prop }, ..` and wraps
* them so that the whole thing is passed to the component as the `$$props` argument.
* @type {ProxyHandler<{ props: Array<Record<string | symbol, unknown> | (() => Record<string | symbol, unknown>)> }>}}
*/
const spread_props_handler = {
get(target, key) {
let i = target.props.length;
while (i--) {
let p = target.props[i];
if (is_function(p)) p = p();
if (typeof p === 'object' && p !== null && key in p) return p[key];
}
},
set(target, key, value) {
let i = target.props.length;
while (i--) {
let p = target.props[i];
if (is_function(p)) p = p();
const desc = get_descriptor(p, key);
if (desc && desc.set) {
desc.set(value);
return true;
}
}
return false;
},
getOwnPropertyDescriptor(target, key) {
let i = target.props.length;
while (i--) {
let p = target.props[i];
if (is_function(p)) p = p();
if (typeof p === 'object' && p !== null && key in p) {
const descriptor = get_descriptor(p, key);
if (descriptor && !descriptor.configurable) {
// Prevent a "Non-configurability Report Error": The target is an array, it does
// not actually contain this property. If it is now described as non-configurable,
// the proxy throws a validation error. Setting it to true avoids that.
descriptor.configurable = true;
}
return descriptor;
}
}
},
has(target, key) {
// To prevent a false positive `is_entry_props` in the `prop` function
if (key === STATE_SYMBOL || key === LEGACY_PROPS) return false;
for (let p of target.props) {
if (is_function(p)) p = p();
if (p != null && key in p) return true;
}
return false;
},
ownKeys(target) {
/** @type {Array<string | symbol>} */
const keys = [];
for (let p of target.props) {
if (is_function(p)) p = p();
if (!p) continue;
for (const key in p) {
if (!keys.includes(key)) keys.push(key);
}
for (const key of Object.getOwnPropertySymbols(p)) {
if (!keys.includes(key)) keys.push(key);
}
}
return keys;
}
};
/**
* @param {Array<Record<string, unknown> | (() => Record<string, unknown>)>} props
* @returns {any}
*/
export function spread_props(...props) {
return new Proxy({ props }, spread_props_handler);
}
/**
* This function is responsible for synchronizing a possibly bound prop with the inner component state.
* It is used whenever the compiler sees that the component writes to the prop, or when it has a default prop_value.
* @template V
* @param {Record<string, unknown>} props
* @param {string} key
* @param {number} flags
* @param {V | (() => V)} [fallback]
* @returns {(() => V | ((arg: V) => V) | ((arg: V, mutation: boolean) => V))}
*/
export function prop(props, key, flags, fallback) {
var runes = !legacy_mode_flag || (flags & PROPS_IS_RUNES) !== 0;
var bindable = (flags & PROPS_IS_BINDABLE) !== 0;
var lazy = (flags & PROPS_IS_LAZY_INITIAL) !== 0;
var fallback_value = /** @type {V} */ (fallback);
var fallback_dirty = true;
var get_fallback = () => {
if (fallback_dirty) {
fallback_dirty = false;
fallback_value = lazy
? untrack(/** @type {() => V} */ (fallback))
: /** @type {V} */ (fallback);
}
return fallback_value;
};
/** @type {((v: V) => void) | undefined} */
var setter;
if (bindable) {
// Can be the case when someone does `mount(Component, props)` with `let props = $state({...})`
// or `createClassComponent(Component, props)`
var is_entry_props = STATE_SYMBOL in props || LEGACY_PROPS in props;
setter =
get_descriptor(props, key)?.set ??
(is_entry_props && key in props ? (v) => (props[key] = v) : undefined);
}
var initial_value;
var is_store_sub = false;
if (bindable) {
[initial_value, is_store_sub] = capture_store_binding(() => /** @type {V} */ (props[key]));
} else {
initial_value = /** @type {V} */ (props[key]);
}
if (initial_value === undefined && fallback !== undefined) {
initial_value = get_fallback();
if (setter) {
if (runes) e.props_invalid_value(key);
setter(initial_value);
}
}
/** @type {() => V} */
var getter;
if (runes) {
getter = () => {
var value = /** @type {V} */ (props[key]);
if (value === undefined) return get_fallback();
fallback_dirty = true;
return value;
};
} else {
getter = () => {
var value = /** @type {V} */ (props[key]);
if (value !== undefined) {
// in legacy mode, we don't revert to the fallback value
// if the prop goes from defined to undefined. The easiest
// way to model this is to make the fallback undefined
// as soon as the prop has a value
fallback_value = /** @type {V} */ (undefined);
}
return value === undefined ? fallback_value : value;
};
}
// prop is never written to — we only need a getter
if (runes && (flags & PROPS_IS_UPDATED) === 0) {
return getter;
}
// prop is written to, but the parent component had `bind:foo` which
// means we can just call `$$props.foo = value` directly
if (setter) {
var legacy_parent = props.$$legacy;
return /** @type {() => V} */ (
function (/** @type {V} */ value, /** @type {boolean} */ mutation) {
if (arguments.length > 0) {
// We don't want to notify if the value was mutated and the parent is in runes mode.
// In that case the state proxy (if it exists) should take care of the notification.
// If the parent is not in runes mode, we need to notify on mutation, too, that the prop
// has changed because the parent will not be able to detect the change otherwise.
if (!runes || !mutation || legacy_parent || is_store_sub) {
/** @type {Function} */ (setter)(mutation ? getter() : value);
}
return value;
}
return getter();
}
);
}
// Either prop is written to, but there's no binding, which means we
// create a derived that we can write to locally.
// Or we are in legacy mode where we always create a derived to replicate that
// Svelte 4 did not trigger updates when a primitive value was updated to the same value.
var overridden = false;
var d = ((flags & PROPS_IS_IMMUTABLE) !== 0 ? derived : derived_safe_equal)(() => {
overridden = false;
return getter();
});
if (DEV) {
d.label = key;
}
// Capture the initial value if it's bindable
if (bindable) get(d);
var parent_effect = /** @type {Effect} */ (active_effect);
return /** @type {() => V} */ (
function (/** @type {any} */ value, /** @type {boolean} */ mutation) {
if (arguments.length > 0) {
const new_value = mutation ? get(d) : runes && bindable ? proxy(value) : value;
set(d, new_value);
overridden = true;
if (fallback_value !== undefined) {
fallback_value = new_value;
}
return value;
}
// special case — avoid recalculating the derived if we're in a
// teardown function and the prop was overridden locally, or the
// component was already destroyed (this latter part is necessary
// because `bind:this` can read props after the component has
// been destroyed. TODO simplify `bind:this`
if ((is_destroying_effect && overridden) || (parent_effect.f & DESTROYED) !== 0) {
return d.v;
}
return get(d);
}
);
}
+378
View File
@@ -0,0 +1,378 @@
/** @import { Derived, Effect, Source, Value } from '#client' */
import { DEV } from 'esm-env';
import {
active_reaction,
active_effect,
untracked_writes,
get,
set_untracked_writes,
untrack,
increment_write_version,
update_effect,
current_sources,
is_dirty,
untracking,
is_destroying_effect,
push_reaction_value,
set_is_updating_effect,
is_updating_effect
} from '../runtime.js';
import { equals, safe_equals } from './equality.js';
import {
CLEAN,
DERIVED,
DIRTY,
BRANCH_EFFECT,
EAGER_EFFECT,
MAYBE_DIRTY,
BLOCK_EFFECT,
ROOT_EFFECT,
ASYNC,
WAS_MARKED,
CONNECTED
} from '#client/constants';
import * as e from '../errors.js';
import { legacy_mode_flag, tracing_mode_flag } from '../../flags/index.js';
import { tag_proxy } from '../dev/tracing.js';
import { get_error } from '../../shared/dev.js';
import { component_context, is_runes } from '../context.js';
import { Batch, batch_values, eager_block_effects, schedule_effect } from './batch.js';
import { proxy } from '../proxy.js';
import { execute_derived } from './deriveds.js';
import { set_signal_status, update_derived_status } from './status.js';
/** @type {Set<any>} */
export let eager_effects = new Set();
/** @type {Map<Source, any>} */
export const old_values = new Map();
/**
* @param {Set<any>} v
*/
export function set_eager_effects(v) {
eager_effects = v;
}
let eager_effects_deferred = false;
export function set_eager_effects_deferred() {
eager_effects_deferred = true;
}
/**
* @template V
* @param {V} v
* @param {Error | null} [stack]
* @returns {Source<V>}
*/
// TODO rename this to `state` throughout the codebase
export function source(v, stack) {
/** @type {Value} */
var signal = {
f: 0, // TODO ideally we could skip this altogether, but it causes type errors
v,
reactions: null,
equals,
rv: 0,
wv: 0
};
if (DEV && tracing_mode_flag) {
signal.created = stack ?? get_error('created at');
signal.updated = null;
signal.set_during_effect = false;
signal.trace = null;
}
return signal;
}
/**
* @template V
* @param {V} v
* @param {Error | null} [stack]
*/
/*#__NO_SIDE_EFFECTS__*/
export function state(v, stack) {
const s = source(v, stack);
push_reaction_value(s);
return s;
}
/**
* @template V
* @param {V} initial_value
* @param {boolean} [immutable]
* @returns {Source<V>}
*/
/*#__NO_SIDE_EFFECTS__*/
export function mutable_source(initial_value, immutable = false, trackable = true) {
const s = source(initial_value);
if (!immutable) {
s.equals = safe_equals;
}
// bind the signal to the component context, in case we need to
// track updates to trigger beforeUpdate/afterUpdate callbacks
if (legacy_mode_flag && trackable && component_context !== null && component_context.l !== null) {
(component_context.l.s ??= []).push(s);
}
return s;
}
/**
* @template V
* @param {Value<V>} source
* @param {V} value
*/
export function mutate(source, value) {
set(
source,
untrack(() => get(source))
);
return value;
}
/**
* @template V
* @param {Source<V>} source
* @param {V} value
* @param {boolean} [should_proxy]
* @returns {V}
*/
export function set(source, value, should_proxy = false) {
if (
active_reaction !== null &&
// since we are untracking the function inside `$inspect.with` we need to add this check
// to ensure we error if state is set inside an inspect effect
(!untracking || (active_reaction.f & EAGER_EFFECT) !== 0) &&
is_runes() &&
(active_reaction.f & (DERIVED | BLOCK_EFFECT | ASYNC | EAGER_EFFECT)) !== 0 &&
!current_sources?.includes(source)
) {
e.state_unsafe_mutation();
}
let new_value = should_proxy ? proxy(value) : value;
if (DEV) {
tag_proxy(new_value, /** @type {string} */ (source.label));
}
return internal_set(source, new_value);
}
/**
* @template V
* @param {Source<V>} source
* @param {V} value
* @returns {V}
*/
export function internal_set(source, value) {
if (!source.equals(value)) {
var old_value = source.v;
if (is_destroying_effect) {
old_values.set(source, value);
} else {
old_values.set(source, old_value);
}
source.v = value;
var batch = Batch.ensure();
batch.capture(source, old_value);
if (DEV) {
if (tracing_mode_flag || active_effect !== null) {
source.updated ??= new Map();
// For performance reasons, when not using $inspect.trace, we only start collecting stack traces
// after the same source has been updated more than 5 times in the same flush cycle.
const count = (source.updated.get('')?.count ?? 0) + 1;
source.updated.set('', { error: /** @type {any} */ (null), count });
if (tracing_mode_flag || count > 5) {
const error = get_error('updated at');
if (error !== null) {
let entry = source.updated.get(error.stack);
if (!entry) {
entry = { error, count: 0 };
source.updated.set(error.stack, entry);
}
entry.count++;
}
}
}
if (active_effect !== null) {
source.set_during_effect = true;
}
}
if ((source.f & DERIVED) !== 0) {
const derived = /** @type {Derived} */ (source);
// if we are assigning to a dirty derived we set it to clean/maybe dirty but we also eagerly execute it to track the dependencies
if ((source.f & DIRTY) !== 0) {
execute_derived(derived);
}
update_derived_status(derived);
}
source.wv = increment_write_version();
// For debugging, in case you want to know which reactions are being scheduled:
// log_reactions(source);
mark_reactions(source, DIRTY);
// It's possible that the current reaction might not have up-to-date dependencies
// whilst it's actively running. So in the case of ensuring it registers the reaction
// properly for itself, we need to ensure the current effect actually gets
// scheduled. i.e: `$effect(() => x++)`
if (
is_runes() &&
active_effect !== null &&
(active_effect.f & CLEAN) !== 0 &&
(active_effect.f & (BRANCH_EFFECT | ROOT_EFFECT)) === 0
) {
if (untracked_writes === null) {
set_untracked_writes([source]);
} else {
untracked_writes.push(source);
}
}
if (!batch.is_fork && eager_effects.size > 0 && !eager_effects_deferred) {
flush_eager_effects();
}
}
return value;
}
export function flush_eager_effects() {
eager_effects_deferred = false;
var prev_is_updating_effect = is_updating_effect;
set_is_updating_effect(true);
const inspects = Array.from(eager_effects);
try {
for (const effect of inspects) {
// Mark clean inspect-effects as maybe dirty and then check their dirtiness
// instead of just updating the effects - this way we avoid overfiring.
if ((effect.f & CLEAN) !== 0) {
set_signal_status(effect, MAYBE_DIRTY);
}
if (is_dirty(effect)) {
update_effect(effect);
}
}
} finally {
set_is_updating_effect(prev_is_updating_effect);
}
eager_effects.clear();
}
/**
* @template {number | bigint} T
* @param {Source<T>} source
* @param {1 | -1} [d]
* @returns {T}
*/
export function update(source, d = 1) {
var value = get(source);
var result = d === 1 ? value++ : value--;
set(source, value);
// @ts-expect-error
return result;
}
/**
* @template {number | bigint} T
* @param {Source<T>} source
* @param {1 | -1} [d]
* @returns {T}
*/
export function update_pre(source, d = 1) {
var value = get(source);
// @ts-expect-error
return set(source, d === 1 ? ++value : --value);
}
/**
* Silently (without using `get`) increment a source
* @param {Source<number>} source
*/
export function increment(source) {
set(source, source.v + 1);
}
/**
* @param {Value} signal
* @param {number} status should be DIRTY or MAYBE_DIRTY
* @returns {void}
*/
function mark_reactions(signal, status) {
var reactions = signal.reactions;
if (reactions === null) return;
var runes = is_runes();
var length = reactions.length;
for (var i = 0; i < length; i++) {
var reaction = reactions[i];
var flags = reaction.f;
// In legacy mode, skip the current effect to prevent infinite loops
if (!runes && reaction === active_effect) continue;
// Inspect effects need to run immediately, so that the stack trace makes sense
if (DEV && (flags & EAGER_EFFECT) !== 0) {
eager_effects.add(reaction);
continue;
}
var not_dirty = (flags & DIRTY) === 0;
// don't set a DIRTY reaction to MAYBE_DIRTY
if (not_dirty) {
set_signal_status(reaction, status);
}
if ((flags & DERIVED) !== 0) {
var derived = /** @type {Derived} */ (reaction);
batch_values?.delete(derived);
if ((flags & WAS_MARKED) === 0) {
// Only connected deriveds can be reliably unmarked right away
if (flags & CONNECTED) {
reaction.f |= WAS_MARKED;
}
mark_reactions(derived, MAYBE_DIRTY);
}
} else if (not_dirty) {
if ((flags & BLOCK_EFFECT) !== 0 && eager_block_effects !== null) {
eager_block_effects.add(/** @type {Effect} */ (reaction));
}
schedule_effect(/** @type {Effect} */ (reaction));
}
}
}
+25
View File
@@ -0,0 +1,25 @@
/** @import { Derived, Signal } from '#client' */
import { CLEAN, CONNECTED, DIRTY, MAYBE_DIRTY } from '#client/constants';
const STATUS_MASK = ~(DIRTY | MAYBE_DIRTY | CLEAN);
/**
* @param {Signal} signal
* @param {number} status
*/
export function set_signal_status(signal, status) {
signal.f = (signal.f & STATUS_MASK) | status;
}
/**
* Set a derived's status to CLEAN or MAYBE_DIRTY based on its connection state.
* @param {Derived} derived
*/
export function update_derived_status(derived) {
// Only mark as MAYBE_DIRTY if disconnected and has dependencies.
if ((derived.f & CONNECTED) !== 0 || derived.deps === null) {
set_signal_status(derived, CLEAN);
} else {
set_signal_status(derived, MAYBE_DIRTY);
}
}
+203
View File
@@ -0,0 +1,203 @@
/** @import { StoreReferencesContainer } from '#client' */
/** @import { Store } from '#shared' */
import { subscribe_to_store } from '../../../store/utils.js';
import { get as get_store } from '../../../store/shared/index.js';
import { define_property, noop } from '../../shared/utils.js';
import { get } from '../runtime.js';
import { teardown } from './effects.js';
import { mutable_source, set } from './sources.js';
import { DEV } from 'esm-env';
/**
* Whether or not the prop currently being read is a store binding, as in
* `<Child bind:x={$y} />`. If it is, we treat the prop as mutable even in
* runes mode, and skip `binding_property_non_reactive` validation
*/
let is_store_binding = false;
let IS_UNMOUNTED = Symbol();
/**
* Gets the current value of a store. If the store isn't subscribed to yet, it will create a proxy
* signal that will be updated when the store is. The store references container is needed to
* track reassignments to stores and to track the correct component context.
* @template V
* @param {Store<V> | null | undefined} store
* @param {string} store_name
* @param {StoreReferencesContainer} stores
* @returns {V}
*/
export function store_get(store, store_name, stores) {
const entry = (stores[store_name] ??= {
store: null,
source: mutable_source(undefined),
unsubscribe: noop
});
if (DEV) {
entry.source.label = store_name;
}
// if the component that setup this is already unmounted we don't want to register a subscription
if (entry.store !== store && !(IS_UNMOUNTED in stores)) {
entry.unsubscribe();
entry.store = store ?? null;
if (store == null) {
entry.source.v = undefined; // see synchronous callback comment below
entry.unsubscribe = noop;
} else {
var is_synchronous_callback = true;
entry.unsubscribe = subscribe_to_store(store, (v) => {
if (is_synchronous_callback) {
// If the first updates to the store value (possibly multiple of them) are synchronously
// inside a derived, we will hit the `state_unsafe_mutation` error if we `set` the value
entry.source.v = v;
} else {
set(entry.source, v);
}
});
is_synchronous_callback = false;
}
}
// if the component that setup this stores is already unmounted the source will be out of sync
// so we just use the `get` for the stores, less performant but it avoids to create a memory leak
// and it will keep the value consistent
if (store && IS_UNMOUNTED in stores) {
return get_store(store);
}
return get(entry.source);
}
/**
* Unsubscribe from a store if it's not the same as the one in the store references container.
* We need this in addition to `store_get` because someone could unsubscribe from a store but
* then never subscribe to the new one (if any), causing the subscription to stay open wrongfully.
* @param {Store<any> | null | undefined} store
* @param {string} store_name
* @param {StoreReferencesContainer} stores
*/
export function store_unsub(store, store_name, stores) {
/** @type {StoreReferencesContainer[''] | undefined} */
let entry = stores[store_name];
if (entry && entry.store !== store) {
// Don't reset store yet, so that store_get above can resubscribe to new store if necessary
entry.unsubscribe();
entry.unsubscribe = noop;
}
return store;
}
/**
* 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;
}
/**
* @param {StoreReferencesContainer} stores
* @param {string} store_name
*/
export function invalidate_store(stores, store_name) {
var entry = stores[store_name];
if (entry.store !== null) {
store_set(entry.store, entry.source.v);
}
}
/**
* Unsubscribes from all auto-subscribed stores on destroy
* @returns {[StoreReferencesContainer, ()=>void]}
*/
export function setup_stores() {
/** @type {StoreReferencesContainer} */
const stores = {};
function cleanup() {
teardown(() => {
for (var store_name in stores) {
const ref = stores[store_name];
ref.unsubscribe();
}
define_property(stores, IS_UNMOUNTED, {
enumerable: false,
value: true
});
});
}
return [stores, cleanup];
}
/**
* Updates a store with a new value.
* @param {Store<V>} store the store to update
* @param {any} expression the expression that mutates the store
* @param {V} new_value the new store value
* @template V
*/
export function store_mutate(store, expression, new_value) {
store.set(new_value);
return expression;
}
/**
* @param {Store<number>} store
* @param {number} store_value
* @param {1 | -1} [d]
* @returns {number}
*/
export function update_store(store, store_value, d = 1) {
store.set(store_value + d);
return store_value;
}
/**
* @param {Store<number>} store
* @param {number} store_value
* @param {1 | -1} [d]
* @returns {number}
*/
export function update_pre_store(store, store_value, d = 1) {
const value = store_value + d;
store.set(value);
return value;
}
/**
* Called inside prop getters to communicate that the prop is a store binding
*/
export function mark_store_binding() {
is_store_binding = true;
}
/**
* Returns a tuple that indicates whether `fn()` reads a prop that is a store binding.
* Used to prevent `binding_property_non_reactive` validation false positives and
* ensure that these props are treated as mutable even in runes mode
* @template T
* @param {() => T} fn
* @returns {[T, boolean]}
*/
export function capture_store_binding(fn) {
var previous_is_store_binding = is_store_binding;
try {
is_store_binding = false;
return [fn(), is_store_binding];
} finally {
is_store_binding = previous_is_store_binding;
}
}
+40
View File
@@ -0,0 +1,40 @@
/** @import { Derived, Effect, Value } from '#client' */
import { CLEAN, DERIVED, DIRTY, MAYBE_DIRTY, WAS_MARKED } from '#client/constants';
import { set_signal_status } from './status.js';
/**
* @param {Value[] | null} deps
*/
function clear_marked(deps) {
if (deps === null) return;
for (const dep of deps) {
if ((dep.f & DERIVED) === 0 || (dep.f & WAS_MARKED) === 0) {
continue;
}
dep.f ^= WAS_MARKED;
clear_marked(/** @type {Derived} */ (dep).deps);
}
}
/**
* @param {Effect} effect
* @param {Set<Effect>} dirty_effects
* @param {Set<Effect>} maybe_dirty_effects
*/
export function defer_effect(effect, dirty_effects, maybe_dirty_effects) {
if ((effect.f & DIRTY) !== 0) {
dirty_effects.add(effect);
} else if ((effect.f & MAYBE_DIRTY) !== 0) {
maybe_dirty_effects.add(effect);
}
// Since we're not executing these effects now, we need to clear any WAS_MARKED flags
// so that other batches can correctly reach these effects during their own traversal
clear_marked(effect.deps);
// mark as clean so they get scheduled if they depend on pending async state
set_signal_status(effect, CLEAN);
}
+319
View File
@@ -0,0 +1,319 @@
/** @import { ComponentContext, Effect, EffectNodes, TemplateNode } from '#client' */
/** @import { Component, ComponentType, SvelteComponent, MountOptions } from '../../index.js' */
import { DEV } from 'esm-env';
import {
clear_text_content,
create_text,
get_first_child,
get_next_sibling,
init_operations
} from './dom/operations.js';
import { HYDRATION_END, HYDRATION_ERROR, HYDRATION_START } from '../../constants.js';
import { active_effect } from './runtime.js';
import { push, pop, component_context } from './context.js';
import { component_root } from './reactivity/effects.js';
import { hydrate_node, hydrating, set_hydrate_node, set_hydrating } from './dom/hydration.js';
import { array_from } from '../shared/utils.js';
import {
all_registered_events,
handle_event_propagation,
root_event_handles
} from './dom/elements/events.js';
import * as w from './warnings.js';
import * as e from './errors.js';
import { assign_nodes } from './dom/template.js';
import { is_passive_event } from '../../utils.js';
import { COMMENT_NODE, STATE_SYMBOL } from './constants.js';
import { boundary } from './dom/blocks/boundary.js';
/**
* This is normally true — block effects should run their intro transitions —
* but is false during hydration (unless `options.intro` is `true`) and
* when creating the children of a `<svelte:element>` that just changed tag
*/
export let should_intro = true;
/** @param {boolean} value */
export function set_should_intro(value) {
should_intro = value;
}
/**
* @param {Element} text
* @param {string} value
* @returns {void}
*/
export function set_text(text, value) {
// For objects, we apply string coercion (which might make things like $state array references in the template reactive) before diffing
var str = value == null ? '' : typeof value === 'object' ? value + '' : value;
// @ts-expect-error
if (str !== (text.__t ??= text.nodeValue)) {
// @ts-expect-error
text.__t = str;
text.nodeValue = str + '';
}
}
/**
* Mounts a component to the given target and returns the exports and potentially the props (if compiled with `accessors: true`) of the component.
* Transitions will play during the initial render unless the `intro` option is set to `false`.
*
* @template {Record<string, any>} Props
* @template {Record<string, any>} Exports
* @param {ComponentType<SvelteComponent<Props>> | Component<Props, Exports, any>} component
* @param {MountOptions<Props>} options
* @returns {Exports}
*/
export function mount(component, options) {
return _mount(component, options);
}
/**
* Hydrates a component on the given target and returns the exports and potentially the props (if compiled with `accessors: true`) of the component
*
* @template {Record<string, any>} Props
* @template {Record<string, any>} Exports
* @param {ComponentType<SvelteComponent<Props>> | Component<Props, Exports, any>} component
* @param {{} extends Props ? {
* target: Document | Element | ShadowRoot;
* props?: Props;
* events?: Record<string, (e: any) => any>;
* context?: Map<any, any>;
* intro?: boolean;
* recover?: boolean;
* } : {
* target: Document | Element | ShadowRoot;
* props: Props;
* events?: Record<string, (e: any) => any>;
* context?: Map<any, any>;
* intro?: boolean;
* recover?: boolean;
* }} options
* @returns {Exports}
*/
export function hydrate(component, options) {
init_operations();
options.intro = options.intro ?? false;
const target = options.target;
const was_hydrating = hydrating;
const previous_hydrate_node = hydrate_node;
try {
var anchor = get_first_child(target);
while (
anchor &&
(anchor.nodeType !== COMMENT_NODE || /** @type {Comment} */ (anchor).data !== HYDRATION_START)
) {
anchor = get_next_sibling(anchor);
}
if (!anchor) {
throw HYDRATION_ERROR;
}
set_hydrating(true);
set_hydrate_node(/** @type {Comment} */ (anchor));
const instance = _mount(component, { ...options, anchor });
set_hydrating(false);
return /** @type {Exports} */ (instance);
} catch (error) {
// re-throw Svelte errors - they are certainly not related to hydration
if (
error instanceof Error &&
error.message.split('\n').some((line) => line.startsWith('https://svelte.dev/e/'))
) {
throw error;
}
if (error !== HYDRATION_ERROR) {
// eslint-disable-next-line no-console
console.warn('Failed to hydrate: ', error);
}
if (options.recover === false) {
e.hydration_failed();
}
// If an error occurred above, the operations might not yet have been initialised.
init_operations();
clear_text_content(target);
set_hydrating(false);
return mount(component, options);
} finally {
set_hydrating(was_hydrating);
set_hydrate_node(previous_hydrate_node);
}
}
/** @type {Map<string, number>} */
const document_listeners = new Map();
/**
* @template {Record<string, any>} Exports
* @param {ComponentType<SvelteComponent<any>> | Component<any>} Component
* @param {MountOptions} options
* @returns {Exports}
*/
function _mount(Component, { target, anchor, props = {}, events, context, intro = true }) {
init_operations();
/** @type {Set<string>} */
var registered_events = new Set();
/** @param {Array<string>} events */
var event_handle = (events) => {
for (var i = 0; i < events.length; i++) {
var event_name = events[i];
if (registered_events.has(event_name)) continue;
registered_events.add(event_name);
var passive = is_passive_event(event_name);
// Add the event listener to both the container and the document.
// The container listener ensures we catch events from within in case
// the outer content stops propagation of the event.
target.addEventListener(event_name, handle_event_propagation, { passive });
var n = document_listeners.get(event_name);
if (n === undefined) {
// The document listener ensures we catch events that originate from elements that were
// manually moved outside of the container (e.g. via manual portals).
document.addEventListener(event_name, handle_event_propagation, { passive });
document_listeners.set(event_name, 1);
} else {
document_listeners.set(event_name, n + 1);
}
}
};
event_handle(array_from(all_registered_events));
root_event_handles.add(event_handle);
/** @type {Exports} */
// @ts-expect-error will be defined because the render effect runs synchronously
var component = undefined;
var unmount = component_root(() => {
var anchor_node = anchor ?? target.appendChild(create_text());
boundary(
/** @type {TemplateNode} */ (anchor_node),
{
pending: () => {}
},
(anchor_node) => {
if (context) {
push({});
var ctx = /** @type {ComponentContext} */ (component_context);
ctx.c = context;
}
if (events) {
// We can't spread the object or else we'd lose the state proxy stuff, if it is one
/** @type {any} */ (props).$$events = events;
}
if (hydrating) {
assign_nodes(/** @type {TemplateNode} */ (anchor_node), null);
}
should_intro = intro;
// @ts-expect-error the public typings are not what the actual function looks like
component = Component(anchor_node, props) || {};
should_intro = true;
if (hydrating) {
/** @type {Effect & { nodes: EffectNodes }} */ (active_effect).nodes.end = hydrate_node;
if (
hydrate_node === null ||
hydrate_node.nodeType !== COMMENT_NODE ||
/** @type {Comment} */ (hydrate_node).data !== HYDRATION_END
) {
w.hydration_mismatch();
throw HYDRATION_ERROR;
}
}
if (context) {
pop();
}
}
);
return () => {
for (var event_name of registered_events) {
target.removeEventListener(event_name, handle_event_propagation);
var n = /** @type {number} */ (document_listeners.get(event_name));
if (--n === 0) {
document.removeEventListener(event_name, handle_event_propagation);
document_listeners.delete(event_name);
} else {
document_listeners.set(event_name, n);
}
}
root_event_handles.delete(event_handle);
if (anchor_node !== anchor) {
anchor_node.parentNode?.removeChild(anchor_node);
}
};
});
mounted_components.set(component, unmount);
return component;
}
/**
* References of the components that were mounted or hydrated.
* Uses a `WeakMap` to avoid memory leaks.
*/
let mounted_components = new WeakMap();
/**
* Unmounts a component that was previously mounted using `mount` or `hydrate`.
*
* Since 5.13.0, if `options.outro` is `true`, [transitions](https://svelte.dev/docs/svelte/transition) will play before the component is removed from the DOM.
*
* Returns a `Promise` that resolves after transitions have completed if `options.outro` is true, or immediately otherwise (prior to 5.13.0, returns `void`).
*
* ```js
* import { mount, unmount } from 'svelte';
* import App from './App.svelte';
*
* const app = mount(App, { target: document.body });
*
* // later...
* unmount(app, { outro: true });
* ```
* @param {Record<string, any>} component
* @param {{ outro?: boolean }} [options]
* @returns {Promise<void>}
*/
export function unmount(component, options) {
const fn = mounted_components.get(component);
if (fn) {
mounted_components.delete(component);
return fn(options);
}
if (DEV) {
if (STATE_SYMBOL in component) {
w.state_proxy_unmount();
} else {
w.lifecycle_double_unmount();
}
}
return Promise.resolve();
}
+842
View File
@@ -0,0 +1,842 @@
/** @import { Derived, Effect, Reaction, Source, Value } from '#client' */
import { DEV } from 'esm-env';
import { get_descriptors, get_prototype_of, index_of } from '../shared/utils.js';
import {
destroy_block_effect_children,
destroy_effect_children,
effect_tracking,
execute_effect_teardown
} from './reactivity/effects.js';
import {
DIRTY,
MAYBE_DIRTY,
CLEAN,
DERIVED,
DESTROYED,
BRANCH_EFFECT,
STATE_SYMBOL,
BLOCK_EFFECT,
ROOT_EFFECT,
CONNECTED,
REACTION_IS_UPDATING,
STALE_REACTION,
ERROR_VALUE,
WAS_MARKED,
MANAGED_EFFECT
} from './constants.js';
import { old_values } from './reactivity/sources.js';
import {
destroy_derived_effects,
execute_derived,
recent_async_deriveds,
update_derived
} from './reactivity/deriveds.js';
import { async_mode_flag, tracing_mode_flag } from '../flags/index.js';
import { tracing_expressions } from './dev/tracing.js';
import { get_error } from '../shared/dev.js';
import {
component_context,
dev_current_component_function,
dev_stack,
is_runes,
set_component_context,
set_dev_current_component_function,
set_dev_stack
} from './context.js';
import {
Batch,
batch_values,
current_batch,
flushSync,
schedule_effect
} from './reactivity/batch.js';
import { handle_error } from './error-handling.js';
import { UNINITIALIZED } from '../../constants.js';
import { captured_signals } from './legacy.js';
import { without_reactive_context } from './dom/elements/bindings/shared.js';
import { set_signal_status, update_derived_status } from './reactivity/status.js';
export let is_updating_effect = false;
/** @param {boolean} value */
export function set_is_updating_effect(value) {
is_updating_effect = value;
}
export let is_destroying_effect = false;
/** @param {boolean} value */
export function set_is_destroying_effect(value) {
is_destroying_effect = value;
}
/** @type {null | Reaction} */
export let active_reaction = null;
export let untracking = false;
/** @param {null | Reaction} reaction */
export function set_active_reaction(reaction) {
active_reaction = reaction;
}
/** @type {null | Effect} */
export let active_effect = null;
/** @param {null | Effect} effect */
export function set_active_effect(effect) {
active_effect = effect;
}
/**
* When sources are created within a reaction, reading and writing
* them within that reaction should not cause a re-run
* @type {null | Source[]}
*/
export let current_sources = null;
/** @param {Value} value */
export function push_reaction_value(value) {
if (active_reaction !== null && (!async_mode_flag || (active_reaction.f & DERIVED) !== 0)) {
if (current_sources === null) {
current_sources = [value];
} else {
current_sources.push(value);
}
}
}
/**
* The dependencies of the reaction that is currently being executed. In many cases,
* the dependencies are unchanged between runs, and so this will be `null` unless
* and until a new dependency is accessed — we track this via `skipped_deps`
* @type {null | Value[]}
*/
let new_deps = null;
let skipped_deps = 0;
/**
* Tracks writes that the effect it's executed in doesn't listen to yet,
* so that the dependency can be added to the effect later on if it then reads it
* @type {null | Source[]}
*/
export let untracked_writes = null;
/** @param {null | Source[]} value */
export function set_untracked_writes(value) {
untracked_writes = value;
}
/**
* @type {number} Used by sources and deriveds for handling updates.
* Version starts from 1 so that unowned deriveds differentiate between a created effect and a run one for tracing
**/
export let write_version = 1;
/** @type {number} Used to version each read of a source of derived to avoid duplicating depedencies inside a reaction */
let read_version = 0;
export let update_version = read_version;
/** @param {number} value */
export function set_update_version(value) {
update_version = value;
}
export function increment_write_version() {
return ++write_version;
}
/**
* Determines whether a derived or effect is dirty.
* If it is MAYBE_DIRTY, will set the status to CLEAN
* @param {Reaction} reaction
* @returns {boolean}
*/
export function is_dirty(reaction) {
var flags = reaction.f;
if ((flags & DIRTY) !== 0) {
return true;
}
if (flags & DERIVED) {
reaction.f &= ~WAS_MARKED;
}
if ((flags & MAYBE_DIRTY) !== 0) {
var dependencies = /** @type {Value[]} */ (reaction.deps);
var length = dependencies.length;
for (var i = 0; i < length; i++) {
var dependency = dependencies[i];
if (is_dirty(/** @type {Derived} */ (dependency))) {
update_derived(/** @type {Derived} */ (dependency));
}
if (dependency.wv > reaction.wv) {
return true;
}
}
if (
(flags & CONNECTED) !== 0 &&
// During time traveling we don't want to reset the status so that
// traversal of the graph in the other batches still happens
batch_values === null
) {
set_signal_status(reaction, CLEAN);
}
}
return false;
}
/**
* @param {Value} signal
* @param {Effect} effect
* @param {boolean} [root]
*/
function schedule_possible_effect_self_invalidation(signal, effect, root = true) {
var reactions = signal.reactions;
if (reactions === null) return;
if (!async_mode_flag && current_sources?.includes(signal)) {
return;
}
for (var i = 0; i < reactions.length; i++) {
var reaction = reactions[i];
if ((reaction.f & DERIVED) !== 0) {
schedule_possible_effect_self_invalidation(/** @type {Derived} */ (reaction), effect, false);
} else if (effect === reaction) {
if (root) {
set_signal_status(reaction, DIRTY);
} else if ((reaction.f & CLEAN) !== 0) {
set_signal_status(reaction, MAYBE_DIRTY);
}
schedule_effect(/** @type {Effect} */ (reaction));
}
}
}
/** @param {Reaction} reaction */
export function update_reaction(reaction) {
var previous_deps = new_deps;
var previous_skipped_deps = skipped_deps;
var previous_untracked_writes = untracked_writes;
var previous_reaction = active_reaction;
var previous_sources = current_sources;
var previous_component_context = component_context;
var previous_untracking = untracking;
var previous_update_version = update_version;
var flags = reaction.f;
new_deps = /** @type {null | Value[]} */ (null);
skipped_deps = 0;
untracked_writes = null;
active_reaction = (flags & (BRANCH_EFFECT | ROOT_EFFECT)) === 0 ? reaction : null;
current_sources = null;
set_component_context(reaction.ctx);
untracking = false;
update_version = ++read_version;
if (reaction.ac !== null) {
without_reactive_context(() => {
/** @type {AbortController} */ (reaction.ac).abort(STALE_REACTION);
});
reaction.ac = null;
}
try {
reaction.f |= REACTION_IS_UPDATING;
var fn = /** @type {Function} */ (reaction.fn);
var result = fn();
var deps = reaction.deps;
if (new_deps !== null) {
var i;
remove_reactions(reaction, skipped_deps);
if (deps !== null && skipped_deps > 0) {
deps.length = skipped_deps + new_deps.length;
for (i = 0; i < new_deps.length; i++) {
deps[skipped_deps + i] = new_deps[i];
}
} else {
reaction.deps = deps = new_deps;
}
if (effect_tracking() && (reaction.f & CONNECTED) !== 0) {
for (i = skipped_deps; i < deps.length; i++) {
(deps[i].reactions ??= []).push(reaction);
}
}
} else if (deps !== null && skipped_deps < deps.length) {
remove_reactions(reaction, skipped_deps);
deps.length = skipped_deps;
}
// If we're inside an effect and we have untracked writes, then we need to
// ensure that if any of those untracked writes result in re-invalidation
// of the current effect, then that happens accordingly
if (
is_runes() &&
untracked_writes !== null &&
!untracking &&
deps !== null &&
(reaction.f & (DERIVED | MAYBE_DIRTY | DIRTY)) === 0
) {
for (i = 0; i < /** @type {Source[]} */ (untracked_writes).length; i++) {
schedule_possible_effect_self_invalidation(
untracked_writes[i],
/** @type {Effect} */ (reaction)
);
}
}
// If we are returning to an previous reaction then
// we need to increment the read version to ensure that
// any dependencies in this reaction aren't marked with
// the same version
if (previous_reaction !== null && previous_reaction !== reaction) {
read_version++;
// update the `rv` of the previous reaction's deps — both existing and new —
// so that they are not added again
if (previous_reaction.deps !== null) {
for (let i = 0; i < previous_skipped_deps; i += 1) {
previous_reaction.deps[i].rv = read_version;
}
}
if (previous_deps !== null) {
for (const dep of previous_deps) {
dep.rv = read_version;
}
}
if (untracked_writes !== null) {
if (previous_untracked_writes === null) {
previous_untracked_writes = untracked_writes;
} else {
previous_untracked_writes.push(.../** @type {Source[]} */ (untracked_writes));
}
}
}
if ((reaction.f & ERROR_VALUE) !== 0) {
reaction.f ^= ERROR_VALUE;
}
return result;
} catch (error) {
return handle_error(error);
} finally {
reaction.f ^= REACTION_IS_UPDATING;
new_deps = previous_deps;
skipped_deps = previous_skipped_deps;
untracked_writes = previous_untracked_writes;
active_reaction = previous_reaction;
current_sources = previous_sources;
set_component_context(previous_component_context);
untracking = previous_untracking;
update_version = previous_update_version;
}
}
/**
* @template V
* @param {Reaction} signal
* @param {Value<V>} dependency
* @returns {void}
*/
function remove_reaction(signal, dependency) {
let reactions = dependency.reactions;
if (reactions !== null) {
var index = index_of.call(reactions, signal);
if (index !== -1) {
var new_length = reactions.length - 1;
if (new_length === 0) {
reactions = dependency.reactions = null;
} else {
// Swap with last element and then remove.
reactions[index] = reactions[new_length];
reactions.pop();
}
}
}
// If the derived has no reactions, then we can disconnect it from the graph,
// allowing it to either reconnect in the future, or be GC'd by the VM.
if (
reactions === null &&
(dependency.f & DERIVED) !== 0 &&
// Destroying a child effect while updating a parent effect can cause a dependency to appear
// to be unused, when in fact it is used by the currently-updating parent. Checking `new_deps`
// allows us to skip the expensive work of disconnecting and immediately reconnecting it
(new_deps === null || !new_deps.includes(dependency))
) {
var derived = /** @type {Derived} */ (dependency);
// If we are working with a derived that is owned by an effect, then mark it as being
// disconnected and remove the mark flag, as it cannot be reliably removed otherwise
if ((derived.f & CONNECTED) !== 0) {
derived.f ^= CONNECTED;
derived.f &= ~WAS_MARKED;
}
update_derived_status(derived);
// Disconnect any reactions owned by this reaction
destroy_derived_effects(derived);
remove_reactions(derived, 0);
}
}
/**
* @param {Reaction} signal
* @param {number} start_index
* @returns {void}
*/
export function remove_reactions(signal, start_index) {
var dependencies = signal.deps;
if (dependencies === null) return;
for (var i = start_index; i < dependencies.length; i++) {
remove_reaction(signal, dependencies[i]);
}
}
/**
* @param {Effect} effect
* @returns {void}
*/
export function update_effect(effect) {
var flags = effect.f;
if ((flags & DESTROYED) !== 0) {
return;
}
set_signal_status(effect, CLEAN);
var previous_effect = active_effect;
var was_updating_effect = is_updating_effect;
active_effect = effect;
is_updating_effect = true;
if (DEV) {
var previous_component_fn = dev_current_component_function;
set_dev_current_component_function(effect.component_function);
var previous_stack = /** @type {any} */ (dev_stack);
// only block effects have a dev stack, keep the current one otherwise
set_dev_stack(effect.dev_stack ?? dev_stack);
}
try {
if ((flags & (BLOCK_EFFECT | MANAGED_EFFECT)) !== 0) {
destroy_block_effect_children(effect);
} else {
destroy_effect_children(effect);
}
execute_effect_teardown(effect);
var teardown = update_reaction(effect);
effect.teardown = typeof teardown === 'function' ? teardown : null;
effect.wv = write_version;
// In DEV, increment versions of any sources that were written to during the effect,
// so that they are correctly marked as dirty when the effect re-runs
if (DEV && tracing_mode_flag && (effect.f & DIRTY) !== 0 && effect.deps !== null) {
for (var dep of effect.deps) {
if (dep.set_during_effect) {
dep.wv = increment_write_version();
dep.set_during_effect = false;
}
}
}
} finally {
is_updating_effect = was_updating_effect;
active_effect = previous_effect;
if (DEV) {
set_dev_current_component_function(previous_component_fn);
set_dev_stack(previous_stack);
}
}
}
/**
* Returns a promise that resolves once any pending state changes have been applied.
* @returns {Promise<void>}
*/
export async function tick() {
if (async_mode_flag) {
return new Promise((f) => {
// Race them against each other - in almost all cases requestAnimationFrame will fire first,
// but e.g. in case the window is not focused or a view transition happens, requestAnimationFrame
// will be delayed and setTimeout helps us resolve fast enough in that case
requestAnimationFrame(() => f());
setTimeout(() => f());
});
}
await Promise.resolve();
// By calling flushSync we guarantee that any pending state changes are applied after one tick.
// TODO look into whether we can make flushing subsequent updates synchronously in the future.
flushSync();
}
/**
* Returns a promise that resolves once any state changes, and asynchronous work resulting from them,
* have resolved and the DOM has been updated
* @returns {Promise<void>}
* @since 5.36
*/
export function settled() {
return Batch.ensure().settled();
}
/**
* @template V
* @param {Value<V>} signal
* @returns {V}
*/
export function get(signal) {
var flags = signal.f;
var is_derived = (flags & DERIVED) !== 0;
captured_signals?.add(signal);
// Register the dependency on the current reaction signal.
if (active_reaction !== null && !untracking) {
// if we're in a derived that is being read inside an _async_ derived,
// it's possible that the effect was already destroyed. In this case,
// we don't add the dependency, because that would create a memory leak
var destroyed = active_effect !== null && (active_effect.f & DESTROYED) !== 0;
if (!destroyed && !current_sources?.includes(signal)) {
var deps = active_reaction.deps;
if ((active_reaction.f & REACTION_IS_UPDATING) !== 0) {
// we're in the effect init/update cycle
if (signal.rv < read_version) {
signal.rv = read_version;
// If the signal is accessing the same dependencies in the same
// order as it did last time, increment `skipped_deps`
// rather than updating `new_deps`, which creates GC cost
if (new_deps === null && deps !== null && deps[skipped_deps] === signal) {
skipped_deps++;
} else if (new_deps === null) {
new_deps = [signal];
} else {
new_deps.push(signal);
}
}
} else {
// we're adding a dependency outside the init/update cycle
// (i.e. after an `await`)
(active_reaction.deps ??= []).push(signal);
var reactions = signal.reactions;
if (reactions === null) {
signal.reactions = [active_reaction];
} else if (!reactions.includes(active_reaction)) {
reactions.push(active_reaction);
}
}
}
}
if (DEV) {
// TODO reinstate this, but make it actually work
// if (current_async_effect) {
// var tracking = (current_async_effect.f & REACTION_IS_UPDATING) !== 0;
// var was_read = current_async_effect.deps?.includes(signal);
// if (!tracking && !untracking && !was_read) {
// w.await_reactivity_loss(/** @type {string} */ (signal.label));
// var trace = get_error('traced at');
// // eslint-disable-next-line no-console
// if (trace) console.warn(trace);
// }
// }
recent_async_deriveds.delete(signal);
if (
tracing_mode_flag &&
!untracking &&
tracing_expressions !== null &&
active_reaction !== null &&
tracing_expressions.reaction === active_reaction
) {
// Used when mapping state between special blocks like `each`
if (signal.trace) {
signal.trace();
} else {
var trace = get_error('traced at');
if (trace) {
var entry = tracing_expressions.entries.get(signal);
if (entry === undefined) {
entry = { traces: [] };
tracing_expressions.entries.set(signal, entry);
}
var last = entry.traces[entry.traces.length - 1];
// traces can be duplicated, e.g. by `snapshot` invoking both
// both `getOwnPropertyDescriptor` and `get` traps at once
if (trace.stack !== last?.stack) {
entry.traces.push(trace);
}
}
}
}
}
if (is_destroying_effect && old_values.has(signal)) {
return old_values.get(signal);
}
if (is_derived) {
var derived = /** @type {Derived} */ (signal);
if (is_destroying_effect) {
var value = derived.v;
// if the derived is dirty and has reactions, or depends on the values that just changed, re-execute
// (a derived can be maybe_dirty due to the effect destroy removing its last reaction)
if (
((derived.f & CLEAN) === 0 && derived.reactions !== null) ||
depends_on_old_values(derived)
) {
value = execute_derived(derived);
}
old_values.set(derived, value);
return value;
}
// connect disconnected deriveds if we are reading them inside an effect,
// or inside another derived that is already connected
var should_connect =
(derived.f & CONNECTED) === 0 &&
!untracking &&
active_reaction !== null &&
(is_updating_effect || (active_reaction.f & CONNECTED) !== 0);
var is_new = derived.deps === null;
if (is_dirty(derived)) {
if (should_connect) {
// set the flag before `update_derived`, so that the derived
// is added as a reaction to its dependencies
derived.f |= CONNECTED;
}
update_derived(derived);
}
if (should_connect && !is_new) {
reconnect(derived);
}
}
if (batch_values?.has(signal)) {
return batch_values.get(signal);
}
if ((signal.f & ERROR_VALUE) !== 0) {
throw signal.v;
}
return signal.v;
}
/**
* (Re)connect a disconnected derived, so that it is notified
* of changes in `mark_reactions`
* @param {Derived} derived
*/
function reconnect(derived) {
if (derived.deps === null) return;
derived.f |= CONNECTED;
for (const dep of derived.deps) {
(dep.reactions ??= []).push(derived);
if ((dep.f & DERIVED) !== 0 && (dep.f & CONNECTED) === 0) {
reconnect(/** @type {Derived} */ (dep));
}
}
}
/** @param {Derived} derived */
function depends_on_old_values(derived) {
if (derived.v === UNINITIALIZED) return true; // we don't know, so assume the worst
if (derived.deps === null) return false;
for (const dep of derived.deps) {
if (old_values.has(dep)) {
return true;
}
if ((dep.f & DERIVED) !== 0 && depends_on_old_values(/** @type {Derived} */ (dep))) {
return true;
}
}
return false;
}
/**
* Like `get`, but checks for `undefined`. Used for `var` declarations because they can be accessed before being declared
* @template V
* @param {Value<V> | undefined} signal
* @returns {V | undefined}
*/
export function safe_get(signal) {
return signal && get(signal);
}
/**
* When used inside a [`$derived`](https://svelte.dev/docs/svelte/$derived) or [`$effect`](https://svelte.dev/docs/svelte/$effect),
* any state read inside `fn` will not be treated as a dependency.
*
* ```ts
* $effect(() => {
* // this will run when `data` changes, but not when `time` changes
* save(data, {
* timestamp: untrack(() => time)
* });
* });
* ```
* @template T
* @param {() => T} fn
* @returns {T}
*/
export function untrack(fn) {
var previous_untracking = untracking;
try {
untracking = true;
return fn();
} finally {
untracking = previous_untracking;
}
}
/**
* @param {Record<string | symbol, unknown>} obj
* @param {Array<string | symbol>} keys
* @returns {Record<string | symbol, unknown>}
*/
export function exclude_from_object(obj, keys) {
/** @type {Record<string | symbol, unknown>} */
var result = {};
for (var key in obj) {
if (!keys.includes(key)) {
result[key] = obj[key];
}
}
for (var symbol of Object.getOwnPropertySymbols(obj)) {
if (Object.propertyIsEnumerable.call(obj, symbol) && !keys.includes(symbol)) {
result[symbol] = obj[symbol];
}
}
return result;
}
/**
* Possibly traverse an object and read all its properties so that they're all reactive in case this is `$state`.
* Does only check first level of an object for performance reasons (heuristic should be good for 99% of all cases).
* @param {any} value
* @returns {void}
*/
export function deep_read_state(value) {
if (typeof value !== 'object' || !value || value instanceof EventTarget) {
return;
}
if (STATE_SYMBOL in value) {
deep_read(value);
} else if (!Array.isArray(value)) {
for (let key in value) {
const prop = value[key];
if (typeof prop === 'object' && prop && STATE_SYMBOL in prop) {
deep_read(prop);
}
}
}
}
/**
* Deeply traverse an object and read all its properties
* so that they're all reactive in case this is `$state`
* @param {any} value
* @param {Set<any>} visited
* @returns {void}
*/
export function deep_read(value, visited = new Set()) {
if (
typeof value === 'object' &&
value !== null &&
// We don't want to traverse DOM elements
!(value instanceof EventTarget) &&
!visited.has(value)
) {
visited.add(value);
// When working with a possible SvelteDate, this
// will ensure we capture changes to it.
if (value instanceof Date) {
value.getTime();
}
for (let key in value) {
try {
deep_read(value[key], visited);
} catch (e) {
// continue
}
}
const proto = get_prototype_of(value);
if (
proto !== Object.prototype &&
proto !== Array.prototype &&
proto !== Map.prototype &&
proto !== Set.prototype &&
proto !== Date.prototype
) {
const descriptors = get_descriptors(proto);
for (let key in descriptors) {
const get = descriptors[key].get;
if (get) {
try {
get.call(value);
} catch (e) {
// continue
}
}
}
}
}
}
+16
View File
@@ -0,0 +1,16 @@
/** @import { Raf } from '#client' */
import { noop } from '../shared/utils.js';
import { BROWSER } from 'esm-env';
const now = BROWSER ? () => performance.now() : () => Date.now();
/** @type {Raf} */
export const raf = {
// don't access requestAnimationFrame eagerly outside method
// this allows basic testing of user code without JSDOM
// bunder will eval and remove ternary when the user's app is built
tick: /** @param {any} _ */ (_) => (BROWSER ? requestAnimationFrame : noop)(_),
now: () => now(),
tasks: new Set()
};
+87
View File
@@ -0,0 +1,87 @@
import { dev_current_component_function } from './context.js';
import { is_array } from '../shared/utils.js';
import * as e from './errors.js';
import { FILENAME } from '../../constants.js';
import { render_effect } from './reactivity/effects.js';
import * as w from './warnings.js';
import { capture_store_binding } from './reactivity/store.js';
import { run_after_blockers } from './reactivity/async.js';
/**
* @param {() => any} collection
* @param {(item: any, index: number) => string} key_fn
* @returns {void}
*/
export function validate_each_keys(collection, key_fn) {
render_effect(() => {
const keys = new Map();
const maybe_array = collection();
const array = is_array(maybe_array)
? maybe_array
: maybe_array == null
? []
: Array.from(maybe_array);
const length = array.length;
for (let i = 0; i < length; i++) {
const key = key_fn(array[i], i);
if (keys.has(key)) {
const a = String(keys.get(key));
const b = String(i);
/** @type {string | null} */
let k = String(key);
if (k.startsWith('[object ')) k = null;
e.each_key_duplicate(a, b, k);
}
keys.set(key, i);
}
});
}
/**
* @param {string} binding
* @param {Array<Promise<void>>} blockers
* @param {() => Record<string, any>} get_object
* @param {() => string} get_property
* @param {number} line
* @param {number} column
*/
export function validate_binding(binding, blockers, get_object, get_property, line, column) {
run_after_blockers(blockers, () => {
var warned = false;
var filename = dev_current_component_function?.[FILENAME];
render_effect(() => {
if (warned) return;
var [object, is_store_sub] = capture_store_binding(get_object);
if (is_store_sub) return;
var property = get_property();
var ran = false;
// by making the (possibly false, but it would be an extreme edge case) assumption
// that a getter has a corresponding setter, we can determine if a property is
// reactive by seeing if this effect has dependencies
var effect = render_effect(() => {
if (ran) return;
// eslint-disable-next-line @typescript-eslint/no-unused-expressions
object[property];
});
ran = true;
if (effect.deps === null) {
var location = `${filename}:${line}:${column}`;
w.binding_property_non_reactive(binding, location);
warned = true;
}
});
});
}
+271
View File
@@ -0,0 +1,271 @@
/* 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';
/**
* Assignment to `%property%` property (%location%) will evaluate to the right-hand side, not the value of `%property%` following the assignment. This may result in unexpected behaviour.
* @param {string} property
* @param {string} location
*/
export function assignment_value_stale(property, location) {
if (DEV) {
console.warn(`%c[svelte] assignment_value_stale\n%cAssignment to \`${property}\` property (${location}) will evaluate to the right-hand side, not the value of \`${property}\` following the assignment. This may result in unexpected behaviour.\nhttps://svelte.dev/e/assignment_value_stale`, bold, normal);
} else {
console.warn(`https://svelte.dev/e/assignment_value_stale`);
}
}
/**
* Detected reactivity loss when reading `%name%`. This happens when state is read in an async function after an earlier `await`
* @param {string} name
*/
export function await_reactivity_loss(name) {
if (DEV) {
console.warn(`%c[svelte] await_reactivity_loss\n%cDetected reactivity loss when reading \`${name}\`. This happens when state is read in an async function after an earlier \`await\`\nhttps://svelte.dev/e/await_reactivity_loss`, bold, normal);
} else {
console.warn(`https://svelte.dev/e/await_reactivity_loss`);
}
}
/**
* An async derived, `%name%` (%location%) was not read immediately after it resolved. This often indicates an unnecessary waterfall, which can slow down your app
* @param {string} name
* @param {string} location
*/
export function await_waterfall(name, location) {
if (DEV) {
console.warn(`%c[svelte] await_waterfall\n%cAn async derived, \`${name}\` (${location}) was not read immediately after it resolved. This often indicates an unnecessary waterfall, which can slow down your app\nhttps://svelte.dev/e/await_waterfall`, bold, normal);
} else {
console.warn(`https://svelte.dev/e/await_waterfall`);
}
}
/**
* `%binding%` (%location%) is binding to a non-reactive property
* @param {string} binding
* @param {string | undefined | null} [location]
*/
export function binding_property_non_reactive(binding, location) {
if (DEV) {
console.warn(
`%c[svelte] binding_property_non_reactive\n%c${location
? `\`${binding}\` (${location}) is binding to a non-reactive property`
: `\`${binding}\` is binding to a non-reactive property`}\nhttps://svelte.dev/e/binding_property_non_reactive`,
bold,
normal
);
} else {
console.warn(`https://svelte.dev/e/binding_property_non_reactive`);
}
}
/**
* Your `console.%method%` contained `$state` proxies. Consider using `$inspect(...)` or `$state.snapshot(...)` instead
* @param {string} method
*/
export function console_log_state(method) {
if (DEV) {
console.warn(`%c[svelte] console_log_state\n%cYour \`console.${method}\` contained \`$state\` proxies. Consider using \`$inspect(...)\` or \`$state.snapshot(...)\` instead\nhttps://svelte.dev/e/console_log_state`, bold, normal);
} else {
console.warn(`https://svelte.dev/e/console_log_state`);
}
}
/**
* %handler% should be a function. Did you mean to %suggestion%?
* @param {string} handler
* @param {string} suggestion
*/
export function event_handler_invalid(handler, suggestion) {
if (DEV) {
console.warn(`%c[svelte] event_handler_invalid\n%c${handler} should be a function. Did you mean to ${suggestion}?\nhttps://svelte.dev/e/event_handler_invalid`, bold, normal);
} else {
console.warn(`https://svelte.dev/e/event_handler_invalid`);
}
}
/**
* Expected to find a hydratable with key `%key%` during hydration, but did not.
* @param {string} key
*/
export function hydratable_missing_but_expected(key) {
if (DEV) {
console.warn(`%c[svelte] hydratable_missing_but_expected\n%cExpected to find a hydratable with key \`${key}\` during hydration, but did not.\nhttps://svelte.dev/e/hydratable_missing_but_expected`, bold, normal);
} else {
console.warn(`https://svelte.dev/e/hydratable_missing_but_expected`);
}
}
/**
* The `%attribute%` attribute on `%html%` changed its value between server and client renders. The client value, `%value%`, will be ignored in favour of the server value
* @param {string} attribute
* @param {string} html
* @param {string} value
*/
export function hydration_attribute_changed(attribute, html, value) {
if (DEV) {
console.warn(`%c[svelte] hydration_attribute_changed\n%cThe \`${attribute}\` attribute on \`${html}\` changed its value between server and client renders. The client value, \`${value}\`, will be ignored in favour of the server value\nhttps://svelte.dev/e/hydration_attribute_changed`, bold, normal);
} else {
console.warn(`https://svelte.dev/e/hydration_attribute_changed`);
}
}
/**
* The value of an `{@html ...}` block %location% changed between server and client renders. The client value will be ignored in favour of the server value
* @param {string | undefined | null} [location]
*/
export function hydration_html_changed(location) {
if (DEV) {
console.warn(
`%c[svelte] hydration_html_changed\n%c${location
? `The value of an \`{@html ...}\` block ${location} changed between server and client renders. The client value will be ignored in favour of the server value`
: 'The value of an `{@html ...}` block changed between server and client renders. The client value will be ignored in favour of the server value'}\nhttps://svelte.dev/e/hydration_html_changed`,
bold,
normal
);
} else {
console.warn(`https://svelte.dev/e/hydration_html_changed`);
}
}
/**
* Hydration failed because the initial UI does not match what was rendered on the server. The error occurred near %location%
* @param {string | undefined | null} [location]
*/
export function hydration_mismatch(location) {
if (DEV) {
console.warn(
`%c[svelte] hydration_mismatch\n%c${location
? `Hydration failed because the initial UI does not match what was rendered on the server. The error occurred near ${location}`
: 'Hydration failed because the initial UI does not match what was rendered on the server'}\nhttps://svelte.dev/e/hydration_mismatch`,
bold,
normal
);
} else {
console.warn(`https://svelte.dev/e/hydration_mismatch`);
}
}
/**
* The `render` function passed to `createRawSnippet` should return HTML for a single element
*/
export function invalid_raw_snippet_render() {
if (DEV) {
console.warn(`%c[svelte] invalid_raw_snippet_render\n%cThe \`render\` function passed to \`createRawSnippet\` should return HTML for a single element\nhttps://svelte.dev/e/invalid_raw_snippet_render`, bold, normal);
} else {
console.warn(`https://svelte.dev/e/invalid_raw_snippet_render`);
}
}
/**
* Detected a migrated `$:` reactive block in `%filename%` that both accesses and updates the same reactive value. This may cause recursive updates when converted to an `$effect`.
* @param {string} filename
*/
export function legacy_recursive_reactive_block(filename) {
if (DEV) {
console.warn(`%c[svelte] legacy_recursive_reactive_block\n%cDetected a migrated \`$:\` reactive block in \`${filename}\` that both accesses and updates the same reactive value. This may cause recursive updates when converted to an \`$effect\`.\nhttps://svelte.dev/e/legacy_recursive_reactive_block`, bold, normal);
} else {
console.warn(`https://svelte.dev/e/legacy_recursive_reactive_block`);
}
}
/**
* Tried to unmount a component that was not mounted
*/
export function lifecycle_double_unmount() {
if (DEV) {
console.warn(`%c[svelte] lifecycle_double_unmount\n%cTried to unmount a component that was not mounted\nhttps://svelte.dev/e/lifecycle_double_unmount`, bold, normal);
} else {
console.warn(`https://svelte.dev/e/lifecycle_double_unmount`);
}
}
/**
* %parent% passed property `%prop%` to %child% with `bind:`, but its parent component %owner% did not declare `%prop%` as a binding. Consider creating a binding between %owner% and %parent% (e.g. `bind:%prop%={...}` instead of `%prop%={...}`)
* @param {string} parent
* @param {string} prop
* @param {string} child
* @param {string} owner
*/
export function ownership_invalid_binding(parent, prop, child, owner) {
if (DEV) {
console.warn(`%c[svelte] ownership_invalid_binding\n%c${parent} passed property \`${prop}\` to ${child} with \`bind:\`, but its parent component ${owner} did not declare \`${prop}\` as a binding. Consider creating a binding between ${owner} and ${parent} (e.g. \`bind:${prop}={...}\` instead of \`${prop}={...}\`)\nhttps://svelte.dev/e/ownership_invalid_binding`, bold, normal);
} else {
console.warn(`https://svelte.dev/e/ownership_invalid_binding`);
}
}
/**
* Mutating unbound props (`%name%`, at %location%) is strongly discouraged. Consider using `bind:%prop%={...}` in %parent% (or using a callback) instead
* @param {string} name
* @param {string} location
* @param {string} prop
* @param {string} parent
*/
export function ownership_invalid_mutation(name, location, prop, parent) {
if (DEV) {
console.warn(`%c[svelte] ownership_invalid_mutation\n%cMutating unbound props (\`${name}\`, at ${location}) is strongly discouraged. Consider using \`bind:${prop}={...}\` in ${parent} (or using a callback) instead\nhttps://svelte.dev/e/ownership_invalid_mutation`, bold, normal);
} else {
console.warn(`https://svelte.dev/e/ownership_invalid_mutation`);
}
}
/**
* The `value` property of a `<select multiple>` element should be an array, but it received a non-array value. The selection will be kept as is.
*/
export function select_multiple_invalid_value() {
if (DEV) {
console.warn(`%c[svelte] select_multiple_invalid_value\n%cThe \`value\` property of a \`<select multiple>\` element should be an array, but it received a non-array value. The selection will be kept as is.\nhttps://svelte.dev/e/select_multiple_invalid_value`, bold, normal);
} else {
console.warn(`https://svelte.dev/e/select_multiple_invalid_value`);
}
}
/**
* Reactive `$state(...)` proxies and the values they proxy have different identities. Because of this, comparisons with `%operator%` will produce unexpected results
* @param {string} operator
*/
export function state_proxy_equality_mismatch(operator) {
if (DEV) {
console.warn(`%c[svelte] state_proxy_equality_mismatch\n%cReactive \`$state(...)\` proxies and the values they proxy have different identities. Because of this, comparisons with \`${operator}\` will produce unexpected results\nhttps://svelte.dev/e/state_proxy_equality_mismatch`, bold, normal);
} else {
console.warn(`https://svelte.dev/e/state_proxy_equality_mismatch`);
}
}
/**
* Tried to unmount a state proxy, rather than a component
*/
export function state_proxy_unmount() {
if (DEV) {
console.warn(`%c[svelte] state_proxy_unmount\n%cTried to unmount a state proxy, rather than a component\nhttps://svelte.dev/e/state_proxy_unmount`, bold, normal);
} else {
console.warn(`https://svelte.dev/e/state_proxy_unmount`);
}
}
/**
* A `<svelte:boundary>` `reset` function only resets the boundary the first time it is called
*/
export function svelte_boundary_reset_noop() {
if (DEV) {
console.warn(`%c[svelte] svelte_boundary_reset_noop\n%cA \`<svelte:boundary>\` \`reset\` function only resets the boundary the first time it is called\nhttps://svelte.dev/e/svelte_boundary_reset_noop`, bold, normal);
} else {
console.warn(`https://svelte.dev/e/svelte_boundary_reset_noop`);
}
}
/**
* The `slide` transition does not work correctly for elements with `display: %value%`
* @param {string} value
*/
export function transition_slide_display(value) {
if (DEV) {
console.warn(`%c[svelte] transition_slide_display\n%cThe \`slide\` transition does not work correctly for elements with \`display: ${value}\`\nhttps://svelte.dev/e/transition_slide_display`, bold, normal);
} else {
console.warn(`https://svelte.dev/e/transition_slide_display`);
}
}
+6
View File
@@ -0,0 +1,6 @@
import { PUBLIC_VERSION } from '../version.js';
if (typeof window !== 'undefined') {
// @ts-expect-error
((window.__svelte ??= {}).v ??= new Set()).add(PUBLIC_VERSION);
}
+3
View File
@@ -0,0 +1,3 @@
import { enable_async_mode_flag } from './index.js';
enable_async_mode_flag();
+23
View File
@@ -0,0 +1,23 @@
/** True if experimental.async=true */
export let async_mode_flag = false;
/** True if we're not certain that we only have Svelte 5 code in the compilation */
export let legacy_mode_flag = false;
/** True if $inspect.trace is used */
export let tracing_mode_flag = false;
export function enable_async_mode_flag() {
async_mode_flag = true;
}
/** ONLY USE THIS DURING TESTING */
export function disable_async_mode_flag() {
async_mode_flag = false;
}
export function enable_legacy_mode_flag() {
legacy_mode_flag = true;
}
export function enable_tracing_mode_flag() {
tracing_mode_flag = true;
}
+3
View File
@@ -0,0 +1,3 @@
import { enable_legacy_mode_flag } from './index.js';
enable_legacy_mode_flag();
+3
View File
@@ -0,0 +1,3 @@
import { enable_tracing_mode_flag } from './index.js';
enable_tracing_mode_flag();
+5
View File
@@ -0,0 +1,5 @@
// TODO we may, on a best-effort basis, reimplement some of the legacy private APIs here so that certain libraries continue to work. Those APIs will be marked as deprecated (and should noisily warn the user) and will be removed in a future version of Svelte.
throw new Error(
`Your application, or one of its dependencies, imported from 'svelte/internal', which was a private module used by Svelte 4 components that no longer exists in Svelte 5. It is not intended to be public API. If you're a library author and you used 'svelte/internal' deliberately, please raise an issue on https://github.com/sveltejs/svelte/issues detailing your use case.`
);
+13
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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`);
}
}
+223
View File
@@ -0,0 +1,223 @@
import { escape_html } from '../../escaping.js';
import { clsx as _clsx } from 'clsx';
/**
* `<div translate={false}>` should be rendered as `<div translate="no">` and _not_
* `<div translate="false">`, which is equivalent to `<div translate="yes">`. There
* may be other odd cases that need to be added to this list in future
* @type {Record<string, Map<any, string>>}
*/
const replacements = {
translate: new Map([
[true, 'yes'],
[false, 'no']
])
};
/**
* @template V
* @param {string} name
* @param {V} value
* @param {boolean} [is_boolean]
* @returns {string}
*/
export function attr(name, value, is_boolean = false) {
// attribute hidden for values other than "until-found" behaves like a boolean attribute
if (name === 'hidden' && value !== 'until-found') {
is_boolean = true;
}
if (value == null || (!value && is_boolean)) return '';
const normalized = (name in replacements && replacements[name].get(value)) || value;
const assignment = is_boolean ? '' : `="${escape_html(normalized, true)}"`;
return ` ${name}${assignment}`;
}
/**
* Small wrapper around clsx to preserve Svelte's (weird) handling of falsy values.
* TODO Svelte 6 revisit this, and likely turn all falsy values into the empty string (what clsx also does)
* @param {any} value
*/
export function clsx(value) {
if (typeof value === 'object') {
return _clsx(value);
} else {
return value ?? '';
}
}
const whitespace = [...' \t\n\r\f\u00a0\u000b\ufeff'];
/**
* @param {any} value
* @param {string | null} [hash]
* @param {Record<string, boolean>} [directives]
* @returns {string | null}
*/
export function to_class(value, hash, directives) {
var classname = value == null ? '' : '' + value;
if (hash) {
classname = classname ? classname + ' ' + hash : hash;
}
if (directives) {
for (var key in directives) {
if (directives[key]) {
classname = classname ? classname + ' ' + key : key;
} else if (classname.length) {
var len = key.length;
var a = 0;
while ((a = classname.indexOf(key, a)) >= 0) {
var b = a + len;
if (
(a === 0 || whitespace.includes(classname[a - 1])) &&
(b === classname.length || whitespace.includes(classname[b]))
) {
classname = (a === 0 ? '' : classname.substring(0, a)) + classname.substring(b + 1);
} else {
a = b;
}
}
}
}
}
return classname === '' ? null : classname;
}
/**
*
* @param {Record<string,any>} styles
* @param {boolean} important
*/
function append_styles(styles, important = false) {
var separator = important ? ' !important;' : ';';
var css = '';
for (var key in styles) {
var value = styles[key];
if (value != null && value !== '') {
css += ' ' + key + ': ' + value + separator;
}
}
return css;
}
/**
* @param {string} name
* @returns {string}
*/
function to_css_name(name) {
if (name[0] !== '-' || name[1] !== '-') {
return name.toLowerCase();
}
return name;
}
/**
* @param {any} value
* @param {Record<string, any> | [Record<string, any>, Record<string, any>]} [styles]
* @returns {string | null}
*/
export function to_style(value, styles) {
if (styles) {
var new_style = '';
/** @type {Record<string,any> | undefined} */
var normal_styles;
/** @type {Record<string,any> | undefined} */
var important_styles;
if (Array.isArray(styles)) {
normal_styles = styles[0];
important_styles = styles[1];
} else {
normal_styles = styles;
}
if (value) {
value = String(value)
.replaceAll(/\s*\/\*.*?\*\/\s*/g, '')
.trim();
/** @type {boolean | '"' | "'"} */
var in_str = false;
var in_apo = 0;
var in_comment = false;
var reserved_names = [];
if (normal_styles) {
reserved_names.push(...Object.keys(normal_styles).map(to_css_name));
}
if (important_styles) {
reserved_names.push(...Object.keys(important_styles).map(to_css_name));
}
var start_index = 0;
var name_index = -1;
const len = value.length;
for (var i = 0; i < len; i++) {
var c = value[i];
if (in_comment) {
if (c === '/' && value[i - 1] === '*') {
in_comment = false;
}
} else if (in_str) {
if (in_str === c) {
in_str = false;
}
} else if (c === '/' && value[i + 1] === '*') {
in_comment = true;
} else if (c === '"' || c === "'") {
in_str = c;
} else if (c === '(') {
in_apo++;
} else if (c === ')') {
in_apo--;
}
if (!in_comment && in_str === false && in_apo === 0) {
if (c === ':' && name_index === -1) {
name_index = i;
} else if (c === ';' || i === len - 1) {
if (name_index !== -1) {
var name = to_css_name(value.substring(start_index, name_index).trim());
if (!reserved_names.includes(name)) {
if (c !== ';') {
i++;
}
var property = value.substring(start_index, i).trim();
new_style += ' ' + property + ';';
}
}
start_index = i + 1;
name_index = -1;
}
}
}
}
if (normal_styles) {
new_style += append_styles(normal_styles);
}
if (important_styles) {
new_style += append_styles(important_styles, true);
}
new_style = new_style.trim();
return new_style === '' ? null : new_style;
}
return value == null ? null : String(value);
}

Some files were not shown because too many files have changed in this diff Show More