Merge remote-tracking branch 'origin/main' into thunkify-deriveds-on-server

thunkify-deriveds-on-server
paoloricciuti 4 months ago
commit 2f8baacac9

@ -0,0 +1,5 @@
---
'svelte': patch
---
fix: improve error message for migration errors when slot would be renamed

@ -0,0 +1,5 @@
---
'svelte': patch
---
fix: allow characters in the supplementary special-purpose plane

@ -52,6 +52,7 @@ This rune, added in 5.14, causes the surrounding function to be _traced_ in deve
import { doSomeWork } from './elsewhere';
$effect(() => {
+++// $inspect.trace must be the first statement of a function body+++
+++$inspect.trace();+++
doSomeWork();
});

@ -274,6 +274,12 @@ A `:global` selector cannot modify an existing selector
A `:global` selector can only be modified if it is a descendant of other selectors
```
### css_global_block_invalid_placement
```
A `:global` selector cannot be inside a pseudoclass
```
### css_global_invalid_placement
```

@ -1,5 +1,33 @@
# svelte
## 5.28.2
### Patch Changes
- fix: don't mark selector lists inside `:global` with multiple items as unused ([#15817](https://github.com/sveltejs/svelte/pull/15817))
## 5.28.1
### Patch Changes
- fix: ensure `<svelte:boundary>` properly removes error content in production mode ([#15793](https://github.com/sveltejs/svelte/pull/15793))
- fix: `update_version` after `delete` if `source` is `undefined` and `prop` in `target` ([#15796](https://github.com/sveltejs/svelte/pull/15796))
- fix: emit error on wrong placement of the `:global` block selector ([#15794](https://github.com/sveltejs/svelte/pull/15794))
## 5.28.0
### Minor Changes
- feat: partially evaluate more expressions ([#15781](https://github.com/sveltejs/svelte/pull/15781))
## 5.27.3
### Patch Changes
- fix: use function declaration for snippets in server output to avoid TDZ violation ([#15789](https://github.com/sveltejs/svelte/pull/15789))
## 5.27.2
### Patch Changes

@ -50,6 +50,10 @@ x y {
> A `:global` selector can only be modified if it is a descendant of other selectors
## css_global_block_invalid_placement
> A `:global` selector cannot be inside a pseudoclass
## css_global_invalid_placement
> `:global(...)` can be at the start or end of a selector sequence, but not in the middle

@ -2,7 +2,7 @@
"name": "svelte",
"description": "Cybernetically enhanced web apps",
"license": "MIT",
"version": "5.27.2",
"version": "5.28.2",
"type": "module",
"types": "./types/index.d.ts",
"engines": {

@ -581,6 +581,15 @@ export function css_global_block_invalid_modifier_start(node) {
e(node, 'css_global_block_invalid_modifier_start', `A \`:global\` selector can only be modified if it is a descendant of other selectors\nhttps://svelte.dev/e/css_global_block_invalid_modifier_start`);
}
/**
* A `:global` selector cannot be inside a pseudoclass
* @param {null | number | NodeLike} node
* @returns {never}
*/
export function css_global_block_invalid_placement(node) {
e(node, 'css_global_block_invalid_placement', `A \`:global\` selector cannot be inside a pseudoclass\nhttps://svelte.dev/e/css_global_block_invalid_placement`);
}
/**
* `:global(...)` can be at the start or end of a selector sequence, but not in the middle
* @param {null | number | NodeLike} node

@ -1307,7 +1307,7 @@ const template = {
name = state.scope.generate(slot_name);
if (name !== slot_name) {
throw new MigrationError(
'This migration would change the name of a slot making the component unusable'
`This migration would change the name of a slot (${slot_name} to ${name}) making the component unusable`
);
}
}
@ -1880,7 +1880,7 @@ function handle_identifier(node, state, path) {
let new_name = state.scope.generate(name);
if (new_name !== name) {
throw new MigrationError(
'This migration would change the name of a slot making the component unusable'
`This migration would change the name of a slot (${name} to ${new_name}) making the component unusable`
);
}
}

@ -72,6 +72,8 @@ const NUL = 0;
// to replace them ourselves
//
// Source: http://en.wikipedia.org/wiki/Character_encodings_in_HTML#Illegal_characters
// Also see: https://en.wikipedia.org/wiki/Plane_(Unicode)
// Also see: https://html.spec.whatwg.org/multipage/parsing.html#preprocessing-the-input-stream
/** @param {number} code */
function validate_code(code) {
@ -116,5 +118,10 @@ function validate_code(code) {
return code;
}
// supplementary special-purpose plane 0xe0000 - 0xe07f and 0xe0100 - 0xe01ef
if ((code >= 917504 && code <= 917631) || (code >= 917760 && code <= 917999)) {
return code;
}
return NUL;
}

@ -68,8 +68,12 @@ const css_visitors = {
const global = node.children.find(is_global);
if (global) {
const idx = node.children.indexOf(global);
const is_nested = context.path.at(-2)?.type === 'PseudoClassSelector';
if (is_nested && !global.selectors[0].args) {
e.css_global_block_invalid_placement(global.selectors[0]);
}
const idx = node.children.indexOf(global);
if (global.selectors[0].args !== null && idx !== 0 && idx !== node.children.length - 1) {
// ensure `:global(...)` is not used in the middle of a selector (but multiple `global(...)` in sequence are ok)
for (let i = idx + 1; i < node.children.length; i++) {

@ -69,11 +69,17 @@ export function build_template_chunk(
node.metadata.expression
);
has_state ||= node.metadata.expression.has_state;
const evaluated = state.scope.evaluate(value);
has_state ||= node.metadata.expression.has_state && !evaluated.is_known;
if (values.length === 1) {
// If we have a single expression, then pass that in directly to possibly avoid doing
// extra work in the template_effect (instead we do the work in set_text).
if (evaluated.is_known) {
value = b.literal(evaluated.value);
}
return { value, has_state };
}
@ -89,8 +95,6 @@ export function build_template_chunk(
}
}
const evaluated = state.scope.evaluate(value);
if (evaluated.is_known) {
quasi.value.cooked += evaluated.value + '';
} else {

@ -196,9 +196,12 @@ const visitors = {
next();
},
SelectorList(node, { state, next, path }) {
const parent = path.at(-1);
// Only add comments if we're not inside a complex selector that itself is unused or a global block
if (
(!is_in_global_block(path) || node.children.length > 1) &&
(!is_in_global_block(path) ||
(node.children.length > 1 && parent?.type === 'Rule' && parent.metadata.is_global_block)) &&
!path.find((n) => n.type === 'ComplexSelector' && !n.metadata.used)
) {
const children = node.children;
@ -260,7 +263,6 @@ const visitors = {
// if this selector list belongs to a rule, require a specificity bump for the
// first scoped selector but only if we're at the top level
let parent = path.at(-1);
if (parent?.type === 'Rule') {
specificity = { bumped: false };
@ -376,7 +378,6 @@ const visitors = {
};
/**
*
* @param {Array<AST.CSS.Node>} path
*/
function is_in_global_block(path) {

@ -1,4 +1,4 @@
/** @import { ArrowFunctionExpression, BlockStatement, CallExpression } from 'estree' */
/** @import { BlockStatement } from 'estree' */
/** @import { AST } from '#compiler' */
/** @import { ComponentContext } from '../types.js' */
import { dev } from '../../../../state.js';
@ -9,27 +9,21 @@ import * as b from '#compiler/builders';
* @param {ComponentContext} context
*/
export function SnippetBlock(node, context) {
const body = /** @type {BlockStatement} */ (context.visit(node.body));
let fn = b.function_declaration(
node.expression,
[b.id('$$payload'), ...node.parameters],
/** @type {BlockStatement} */ (context.visit(node.body))
);
if (dev) {
body.body.unshift(b.stmt(b.call('$.validate_snippet_args', b.id('$$payload'))));
}
// @ts-expect-error - TODO remove this hack once $$render_inner for legacy bindings is gone
fn.___snippet = true;
/** @type {ArrowFunctionExpression | CallExpression} */
let fn = b.arrow([b.id('$$payload'), ...node.parameters], body);
const statements = node.metadata.can_hoist ? context.state.hoisted : context.state.init;
if (dev) {
fn = b.call('$.prevent_snippet_stringification', fn);
fn.body.body.unshift(b.stmt(b.call('$.validate_snippet_args', b.id('$$payload'))));
statements.push(b.stmt(b.call('$.prevent_snippet_stringification', fn.id)));
}
const declaration = b.declaration('const', [b.declarator(node.expression, fn)]);
// @ts-expect-error - TODO remove this hack once $$render_inner for legacy bindings is gone
fn.___snippet = true;
if (node.metadata.can_hoist) {
context.state.hoisted.push(declaration);
} else {
context.state.init.push(declaration);
}
statements.push(fn);
}

@ -1,4 +1,4 @@
/** @import { ArrowFunctionExpression, BinaryOperator, ClassDeclaration, Expression, FunctionDeclaration, FunctionExpression, Identifier, ImportDeclaration, MemberExpression, LogicalOperator, Node, Pattern, UnaryOperator, VariableDeclarator } from 'estree' */
/** @import { ArrowFunctionExpression, BinaryOperator, ClassDeclaration, Expression, FunctionDeclaration, FunctionExpression, Identifier, ImportDeclaration, MemberExpression, LogicalOperator, Node, Pattern, UnaryOperator, VariableDeclarator, Super } from 'estree' */
/** @import { Context, Visitor } from 'zimmerframe' */
/** @import { AST, BindingKind, DeclarationKind } from '#compiler' */
import is_reference from 'is-reference';
@ -18,8 +18,71 @@ import { validate_identifier_name } from './2-analyze/visitors/shared/utils.js';
const UNKNOWN = Symbol('unknown');
/** Includes `BigInt` */
const NUMBER = Symbol('number');
const STRING = Symbol('string');
export const NUMBER = Symbol('number');
export const STRING = Symbol('string');
/** @type {Record<string, [type: NUMBER | STRING | UNKNOWN, fn?: Function]>} */
const globals = {
BigInt: [NUMBER, BigInt],
'Math.min': [NUMBER, Math.min],
'Math.max': [NUMBER, Math.max],
'Math.random': [NUMBER],
'Math.floor': [NUMBER, Math.floor],
// @ts-expect-error
'Math.f16round': [NUMBER, Math.f16round],
'Math.round': [NUMBER, Math.round],
'Math.abs': [NUMBER, Math.abs],
'Math.acos': [NUMBER, Math.acos],
'Math.asin': [NUMBER, Math.asin],
'Math.atan': [NUMBER, Math.atan],
'Math.atan2': [NUMBER, Math.atan2],
'Math.ceil': [NUMBER, Math.ceil],
'Math.cos': [NUMBER, Math.cos],
'Math.sin': [NUMBER, Math.sin],
'Math.tan': [NUMBER, Math.tan],
'Math.exp': [NUMBER, Math.exp],
'Math.log': [NUMBER, Math.log],
'Math.pow': [NUMBER, Math.pow],
'Math.sqrt': [NUMBER, Math.sqrt],
'Math.clz32': [NUMBER, Math.clz32],
'Math.imul': [NUMBER, Math.imul],
'Math.sign': [NUMBER, Math.sign],
'Math.log10': [NUMBER, Math.log10],
'Math.log2': [NUMBER, Math.log2],
'Math.log1p': [NUMBER, Math.log1p],
'Math.expm1': [NUMBER, Math.expm1],
'Math.cosh': [NUMBER, Math.cosh],
'Math.sinh': [NUMBER, Math.sinh],
'Math.tanh': [NUMBER, Math.tanh],
'Math.acosh': [NUMBER, Math.acosh],
'Math.asinh': [NUMBER, Math.asinh],
'Math.atanh': [NUMBER, Math.atanh],
'Math.trunc': [NUMBER, Math.trunc],
'Math.fround': [NUMBER, Math.fround],
'Math.cbrt': [NUMBER, Math.cbrt],
Number: [NUMBER, Number],
'Number.isInteger': [NUMBER, Number.isInteger],
'Number.isFinite': [NUMBER, Number.isFinite],
'Number.isNaN': [NUMBER, Number.isNaN],
'Number.isSafeInteger': [NUMBER, Number.isSafeInteger],
'Number.parseFloat': [NUMBER, Number.parseFloat],
'Number.parseInt': [NUMBER, Number.parseInt],
String: [STRING, String],
'String.fromCharCode': [STRING, String.fromCharCode],
'String.fromCodePoint': [STRING, String.fromCodePoint]
};
/** @type {Record<string, any>} */
const global_constants = {
'Math.PI': Math.PI,
'Math.E': Math.E,
'Math.LN10': Math.LN10,
'Math.LN2': Math.LN2,
'Math.LOG10E': Math.LOG10E,
'Math.LOG2E': Math.LOG2E,
'Math.SQRT2': Math.SQRT2,
'Math.SQRT1_2': Math.SQRT1_2
};
export class Binding {
/** @type {Scope} */
@ -107,7 +170,7 @@ export class Binding {
class Evaluation {
/** @type {Set<any>} */
values = new Set();
values;
/**
* True if there is exactly one possible value
@ -147,8 +210,11 @@ class Evaluation {
*
* @param {Scope} scope
* @param {Expression} expression
* @param {Set<any>} values
*/
constructor(scope, expression) {
constructor(scope, expression, values) {
this.values = values;
switch (expression.type) {
case 'Literal': {
this.values.add(expression.value);
@ -172,15 +238,18 @@ class Evaluation {
binding.kind === 'rest_prop' ||
binding.kind === 'bindable_prop';
if (!binding.updated && binding.initial !== null && !is_prop) {
const evaluation = binding.scope.evaluate(/** @type {Expression} */ (binding.initial));
for (const value of evaluation.values) {
this.values.add(value);
}
if (binding.initial?.type === 'EachBlock' && binding.initial.index === expression.name) {
this.values.add(NUMBER);
break;
}
// TODO each index is always defined
if (!binding.updated && binding.initial !== null && !is_prop) {
binding.scope.evaluate(/** @type {Expression} */ (binding.initial), this.values);
break;
}
} else if (expression.name === 'undefined') {
this.values.add(undefined);
break;
}
// TODO glean what we can from reassignments
@ -336,6 +405,101 @@ class Evaluation {
break;
}
case 'CallExpression': {
const keypath = get_global_keypath(expression.callee, scope);
if (keypath) {
if (is_rune(keypath)) {
const arg = /** @type {Expression | undefined} */ (expression.arguments[0]);
switch (keypath) {
case '$state':
case '$state.raw':
case '$derived':
if (arg) {
scope.evaluate(arg, this.values);
} else {
this.values.add(undefined);
}
break;
case '$props.id':
this.values.add(STRING);
break;
case '$effect.tracking':
this.values.add(false);
this.values.add(true);
break;
case '$derived.by':
if (arg?.type === 'ArrowFunctionExpression' && arg.body.type !== 'BlockStatement') {
scope.evaluate(arg.body, this.values);
break;
}
this.values.add(UNKNOWN);
break;
default: {
this.values.add(UNKNOWN);
}
}
break;
}
if (
Object.hasOwn(globals, keypath) &&
expression.arguments.every((arg) => arg.type !== 'SpreadElement')
) {
const [type, fn] = globals[keypath];
const values = expression.arguments.map((arg) => scope.evaluate(arg));
if (fn && values.every((e) => e.is_known)) {
this.values.add(fn(...values.map((e) => e.value)));
} else {
this.values.add(type);
}
break;
}
}
this.values.add(UNKNOWN);
break;
}
case 'TemplateLiteral': {
let result = expression.quasis[0].value.cooked;
for (let i = 0; i < expression.expressions.length; i += 1) {
const e = scope.evaluate(expression.expressions[i]);
if (e.is_known) {
result += e.value + expression.quasis[i + 1].value.cooked;
} else {
this.values.add(STRING);
break;
}
}
this.values.add(result);
break;
}
case 'MemberExpression': {
const keypath = get_global_keypath(expression, scope);
if (keypath && Object.hasOwn(global_constants, keypath)) {
this.values.add(global_constants[keypath]);
break;
}
this.values.add(UNKNOWN);
break;
}
default: {
this.values.add(UNKNOWN);
}
@ -548,10 +712,10 @@ export class Scope {
* Only call this once scope has been fully generated in a first pass,
* else this evaluates on incomplete data and may yield wrong results.
* @param {Expression} expression
* @param {Set<any>} values
* @param {Set<any>} [values]
*/
evaluate(expression, values = new Set()) {
return new Evaluation(this, expression);
return new Evaluation(this, expression, values);
}
}
@ -1115,7 +1279,19 @@ export function get_rune(node, scope) {
if (!node) return null;
if (node.type !== 'CallExpression') return null;
let n = node.callee;
const keypath = get_global_keypath(node.callee, scope);
if (!keypath || !is_rune(keypath)) return null;
return keypath;
}
/**
* Returns the name of the rune if the given expression is a `CallExpression` using a rune.
* @param {Expression | Super} node
* @param {Scope} scope
*/
function get_global_keypath(node, scope) {
let n = node;
let joined = '';
@ -1133,12 +1309,8 @@ export function get_rune(node, scope) {
if (n.type !== 'Identifier') return null;
joined = n.name + joined;
if (!is_rune(joined)) return null;
const binding = scope.get(n.name);
if (binding !== null) return null; // rune name, but references a variable or store
return joined;
return n.name + joined;
}

@ -100,6 +100,7 @@ export function proxy(value) {
prop,
with_parent(() => source(UNINITIALIZED, stack))
);
update_version(version);
}
} else {
// When working with arrays, we need to also ensure we update the length when removing

@ -291,31 +291,21 @@ export function handle_error(error, effect, previous_effect, component_context)
is_throwing_error = true;
}
if (
!DEV ||
component_context === null ||
!(error instanceof Error) ||
handled_errors.has(error)
) {
propagate_error(error, effect);
return;
}
handled_errors.add(error);
if (DEV && component_context !== null && error instanceof Error && !handled_errors.has(error)) {
handled_errors.add(error);
const component_stack = [];
const component_stack = [];
const effect_name = effect.fn?.name;
const effect_name = effect.fn?.name;
if (effect_name) {
component_stack.push(effect_name);
}
if (effect_name) {
component_stack.push(effect_name);
}
/** @type {ComponentContext | null} */
let current_context = component_context;
/** @type {ComponentContext | null} */
let current_context = component_context;
while (current_context !== null) {
if (DEV) {
while (current_context !== null) {
/** @type {string} */
var filename = current_context.function?.[FILENAME];
@ -323,35 +313,36 @@ export function handle_error(error, effect, previous_effect, component_context)
const file = filename.split('/').pop();
component_stack.push(file);
}
current_context = current_context.p;
}
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 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;
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);
}
new_lines.push(line);
define_property(error, 'stack', {
value: new_lines.join('\n')
});
}
define_property(error, 'stack', {
value: new_lines.join('\n')
});
}
propagate_error(error, effect);
@ -789,19 +780,12 @@ function process_effects(root) {
} else if (is_branch) {
effect.f ^= CLEAN;
} else {
// Ensure we set the effect to be the active reaction
// to ensure that unowned deriveds are correctly tracked
// because we're flushing the current effect
var previous_active_reaction = active_reaction;
try {
active_reaction = effect;
if (check_dirtiness(effect)) {
update_effect(effect);
}
} catch (error) {
handle_error(error, effect, null, effect.ctx);
} finally {
active_reaction = previous_active_reaction;
}
}

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

@ -0,0 +1,9 @@
import { test } from '../../test';
export default test({
error: {
code: 'css_global_block_invalid_placement',
message: 'A `:global` selector cannot be inside a pseudoclass',
position: [28, 35]
}
});

@ -0,0 +1,4 @@
<style>
/* invalid */
:is(:global) { color: red }
</style>

@ -7,28 +7,28 @@ export default test({
code: 'css_unused_selector',
message: 'Unused CSS selector ".unused :global"',
start: {
line: 69,
line: 73,
column: 1,
character: 917
character: 964
},
end: {
line: 69,
line: 73,
column: 16,
character: 932
character: 979
}
},
{
code: 'css_unused_selector',
message: 'Unused CSS selector "unused :global"',
start: {
line: 100,
line: 104,
column: 29,
character: 1223
character: 1270
},
end: {
line: 100,
line: 104,
column: 43,
character: 1237
character: 1284
}
}
]

@ -3,6 +3,10 @@
.x {
color: green;
}
.a, .selector, .list {
color: green;
}
/*}*/
div.svelte-xyz {

@ -5,6 +5,10 @@
.x {
color: green;
}
.a, .selector, .list {
color: green;
}
}
div :global {

@ -1,7 +1,7 @@
<!-- @migration-task Error while migrating Svelte code: This migration would change the name of a slot making the component unusable -->
<!-- @migration-task Error while migrating Svelte code: This migration would change the name of a slot (derived to derived_1) making the component unusable -->
<script>
let derived;
</script>
<Component>
<slot name="derived" slot="derived" />
</Component>
</Component>

@ -1,6 +1,6 @@
<!-- @migration-task Error while migrating Svelte code: This migration would change the name of a slot making the component unusable -->
<!-- @migration-task Error while migrating Svelte code: This migration would change the name of a slot (body to body_1) making the component unusable -->
<script>
let body;
</script>
<slot name="body"></slot>
<slot name="body"></slot>

@ -1,2 +1,2 @@
<!-- @migration-task Error while migrating Svelte code: This migration would change the name of a slot making the component unusable -->
<slot name="dashed-name"></slot>
<!-- @migration-task Error while migrating Svelte code: This migration would change the name of a slot (dashed-name to dashed_name) making the component unusable -->
<slot name="dashed-name"></slot>

@ -36,8 +36,6 @@ export default test({
btn1.click();
});
console.warn(logs);
// the five components guarded by `count < 2` unmount and log
assert.deepEqual(logs, [1, true, 1, true, 1, true, 1, true, 1, true]);

@ -351,7 +351,7 @@ async function run_test_variant(
// @ts-expect-error
globalThis.__svelte.uid = 1;
if (manual_hydrate) {
if (manual_hydrate && variant === 'hydrate') {
hydrate_fn = () => {
instance = hydrate(mod.default, {
target,
@ -469,10 +469,6 @@ async function run_test_variant(
throw err;
}
} finally {
console.log = console_log;
console.warn = console_warn;
console.error = console_error;
config.after_test?.();
// Free up the microtask queue
@ -486,6 +482,10 @@ async function run_test_variant(
process.on('unhandledRejection', listener);
});
}
console.log = console_log;
console.warn = console_warn;
console.error = console_error;
}
}

@ -0,0 +1,13 @@
import { test } from '../../test';
import { flushSync } from 'svelte';
export default test({
html: `<button>delete</button><p>test</p>`,
async test({ assert, target }) {
const btn = target.querySelector('button');
flushSync(() => btn?.click());
assert.htmlEqual(target.innerHTML, '<button>delete</button>');
}
});

@ -0,0 +1,13 @@
<script>
let obj = $state({test: 0})
let keys = $derived(Object.keys(obj));
</script>
<button onclick={() => delete obj.test}>
delete
</button>
{#each keys as key}
<p>{key}</p>
{/each}

@ -0,0 +1,11 @@
import { flushSync } from 'svelte';
import { test } from '../../test';
export default test({
mode: ['client'],
test({ assert, target }) {
flushSync();
assert.htmlEqual(target.innerHTML, '<p>error occurred</p>');
}
});

@ -0,0 +1,15 @@
<script>
import Child from "./Child.svelte"
</script>
<svelte:boundary>
<p>This should be removed</p>
{#if true}
<Child />
{/if}
{#snippet failed()}
<p>error occurred</p>
{/snippet}
</svelte:boundary>

@ -7,12 +7,8 @@ export default test({
dev: true
},
test({ assert, target, warnings }) {
/** @type {any} */
let error;
test({ assert, target, warnings, errors }) {
const handler = (/** @type {any}} */ e) => {
error = e.error;
e.stopImmediatePropagation();
};
@ -20,9 +16,7 @@ export default test({
target.querySelector('button')?.click();
assert.throws(() => {
throw error;
}, /state_unsafe_mutation/);
assert.include(errors[0], 'state_unsafe_mutation');
window.removeEventListener('error', handler, true);

@ -1,4 +1,3 @@
import { assertType } from 'vitest';
import { test } from '../../test';
export default test({
@ -8,12 +7,8 @@ export default test({
dev: true
},
test({ assert, target, warnings, logs }) {
/** @type {any} */
let error = null;
test({ assert, target, warnings, logs, errors }) {
const handler = (/** @type {any} */ e) => {
error = e.error;
e.stopImmediatePropagation();
};
@ -23,16 +18,12 @@ export default test({
b1.click();
assert.deepEqual(logs, []);
assert.equal(error, null);
error = null;
logs.length = 0;
assert.deepEqual(errors, []);
b2.click();
assert.deepEqual(logs, ['clicked']);
assert.equal(error, null);
assert.deepEqual(errors, []);
error = null;
logs.length = 0;
b3.click();
@ -40,8 +31,7 @@ export default test({
assert.deepEqual(warnings, [
'`click` handler at main.svelte:10:17 should be a function. Did you mean to add a leading `() =>`?'
]);
assert.isNotNull(error);
assert.match(error.message, /is not a function/);
assert.include(errors[0], 'is not a function');
window.removeEventListener('error', handler, true);
}

@ -7,12 +7,8 @@ export default test({
dev: true
},
test({ assert, target, warnings }) {
/** @type {any} */
let error;
test({ assert, target, warnings, errors }) {
const handler = (/** @type {any} */ e) => {
error = e.error;
e.stopImmediatePropagation();
};
@ -20,9 +16,7 @@ export default test({
target.querySelector('button')?.click();
assert.throws(() => {
throw error;
}, /state_unsafe_mutation/);
assert.include(errors[0], 'state_unsafe_mutation');
window.removeEventListener('error', handler, true);

@ -6,11 +6,12 @@ export default test({
dev: true
},
async test({ assert, target, logs }) {
async test({ assert, target, logs, errors }) {
const b1 = target.querySelector('button');
b1?.click();
flushSync();
assert.ok(errors.length > 0);
assert.deepEqual(logs, ['init', 'a', 'init', 'b']);
}
});

@ -1,8 +1,12 @@
import { assert } from 'vitest';
import { test } from '../../test';
export default test({
compileOptions: {
dev: true
},
test() {}
test({ logs }) {
assert.ok(logs.length > 0);
}
});

@ -0,0 +1,7 @@
import { test } from '../../test';
export default test({
compileOptions: {
dev: true
}
});

@ -0,0 +1,3 @@
export function fn(snippet) {
return snippet;
}

@ -0,0 +1,10 @@
<script>
import { fn } from "./fn.js";
let variable = $state("var");
fn(test);
</script>
{#snippet test()}
{variable}
{/snippet}

@ -0,0 +1,9 @@
import { test } from '../../test';
export default test({
compileOptions: {
dev: true
},
error: 'invalid_snippet_arguments'
});

@ -0,0 +1,7 @@
<script>
test();
</script>
{#snippet test()}
<p>hello</p>
{/snippet}

@ -1,6 +1,6 @@
<script>
const log = () => {
console.log(snip);
if (!snip) throw new Error('oops');
}
let x = $state(0);
</script>
@ -9,4 +9,4 @@
{#snippet snip()}
snippet {x}
{/snippet}
{/snippet}

@ -15,6 +15,7 @@ import { derived } from '../../src/internal/client/reactivity/deriveds';
import { snapshot } from '../../src/internal/shared/clone.js';
import { SvelteSet } from '../../src/reactivity/set';
import { DESTROYED } from '../../src/internal/client/constants';
import { noop } from 'svelte/internal/client';
/**
* @param runes runes mode
@ -469,6 +470,9 @@ describe('signals', () => {
test('schedules rerun when writing to signal before reading it', (runes) => {
if (!runes) return () => {};
const error = console.error;
console.error = noop;
const value = state({ count: 0 });
user_effect(() => {
set(value, { count: 0 });
@ -482,14 +486,19 @@ describe('signals', () => {
} catch (e: any) {
assert.include(e.message, 'effect_update_depth_exceeded');
errored = true;
} finally {
assert.equal(errored, true);
console.error = error;
}
assert.equal(errored, true);
};
});
test('schedules rerun when updating deeply nested value', (runes) => {
if (!runes) return () => {};
const error = console.error;
console.error = noop;
const value = proxy({ a: { b: { c: 0 } } });
user_effect(() => {
value.a.b.c += 1;
@ -502,14 +511,19 @@ describe('signals', () => {
} catch (e: any) {
assert.include(e.message, 'effect_update_depth_exceeded');
errored = true;
} finally {
assert.equal(errored, true);
console.error = error;
}
assert.equal(errored, true);
};
});
test('schedules rerun when writing to signal before reading it', (runes) => {
if (!runes) return () => {};
const error = console.error;
console.error = noop;
const value = proxy({ arr: [] });
user_effect(() => {
value.arr = [];
@ -523,8 +537,10 @@ describe('signals', () => {
} catch (e: any) {
assert.include(e.message, 'effect_update_depth_exceeded');
errored = true;
} finally {
assert.equal(errored, true);
console.error = error;
}
assert.equal(errored, true);
};
});

@ -1,9 +1,9 @@
import * as $ from 'svelte/internal/server';
import TextInput from './Child.svelte';
const snippet = ($$payload) => {
function snippet($$payload) {
$$payload.out += `<!---->Something`;
};
}
export default function Bind_component_snippet($$payload) {
let value = '';

@ -0,0 +1,3 @@
import { test } from '../../test';
export default test({});

@ -0,0 +1,19 @@
import 'svelte/internal/disclose-version';
import 'svelte/internal/flags/legacy';
import * as $ from 'svelte/internal/client';
var root_1 = $.template(`<p></p>`);
export default function Each_index_non_null($$anchor) {
var fragment = $.comment();
var node = $.first_child(fragment);
$.each(node, 0, () => Array(10), $.index, ($$anchor, $$item, i) => {
var p = root_1();
p.textContent = `index: ${i}`;
$.append($$anchor, p);
});
$.append($$anchor, fragment);
}

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

@ -0,0 +1,3 @@
{#each Array(10), i}
<p>index: {i}</p>
{/each}

@ -8,7 +8,7 @@ export default function Purity($$anchor) {
var fragment = root();
var p = $.first_child(fragment);
p.textContent = Math.max(0, Math.min(0, 100));
p.textContent = 0;
var p_1 = $.sibling(p, 2);

@ -1,7 +1,7 @@
import * as $ from 'svelte/internal/server';
export default function Purity($$payload) {
$$payload.out += `<p>${$.escape(Math.max(0, Math.min(0, 100)))}</p> <p>${$.escape(location.href)}</p> `;
$$payload.out += `<p>0</p> <p>${$.escape(location.href)}</p> `;
Child($$payload, { prop: encodeURIComponent('hello') });
$$payload.out += `<!---->`;
}
Loading…
Cancel
Save