Merge branch 'main' into $inspect.trace-source-tagging

pull/16060/head
ComputerGuy 3 months ago
commit c6d0db4309

@ -0,0 +1,5 @@
---
'svelte': patch
---
fix: reset `is_flushing` if `flushSync` is called and there's no scheduled effect

@ -833,9 +833,9 @@ Svelte 5 is more strict about the HTML structure and will throw a compiler error
Assignments to destructured parts of a `@const` declaration are no longer allowed. It was an oversight that this was ever allowed.
### :is(...) and :where(...) are scoped
### :is(...), :has(...), and :where(...) are scoped
Previously, Svelte did not analyse selectors inside `:is(...)` and `:where(...)`, effectively treating them as global. Svelte 5 analyses them in the context of the current component. As such, some selectors may now be treated as unused if they were relying on this treatment. To fix this, use `:global(...)` inside the `:is(...)/:where(...)` selectors.
Previously, Svelte did not analyse selectors inside `:is(...)`, `:has(...)`, and `:where(...)`, effectively treating them as global. Svelte 5 analyses them in the context of the current component. As such, some selectors may now be treated as unused if they were relying on this treatment. To fix this, use `:global(...)` inside the `:is(...)/:has(...)/:where(...)` selectors.
When using Tailwind's `@apply` directive, add a `:global` selector to preserve rules that use Tailwind-generated `:is(...)` selectors:

