Merge branch 'main' into svelte-custom-renderer

pull/18505/head
Paolo Ricciuti 3 months ago committed by GitHub
commit 114be1c593
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -1,5 +0,0 @@
---
'svelte': patch
---
fix: correctly transform references to earlier declarators in a declaration tag (e.g. `{let a = $state(0), b = $derived(a * 2)}`)

@ -1,5 +0,0 @@
---
'svelte': patch
---
fix: avoid spurious `state_referenced_locally` warnings for `$derived` declarations in declaration tags

@ -1,5 +0,0 @@
---
'svelte': patch
---
fix: tolerate whitespace before `let`/`const` in declaration tags

@ -1,5 +0,0 @@
---
'svelte': patch
---
fix: prevent infinite loop when a tag's expression ends with a trailing `/` at the end of the input

@ -1,5 +0,0 @@
---
'svelte': patch
---
fix: more robust parsing of declaration tags with regards to `type`

@ -1,5 +0,0 @@
---
'svelte': patch
---
fix: preserve newlines in spread input values when the `type` attribute is applied after `value`

@ -1,5 +0,0 @@
---
'svelte': patch
---
fix: update `SvelteURLSearchParams` when setting duplicate keys to the same joined value

@ -0,0 +1,5 @@
---
'svelte': patch
---
fix: ignore declaration tags for animation directive

@ -0,0 +1,5 @@
---
'svelte': patch
---
fix: reject pending async deriveds on discard

@ -18,12 +18,12 @@ jobs:
strategy:
matrix:
include:
- node-version: 18
# Vitest 4 requires Node 20+, so tests run on 20/22/24. The published
# Svelte package still supports Node >=18 (see packages/svelte/package.json).
- node-version: 20
os: windows-latest
- node-version: 18
- node-version: 20
os: macOS-latest
- node-version: 18
os: ubuntu-latest
- node-version: 20
os: ubuntu-latest
- node-version: 22

@ -106,7 +106,7 @@ In case you just want to render something `n` times, you can omit the `as` part:
.chess-board {
display: grid;
grid-template-columns: repeat(8, 1fr);
rows: repeat(8, 1fr);
grid-template-rows: repeat(8, 1fr);
border: 1px solid black;
aspect-ratio: 1;

@ -32,7 +32,7 @@
"@svitejs/changesets-changelog-github-compact": "^1.1.0",
"@types/node": "^20.11.5",
"@types/picomatch": "^4.0.2",
"@vitest/coverage-v8": "^2.1.9",
"@vitest/coverage-v8": "^4.1.7",
"eslint": "^10.0.0",
"eslint-plugin-lube": "^0.5.1",
"eslint-plugin-svelte": "^3.15.0",
@ -44,6 +44,6 @@
"typescript": "^5.5.4",
"typescript-eslint": "^8.56.0",
"v8-natives": "^1.2.5",
"vitest": "^2.1.9"
"vitest": "^4.1.7"
}
}