@ -1,5 +1,47 @@
# svelte
## 5.33.18
### Patch Changes
- chore: bump `esrap` dependency ([#16106](https://github.com/sveltejs/svelte/pull/16106))
- fix: destructuring state in ssr ([#16102](https://github.com/sveltejs/svelte/pull/16102))
## 5.33.17
### Patch Changes
- chore: update acorn parser `ecmaVersion` to parse import attributes ([#16098](https://github.com/sveltejs/svelte/pull/16098))
## 5.33.16
### Patch Changes
- fix: visit expression when destructuring state declarations ([#16081](https://github.com/sveltejs/svelte/pull/16081))
- fix: move xmlns attribute from SVGAttributes to to DOMAttributes ([#16080](https://github.com/sveltejs/svelte/pull/16080))
## 5.33.15
### Patch Changes
- fix: invoke parent boundary of deriveds that throw ([#16091](https://github.com/sveltejs/svelte/pull/16091))
## 5.33.14
### Patch Changes
- Revert "feat: enable TS autocomplete for Svelte HTML element definitions" ([#16063](https://github.com/sveltejs/svelte/pull/16063))
- fix: destructuring snippet arguments ([#16068](https://github.com/sveltejs/svelte/pull/16068))
## 5.33.13
### Patch Changes
- fix: avoid recursion error in `EachBlock` visitor ([#16058](https://github.com/sveltejs/svelte/pull/16058))
## 5.33.12
### Patch Changes

@ -463,6 +463,8 @@ export interface DOMAttributes<T extends EventTarget> {
'on:fullscreenerror'?: EventHandler<Event, T> | undefined | null;
onfullscreenerror?: EventHandler<Event, T> | undefined | null;
onfullscreenerrorcapture?: EventHandler<Event, T> | undefined | null;
xmlns?: string | undefined | null;
}
// All the WAI-ARIA 1.1 attributes from https://www.w3.org/TR/wai-aria-1.1/
@ -1809,7 +1811,6 @@ export interface SVGAttributes<T extends EventTarget> extends AriaAttributes, DO
'xlink:type'?: string | undefined | null;
'xml:base'?: string | undefined | null;
'xml:lang'?: string | undefined | null;
xmlns?: string | undefined | null;
'xmlns:xlink'?: string | undefined | null;
'xml:space'?: string | undefined | null;
y1?: number | string | undefined | null;
@ -2066,7 +2067,7 @@ export interface SvelteHTMLElements {
failed?: import('svelte').Snippet<[error: unknown, reset: () => void]>;
};
[name: string & {}]: { [name: string]: any };
[name: string]: { [name: string]: any };
}
export type ClassValue = string | import('clsx').ClassArray | import('clsx').ClassDictionary;

@ -2,7 +2,7 @@
"name": "svelte",
"description": "Cybernetically enhanced web apps",
"license": "MIT",
"version": "5.33.12",
"version": "5.33.18",
"type": "module",
"types": "./types/index.d.ts",
"engines": {
@ -171,7 +171,7 @@
"axobject-query": "^4.1.0",
"clsx": "^2.1.1",
"esm-env": "^1.2.1",
"esrap": "^1.4.6",
"esrap": "^1.4.8",
"is-reference": "^3.0.3",
"locate-character": "^3.0.0",
"magic-string": "^0.30.11",

@ -36,7 +36,7 @@ export function parse(source, typescript, is_script) {
ast = parser.parse(source, {
onComment,
sourceType: 'module',
ecmaVersion: 13,
ecmaVersion: 16,
locations: true
});
} finally {
@ -64,7 +64,7 @@ export function parse_expression_at(source, typescript, index) {
const ast = parser.parseExpressionAt(source, index, {
onComment,
sourceType: 'module',
ecmaVersion: 13,
ecmaVersion: 16,
locations: true
});

@ -77,6 +77,9 @@ export function EachBlock(node, context) {
* @returns {void}
*/
function collect_transitive_dependencies(binding, bindings) {
if (bindings.has(binding)) {
return;
}
bindings.add(binding);
if (binding.kind === 'legacy_reactive') {

@ -57,7 +57,7 @@ export function SnippetBlock(node, context) {
for (const path of paths) {
const name = /** @type {Identifier} */ (path.node).name;
const needs_derived = path.has_default_value; // to ensure that default value is only called once
const fn = b.thunk(/** @type {Expression} */ (context.visit(path.expression)));
const fn = b.thunk(/** @type {Expression} */ (context.visit(path.expression, child_state)));
declarations.push(b.let(path.node, needs_derived ? b.call('$.derived_safe_equal', fn) : fn));

@ -1,4 +1,4 @@
/** @import { BlockStatement, Statement, Expression } from 'estree' */
/** @import { BlockStatement, Statement, Expression, FunctionDeclaration, VariableDeclaration, ArrowFunctionExpression } from 'estree' */
/** @import { AST } from '#compiler' */
/** @import { ComponentContext } from '../types' */
import { dev } from '../../../../state.js';
@ -34,62 +34,61 @@ export function SvelteBoundary(node, context) {
const nodes = [];
/** @type {Statement[]} */
const external_statements = [];
const const_tags = [];
/** @type {Statement[]} */
const internal_statements = [];
const hoisted = [];
const snippets_visits = [];
// const tags need to live inside the boundary, but might also be referenced in hoisted snippets.
// to resolve this we cheat: we duplicate const tags inside snippets
for (const child of node.fragment.nodes) {
if (child.type === 'ConstTag') {
context.visit(child, { ...context.state, init: const_tags });
}
}
// Capture the `failed` implicit snippet prop
for (const child of node.fragment.nodes) {
if (child.type === 'SnippetBlock' && child.expression.name === 'failed') {
// we need to delay the visit of the snippets in case they access a ConstTag that is declared
// after the snippets so that the visitor for the const tag can be updated
snippets_visits.push(() => {
/** @type {Statement[]} */
const init = [];
context.visit(child, { ...context.state, init });
props.properties.push(b.prop('init', child.expression, child.expression));
external_statements.push(...init);
});
} else if (child.type === 'ConstTag') {
if (child.type === 'ConstTag') {
continue;
}
if (child.type === 'SnippetBlock') {
/** @type {Statement[]} */
const init = [];
context.visit(child, { ...context.state, init });
if (dev) {
// In dev we must separate the declarations from the code
// that eagerly evaluate the expression...
for (const statement of init) {
if (statement.type === 'VariableDeclaration') {
external_statements.push(statement);
} else {
internal_statements.push(statement);
}
}
} else {
external_statements.push(...init);
const statements = [];
context.visit(child, { ...context.state, init: statements });
const snippet = /** @type {VariableDeclaration} */ (statements[0]);
const snippet_fn = dev
? // @ts-expect-error we know this shape is correct
snippet.declarations[0].init.arguments[1]
: snippet.declarations[0].init;
snippet_fn.body.body.unshift(
...const_tags.filter((node) => node.type === 'VariableDeclaration')
);
hoisted.push(snippet);
if (child.expression.name === 'failed') {
props.properties.push(b.prop('init', child.expression, child.expression));
}
} else {
nodes.push(child);
continue;
}
}
snippets_visits.forEach((visit) => visit());
nodes.push(child);
}
const block = /** @type {BlockStatement} */ (context.visit({ ...node.fragment, nodes }));
if (dev && internal_statements.length) {
block.body.unshift(...internal_statements);
}
block.body.unshift(...const_tags);
const boundary = b.stmt(
b.call('$.boundary', context.state.node, props, b.arrow([b.id('$$anchor')], block))
);
context.state.template.push_comment();
context.state.init.push(
external_statements.length > 0 ? b.block([...external_statements, boundary]) : boundary
);
context.state.init.push(hoisted.length > 0 ? b.block([...hoisted, boundary]) : boundary);
}

@ -164,7 +164,7 @@ export function VariableDeclaration(node, context) {
const { inserts, paths } = extract_paths(declarator.id, tmp);
declarations.push(
b.declarator(tmp, value),
b.declarator(tmp, /** @type {Expression} */ (context.visit(value))),
...inserts.map(({ id, value }) => {
id.name = context.state.scope.generate('$$array');
context.state.transform[id.name] = { read: get_value };

@ -197,9 +197,13 @@ function create_state_declarators(declarator, scope, value) {
}
const tmp = b.id(scope.generate('tmp'));
const { paths } = extract_paths(declarator.id, tmp);
const { paths, inserts } = extract_paths(declarator.id, tmp);
return [
b.declarator(tmp, value), // TODO inject declarator for opts, so we can use it below
...inserts.map(({ id, value }) => {
id.name = scope.generate('$$array');
return b.declarator(id, value);
}),
...paths.map((path) => {
const value = path.expression;
return b.declarator(path.node, value);

@ -2,14 +2,13 @@
import { BOUNDARY_EFFECT, EFFECT_TRANSPARENT } from '#client/constants';
import { component_context, set_component_context } from '../../context.js';
import { invoke_error_boundary } from '../../error-handling.js';
import { block, branch, destroy_effect, pause_effect } from '../../reactivity/effects.js';
import {
active_effect,
active_reaction,
handle_error,
set_active_effect,
set_active_reaction,
reset_is_throwing_error
set_active_reaction
} from '../../runtime.js';
import {
hydrate_next,
@ -80,11 +79,17 @@ export function boundary(node, props, boundary_fn) {
with_boundary(boundary, () => {
is_creating_fallback = false;
boundary_effect = branch(() => boundary_fn(anchor));
reset_is_throwing_error();
});
};
onerror?.(error, reset);
var previous_reaction = active_reaction;
try {
set_active_reaction(null);
onerror?.(error, reset);
} finally {
set_active_reaction(previous_reaction);
}
if (boundary_effect) {
destroy_effect(boundary_effect);
@ -109,10 +114,9 @@ export function boundary(node, props, boundary_fn) {
);
});
} catch (error) {
handle_error(error, boundary, null, boundary.ctx);
invoke_error_boundary(error, /** @type {Effect} */ (boundary.parent));
}
reset_is_throwing_error();
is_creating_fallback = false;
});
});
@ -124,7 +128,6 @@ export function boundary(node, props, boundary_fn) {
}
boundary_effect = branch(() => boundary_fn(anchor));
reset_is_throwing_error();
}, EFFECT_TRANSPARENT | BOUNDARY_EFFECT);
if (hydrating) {

@ -0,0 +1,88 @@
/** @import { Effect } from '#client' */
import { DEV } from 'esm-env';
import { FILENAME } from '../../constants.js';
import { is_firefox } from './dom/operations.js';
import { BOUNDARY_EFFECT, EFFECT_RAN } from './constants.js';
import { define_property } from '../shared/utils.js';
import { active_effect } from './runtime.js';
/**
* @param {unknown} error
*/
export function handle_error(error) {
var effect = /** @type {Effect} */ (active_effect);
if (DEV && error instanceof Error) {
adjust_error(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) {
throw error;
}
// @ts-expect-error
effect.fn(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 {
// @ts-expect-error
effect.fn(error);
return;
} catch {}
}
effect = effect.parent;
}
throw error;
}
/** @type {WeakSet<Error>} */
const adjusted_errors = new WeakSet();
/**
* Add useful information to the error message/stack in development
* @param {Error} error
* @param {Effect} effect
*/
function adjust_error(error, effect) {
if (adjusted_errors.has(error)) return;
adjusted_errors.add(error);
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;
}
define_property(error, 'message', {
value: error.message + `\n${component_stack}\n`
});
if (error.stack) {
// Filter out internal modules
define_property(error, 'stack', {
value: error.stack
.split('\n')
.filter((line) => !line.includes('svelte/src/internal'))
.join('\n')
});
}
}

@ -332,16 +332,23 @@ export function render_effect(fn) {
* @returns {Effect}
*/
export function template_effect(fn, thunks = [], d = derived) {
const deriveds = thunks.map(d);
const effect = () => fn(...deriveds.map(get));
if (DEV) {
define_property(effect, 'name', {
value: '{expression}'
// wrap the effect so that we can decorate stack trace with `in {expression}`
// (TODO maybe there's a better approach?)
return render_effect(() => {
var outer = /** @type {Effect} */ (active_effect);
var inner = () => fn(...deriveds.map(get));
define_property(outer.fn, 'name', { value: '{expression}' });
define_property(inner, 'name', { value: '{expression}' });
const deriveds = thunks.map(d);
block(inner);
});
}
return block(effect);
const deriveds = thunks.map(d);
return block(() => fn(...deriveds.map(get)));
}
/**
@ -426,7 +433,11 @@ export function destroy_block_effect_children(signal) {
export function destroy_effect(effect, remove_dom = true) {
var removed = false;
if ((remove_dom || (effect.f & HEAD_EFFECT) !== 0) && effect.nodes_start !== null) {
if (
(remove_dom || (effect.f & HEAD_EFFECT) !== 0) &&
effect.nodes_start !== null &&
effect.nodes_end !== null
) {
remove_effect_dom(effect.nodes_start, /** @type {TemplateNode} */ (effect.nodes_end));
removed = true;
}

@ -1,4 +1,4 @@
/** @import { ComponentContext, Derived, Effect, Reaction, Signal, Source, Value } from '#client' */
/** @import { Derived, Effect, Reaction, Signal, Source, Value } from '#client' */
import { DEV } from 'esm-env';
import { define_property, get_descriptors, get_prototype_of, index_of } from '../shared/utils.js';
import {
@ -22,14 +22,13 @@ import {
ROOT_EFFECT,
LEGACY_DERIVED_PROP,
DISCONNECTED,
BOUNDARY_EFFECT,
EFFECT_IS_UPDATING
} from './constants.js';
import { flush_tasks } from './dom/task.js';
import { internal_set, old_values } from './reactivity/sources.js';
import { destroy_derived_effects, update_derived } from './reactivity/deriveds.js';
import * as e from './errors.js';
import { FILENAME } from '../../constants.js';
import { tracing_mode_flag } from '../flags/index.js';
import { tracing_expressions, get_stack } from './dev/tracing.js';
import {
@ -39,12 +38,7 @@ import {
set_component_context,
set_dev_current_component_function
} from './context.js';
import { is_firefox } from './dom/operations.js';
// Used for DEV time error handling
/** @param {WeakSet<Error>} value */
const handled_errors = new WeakSet();
let is_throwing_error = false;
import { handle_error, invoke_error_boundary } from './error-handling.js';
let is_flushing = false;
@ -227,131 +221,6 @@ export function check_dirtiness(reaction) {
return false;
}
/**
* @param {unknown} error
* @param {Effect} effect
*/
function propagate_error(error, effect) {
/** @type {Effect | null} */
var current = effect;
while (current !== null) {
if ((current.f & BOUNDARY_EFFECT) !== 0) {
try {
// @ts-expect-error
current.fn(error);
return;
} catch {
// Remove boundary flag from effect
current.f ^= BOUNDARY_EFFECT;
}
}
current = current.parent;
}
is_throwing_error = false;
throw error;
}
/**
* @param {Effect} effect
*/
function should_rethrow_error(effect) {
return (
(effect.f & DESTROYED) === 0 &&
(effect.parent === null || (effect.parent.f & BOUNDARY_EFFECT) === 0)
);
}
export function reset_is_throwing_error() {
is_throwing_error = false;
}
/**
* @param {unknown} error
* @param {Effect} effect
* @param {Effect | null} previous_effect
* @param {ComponentContext | null} component_context
*/
export function handle_error(error, effect, previous_effect, component_context) {
if (is_throwing_error) {
if (previous_effect === null) {
is_throwing_error = false;
}
if (should_rethrow_error(effect)) {
throw error;
}
return;
}
if (previous_effect !== null) {
is_throwing_error = true;
}
if (DEV && component_context !== null && error instanceof Error && !handled_errors.has(error)) {
handled_errors.add(error);
const component_stack = [];
const effect_name = effect.fn?.name;
if (effect_name) {
component_stack.push(effect_name);
}
/** @type {ComponentContext | null} */
let current_context = component_context;
while (current_context !== null) {
/** @type {string} */
var filename = current_context.function?.[FILENAME];
if (filename) {
const file = filename.split('/').pop();
component_stack.push(file);
}
current_context = current_context.p;
}
const indent = is_firefox ? ' ' : '\t';
define_property(error, 'message', {
value:
error.message + `\n${component_stack.map((name) => `\n${indent}in ${name}`).join('')}\n`
});
define_property(error, 'component_stack', {
value: component_stack
});
const stack = error.stack;
// Filter out internal files from callstack
if (stack) {
const lines = stack.split('\n');
const new_lines = [];
for (let i = 0; i < lines.length; i++) {
const line = lines[i];
if (line.includes('svelte/src/internal')) {
continue;
}
new_lines.push(line);
}
define_property(error, 'stack', {
value: new_lines.join('\n')
});
}
}
propagate_error(error, effect);
if (should_rethrow_error(effect)) {
throw error;
}
}
/**
* @param {Value} signal
* @param {Effect} effect
@ -379,11 +248,7 @@ function schedule_possible_effect_self_invalidation(signal, effect, root = true)
}
}
/**
* @template V
* @param {Reaction} reaction
* @returns {V}
*/
/** @param {Reaction} reaction */
export function update_reaction(reaction) {
var previous_deps = new_deps;
var previous_skipped_deps = skipped_deps;
@ -473,6 +338,8 @@ export function update_reaction(reaction) {
}
return result;
} catch (error) {
handle_error(error);
} finally {
new_deps = previous_deps;
skipped_deps = previous_skipped_deps;
@ -558,7 +425,6 @@ export function update_effect(effect) {
set_signal_status(effect, CLEAN);
var previous_effect = active_effect;
var previous_component_context = component_context;
var was_updating_effect = is_updating_effect;
active_effect = effect;
@ -601,8 +467,6 @@ export function update_effect(effect) {
if (DEV) {
dev_effect_stack.push(effect);
}
} catch (error) {
handle_error(error, effect, previous_effect, previous_component_context || effect.ctx);
} finally {
is_updating_effect = was_updating_effect;
active_effect = previous_effect;
@ -637,14 +501,14 @@ function infinite_loop_guard() {
if (last_scheduled_effect !== null) {
if (DEV) {
try {
handle_error(error, last_scheduled_effect, null, null);
invoke_error_boundary(error, last_scheduled_effect);
} catch (e) {
// Only log the effect stack if the error is re-thrown
log_effect_stack();
throw e;
}
} else {
handle_error(error, last_scheduled_effect, null, null);
invoke_error_boundary(error, last_scheduled_effect);
}
} else {
if (DEV) {
@ -701,27 +565,23 @@ function flush_queued_effects(effects) {
var effect = effects[i];
if ((effect.f & (DESTROYED | INERT)) === 0) {
try {
if (check_dirtiness(effect)) {
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_start === null) {
if (effect.teardown === 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 (check_dirtiness(effect)) {
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_start === null) {
if (effect.teardown === 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;
}
}
} catch (error) {
handle_error(error, effect, null, effect.ctx);
}
}
}
@ -780,12 +640,8 @@ function process_effects(root) {
} else if (is_branch) {
effect.f ^= CLEAN;
} else {
try {
if (check_dirtiness(effect)) {
update_effect(effect);
}
} catch (error) {
handle_error(error, effect, null, effect.ctx);
if (check_dirtiness(effect)) {
update_effect(effect);
}
}
@ -832,6 +688,13 @@ export function flushSync(fn) {
flush_tasks();
if (queued_root_effects.length === 0) {
// this would be reset in `flush_queued_root_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
is_flushing = false;
last_scheduled_effect = null;
if (DEV) {
dev_effect_stack = [];
}
return /** @type {T} */ (result);
}

@ -4,5 +4,5 @@
* The current version, as set in package.json.
* @type {string}
*/
export const VERSION = '5.33.12';
export const VERSION = '5.33.18';
export const PUBLIC_VERSION = '5';

@ -8,7 +8,7 @@ import * as svelteElements from './elements.js';
/**
* @internal do not use
*/
type HTMLProps<Property extends keyof svelteElements.SvelteHTMLElements, Override> = Omit<
type HTMLProps<Property extends string, Override> = Omit<
import('./elements.js').SvelteHTMLElements[Property],
keyof Override
> &
@ -250,7 +250,7 @@ declare global {
};
// don't type svelte:options, it would override the types in svelte/elements and it isn't extendable anyway
[name: string & {}]: { [name: string]: any };
[name: string]: { [name: string]: any };
}
}
}

@ -1,8 +1,20 @@
import { assert } from 'vitest';
/** @param {Element} node */
function clean_children(node) {
/**
* @param {Element} node
* @param {{ preserveComments: boolean }} opts
*/
function clean_children(node, opts) {
let previous = null;
let has_element_children = false;
let template =
node.nodeName === 'TEMPLATE' ? /** @type {HTMLTemplateElement} */ (node) : undefined;
if (template) {
const div = document.createElement('div');
div.append(template.content);
node = div;
}
// sort attributes
const attributes = Array.from(node.attributes).sort((a, b) => {
@ -14,6 +26,11 @@ function clean_children(node) {
});
attributes.forEach((attr) => {
// Strip out the special onload/onerror hydration events from the test output
if ((attr.name === 'onload' || attr.name === 'onerror') && attr.value === 'this.__e=event') {
return;
}
node.setAttribute(attr.name, attr.value);
});
@ -27,24 +44,43 @@ function clean_children(node) {
node.tagName !== 'tspan'
) {
node.removeChild(child);
continue;
}
text.data = text.data.replace(/[ \t\n\r\f]+/g, '\n');
text.data = text.data.replace(/[^\S]+/g, ' ');
if (previous && previous.nodeType === 3) {
const prev = /** @type {Text} */ (previous);
prev.data += text.data;
prev.data = prev.data.replace(/[ \t\n\r\f]+/g, '\n');
node.removeChild(text);
text = prev;
text.data = text.data.replace(/[^\S]+/g, ' ');
continue;
}
} else if (child.nodeType === 8) {
}
if (child.nodeType === 8 && !opts.preserveComments) {
// comment
// do nothing
} else {
clean_children(/** @type {Element} */ (child));
child.remove();
continue;
}
// add newlines for better readability and potentially recurse into children
if (child.nodeType === 1 || child.nodeType === 8) {
if (previous?.nodeType === 3) {
const prev = /** @type {Text} */ (previous);
prev.data = prev.data.replace(/^[^\S]+$/, '\n');
} else if (previous?.nodeType === 1 || previous?.nodeType === 8) {
node.insertBefore(document.createTextNode('\n'), child);
}
if (child.nodeType === 1) {
has_element_children = true;
clean_children(/** @type {Element} */ (child), opts);
}
}
previous = child;
@ -53,37 +89,36 @@ function clean_children(node) {
// collapse whitespace
if (node.firstChild && node.firstChild.nodeType === 3) {
const text = /** @type {Text} */ (node.firstChild);
text.data = text.data.replace(/^[ \t\n\r\f]+/, '');
if (!text.data.length) node.removeChild(text);
text.data = text.data.trimStart();
}
if (node.lastChild && node.lastChild.nodeType === 3) {
const text = /** @type {Text} */ (node.lastChild);
text.data = text.data.replace(/[ \t\n\r\f]+$/, '');
if (!text.data.length) node.removeChild(text);
text.data = text.data.trimEnd();
}
// indent code for better readability
if (has_element_children && node.parentNode) {
node.innerHTML = `\n\ ${node.innerHTML.replace(/\n/g, '\n ')}\n`;
}
if (template) {
template.innerHTML = node.innerHTML;
}
}
/**
* @param {Window} window
* @param {string} html
* @param {{ removeDataSvelte?: boolean, preserveComments?: boolean }} param2
* @param {{ preserveComments?: boolean }} opts
*/
export function normalize_html(
window,
html,
{ removeDataSvelte = false, preserveComments = false }
) {
export function normalize_html(window, html, { preserveComments = false } = {}) {
try {
const node = window.document.createElement('div');
node.innerHTML = html
.replace(/(<!(--)?.*?\2>)/g, preserveComments ? '$1' : '')
.replace(/(data-svelte-h="[^"]+")/g, removeDataSvelte ? '' : '$1')
.replace(/>[ \t\n\r\f]+</g, '><')
// Strip out the special onload/onerror hydration events from the test output
.replace(/\s?onerror="this.__e=event"|\s?onload="this.__e=event"/g, '')
.trim();
clean_children(node);
node.innerHTML = html.trim();
clean_children(node, { preserveComments });
return node.innerHTML;
} catch (err) {
throw new Error(`Failed to normalize HTML:\n${html}\nCause: ${err}`);
@ -99,67 +134,52 @@ export function normalize_new_line(html) {
}
/**
* @param {{ removeDataSvelte?: boolean }} options
* @param {string} actual
* @param {string} expected
* @param {string} [message]
*/
export function setup_html_equal(options = {}) {
/**
* @param {string} actual
* @param {string} expected
* @param {string} [message]
*/
const assert_html_equal = (actual, expected, message) => {
try {
assert.deepEqual(
normalize_html(window, actual, options),
normalize_html(window, expected, options),
message
);
} catch (e) {
if (Error.captureStackTrace)
Error.captureStackTrace(/** @type {Error} */ (e), assert_html_equal);
throw e;
}
};
/**
*
* @param {string} actual
* @param {string} expected
* @param {{ preserveComments?: boolean, withoutNormalizeHtml?: boolean }} param2
* @param {string} [message]
*/
const assert_html_equal_with_options = (
actual,
expected,
{ preserveComments, withoutNormalizeHtml },
message
) => {
try {
assert.deepEqual(
withoutNormalizeHtml
? normalize_new_line(actual.trim())
.replace(/(\sdata-svelte-h="[^"]+")/g, options.removeDataSvelte ? '' : '$1')
.replace(/(<!(--)?.*?\2>)/g, preserveComments !== false ? '$1' : '')
: normalize_html(window, actual.trim(), { ...options, preserveComments }),
withoutNormalizeHtml
? normalize_new_line(expected.trim())
.replace(/(\sdata-svelte-h="[^"]+")/g, options.removeDataSvelte ? '' : '$1')
.replace(/(<!(--)?.*?\2>)/g, preserveComments !== false ? '$1' : '')
: normalize_html(window, expected.trim(), { ...options, preserveComments }),
message
);
} catch (e) {
if (Error.captureStackTrace)
Error.captureStackTrace(/** @type {Error} */ (e), assert_html_equal_with_options);
throw e;
}
};
return {
assert_html_equal,
assert_html_equal_with_options
};
}
export const assert_html_equal = (actual, expected, message) => {
try {
assert.deepEqual(normalize_html(window, actual), normalize_html(window, expected), message);
} catch (e) {
if (Error.captureStackTrace)
Error.captureStackTrace(/** @type {Error} */ (e), assert_html_equal);
throw e;
}
};
// Common case without options
export const { assert_html_equal, assert_html_equal_with_options } = setup_html_equal();
/**
*
* @param {string} actual
* @param {string} expected
* @param {{ preserveComments?: boolean, withoutNormalizeHtml?: boolean }} param2
* @param {string} [message]
*/
export const assert_html_equal_with_options = (
actual,
expected,
{ preserveComments, withoutNormalizeHtml },
message
) => {
try {
assert.deepEqual(
withoutNormalizeHtml
? normalize_new_line(actual.trim()).replace(
/(<!(--)?.*?\2>)/g,
preserveComments !== false ? '$1' : ''
)
: normalize_html(window, actual.trim(), { preserveComments }),
withoutNormalizeHtml
? normalize_new_line(expected.trim()).replace(
/(<!(--)?.*?\2>)/g,
preserveComments !== false ? '$1' : ''
)
: normalize_html(window, expected.trim(), { preserveComments }),
message
);
} catch (e) {
if (Error.captureStackTrace)
Error.captureStackTrace(/** @type {Error} */ (e), assert_html_equal_with_options);
throw e;
}
};

@ -15,16 +15,16 @@
"end": 20,
"type": "Action",
"name": "autofocus",
"modifiers": [],
"expression": null
"expression": null,
"modifiers": []
},
{
"start": 21,
"end": 34,
"type": "Action",
"name": "autofocus",
"modifiers": [],
"expression": null
"expression": null,
"modifiers": []
}
],
"children": []

@ -15,7 +15,6 @@
"end": 39,
"type": "Action",
"name": "tooltip",
"modifiers": [],
"expression": {
"type": "CallExpression",
"start": 21,
@ -66,7 +65,8 @@
}
],
"optional": false
}
},
"modifiers": []
}
],
"children": []

@ -15,7 +15,6 @@
"end": 28,
"type": "Action",
"name": "tooltip",
"modifiers": [],
"expression": {
"type": "Identifier",
"start": 20,
@ -31,7 +30,8 @@
}
},
"name": "message"
}
},
"modifiers": []
}
],
"children": []

@ -15,7 +15,6 @@
"end": 36,
"type": "Action",
"name": "tooltip",
"modifiers": [],
"expression": {
"type": "Literal",
"start": 21,
@ -32,7 +31,8 @@
},
"value": "tooltip msg",
"raw": "'tooltip msg'"
}
},
"modifiers": []
}
],
"children": []

@ -15,8 +15,8 @@
"end": 20,
"type": "Action",
"name": "autofocus",
"modifiers": [],
"expression": null
"expression": null,
"modifiers": []
}
],
"children": []

@ -20,8 +20,8 @@
"end": 50,
"type": "Animation",
"name": "flip",
"modifiers": [],
"expression": null
"expression": null,
"modifiers": []
}
],
"children": [

@ -15,7 +15,6 @@
"end": 22,
"type": "Class",
"name": "foo",
"modifiers": [],
"expression": {
"type": "Identifier",
"start": 16,
@ -31,7 +30,8 @@
}
},
"name": "isFoo"
}
},
"modifiers": []
}
],
"children": []

@ -15,7 +15,6 @@
"end": 23,
"type": "EventHandler",
"name": "click",
"modifiers": [],
"expression": {
"type": "Identifier",
"start": 19,
@ -31,7 +30,8 @@
}
},
"name": "foo"
}
},
"modifiers": []
}
],
"children": [

@ -22,13 +22,13 @@
"end": 46,
"type": "Binding",
"name": "foo",
"modifiers": [],
"expression": {
"start": 43,
"end": 46,
"type": "Identifier",
"name": "foo"
}
},
"modifiers": []
}
],
"children": []

@ -22,7 +22,6 @@
"end": 55,
"type": "Binding",
"name": "value",
"modifiers": [],
"expression": {
"type": "Identifier",
"start": 50,
@ -38,7 +37,8 @@
}
},
"name": "name"
}
},
"modifiers": []
}
],
"children": []

@ -104,7 +104,8 @@
},
"value": "svelte",
"raw": "'svelte'"
}
},
"attributes": []
},
{
"type": "ExpressionStatement",
@ -257,7 +258,8 @@
},
"value": "./foo.js",
"raw": "'./foo.js'"
}
},
"options": null
},
"property": {
"type": "Identifier",

@ -15,7 +15,6 @@
"end": 45,
"type": "EventHandler",
"name": "click",
"modifiers": [],
"expression": {
"type": "ArrowFunctionExpression",
"start": 19,
@ -100,7 +99,8 @@
}
}
}
}
},
"modifiers": []
}
],
"children": [

@ -22,7 +22,6 @@
"end": 692,
"type": "EventHandler",
"name": "click",
"modifiers": [],
"expression": {
"type": "ArrowFunctionExpression",
"start": 596,
@ -137,7 +136,8 @@
"end": 594
}
]
}
},
"modifiers": []
}
],
"children": [

@ -22,7 +22,6 @@
"end": 53,
"type": "Binding",
"name": "this",
"modifiers": [],
"expression": {
"type": "Identifier",
"start": 49,
@ -38,7 +37,8 @@
}
},
"name": "foo"
}
},
"modifiers": []
}
],
"children": []

@ -15,8 +15,8 @@
"end": 12,
"type": "Transition",
"name": "fade",
"modifiers": [],
"expression": null,
"modifiers": [],
"intro": true,
"outro": false
}

@ -15,7 +15,6 @@
"end": 30,
"type": "Transition",
"name": "style",
"modifiers": [],
"expression": {
"type": "ObjectExpression",
"start": 16,
@ -85,6 +84,7 @@
}
]
},
"modifiers": [],
"intro": true,
"outro": false
}

@ -25,7 +25,7 @@ export default test({
<p>selected: one</p>
<select>
<option${variant === 'hydrate' ? ' selected' : ''}>one</option$>
<option${variant === 'hydrate' ? ' selected' : ''}>one</option>
<option>two</option>
<option>three</option>
</select>
@ -54,7 +54,7 @@ export default test({
<p>selected: two</p>
<select>
<option${variant === 'hydrate' ? ' selected' : ''}>one</option$>
<option${variant === 'hydrate' ? ' selected' : ''}>one</option>
<option>two</option>
<option>three</option>
</select>

@ -4,7 +4,9 @@ export default test({
html: `
<input list='suggestions'>
<datalist id='suggestions'>
<option value='foo'/><option value='bar'/><option value='baz'/>
<option value='foo'></option>
<option value='bar'></option>
<option value='baz'></option>
</datalist>
`
});

@ -9,7 +9,7 @@ export default test({
<math>
<mrow></mrow>
</svg>
</math>
<div class="hi">hi</div>
`,

@ -7,7 +7,7 @@ export default test({
target.innerHTML,
`
<select>
<option${variant === 'hydrate' ? ' selected' : ''} value="a">A</option$>
<option${variant === 'hydrate' ? ' selected' : ''} value="a">A</option>
<option value="b">B</option>
</select>
selected: a
@ -23,7 +23,7 @@ export default test({
target.innerHTML,
`
<select>
<option${variant === 'hydrate' ? ' selected' : ''} value="a">A</option$>
<option${variant === 'hydrate' ? ' selected' : ''} value="a">A</option>
<option value="b">B</option>
</select>
selected: b

@ -7,7 +7,7 @@ import { flushSync, hydrate, mount, unmount } from 'svelte';
import { render } from 'svelte/server';
import { afterAll, assert, beforeAll } from 'vitest';
import { compile_directory, fragments } from '../helpers.js';
import { setup_html_equal } from '../html_equal.js';
import { assert_html_equal, assert_html_equal_with_options } from '../html_equal.js';
import { raf } from '../animation-helpers.js';
import type { CompileOptions } from '#compiler';
import { suite_with_variants, type BaseTest } from '../suite.js';
@ -86,10 +86,6 @@ function unhandled_rejection_handler(err: Error) {
const listeners = process.rawListeners('unhandledRejection');
const { assert_html_equal, assert_html_equal_with_options } = setup_html_equal({
removeDataSvelte: true
});
beforeAll(() => {
// @ts-expect-error TODO huh?
process.prependListener('unhandledRejection', unhandled_rejection_handler);

@ -0,0 +1,6 @@
<script>
let { data } = $props();
let { foo } = $state(data);
</script>
{foo}

@ -0,0 +1,5 @@
import { test } from '../../test';
export default test({
html: `bar`
});

@ -0,0 +1,5 @@
<script>
import Child from './Child.svelte';
</script>
<Child data={{ foo: 'bar' }} />

@ -5,9 +5,8 @@ export default test({
test({ assert, target }) {
const btn = target.querySelector('button');
btn?.click();
flushSync();
assert.htmlEqual(target.innerHTML, `<button>change</button><p>Error occured</p>`);
assert.throws(() => {
flushSync(() => btn?.click());
}, /kaboom/);
}
});

@ -1,20 +1,18 @@
<script>
let count = $state(0);
const things = $derived.by(() => {
const d = $derived.by(() => {
if (count === 1) {
throw new Error('123')
throw new Error('kaboom')
}
return [1, 2 ,3]
return count
})
</script>
<button onclick={() => count++}>change</button>
<svelte:boundary>
{#each things as thing}
<p>{thing}</p>
{/each}
{d}
{#snippet failed()}
<p>Error occured</p>

@ -1,7 +1,14 @@
<script>
const { things } = $props();
const { count } = $props();
const d = $derived.by(() => {
if (count === 1) {
throw new Error('kaboom')
}
return count
});
$effect(() => {
things
})
d;
});
</script>

@ -8,6 +8,6 @@ export default test({
btn?.click();
flushSync();
assert.htmlEqual(target.innerHTML, `<button>change</button><p>Error occured</p>`);
assert.htmlEqual(target.innerHTML, `<button>change</button><p>Error occurred</p>`);
}
});

@ -2,21 +2,14 @@
import Child from './Child.svelte';
let count = $state(0);
const things = $derived.by(() => {
if (count === 1) {
throw new Error('123')
}
return [1, 2 ,3]
})
</script>
<button onclick={() => count++}>change</button>
<svelte:boundary>
<Child {things} />
<Child {count} />
{#snippet failed()}
<p>Error occured</p>
<p>Error occurred</p>
{/snippet}
</svelte:boundary>

@ -5,11 +5,11 @@ export default test({
test({ assert, target }) {
let btn = target.querySelector('button');
btn?.click();
btn?.click();
assert.throws(() => {
flushSync();
flushSync(() => {
btn?.click();
btn?.click();
});
}, /test\n\n\tin {expression}\n/);
}
});

@ -1,16 +1,18 @@
<script>
let count = $state(0);
let test = $derived.by(() => {
function maybe_throw() {
if (count > 1) {
throw new Error('test');
}
});
return count;
}
</script>
<svelte:boundary onerror={(e) => { throw(e) }}>
<div>Count: {count}</div>
<button onclick={() => count++}>Increment</button>
{count} / {test}
<div>Count: {count}</div>
<button onclick={() => count++}>Increment</button>
{count} / {maybe_throw()}
</svelte:boundary>

@ -0,0 +1,16 @@
import { ok, test } from '../../test';
export default test({
async test({ assert, target }) {
const btn = target.querySelector('button');
const main = target.querySelector('main');
ok(main);
console.log(main.innerHTML);
assert.htmlEqual(main.innerHTML, `<div>true</div>`);
// we don't want to use flush sync (or tick that use it inside) since we are testing that calling `flushSync` once
// when there are no scheduled effects does not cause reactivity to break
btn?.click();
await Promise.resolve();
assert.htmlEqual(main.innerHTML, `<div>false</div> <div>false</div>`);
}
});

@ -0,0 +1,23 @@
<script>
import { flushSync } from 'svelte'
let flag = $state(true)
let test = $state(true);
</script>
<button onclick={()=>{
flushSync(() => {
test = !test
})
flag = !flag;
}}>switch</button>
<main>
<div>{flag}</div>
{#if !flag}
<div>{test} </div>
{/if}
</main>

@ -0,0 +1,5 @@
import { test } from '../../test';
export default test({
html: `a`
});

@ -0,0 +1,9 @@
<script>
let array = $state(['a', 'b', 'c'])
</script>
{#snippet content([x])}
{x}
{/snippet}
{@render content(array)}

@ -0,0 +1,11 @@
<script lang="ts">
let count = 0;
function* test(){
while (true) {
yield count++;
}
}
let [one, two] = $state(test())
</script>
{one}, {two}

@ -0,0 +1,5 @@
<script lang="ts">
let [level, custom] = $state([10, "Admin"])
</script>
{level}, {custom}

@ -87,8 +87,8 @@ importers:
specifier: ^1.2.1
version: 1.2.1
esrap:
specifier: ^1.4.6
version: 1.4.6
specifier: ^1.4.8
version: 1.4.8
is-reference:
specifier: ^3.0.3
version: 3.0.3
@ -1261,8 +1261,8 @@ packages:
resolution: {integrity: sha512-YQLXUplAwJgCydQ78IMJywZCceoqk1oH01OERdSAJc/7U2AylwjhSCLDEtqwg811idIS/9fIU5GjG73IgjKMVg==}
engines: {node: '>=0.10'}
esrap@1.4.6:
resolution: {integrity: sha512-F/D2mADJ9SHY3IwksD4DAXjTt7qt7GWUf3/8RhCNWmC/67tyb55dpimHmy7EplakFaflV0R/PC+fdSPqrRHAQw==}
esrap@1.4.8:
resolution: {integrity: sha512-jlENbjZ7lqgJV9/OmgAtVqrFFMwsl70ctOgPIg5oTdQVGC13RSkMdtvAmu7ZTLax92c9ljnIG0xleEkdL69hwg==}
esrecurse@4.3.0:
resolution: {integrity: sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==}
@ -3622,7 +3622,7 @@ snapshots:
dependencies:
estraverse: 5.3.0
esrap@1.4.6:
esrap@1.4.8:
dependencies:
'@jridgewell/sourcemap-codec': 1.5.0

Loading…
Cancel
Save