@ -1,5 +1,29 @@
# svelte
## 5.56.1
### Patch Changes
- fix: error at compile time on duplicate snippet/declaration tag definitions ([#18351](https://github.com/sveltejs/svelte/pull/18351))
- fix: parse declaration tag contents more robustly ([#18353](https://github.com/sveltejs/svelte/pull/18353))
- fix: correctly transform references to earlier declarators in a declaration tag (e.g. `{let a = $state(0), b = $derived(a * 2)}`) ([#18348](https://github.com/sveltejs/svelte/pull/18348))
- fix: avoid spurious `state_referenced_locally` warnings for `$derived` declarations in declaration tags ([#18348](https://github.com/sveltejs/svelte/pull/18348))
- fix: tolerate whitespace before `let`/`const` in declaration tags ([#18348](https://github.com/sveltejs/svelte/pull/18348))
- fix: prevent infinite loop when a tag's expression ends with a trailing `/` at the end of the input ([#18350](https://github.com/sveltejs/svelte/pull/18350))
- fix: more robust parsing of declaration tags with regards to `type` ([#18330](https://github.com/sveltejs/svelte/pull/18330))
- fix: preserve newlines in spread input values when the `type` attribute is applied after `value` ([#18345](https://github.com/sveltejs/svelte/pull/18345))
- fix: update `SvelteURLSearchParams` when setting duplicate keys to the same joined value ([#18336](https://github.com/sveltejs/svelte/pull/18336))
- fix: check references for blockers on server, too ([#18352](https://github.com/sveltejs/svelte/pull/18352))
## 5.56.0
### Minor Changes

@ -2,7 +2,7 @@
"name": "svelte",
"description": "Cybernetically enhanced web apps",
"license": "MIT",
"version": "5.56.0",
"version": "5.56.1",
"type": "module",
"types": "./types/index.d.ts",
"engines": {
@ -173,7 +173,7 @@
"source-map": "^0.7.4",
"tinyglobby": "^0.2.12",
"typescript": "^5.5.4",
"vitest": "^2.1.9",
"vitest": "^4.1.7",
"web-features": "^3.29.0"
},
"dependencies": {

@ -5,7 +5,6 @@ import * as acorn from 'acorn';
import { walk } from 'zimmerframe';
import { tsPlugin } from '@sveltejs/acorn-typescript';
import * as e from '../../errors.js';
import { find_matching_bracket } from './utils/bracket.js';
const JSParser = acorn.Parser;
const TSParser = JSParser.extend(tsPlugin());
@ -106,38 +105,26 @@ export function parse_expression_at(parser, source, index) {
* @returns {Statement}
*/
export function parse_statement_at(parser, source, index) {
const acorn = parser.ts ? TSParser : JSParser;
let end = find_matching_bracket(source, index, '{');
if (end === undefined) e.unexpected_eof(source.length);
while (source[end - 1] === ';') {
end -= 1;
}
const padded_source = `${' '.repeat(index)}${source.slice(index, end)}`;
const { onComment, add_comments } = get_comment_handlers(
padded_source,
parser.root.comments,
index
);
// cast to `any`: acorn's Parser constructor and parseStatement/nextToken aren't in its public types
const acorn = /** @type {any} */ (parser.ts ? TSParser : JSParser);
const { onComment, add_comments } = get_comment_handlers(source, parser.root.comments, index);
try {
const ast = acorn.parse(padded_source, {
onComment,
sourceType: 'module',
ecmaVersion: 16,
locations: true
});
add_comments(ast);
const statement = /** @type {Statement} */ (
/** @type {unknown} */ (/** @type {Program} */ (ast).body[0])
// This is like parseExpressionAt but for statements
const p = new acorn(
{ onComment, sourceType: 'module', ecmaVersion: 16, locations: true },
source,
index
);
statement.end = Math.min(/** @type {number} */ (statement.end), end);
p.nextToken();
const statement = /** @type {Statement} */ (p.parseStatement(null, true, Object.create(null)));
add_comments(/** @type {acorn.Node} */ (statement));
return statement;
} catch (e) {
handle_parse_error(e);
} catch (err) {
// A statement that runs to the end of the source (e.g. an unterminated declaration tag)
// is an EOF, not a stray token; preserve the friendlier `unexpected_eof` diagnostic.
if (/** @type {any} */ (err).pos === source.length) e.unexpected_eof(source.length);
handle_parse_error(err);
}
}

@ -2,6 +2,7 @@
/** @import { Context } from '../types' */
import * as b from '#compiler/builders';
import * as e from '../../../errors.js';
import { extract_identifiers } from '../../../utils/ast.js';
/**
* @param {AST.DeclarationTag} node
@ -12,6 +13,16 @@ export function DeclarationTag(node, context) {
e.declaration_tag_no_legacy_mode(node);
}
const is_top_level = context.path.length === 1 && context.path[0].type === 'Fragment';
if (is_top_level) {
const duplicate = node.declaration.declarations
.flatMap((declaration) => extract_identifiers(declaration.id))
.find((id) => context.state.analysis.instance.scope.declarations.has(id.name));
if (duplicate) {
e.declaration_duplicate(duplicate, duplicate.name);
}
}
context.visit(node.declaration, {
...context.state,
in_declaration_tag: true,

@ -25,19 +25,24 @@ export function SnippetBlock(node, context) {
context.next({ ...context.state, parent_element: null });
const can_hoist =
context.path.length === 1 &&
context.path[0].type === 'Fragment' &&
can_hoist_snippet(context.state.scope, context.state.scopes);
const is_top_level = context.path.length === 1 && context.path[0].type === 'Fragment';
const name = node.expression.name;
if (is_top_level) {
const name = node.expression.name;
if (can_hoist) {
const binding = /** @type {Binding} */ (context.state.scope.get(name));
context.state.analysis.module.scope.declarations.set(name, binding);
}
if (context.state.analysis.instance.scope.declarations.has(name)) {
e.declaration_duplicate(node.expression, name);
}
node.metadata.can_hoist =
is_top_level && can_hoist_snippet(context.state.scope, context.state.scopes);
node.metadata.can_hoist = can_hoist;
if (node.metadata.can_hoist) {
const name = node.expression.name;
const binding = /** @type {Binding} */ (context.state.scope.get(name));
context.state.analysis.module.scope.declarations.set(name, binding);
}
}
const { path } = context;
const parent = path.at(-2);

@ -101,6 +101,7 @@ export function validate_element(node, context) {
(n) =>
n.type !== 'Comment' &&
n.type !== 'ConstTag' &&
n.type !== 'DeclarationTag' &&
(n.type !== 'Text' || n.data.trim() !== '')
).length > 1
) {

@ -343,7 +343,7 @@ export class PromiseOptimiser {
* @param {ExpressionMetadata} metadata
*/
check_blockers(metadata) {
for (const binding of metadata.dependencies) {
for (const binding of metadata.references) {
if (binding.blocker) {
this.#blockers.add(binding.blocker);
}

@ -41,6 +41,7 @@ import { set_signal_status } from './status.js';
import { legacy_is_updating_store } from './store.js';
import { invariant } from '../../shared/dev.js';
import { log_effect_tree } from '../dev/debug.js';
import { OBSOLETE } from './deriveds.js';
/** @type {Batch | null} */
let first_batch = null;
@ -511,6 +512,10 @@ export class Batch {
if (d) deferred.promise.then(d.resolve).catch(d.reject);
}
// Clear them or else those that are still pending might get rejected on discard (after merged-into batch is done).
// This can happen when batch Y merged into X and Y has a pending boundary and therefore still-pending async deriveds inside.
batch.async_deriveds.clear();
// Mark is not guaranteed not touch these, so we transfer them
this.transfer_effects(batch.#dirty_effects, batch.#maybe_dirty_effects);
@ -629,6 +634,10 @@ export class Batch {
for (const fn of this.#discard_callbacks) fn(this);
this.#discard_callbacks.clear();
for (const deferred of this.async_deriveds.values()) {
deferred.reject(OBSOLETE);
}
this.#unlink();
this.#deferred?.resolve();
}
@ -677,12 +686,15 @@ export class Batch {
}
}
if (!batch.#started) continue;
var current = [...batch.current.keys()].filter(
(source) => !(/** @type {[any, boolean]} */ (batch.current.get(source))[1])
);
// If not started yet or no sources to update (which is e.g. possible for the very first batch) then bail
if (!batch.#started || current.length === 0) continue;
// Re-run async/block effects that depend on distinct values changed in both batches (ignoring deriveds)
var others = [...batch.current.keys()].filter(
(s) => !(/** @type {[any, boolean]} */ (batch.current.get(s))[1]) && !this.current.has(s)
);
var others = current.filter((source) => !this.current.has(source));
if (others.length === 0) {
if (is_earlier) {

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

@ -25,7 +25,21 @@ interface HydrationTest extends BaseTest {
expect_hydration_error?: true;
snapshot?: (target: HTMLElement) => any;
test?: (
assert: typeof import('vitest').assert & {
// `_config.js` test callbacks rely on inferred parameter types, which
// TS treats as non-explicit and rejects for chai 5's assertion-function
// signatures (TS2775). Override the assertion methods we actually use
// with non-assertion equivalents.
assert: Omit<
typeof import('vitest').assert,
'ok' | 'isOk' | 'isTrue' | 'isFalse' | 'exists' | 'notExists' | 'instanceOf'
> & {
ok(value: unknown, message?: string): void;
isOk(value: unknown, message?: string): void;
isTrue(value: unknown, message?: string): void;
isFalse(value: unknown, message?: string): void;
exists(value: unknown, message?: string): void;
notExists(value: unknown, message?: string): void;
instanceOf(value: unknown, type: Function, message?: string): void;
htmlEqual(a: string, b: string, description?: string): void;
},
target: HTMLElement,
@ -152,7 +166,6 @@ const { test, run } = suite<HydrationTest>(async (config, cwd) => {
if (config.test) {
await config.test(
// @ts-expect-error TS doesn't get it
{
...assert,
htmlEqual: assert_html_equal

@ -0,0 +1,20 @@
<script lang="ts">
let visible = true;
let total = 10;
let width = 16;
let height = 9;
let divisor = 2;
let options: { fallback?: number } = {};
</script>
{#if visible}
{const half = total / 2}
{let derived = $derived(total / 4)}
{const member = width / height}
{const call = Math.max(total, 1) / 2}
{const string_then_division = 'ab' / divisor}
{const typed: number = total / 2}
{const { fallback = total / 2 } = options}
{const regex = /[}]/}
<p>{half} {derived} {member} {call} {string_then_division} {typed} {fallback} {regex}</p>
{/if}

@ -0,0 +1,20 @@
<script lang="ts">
let visible = true;
let total = 10;
let width = 16;
let height = 9;
let divisor = 2;
let options: { fallback?: number } = {};
</script>
{#if visible}
{const half = total / 2}
{let derived = $derived(total / 4)}
{const member = width / height}
{const call = Math.max(total, 1) / 2}
{const string_then_division = 'ab' / divisor}
{const typed: number = total / 2}
{const { fallback = total / 2 } = options}
{const regex = /[}]/}
<p>{half} {derived} {member} {call} {string_then_division} {typed} {fallback} {regex}</p>
{/if}

@ -42,9 +42,9 @@ const { run: run_browser_tests } = suite_with_variants<
describe.concurrent(
'runtime-browser',
() => run_browser_tests(__dirname),
// Browser tests are brittle and slow on CI
{ timeout: 20000, retry: process.env.CI ? 1 : 0 }
{ timeout: 20000, retry: process.env.CI ? 1 : 0 },
() => run_browser_tests(__dirname)
);
const { run: run_ce_tests } = suite<ReturnType<typeof import('./assert').test>>(
@ -55,9 +55,9 @@ const { run: run_ce_tests } = suite<ReturnType<typeof import('./assert').test>>(
describe.concurrent(
'custom-elements',
() => run_ce_tests(__dirname, 'custom-elements-samples'),
// Browser tests are brittle and slow on CI
{ timeout: 20000, retry: process.env.CI ? 1 : 0 }
{ timeout: 20000, retry: process.env.CI ? 1 : 0 },
() => run_ce_tests(__dirname, 'custom-elements-samples')
);
async function run_test(

@ -15,19 +15,35 @@ import { clear } from '../../src/internal/client/reactivity/batch.js';
import { hydrating } from '../../src/internal/client/dom/hydration.js';
import { ssr_context } from '../../src/internal/server/context.js';
type Assert = typeof import('vitest').assert & {
htmlEqual(a: string, b: string, description?: string): void;
htmlEqualWithOptions(
a: string,
b: string,
opts: {
preserveComments: boolean;
withoutNormalizeHtml: boolean;
},
description?: string
): void;
// `_config.js` files call `assert.ok` etc. with `assert` typed via parameter
// inference, which TypeScript treats as non-explicit. chai 5 (pulled in by
// vitest 4) declares these as assertion functions (`asserts value`), so TS2775
// fires on every call. Override the affected methods with non-assertion
// signatures — the runtime behavior is unchanged.
type NonAssertingMethods = {
ok(value: unknown, message?: string): void;
isOk(value: unknown, message?: string): void;
isTrue(value: unknown, message?: string): void;
isFalse(value: unknown, message?: string): void;
exists(value: unknown, message?: string): void;
notExists(value: unknown, message?: string): void;
instanceOf(value: unknown, type: Function, message?: string): void;
};
type Assert = Omit<typeof import('vitest').assert, keyof NonAssertingMethods> &
NonAssertingMethods & {
htmlEqual(a: string, b: string, description?: string): void;
htmlEqualWithOptions(
a: string,
b: string,
opts: {
preserveComments: boolean;
withoutNormalizeHtml: boolean;
},
description?: string
): void;
};
// TODO remove this shim when we can
// @ts-expect-error
Promise.withResolvers = () => {
@ -75,6 +91,7 @@ export interface RuntimeTest<Props extends Record<string, any> = Record<string,
raf: {
tick: (ms: number) => void;
};
snapshot: any;
target: HTMLElement;
window: Window & {
Event: typeof Event;
@ -126,6 +143,16 @@ const listeners = process.rawListeners('unhandledRejection');
beforeAll(() => {
// @ts-expect-error TODO huh?
process.prependListener('unhandledRejection', unhandled_rejection_handler);
// Route inline-`<script>` console calls through `globalThis.console` at
// call time, so per-test `console.{log,warn,error}` overrides see them.
// (jsdom scripts run in a VM with their own `console` object, separate
// from Node's `globalThis.console` — vitest 4's jsdom env no longer
// bridges them per-test.)
const vc = (window as any)._virtualConsole;
for (const m of ['log', 'warn', 'error'] as const) {
vc?.on(m, (...args: any[]) => console[m](...args));
}
});
beforeEach(() => {
@ -255,6 +282,26 @@ async function run_test_variant(
let warnings: string[] = [];
let errors: string[] = [];
let manual_hydrate = false;
let intercept_errors = false;
// Capture phase so we still see the error if a test installs its own
// `stopImmediatePropagation` listener (e.g. event-handler-*). Named so we
// can remove it in `finally` — otherwise listeners from earlier tests leak
// and keep writing to module-level `unhandled_rejection`. When the test
// intercepts `errors`, mirror jsdom's `Uncaught [...]` format into the
// array — vitest 4's jsdom env no longer routes `jsdomError` to vitest's
// wrapped console reliably (works locally but not in CI's forks pool).
const window_error_listener = (e: ErrorEvent) => {
if (intercept_errors) {
const detail = e.error;
const is_error = detail && detail.name && detail.message !== undefined && detail.stack;
const error_string = is_error ? `[${detail.name}: ${detail.message}]` : String(detail);
errors.push(`Error: Uncaught ${error_string}\n${detail?.stack ?? ''}`, detail);
} else {
unhandled_rejection = e.error;
}
e.preventDefault();
};
{
// use some crude static analysis to determine if logs/warnings are intercepted.
@ -317,6 +364,7 @@ async function run_test_variant(
}
if (str.slice(0, i).includes('errors') || config.errors) {
intercept_errors = true;
// eslint-disable-next-line no-console
console.error = (...args) => {
errors.push(...args);
@ -348,10 +396,7 @@ async function run_test_variant(
window.document.head.innerHTML = styles ? `<style>${styles}</style>` : '';
window.document.body.innerHTML = '<main></main>';
window.addEventListener('error', (e) => {
unhandled_rejection = e.error;
e.preventDefault();
});
window.addEventListener('error', window_error_listener, true);
globalThis.requestAnimationFrame = globalThis.setTimeout;
@ -423,7 +468,6 @@ async function run_test_variant(
await config.test_ssr({
logs,
warnings,
// @ts-expect-error
assert: {
...assert,
htmlEqual: assert_html_equal,
@ -515,7 +559,6 @@ async function run_test_variant(
}
await config.test({
// @ts-expect-error TS doesn't get it
assert: {
...assert,
htmlEqual: assert_html_equal,
@ -525,6 +568,7 @@ async function run_test_variant(
component: runes ? props : instance,
instance,
mod,
ok,
target,
snapshot,
window,
@ -593,6 +637,8 @@ async function run_test_variant(
throw new Error('Hydration state was not cleared');
}
window.removeEventListener('error', window_error_listener, true);
config.after_test?.();
// Free up the microtask queue

@ -0,0 +1,5 @@
<script>
let { onclick } = $props();
</script>
<button {onclick}>A button</button>

@ -0,0 +1,11 @@
import { tick } from 'svelte';
import { test } from '../../test';
export default test({
mode: ['hydrate'],
async test({ assert, target }) {
await tick();
assert.htmlEqual(target.innerHTML, 'foo <button>A button</button>');
}
});

@ -0,0 +1,12 @@
<script>
import Button from './Child.svelte';
async function getFoo() {
return 'foo';
}
const foo = $derived(await getFoo());
</script>
{foo}
<Button onclick={() => foo} />

@ -0,0 +1,39 @@
import { tick } from 'svelte';
import { test } from '../../test';
export default test({
async test({ assert, target, logs }) {
await tick();
const [fork, real, resolve] = target.querySelectorAll('button');
fork.click();
await tick();
resolve.click();
await tick();
assert.htmlEqual(
target.innerHTML,
`
0
<button>fork</button>
<button>real</button>
<button>resolve</button>
`
);
assert.deepEqual(logs, [0]);
real.click();
await tick();
resolve.click();
await tick();
assert.htmlEqual(
target.innerHTML,
`
1
<button>fork</button>
<button>real</button>
<button>resolve</button>
`
);
assert.deepEqual(logs, [0, 1]);
}
});

@ -0,0 +1,23 @@
<script>
import { fork } from "svelte";
let count = $state(0);
let queued = [];
function push(v) {
if (!v) return v;
return new Promise((resolve) => {
queued.push(() => resolve(v));
});
}
$effect(() => {
console.log(count);
})
</script>
{await push(count)}
<button onclick={() => fork(() => count++).discard()}>fork</button>
<button onclick={() => count++}>real</button>
<button onclick={() => queued.shift()?.()}>resolve</button>

@ -31,7 +31,21 @@ interface SourcemapTest extends BaseTest {
/** The expected `sources` array in the source map */
css_map_sources?: string[];
test?: (obj: {
assert: typeof assert;
// chai 5's `asserts value` signatures trip TS2775 in `_config.js` files
// where `assert` is a destructured parameter (non-explicit). Override
// the assertion methods we use with non-assertion equivalents.
assert: Omit<
typeof assert,
'ok' | 'isOk' | 'isTrue' | 'isFalse' | 'exists' | 'notExists' | 'instanceOf'
> & {
ok(value: unknown, message?: string): void;
isOk(value: unknown, message?: string): void;
isTrue(value: unknown, message?: string): void;
isFalse(value: unknown, message?: string): void;
exists(value: unknown, message?: string): void;
notExists(value: unknown, message?: string): void;
instanceOf(value: unknown, type: Function, message?: string): void;
};
input: string;
map_preprocessed: any;
code_preprocessed: string;

@ -0,0 +1,10 @@
<script>
function flip(){}
</script>
<div>
{#each [] as n (n)}
{const a = n}
<div animate:flip={a}></div>
{/each}
</div>

@ -0,0 +1,14 @@
[
{
"code": "declaration_duplicate",
"message": "`foo` has already been declared",
"start": {
"line": 5,
"column": 5
},
"end": {
"line": 5,
"column": 8
}
}
]

@ -0,0 +1,5 @@
<script>
let foo = 'bar';
</script>
{let foo = 'baz'}

@ -0,0 +1,14 @@
[
{
"code": "declaration_duplicate",
"message": "`foo` has already been declared",
"start": {
"line": 5,
"column": 10
},
"end": {
"line": 5,
"column": 13
}
}
]

@ -0,0 +1,5 @@
<script>
let foo = 'bar';
</script>
{#snippet foo()}baz{/snippet}

File diff suppressed because it is too large Load Diff

@ -1,4 +1,4 @@
import { type Environment, builtinEnvironments } from 'vitest/environments';
import { type Environment, builtinEnvironments } from 'vitest/runtime';
const xhtml_page = `<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML Basic 1.0//EN" "http://www.w3.org/TR/xhtml-basic/xhtml-basic10.dtd">
@ -6,7 +6,7 @@ const xhtml_page = `<?xml version="1.0" encoding="UTF-8"?>
export default <Environment>{
name: 'jsdom-xhtml',
transformMode: 'web',
viteEnvironment: 'client',
setup(global, { jsdom = {} }) {
return builtinEnvironments.jsdom.setup(global, {
jsdom: {

@ -29,6 +29,11 @@ export default defineConfig({
test: {
dir: '.',
reporters: ['dot'],
// A handful of dev-mode tests trigger Svelte's `effect_update_depth_exceeded`
// guard, which involves ~1000 Error objects per flush for stack tracking —
// slow enough under vitest 4's deeper async stacks (and CI's slower workers)
// to overrun the 5s default.
testTimeout: 30_000,
include: [
'packages/svelte/**/*.test.ts',
'packages/svelte/tests/*/test.ts',

Loading…
Cancel
Save