merge main. lots of stuff failing

incremental-batches
Rich Harris 4 weeks ago
commit c006bc043e

@ -1,5 +0,0 @@
---
'svelte': patch
---
fix: invalidate `@const` tags based on visible references in legacy mode

@ -1,5 +0,0 @@
---
'svelte': patch
---
fix: disallow `--` in `idPrefix`

@ -1,5 +0,0 @@
---
'svelte': patch
---
fix: correct types for `ontoggle` on <details> elements

@ -1,5 +0,0 @@
---
'svelte': patch
---
fix: don't override `$destroy/set/on` instance methods in dev mode

@ -1,5 +0,0 @@
---
'svelte': patch
---
fix: never set derived.v inside fork

@ -1,5 +0,0 @@
---
'svelte': patch
---
fix: don't reset status of uninitialized deriveds

@ -43,7 +43,7 @@ The maintainers meet on the final Saturday of each month. While these meetings a
### Prioritization
We do our best to review PRs and RFCs as they are sent, but it is difficult to keep up. We welcome help in reviewing PRs, RFCs, and issues. If an item aligns with the current priority on our [roadmap](https://svelte.dev/roadmap), it is more likely to be reviewed quickly. PRs to the most important and active ones repositories get reviewed more quickly while PRs to smaller inactive repos may sit for a bit before we periodically come by and review the pending PRs in a batch.
We do our best to review PRs and RFCs as they are sent, but it is difficult to keep up. We welcome help in reviewing PRs, RFCs, and issues. If an item aligns with the current priority on our [roadmap](https://svelte.dev/roadmap), it is more likely to be reviewed quickly. PRs to the most important and active repositories get reviewed more quickly while PRs to smaller inactive repos may sit for a bit before we periodically come by and review the pending PRs in a batch.
## Bugs

@ -167,6 +167,8 @@ To take a static snapshot of a deeply reactive `$state` proxy, use `$state.snaps
This is handy when you want to pass some state to an external library or API that doesn't expect a proxy, such as `structuredClone`.
If a value has a `toJSON` method, the snapshot will clone the value returned from `toJSON` instead of the original object.
## `$state.eager`
When state changes, it may not be reflected in the UI immediately if it is used by an `await` expression, because [updates are synchronized](await-expressions#Synchronized-updates).

@ -241,7 +241,7 @@ When the value of an `<option>` matches its text content, the attribute can be o
</select>
```
You can give the `<select>` a default value by adding a `selected` attribute to the`<option>` (or options, in the case of `<select multiple>`) that should be initially selected. If the `<select>` is part of a form, it will revert to that selection when the form is reset. Note that for the initial render the value of the binding takes precedence if it's not `undefined`.
You can give the `<select>` a default value by adding a `selected` attribute to the `<option>` (or options, in the case of `<select multiple>`) that should be initially selected. If the `<select>` is part of a form, it will revert to that selection when the form is reset. Note that for the initial render the value of the binding takes precedence if it's not `undefined`.
```svelte
<select bind:value={selected}>

@ -163,6 +163,8 @@ export const [getCounter, setCounter] = createContext<Counter>();
Svelte will warn you if you get it wrong.
Similarly, to pass primitive values through context, use functions as described in [Passing state into functions]($state#Passing-state-into-functions).
## Component testing
When writing [component tests](testing#Unit-and-component-tests-with-Vitest-Component-testing), it can be useful to create a wrapper component that sets the context in order to check the behaviour of a component that uses it. As of version 5.49, you can do this sort of thing:

@ -38,6 +38,21 @@ You can now write unit tests for code inside your `.js/.ts` files:
```js
/// file: multiplier.svelte.test.js
// @filename: multiplier.svelte.ts
export function multiplier(initial: number, k: number) {
let count = $state(initial);
return {
get value() {
return count * k;
},
set: (c: number) => {
count = c;
}
};
}
// @filename: multiplier.svelte.test.js
// ---cut---
import { flushSync } from 'svelte';
import { expect, test } from 'vitest';
import { multiplier } from './multiplier.svelte.js';
@ -80,6 +95,16 @@ Since Vitest processes your test files the same way as your source files, you ca
```js
/// file: multiplier.svelte.test.js
// @filename: multiplier.svelte.ts
export function multiplier(getCount: () => number, k: number) {
return {
get value() {
return getCount() * k;
}
};
}
// @filename: multiplier.svelte.test.js
// ---cut---
import { flushSync } from 'svelte';
import { expect, test } from 'vitest';
import { multiplier } from './multiplier.svelte.js';
@ -115,6 +140,10 @@ If the code being tested uses effects, you need to wrap the test inside `$effect
```js
/// file: logger.svelte.test.js
// @filename: logger.svelte.ts
export function logger(fn: () => void) {}
// @filename: logger.svelte.test.js
// ---cut---
import { flushSync } from 'svelte';
import { expect, test } from 'vitest';
import { logger } from './logger.svelte.js';
@ -213,7 +242,7 @@ test('Component', () => {
expect(document.body.innerHTML).toBe('<button>0</button>');
// Click the button, then flush the changes so you can synchronously write expectations
document.body.querySelector('button').click();
document.body.querySelector('button')?.click();
flushSync();
expect(document.body.innerHTML).toBe('<button>1</button>');
@ -226,6 +255,7 @@ test('Component', () => {
While the process is very straightforward, it is also low level and somewhat brittle, as the precise structure of your component may change frequently. Tools like [@testing-library/svelte](https://testing-library.com/docs/svelte-testing-library/intro/) can help streamline your tests. The above test could be rewritten like this:
```js
// @errors: 2339
/// file: component.test.js
import { render, screen } from '@testing-library/svelte';
import userEvent from '@testing-library/user-event';
@ -270,9 +300,9 @@ You can create stories for component variations and test interactions with the [
}
});
</script>
<Story name="Empty Form" />
<Story
name="Filled Form"
play={async ({ args, canvas, userEvent }) => {

@ -134,6 +134,14 @@ When logging a [proxy](https://developer.mozilla.org/en-US/docs/Web/JavaScript/R
The easiest way to log a value as it changes over time is to use the [`$inspect`](/docs/svelte/$inspect) rune. Alternatively, to log things on a one-off basis (for example, inside an event handler) you can use [`$state.snapshot`](/docs/svelte/$state#$state.snapshot) to take a snapshot of the current value.
### derived_inert
```
Reading a derived belonging to a now-destroyed effect may result in stale values
```
A `$derived` value created inside an effect will stop updating when the effect is destroyed. You should create the `$derived` outside the effect, or inside an `$effect.root`.
### event_handler_invalid
```

@ -1,5 +1,145 @@
# svelte
## 5.55.9
### Patch Changes
- fix: don't unset batch when calling `{#await ...}` promise ([#18243](https://github.com/sveltejs/svelte/pull/18243))
- fix: promise-ify `{#await await ...}` expressions on the server and correctly hydrate them on the client ([#18243](https://github.com/sveltejs/svelte/pull/18243))
- fix: deduplicate dependencies that are added outside the init/update cycle ([#18243](https://github.com/sveltejs/svelte/pull/18243))
- fix: avoid false-positive batch invariant error ([#18246](https://github.com/sveltejs/svelte/pull/18246))
- fix: inline primitive constants in attribute values during SSR ([#18232](https://github.com/sveltejs/svelte/pull/18232))
## 5.55.8
### Patch Changes
- fix(print): handle `svelte:body` and fix keyframe percentage double-printing ([#18234](https://github.com/sveltejs/svelte/pull/18234))
- fix: execute uninitialized derived even if it's destroyed ([#18228](https://github.com/sveltejs/svelte/pull/18228))
- fix: use named symbols everywhere ([#18238](https://github.com/sveltejs/svelte/pull/18238))
- fix: don't run teardown effects when deriveds are unfreezed ([#18227](https://github.com/sveltejs/svelte/pull/18227))
- fix: unset context synchronously in `run` ([#18236](https://github.com/sveltejs/svelte/pull/18236))
## 5.55.7
### Patch Changes
- fix: prevent XSS on `hydratable` from user contents ([`a16ebc67bbcf8f708360195687e1b2719463e1a4`](https://github.com/sveltejs/svelte/commit/a16ebc67bbcf8f708360195687e1b2719463e1a4))
- chore: bump devalue ([#18219](https://github.com/sveltejs/svelte/pull/18219))
- fix: disallow empty attribute names during SSR ([`547853e2406a2147ad7fb5ffeba95b01bd9642da`](https://github.com/sveltejs/svelte/commit/547853e2406a2147ad7fb5ffeba95b01bd9642da))
- fix: harden regex ([`d2375e2ebcab5c88feb5652f1a9d621b8f06b259`](https://github.com/sveltejs/svelte/commit/d2375e2ebcab5c88feb5652f1a9d621b8f06b259))
- fix: move Svelte runtime properties to symbols ([`e1cbbd96441e82c9eb8a23a2903c0d06d3cda991`](https://github.com/sveltejs/svelte/commit/e1cbbd96441e82c9eb8a23a2903c0d06d3cda991))
## 5.55.6
### Patch Changes
- fix: leave stale promises to wait for a later resolution, instead of rejecting ([#18180](https://github.com/sveltejs/svelte/pull/18180))
- fix: keep dependencies of `$state.eager/pending` ([#18218](https://github.com/sveltejs/svelte/pull/18218))
- fix: reapply context after transforming error during SSR ([#18099](https://github.com/sveltejs/svelte/pull/18099))
- fix: don't rebase just-created batches ([#18117](https://github.com/sveltejs/svelte/pull/18117))
- chore: allow `null` for `pending` in typings ([#18201](https://github.com/sveltejs/svelte/pull/18201))
- fix: flush eager effects in production ([#18107](https://github.com/sveltejs/svelte/pull/18107))
- fix: rethrow error of failed iterable after calling `return()` ([#18169](https://github.com/sveltejs/svelte/pull/18169))
- fix: account for proxified instance when updating `bind:this` ([#18147](https://github.com/sveltejs/svelte/pull/18147))
- fix: ensure scheduled batch is flushed if not obsolete ([#18131](https://github.com/sveltejs/svelte/pull/18131))
- fix: resolve stale deriveds with latest value ([#18167](https://github.com/sveltejs/svelte/pull/18167))
- chore: remove unnecessary `increment_pending` calls ([#18183](https://github.com/sveltejs/svelte/pull/18183))
- fix: correctly compile component member expressions for SSR ([#18192](https://github.com/sveltejs/svelte/pull/18192))
- fix: reset `source.updated` stack traces after `flush` ([#18196](https://github.com/sveltejs/svelte/pull/18196))
- fix: replacing async 'blocking' strategy with 'merging' ([#18205](https://github.com/sveltejs/svelte/pull/18205))
- fix: allow `@debug` tags to reference awaited variables ([#18138](https://github.com/sveltejs/svelte/pull/18138))
- fix: re-run fallback props if dependencies update ([#18146](https://github.com/sveltejs/svelte/pull/18146))
- fix: abort running obsolete async branches ([#18118](https://github.com/sveltejs/svelte/pull/18118))
- fix: ignore comments when reading CSS values ([#18153](https://github.com/sveltejs/svelte/pull/18153))
- fix: wrap `Promise.all` in `save` during SSR ([#18178](https://github.com/sveltejs/svelte/pull/18178))
- fix: ignore false-positive errors of `$inspect` dependencies ([#18106](https://github.com/sveltejs/svelte/pull/18106))
## 5.55.5
### Patch Changes
- fix: don't mark deriveds while an effect is updating ([#18124](https://github.com/sveltejs/svelte/pull/18124))
- fix: do not dispatch introstart event with animation of animate directive ([#18122](https://github.com/sveltejs/svelte/pull/18122))
## 5.55.4
### Patch Changes
- fix: never mark a child effect root as inert ([#18111](https://github.com/sveltejs/svelte/pull/18111))
- fix: reset context after waiting on blockers of `@const` expressions ([#18100](https://github.com/sveltejs/svelte/pull/18100))
- fix: keep flushing new eager effects ([#18102](https://github.com/sveltejs/svelte/pull/18102))
## 5.55.3
### Patch Changes
- fix: ensure proper HMR updates for dynamic components ([#18079](https://github.com/sveltejs/svelte/pull/18079))
- fix: correctly calculate `@const` blockers ([#18039](https://github.com/sveltejs/svelte/pull/18039))
- fix: freeze deriveds once their containing effects are destroyed ([#17921](https://github.com/sveltejs/svelte/pull/17921))
- fix: defer error boundary rendering in forks ([#18076](https://github.com/sveltejs/svelte/pull/18076))
- fix: avoid false positives for reactivity loss warning ([#18088](https://github.com/sveltejs/svelte/pull/18088))
## 5.55.2
### Patch Changes
- fix: invalidate `@const` tags based on visible references in legacy mode ([#18041](https://github.com/sveltejs/svelte/pull/18041))
- fix: handle parens in template expressions more robustly ([#18075](https://github.com/sveltejs/svelte/pull/18075))
- fix: disallow `--` in `idPrefix` ([#18038](https://github.com/sveltejs/svelte/pull/18038))
- fix: correct types for `ontoggle` on `<details>` elements ([#18063](https://github.com/sveltejs/svelte/pull/18063))
- fix: don't override `$destroy/set/on` instance methods in dev mode ([#18034](https://github.com/sveltejs/svelte/pull/18034))
- fix: unskip branches of earlier batches after commit ([#18048](https://github.com/sveltejs/svelte/pull/18048))
- fix: never set derived.v inside fork ([#18037](https://github.com/sveltejs/svelte/pull/18037))
- fix: skip rebase logic in non-async mode ([#18040](https://github.com/sveltejs/svelte/pull/18040))
- fix: don't reset status of uninitialized deriveds ([#18054](https://github.com/sveltejs/svelte/pull/18054))
## 5.55.1
### Patch Changes

@ -2067,9 +2067,9 @@ export interface SvelteHTMLElements {
};
'svelte:head': { [name: string]: any };
'svelte:boundary': {
onerror?: (error: unknown, reset: () => void) => void;
failed?: import('svelte').Snippet<[error: unknown, reset: () => void]>;
pending?: import('svelte').Snippet;
onerror?: ((error: unknown, reset: () => void) => void) | null | undefined;
failed?: import('svelte').Snippet<[error: unknown, reset: () => void]> | null | undefined;
pending?: import('svelte').Snippet | null | undefined;
};
[name: string]: { [name: string]: any };

@ -120,6 +120,12 @@ When logging a [proxy](https://developer.mozilla.org/en-US/docs/Web/JavaScript/R
The easiest way to log a value as it changes over time is to use the [`$inspect`](/docs/svelte/$inspect) rune. Alternatively, to log things on a one-off basis (for example, inside an event handler) you can use [`$state.snapshot`](/docs/svelte/$state#$state.snapshot) to take a snapshot of the current value.
## derived_inert
> Reading a derived belonging to a now-destroyed effect may result in stale values
A `$derived` value created inside an effect will stop updating when the effect is destroyed. You should create the `$derived` outside the effect, or inside an `$effect.root`.
## event_handler_invalid
> %handler% should be a function. Did you mean to %suggestion%?

@ -2,7 +2,7 @@
"name": "svelte",
"description": "Cybernetically enhanced web apps",
"license": "MIT",
"version": "5.55.1",
"version": "5.55.9",
"type": "module",
"types": "./types/index.d.ts",
"engines": {
@ -169,16 +169,16 @@
"dependencies": {
"@jridgewell/remapping": "^2.3.4",
"@jridgewell/sourcemap-codec": "^1.5.0",
"@sveltejs/acorn-typescript": "^1.0.5",
"@sveltejs/acorn-typescript": "^1.0.10",
"@types/estree": "^1.0.5",
"@types/trusted-types": "^2.0.7",
"acorn": "^8.12.1",
"aria-query": "5.3.1",
"axobject-query": "^4.1.0",
"clsx": "^2.1.1",
"devalue": "^5.6.4",
"devalue": "^5.8.1",
"esm-env": "^1.2.1",
"esrap": "^2.2.4",
"esrap": "^2.2.9",
"is-reference": "^3.0.3",
"locate-character": "^3.0.0",
"magic-string": "^0.30.11",

@ -378,7 +378,6 @@ function run() {
};
const block = esrap.print(
// @ts-expect-error some bullshit
/** @type {ESTree.Program} */ ({ ...ast, body: [clone] }),
ts({ comments: [jsdoc_clone] })
).code;

@ -147,6 +147,8 @@ declare namespace $state {
* </script>
* ```
*
* If `state` has a `toJSON` method, the snapshot will clone the value returned from `toJSON` instead of the original object.
*
* @see {@link https://svelte.dev/docs/svelte/$state#$state.snapshot Documentation}
*
* @param state The value to snapshot

@ -1,10 +1,13 @@
/** @import { Comment, Program } from 'estree' */
/** @import { AST } from '#compiler' */
/** @import { Parser } from './index.js' */
import * as acorn from 'acorn';
import { walk } from 'zimmerframe';
import { tsPlugin } from '@sveltejs/acorn-typescript';
import * as e from '../../errors.js';
const ParserWithTS = acorn.Parser.extend(tsPlugin());
const JSParser = acorn.Parser;
const TSParser = JSParser.extend(tsPlugin());
/**
* @typedef {Comment & {
@ -20,15 +23,15 @@ const ParserWithTS = acorn.Parser.extend(tsPlugin());
* @param {boolean} [is_script]
*/
export function parse(source, comments, typescript, is_script) {
const parser = typescript ? ParserWithTS : acorn.Parser;
const acorn = typescript ? TSParser : JSParser;
const { onComment, add_comments } = get_comment_handlers(
source,
/** @type {CommentWithLocation[]} */ (comments)
);
// @ts-ignore
const parse_statement = parser.prototype.parseStatement;
// @ts-expect-error
const parse_statement = acorn.prototype.parseStatement;
// If we're dealing with a <script> then it might contain an export
// for something that doesn't exist directly inside but is inside the
@ -36,7 +39,7 @@ export function parse(source, comments, typescript, is_script) {
// an error in these cases
if (is_script) {
// @ts-ignore
parser.prototype.parseStatement = function (...args) {
acorn.prototype.parseStatement = function (...args) {
const v = parse_statement.call(this, ...args);
// @ts-ignore
this.undefinedExports = {};
@ -44,53 +47,77 @@ export function parse(source, comments, typescript, is_script) {
};
}
let ast;
try {
ast = parser.parse(source, {
const ast = acorn.parse(source, {
onComment,
sourceType: 'module',
ecmaVersion: 16,
locations: true
});
add_comments(ast);
return /** @type {Program} */ (ast);
} catch (err) {
// TODO the `return` in necessary for TS<7 due to a bug; otherwise
// the `finally` block is regarded as unreachable
return handle_parse_error(err);
} finally {
if (is_script) {
// @ts-ignore
parser.prototype.parseStatement = parse_statement;
// @ts-expect-error
acorn.prototype.parseStatement = parse_statement;
}
}
add_comments(ast);
return /** @type {Program} */ (ast);
}
/**
* @param {Parser} parser
* @param {string} source
* @param {Comment[]} comments
* @param {boolean} typescript
* @param {number} index
* @returns {acorn.Expression & { leadingComments?: CommentWithLocation[]; trailingComments?: CommentWithLocation[]; }}
*/
export function parse_expression_at(source, comments, typescript, index) {
const parser = typescript ? ParserWithTS : acorn.Parser;
export function parse_expression_at(parser, source, index) {
const acorn = parser.ts ? TSParser : JSParser;
const { onComment, add_comments } = get_comment_handlers(
source,
/** @type {CommentWithLocation[]} */ (comments),
index
);
const { onComment, add_comments } = get_comment_handlers(source, parser.root.comments, index);
const ast = parser.parseExpressionAt(source, index, {
onComment,
sourceType: 'module',
ecmaVersion: 16,
locations: true
});
try {
const ast = acorn.parseExpressionAt(source, index, {
onComment,
sourceType: 'module',
ecmaVersion: 16,
locations: true,
preserveParens: true
});
add_comments(ast);
return ast;
} catch (e) {
handle_parse_error(e);
}
}
const regex_position_indicator = / \(\d+:\d+\)$/;
add_comments(ast);
/**
* @param {any} err
* @returns {never}
*/
function handle_parse_error(err) {
e.js_parse_error(err.pos, err.message.replace(regex_position_indicator, ''));
}
return ast;
/**
* @param {acorn.Expression} node
* @returns {acorn.Expression}
*/
export function remove_parens(node) {
return walk(node, null, {
ParenthesizedExpression(node, context) {
return context.visit(node.expression);
}
});
}
/**

@ -11,8 +11,6 @@ import { is_reserved } from '../../../utils.js';
import { disallow_children } from '../2-analyze/visitors/shared/special-element.js';
import * as state from '../../state.js';
const regex_position_indicator = / \(\d+:\d+\)$/;
/** @param {number} cc */
function is_whitespace(cc) {
// fast path for common whitespace
@ -175,14 +173,6 @@ export class Parser {
return this.stack[this.stack.length - 1];
}
/**
* @param {any} err
* @returns {never}
*/
acorn_error(err) {
e.js_parse_error(err.pos, err.message.replace(regex_position_indicator, ''));
}
/**
* @param {string} str
* @param {boolean} required

@ -1,7 +1,7 @@
/** @import { Pattern } from 'estree' */
/** @import { Parser } from '../index.js' */
import { match_bracket } from '../utils/bracket.js';
import { parse_expression_at } from '../acorn.js';
import { parse_expression_at, remove_parens } from '../acorn.js';
import { regex_not_newline_characters } from '../../patterns.js';
import * as e from '../../../errors.js';
@ -35,38 +35,32 @@ export default function read_pattern(parser) {
const pattern_string = parser.template.slice(start, i);
try {
// the length of the `space_with_newline` has to be start - 1
// because we added a `(` in front of the pattern_string,
// which shifted the entire string to right by 1
// so we offset it by removing 1 character in the `space_with_newline`
// to achieve that, we remove the 1st space encountered,
// so it will not affect the `column` of the node
let space_with_newline = parser.template
.slice(0, start)
.replace(regex_not_newline_characters, ' ');
const first_space = space_with_newline.indexOf(' ');
space_with_newline =
space_with_newline.slice(0, first_space) + space_with_newline.slice(first_space + 1);
const expression = /** @type {any} */ (
parse_expression_at(
`${space_with_newline}(${pattern_string} = 1)`,
parser.root.comments,
parser.ts,
start - 1
)
).left;
expression.typeAnnotation = read_type_annotation(parser);
if (expression.typeAnnotation) {
expression.end = expression.typeAnnotation.end;
}
return expression;
} catch (error) {
parser.acorn_error(error);
// the length of the `space_with_newline` has to be start - 1
// because we added a `(` in front of the pattern_string,
// which shifted the entire string to right by 1
// so we offset it by removing 1 character in the `space_with_newline`
// to achieve that, we remove the 1st space encountered,
// so it will not affect the `column` of the node
let space_with_newline = parser.template
.slice(0, start)
.replace(regex_not_newline_characters, ' ');
const first_space = space_with_newline.indexOf(' ');
space_with_newline =
space_with_newline.slice(0, first_space) + space_with_newline.slice(first_space + 1);
/** @type {any} */
let expression = remove_parens(
parse_expression_at(parser, `${space_with_newline}(${pattern_string} = 1)`, start - 1)
);
expression = expression.left;
expression.typeAnnotation = read_type_annotation(parser);
if (expression.typeAnnotation) {
expression.end = expression.typeAnnotation.end;
}
return expression;
}
/**
@ -92,13 +86,13 @@ function read_type_annotation(parser) {
// parameters as part of a sequence expression instead, and will then error on optional
// parameters (`?:`). Therefore replace that sequence with something that will not error.
parser.template.slice(parser.index).replace(/\?\s*:/g, ':');
let expression = parse_expression_at(template, parser.root.comments, parser.ts, a);
let expression = remove_parens(parse_expression_at(parser, template, a));
// `foo: bar = baz` gets mangled — fix it
if (expression.type === 'AssignmentExpression') {
let b = expression.right.start;
while (template[b] !== '=') b -= 1;
expression = parse_expression_at(template.slice(0, b), parser.root.comments, parser.ts, a);
expression = remove_parens(parse_expression_at(parser, template.slice(0, b), a));
}
// `array as item: string, index` becomes `string, index`, which is mistaken as a sequence expression - fix that

@ -1,6 +1,6 @@
/** @import { Expression } from 'estree' */
/** @import { Parser } from '../index.js' */
import { parse_expression_at } from '../acorn.js';
import { parse_expression_at, remove_parens } from '../acorn.js';
import { regex_whitespace } from '../../patterns.js';
import * as e from '../../../errors.js';
import { find_matching_bracket } from '../utils/bracket.js';
@ -34,50 +34,16 @@ export function get_loose_identifier(parser, opening_token) {
*/
export default function read_expression(parser, opening_token, disallow_loose) {
try {
let comment_index = parser.root.comments.length;
const node = parse_expression_at(
parser.template,
parser.root.comments,
parser.ts,
parser.index
);
let num_parens = 0;
let i = parser.root.comments.length;
while (i-- > comment_index) {
const comment = parser.root.comments[i];
if (comment.end < node.start) {
parser.index = comment.end;
break;
}
}
for (let i = parser.index; i < /** @type {number} */ (node.start); i += 1) {
if (parser.template[i] === '(') num_parens += 1;
}
const node = parse_expression_at(parser, parser.template, parser.index);
let index = /** @type {number} */ (node.end);
const last_comment = parser.root.comments.at(-1);
if (last_comment && last_comment.end > index) index = last_comment.end;
while (num_parens > 0) {
const char = parser.template[index];
if (char === ')') {
num_parens -= 1;
} else if (!regex_whitespace.test(char)) {
e.expected_token(index, ')');
}
index += 1;
}
parser.index = index;
return /** @type {Expression} */ (node);
return /** @type {Expression} */ (remove_parens(node));
} catch (err) {
// If we are in an each loop we need the error to be thrown in cases like
// `as { y = z }` so we still throw and handle the error there
@ -88,6 +54,6 @@ export default function read_expression(parser, opening_token, disallow_loose) {
}
}
parser.acorn_error(err);
throw err;
}
}

@ -31,14 +31,7 @@ export function read_script(parser, start, attributes) {
parser.template.slice(0, script_start).replace(regex_not_newline_characters, ' ') + data;
parser.read(regex_starts_with_closing_script_tag);
/** @type {Program} */
let ast;
try {
ast = acorn.parse(source, parser.root.comments, parser.ts, true);
} catch (err) {
parser.acorn_error(err);
}
const ast = acorn.parse(source, parser.root.comments, parser.ts, true);
ast.start = script_start;

@ -524,6 +524,21 @@ function read_value(parser) {
in_url = true;
} else if ((char === ';' || char === '{' || char === '}') && !in_url && !quote_mark) {
return value.trim();
} else if (
char === '/' &&
!in_url &&
!quote_mark &&
parser.template[parser.index + 1] === '*'
) {
parser.index += 2;
while (parser.index < parser.template.length) {
if (parser.template[parser.index] === '*' && parser.template[parser.index + 1] === '/') {
parser.index += 2;
break;
}
parser.index++;
}
continue;
}
value += char;

@ -392,12 +392,7 @@ function open(parser) {
let function_expression = matched
? /** @type {ArrowFunctionExpression} */ (
parse_expression_at(
prelude + `${params} => {}`,
parser.root.comments,
parser.ts,
params_start
)
parse_expression_at(parser, prelude + `${params} => {}`, params_start)
)
: { params: [] };

@ -2,6 +2,7 @@ import type { Scope } from '../scope.js';
import type { ComponentAnalysis, ReactiveStatement } from '../types.js';
import type { AST, StateField, ValidatedCompileOptions } from '#compiler';
import type { ExpressionMetadata } from '../nodes.js';
import type { Identifier } from 'estree';
export interface AnalysisState {
scope: Scope;
@ -33,6 +34,13 @@ export interface AnalysisState {
* Set when we're inside a `$derived(...)` expression (but not `$derived.by(...)`) or `@const`
*/
derived_function_depth: number;
/** Collected info about async `{@const }` declarations */
async_consts?: {
id: Identifier;
/** How many `$.run(...)` entries are already allocated in this scope */
declaration_count: number;
};
}
export type Context<State extends AnalysisState = AnalysisState> = import('zimmerframe').Context<

@ -1,6 +1,7 @@
/** @import { AST } from '#compiler' */
/** @import { Context } from '../types' */
import * as e from '../../../errors.js';
import * as b from '#compiler/builders';
import { validate_opening_tag } from './shared/utils.js';
/**
@ -42,4 +43,29 @@ export function ConstTag(node, context) {
function_depth: context.state.function_depth + 1,
derived_function_depth: context.state.function_depth + 1
});
const has_await = node.metadata.expression.has_await;
const blockers = [...node.metadata.expression.dependencies]
.map((dep) => dep.blocker)
.filter((b) => b !== null && b.object !== context.state.async_consts?.id);
if (has_await || context.state.async_consts || blockers.length > 0) {
const run = (context.state.async_consts ??= {
id: context.state.analysis.root.unique('promises'),
declaration_count: 0
});
node.metadata.promises_id = run.id;
const bindings = context.state.scope.get_bindings(declaration);
// keep the counter in sync with the number of thunks pushed in ConstTag in transform
// TODO 6.0 once non-async and non-runes mode is gone investigate making this more robust
// via something like the approach in https://github.com/sveltejs/svelte/pull/18032
const length = run.declaration_count + (blockers.length > 0 ? 1 : 0);
run.declaration_count += blockers.length > 0 ? 2 : 1;
const blocker = b.member(run.id, b.literal(length), true);
for (const binding of bindings) {
binding.blocker = blocker;
}
}
}

@ -6,5 +6,5 @@
* @param {Context} context
*/
export function Fragment(node, context) {
context.next({ ...context.state, fragment: node });
context.next({ ...context.state, fragment: node, async_consts: undefined });
}

@ -71,14 +71,14 @@ export function AwaitBlock(node, context) {
'await'
);
if (node.metadata.expression.has_blockers()) {
if (node.metadata.expression.has_blockers() || node.metadata.expression.has_await) {
context.state.init.push(
b.stmt(
b.call(
'$.async',
context.state.node,
node.metadata.expression.blockers(),
b.array([]),
b.array([]), // {#await await ...} is special insofar that the await should not be waited on
b.arrow([context.state.node], b.block([stmt]))
)
)

@ -1,7 +1,6 @@
/** @import { Expression, Identifier, Pattern } from 'estree' */
/** @import { Expression, Identifier, Pattern, Statement } from 'estree' */
/** @import { AST } from '#compiler' */
/** @import { ComponentContext } from '../types' */
/** @import { ExpressionMetadata } from '../../../nodes.js' */
import { dev } from '../../../../state.js';
import { extract_identifiers } from '../../../../utils/ast.js';
import * as b from '#compiler/builders';
@ -27,13 +26,7 @@ export function ConstTag(node, context) {
context.state.transform[declaration.id.name] = { read: get_value };
add_const_declaration(
context.state,
declaration.id,
expression,
node.metadata.expression,
context.state.scope.get_bindings(declaration)
);
add_const_declaration(context.state, declaration.id, expression, node.metadata);
} else {
const identifiers = extract_identifiers(declaration.id);
const tmp = b.id(context.state.scope.generate('computed_const'));
@ -70,13 +63,7 @@ export function ConstTag(node, context) {
expression = b.call('$.tag', expression, b.literal('[@const]'));
}
add_const_declaration(
context.state,
tmp,
expression,
node.metadata.expression,
context.state.scope.get_bindings(declaration)
);
add_const_declaration(context.state, tmp, expression, node.metadata);
for (const node of identifiers) {
context.state.transform[node.name] = {
@ -90,43 +77,34 @@ export function ConstTag(node, context) {
* @param {ComponentContext['state']} state
* @param {Identifier} id
* @param {Expression} expression
* @param {ExpressionMetadata} metadata
* @param {import('#compiler').Binding[]} bindings
* @param {AST.ConstTag['metadata']} metadata
*/
function add_const_declaration(state, id, expression, metadata, bindings) {
function add_const_declaration(state, id, expression, metadata) {
// we need to eagerly evaluate the expression in order to hit any
// 'Cannot access x before initialization' errors
const after = dev ? [b.stmt(b.call('$.get', id))] : [];
const has_await = metadata.has_await;
const blockers = [...metadata.dependencies]
const blockers = [...metadata.expression.dependencies]
.map((dep) => dep.blocker)
.filter((b) => b !== null && b.object !== state.async_consts?.id);
if (has_await || state.async_consts || blockers.length > 0) {
if (metadata.promises_id) {
const run = (state.async_consts ??= {
id: b.id(state.scope.generate('promises')),
id: metadata.promises_id,
thunks: []
});
state.consts.push(b.let(id));
const assignment = b.assignment('=', id, expression);
const body = after.length === 0 ? assignment : b.block([b.stmt(assignment), ...after]);
if (blockers.length === 1) {
run.thunks.push(b.thunk(b.member(/** @type {Expression} */ (blockers[0]), 'promise')));
} else if (blockers.length > 0) {
run.thunks.push(b.thunk(b.call('$.wait', b.array(blockers))));
}
run.thunks.push(b.thunk(body, has_await));
const blocker = b.member(run.id, b.literal(run.thunks.length - 1), true);
for (const binding of bindings) {
binding.blocker = blocker;
}
// keep the number of thunks pushed in sync with ConstTag in analysis phase
const assignment = b.assignment('=', id, expression);
run.thunks.push(b.thunk(assignment, metadata.expression.has_await));
} else {
state.consts.push(b.const(id, expression));
state.consts.push(...after);

@ -8,6 +8,10 @@ import * as b from '#compiler/builders';
* @param {ComponentContext} context
*/
export function DebugTag(node, context) {
const blockers = node.identifiers
.map((identifier) => context.state.scope.get(identifier.name)?.blocker)
.filter((blocker) => blocker != null);
const object = b.object(
node.identifiers.map((identifier) => {
const visited = b.call('$.snapshot', /** @type {Expression} */ (context.visit(identifier)));
@ -20,9 +24,11 @@ export function DebugTag(node, context) {
})
);
const call = b.call('console.log', object);
const args = [b.thunk(b.block([b.stmt(b.call('console.log', object)), b.debugger]))];
context.state.init.push(
b.stmt(b.call('$.template_effect', b.thunk(b.block([b.stmt(call), b.debugger]))))
);
if (blockers.length > 0) {
args.push(b.array([]), b.array([]), b.array(blockers));
}
context.state.init.push(b.stmt(b.call('$.template_effect', ...args)));
}

@ -386,13 +386,17 @@ export function VariableDeclaration(node, context) {
* @param {Expression} value
*/
function create_state_declarators(declarator, context, value) {
/**
* @param {Expression} value
* @param {string} name
*/
const mutable_source = (value, name) => {
const call = b.call('$.mutable_source', value, context.state.analysis.immutable && b.true);
return dev ? b.call('$.tag', call, b.literal(name)) : call;
};
if (declarator.id.type === 'Identifier') {
return [
b.declarator(
declarator.id,
b.call('$.mutable_source', value, context.state.analysis.immutable ? b.true : undefined)
)
];
return [b.declarator(declarator.id, mutable_source(value, declarator.id.name))];
}
const tmp = b.id(context.state.scope.generate('tmp'));
@ -414,7 +418,7 @@ function create_state_declarators(declarator, context, value) {
return b.declarator(
path.node,
binding?.kind === 'state'
? b.call('$.mutable_source', value, context.state.analysis.immutable ? b.true : undefined)
? mutable_source(value, /** @type {Identifier} */ (path.node).name)
: value
);
})

@ -9,12 +9,19 @@ import { block_close, create_child_block } from './shared/utils.js';
* @param {ComponentContext} context
*/
export function AwaitBlock(node, context) {
let expression = /** @type {Expression} */ (context.visit(node.expression));
if (node.metadata.expression.has_await) {
// If this is an await expression, turn it into a IIFE so that the result is a promise.
// {#await await ...} is special insofar that the await should not be waited on.
expression = b.call(b.arrow([], expression, true));
}
/** @type {Statement} */
let statement = b.stmt(
b.call(
'$.await',
b.id('$$renderer'),
/** @type {Expression} */ (context.visit(node.expression)),
expression,
b.thunk(
node.pending ? /** @type {BlockStatement} */ (context.visit(node.pending)) : b.block([])
),

@ -9,5 +9,9 @@ import { build_inline_component } from './shared/component.js';
* @param {ComponentContext} context
*/
export function Component(node, context) {
build_inline_component(node, /** @type {Expression} */ (context.visit(b.id(node.name))), context);
build_inline_component(
node,
/** @type {Expression} */ (context.visit(b.member_id(node.name))),
context
);
}

@ -1,4 +1,4 @@
/** @import { Expression, Pattern } from 'estree' */
/** @import { Expression, Pattern, Statement } from 'estree' */
/** @import { AST } from '#compiler' */
/** @import { ComponentContext } from '../types.js' */
import * as b from '#compiler/builders';
@ -12,19 +12,17 @@ export function ConstTag(node, context) {
const declaration = node.declaration.declarations[0];
const id = /** @type {Pattern} */ (context.visit(declaration.id));
const init = /** @type {Expression} */ (context.visit(declaration.init));
const has_await = node.metadata.expression.has_await;
const blockers = [...node.metadata.expression.dependencies]
.map((dep) => dep.blocker)
.filter((b) => b !== null && b.object !== context.state.async_consts?.id);
if (has_await || context.state.async_consts || blockers.length > 0) {
if (node.metadata.promises_id) {
const run = (context.state.async_consts ??= {
id: b.id(context.state.scope.generate('promises')),
id: node.metadata.promises_id,
thunks: []
});
const identifiers = extract_identifiers(declaration.id);
const bindings = context.state.scope.get_bindings(declaration);
for (const identifier of identifiers) {
context.state.init.push(b.let(identifier.name));
@ -36,13 +34,9 @@ export function ConstTag(node, context) {
run.thunks.push(b.thunk(b.call('Promise.all', b.array(blockers))));
}
// keep the number of thunks pushed in sync with ConstTag in analysis phase
const assignment = b.assignment('=', id, init);
run.thunks.push(b.thunk(b.block([b.stmt(assignment)]), has_await));
const blocker = b.member(run.id, b.literal(run.thunks.length - 1), true);
for (const binding of bindings) {
binding.blocker = blocker;
}
run.thunks.push(b.thunk(assignment, node.metadata.expression.has_await));
} else {
context.state.init.push(b.const(id, init));
}

@ -2,23 +2,34 @@
/** @import { AST } from '#compiler' */
/** @import { ComponentContext } from '../types.js' */
import * as b from '#compiler/builders';
import { create_child_block } from './shared/utils.js';
/**
* @param {AST.DebugTag} node
* @param {ComponentContext} context
*/
export function DebugTag(node, context) {
const blockers = node.identifiers
.map((identifier) => context.state.scope.get(identifier.name)?.blocker)
.filter((blocker) => blocker != null);
context.state.template.push(
b.stmt(
b.call(
'console.log',
b.object(
node.identifiers.map((identifier) =>
b.prop('init', identifier, /** @type {Expression} */ (context.visit(identifier)))
...create_child_block(
[
b.stmt(
b.call(
'console.log',
b.object(
node.identifiers.map((identifier) =>
b.prop('init', identifier, /** @type {Expression} */ (context.visit(identifier)))
)
)
)
)
)
),
b.debugger
),
b.debugger
],
b.array(blockers),
false
)
);
}

@ -12,7 +12,7 @@ import {
import * as b from '#compiler/builders';
import { sanitize_template_string } from '../../../../../utils/sanitize_template_string.js';
import { regex_whitespaces_strict } from '../../../../patterns.js';
import { has_await_expression } from '../../../../../utils/ast.js';
import { has_await_expression, save } from '../../../../../utils/ast.js';
import { ExpressionMetadata } from '../../../../nodes.js';
/** Opens an if/each block, so that we can remove nodes in the case of a mismatch */
@ -229,18 +229,25 @@ export function build_attribute_value(
? node.data.replace(regex_whitespaces_strict, ' ')
: node.data;
} else {
expressions.push(
b.call(
'$.stringify',
transform(
/** @type {Expression} */ (context.visit(node.expression)),
node.metadata.expression
)
)
);
const evaluated = context.state.scope.evaluate(node.expression);
quasi = b.quasi('', i + 1 === value.length);
quasis.push(quasi);
if (evaluated.is_known) {
quasi.value.cooked += (evaluated.value ?? '') + '';
} else {
const expression = transform(
/** @type {Expression} */ (context.visit(node.expression)),
node.metadata.expression
);
expressions.push(
evaluated.is_string && evaluated.is_defined
? expression
: b.call('$.stringify', expression)
);
quasi = b.quasi('', i + 1 === value.length);
quasis.push(quasi);
}
}
}
@ -248,7 +255,9 @@ export function build_attribute_value(
quasi.value.raw = sanitize_template_string(/** @type {string} */ (quasi.value.cooked));
}
return b.template(quasis, expressions);
return expressions.length > 0
? b.template(quasis, expressions)
: b.literal(/** @type {string} */ (quasi.value.cooked));
}
/**
@ -360,7 +369,7 @@ export class PromiseOptimiser {
return b.const(
b.array_pattern(this.expressions.map((_, i) => b.id(`$$${i}`))),
b.await(b.call('Promise.all', promises))
save(b.call('Promise.all', promises))
);
}

@ -247,7 +247,7 @@ const css_visitors = {
},
Percentage(node, context) {
context.write(`${node.value}%`);
context.write(node.value);
},
PseudoClassSelector(node, context) {
@ -417,6 +417,7 @@ const svelte_visitors = (comments) => ({
const is_block_element =
child_node.type === 'RegularElement' ||
child_node.type === 'Component' ||
child_node.type === 'SvelteBody' ||
child_node.type === 'SvelteHead' ||
child_node.type === 'SvelteFragment' ||
child_node.type === 'SvelteBoundary' ||
@ -821,6 +822,10 @@ const svelte_visitors = (comments) => ({
context.write('</style>');
},
SvelteBody(node, context) {
base_element(node, context, comments);
},
SvelteBoundary(node, context) {
base_element(node, context, comments);
},

@ -155,6 +155,8 @@ export namespace AST {
/** @internal */
metadata: {
expression: ExpressionMetadata;
/** If this const tag contains an await expression, or needs to wait on other async, this is set */
promises_id?: Identifier;
};
}

@ -32,7 +32,7 @@ export const ELEMENT_IS_NAMESPACED = 1;
export const ELEMENT_PRESERVE_ATTRIBUTE_CASE = 1 << 1;
export const ELEMENT_IS_INPUT = 1 << 2;
export const UNINITIALIZED = Symbol();
export const UNINITIALIZED = Symbol('uninitialized');
// Dev-time component properties
export const FILENAME = Symbol('filename');

@ -48,7 +48,8 @@ export const STATE_EAGER_EFFECT = 1 << 27;
/**
* Tells that we marked this derived and its reactions as visited during the "mark as (maybe) dirty"-phase.
* Will be lifted during execution of the derived and during checking its dirty state (both are necessary
* because a derived might be checked but not executed).
* because a derived might be checked but not executed). This is a pure performance optimization flag and
* should not be used for any other purpose!
*/
export const WAS_MARKED = 1 << 16;
@ -62,6 +63,13 @@ export const STATE_SYMBOL = Symbol('$state');
export const LEGACY_PROPS = Symbol('legacy props');
export const LOADING_ATTR_SYMBOL = Symbol('');
export const PROXY_PATH_SYMBOL = Symbol('proxy path');
export const ATTRIBUTES_CACHE = Symbol('attributes');
export const CLASS_CACHE = Symbol('class');
export const STYLE_CACHE = Symbol('style');
export const TEXT_CACHE = Symbol('text');
export const FORM_RESET_HANDLER = Symbol('form reset');
/** An anchor might change, via this symbol on the original anchor we can tell HMR about the updated anchor */
export const HMR_ANCHOR = Symbol('hmr anchor');
/** allow users to ignore aborted signal errors if `reason.name === 'StaleReactionError` */
export const STALE_REACTION = new (class StaleReactionError extends Error {

@ -1,6 +1,6 @@
/** @import { Effect, TemplateNode } from '#client' */
import { FILENAME, HMR } from '../../../constants.js';
import { EFFECT_TRANSPARENT } from '#client/constants';
import { EFFECT_TRANSPARENT, HMR_ANCHOR } from '#client/constants';
import { hydrate_node, hydrating } from '../dom/hydration.js';
import { block, branch, destroy_effect } from '../reactivity/effects.js';
import { set, source } from '../reactivity/sources.js';
@ -15,10 +15,10 @@ export function hmr(fn) {
const current = source(fn);
/**
* @param {TemplateNode} anchor
* @param {TemplateNode} initial_anchor
* @param {any} props
*/
function wrapper(anchor, props) {
function wrapper(initial_anchor, props) {
let component = {};
let instance = {};
@ -26,6 +26,7 @@ export function hmr(fn) {
let effect;
let ran = false;
let anchor = initial_anchor;
block(() => {
if (component === (component = get(current))) {
@ -39,6 +40,8 @@ export function hmr(fn) {
}
effect = branch(() => {
anchor = /** @type {any} */ (anchor)[HMR_ANCHOR] ?? anchor;
// when the component is invalidated, replace it without transitions
if (ran) set_should_intro(false);

@ -20,6 +20,8 @@ export function inspect(get_value, inspector, show_stack = false) {
// in an error (an `$inspect(object.property)` will run before the
// `{#if object}...{/if}` that contains it)
eager_effect(() => {
error = UNINITIALIZED;
try {
var value = get_value();
} catch (e) {

@ -1,5 +1,5 @@
/** @import { Blocker, TemplateNode, Value } from '#client' */
import { flatten, increment_pending } from '../../reactivity/async.js';
import { flatten } from '../../reactivity/async.js';
import { get } from '../../runtime.js';
import {
hydrate_next,
@ -42,8 +42,6 @@ export function async(node, blockers = [], expressions = [], fn) {
return;
}
const decrement_pending = increment_pending();
if (was_hydrating) {
var previous_hydrate_node = hydrate_node;
set_hydrate_node(end);
@ -64,8 +62,6 @@ export function async(node, blockers = [], expressions = [], fn) {
if (was_hydrating) {
set_hydrating(false);
}
decrement_pending();
}
});
}

@ -7,7 +7,8 @@ import {
hydrating,
skip_nodes,
set_hydrate_node,
set_hydrating
set_hydrating,
hydrate_node
} from '../hydration.js';
import { queue_micro_task } from '../task.js';
import { HYDRATION_START_ELSE, UNINITIALIZED } from '../../../../constants.js';
@ -15,6 +16,7 @@ import { is_runes } from '../../context.js';
import { Batch, current_batch, flushSync, is_flushing_sync } from '../../reactivity/batch.js';
import { BranchManager } from './branches.js';
import { capture, unset_context } from '../../reactivity/async.js';
import { DEV } from 'esm-env';
const PENDING = 0;
const THEN = 1;
@ -42,16 +44,16 @@ export function await_block(node, get_input, pending_fn, then_fn, catch_fn) {
var value = runes ? source(v) : mutable_source(v, false, false);
var error = runes ? source(v) : mutable_source(v, false, false);
if (DEV) {
value.label = '{#await ...} value';
error.label = '{#await ...} error';
}
var branches = new BranchManager(node);
block(() => {
var batch = /** @type {Batch} */ (current_batch);
// we null out `current_batch` because otherwise `save(...)` will incorrectly restore it —
// the batch will already have been committed by the time it resolves
batch.deactivate();
var input = get_input();
batch.activate();
var destroyed = false;
@ -79,15 +81,17 @@ export function await_block(node, get_input, pending_fn, then_fn, catch_fn) {
// We don't want to restore the previous batch here; {#await} blocks don't follow the async logic
// we have elsewhere, instead pending/resolve/fail states are each their own batch so to speak.
restore(false);
// ...but it might still be set here. That means a `save(...)` has restored it — but that batch will
// likely already have been committed by the time it resolves, and this resolve should be processed
// in a separate batch. We're not using batch.deactivate()/activate() above because get_input()
// could write to sources, which would then incorrectly create a new batch or could mess with
// async_derived expecting a current_batch to exist.
if (current_batch === batch) {
batch.deactivate();
}
// Make sure we have a batch, since the branch manager expects one to exist
Batch.ensure();
if (hydrating) {
// `restore()` could set `hydrating` to `true`, which we very much
// don't want — we want to restore everything _except_ this
set_hydrating(false);
}
try {
fn();
} finally {

@ -29,7 +29,7 @@ import { queue_micro_task } from '../task.js';
import * as e from '../../errors.js';
import * as w from '../../warnings.js';
import { DEV } from 'esm-env';
import { Batch, current_batch, schedule_effect } from '../../reactivity/batch.js';
import { Batch, current_batch } from '../../reactivity/batch.js';
import { internal_set, source } from '../../reactivity/sources.js';
import { tag } from '../../dev/tracing.js';
import { createSubscriber } from '../../../../reactivity/create-subscriber.js';
@ -38,9 +38,9 @@ import { defer_effect } from '../../reactivity/utils.js';
/**
* @typedef {{
* onerror?: (error: unknown, reset: () => void) => void;
* failed?: (anchor: Node, error: () => unknown, reset: () => () => void) => void;
* pending?: (anchor: Node) => void;
* onerror?: ((error: unknown, reset: () => void) => void) | null;
* failed?: ((anchor: Node, error: () => unknown, reset: () => () => void) => void) | null;
* pending?: ((anchor: Node) => void) | null;
* }} BoundaryProps
*/
@ -376,15 +376,29 @@ export class Boundary {
/** @param {unknown} error */
error(error) {
var onerror = this.#props.onerror;
let failed = this.#props.failed;
// If we have nothing to capture the error, or if we hit an error while
// rendering the fallback, re-throw for another boundary to handle
if (!onerror && !failed) {
if (!this.#props.onerror && !this.#props.failed) {
throw error;
}
if (current_batch?.is_fork) {
if (this.#main_effect) current_batch.skip_effect(this.#main_effect);
if (this.#pending_effect) current_batch.skip_effect(this.#pending_effect);
if (this.#failed_effect) current_batch.skip_effect(this.#failed_effect);
current_batch.on_fork_commit(() => {
this.#handle_error(error);
});
} else {
this.#handle_error(error);
}
}
/**
* @param {unknown} error
*/
#handle_error(error) {
if (this.#main_effect) {
destroy_effect(this.#main_effect);
this.#main_effect = null;
@ -406,6 +420,8 @@ export class Boundary {
set_hydrate_node(skip_nodes());
}
var onerror = this.#props.onerror;
let failed = this.#props.failed;
var did_reset = false;
var calling_on_error = false;

@ -7,8 +7,10 @@ import {
pause_effect,
resume_effect
} from '../../reactivity/effects.js';
import { HMR_ANCHOR } from '../../constants.js';
import { hydrate_node, hydrating } from '../hydration.js';
import { create_text, should_defer_append } from '../operations.js';
import { DEV } from 'esm-env';
/**
* @typedef {{ effect: Effect, fragment: DocumentFragment }} Branch
@ -91,6 +93,12 @@ export class BranchManager {
this.#onscreen.set(key, offscreen.effect);
this.#offscreen.delete(key);
if (DEV) {
// Tell hmr.js about the anchor it should use for updates,
// since the initial one will be removed
/** @type {any} */ (offscreen.fragment.lastChild)[HMR_ANCHOR] = this.anchor;
}
// remove the anchor...
/** @type {TemplateNode} */ (offscreen.fragment.lastChild).remove();

@ -1,8 +1,8 @@
/** @import { TemplateNode } from '#client' */
import { hydrate_node, hydrating, set_hydrate_node, set_hydrating } from '../hydration.js';
import { create_text, get_first_child, get_next_sibling } from '../operations.js';
import { block } from '../../reactivity/effects.js';
import { COMMENT_NODE, EFFECT_PRESERVED, HEAD_EFFECT } from '#client/constants';
import { block, branch } from '../../reactivity/effects.js';
import { COMMENT_NODE, HEAD_EFFECT } from '#client/constants';
/**
* @param {string} hash
@ -49,9 +49,10 @@ export function head(hash, render_fn) {
}
try {
// normally a branch is the child of a block and would have the EFFECT_PRESERVED flag,
// but since head blocks don't necessarily only have direct branch children we add it on the block itself
block(() => render_fn(anchor), HEAD_EFFECT | EFFECT_PRESERVED);
block(() => {
var e = branch(() => render_fn(anchor));
e.f |= HEAD_EFFECT;
});
} finally {
if (was_hydrating) {
set_hydrating(true);

@ -5,7 +5,12 @@ import { get_descriptors, get_prototype_of } from '../../../shared/utils.js';
import { create_event, delegate, delegated, event, event_symbol } from './events.js';
import { add_form_reset_listener, autofocus } from './misc.js';
import * as w from '../../warnings.js';
import { IS_XHTML, LOADING_ATTR_SYMBOL } from '#client/constants';
import {
ATTRIBUTES_CACHE,
FORM_RESET_HANDLER,
IS_XHTML,
LOADING_ATTR_SYMBOL
} from '#client/constants';
import { queue_micro_task } from '../task.js';
import { is_capture_event, can_delegate_event, normalize_attribute } from '../../../../utils.js';
import {
@ -69,8 +74,7 @@ export function remove_input_defaults(input) {
}
};
// @ts-expect-error
input.__on_r = remove_defaults;
/** @type {any} */ (input)[FORM_RESET_HANDLER] = remove_defaults;
queue_micro_task(remove_defaults);
add_form_reset_listener();
}
@ -561,8 +565,7 @@ export function attribute_effect(
*/
function get_attributes(element) {
return /** @type {Record<string | symbol, unknown>} **/ (
// @ts-expect-error
element.__attributes ??= {
/** @type {any} */ (element)[ATTRIBUTES_CACHE] ??= {
[IS_CUSTOM_ELEMENT]: element.nodeName.includes('-'),
[IS_HTML]: element.namespaceURI === NAMESPACE_HTML
}
@ -583,13 +586,19 @@ function get_setters(element) {
var proto = element; // In the case of custom elements there might be setters on the instance
var element_proto = Element.prototype;
// Stop at Element, from there on there's only unnecessary setters we're not interested in
// Do not use contructor.name here as that's unreliable in some browser environments
// Stop at Element, from there on there's only unnecessary (and dangerous, like innerHTML) setters we're not interested in
// Do not use constructor.name here as that's unreliable in some browser environments
while (element_proto !== proto) {
descriptors = get_descriptors(proto);
for (var key in descriptors) {
if (descriptors[key].set) {
if (
descriptors[key].set &&
// better safe than sorry, we don't want spread attributes to mess with HTML content
key !== 'innerHTML' &&
key !== 'textContent' &&
key !== 'innerText'
) {
setters.push(key);
}
}

@ -5,6 +5,7 @@ import {
set_active_effect,
set_active_reaction
} from '../../../runtime.js';
import { FORM_RESET_HANDLER } from '../../../constants.js';
import { add_form_reset_listener } from '../misc.js';
/**
@ -58,18 +59,15 @@ export function without_reactive_context(fn) {
*/
export function listen_to_event_and_reset_event(element, event, handler, on_reset = handler) {
element.addEventListener(event, () => without_reactive_context(handler));
// @ts-expect-error
const prev = element.__on_r;
const prev = /** @type {any} */ (element)[FORM_RESET_HANDLER];
if (prev) {
// special case for checkbox that can have multiple binds (group & checked)
// @ts-expect-error
element.__on_r = () => {
/** @type {any} */ (element)[FORM_RESET_HANDLER] = () => {
prev();
on_reset(true);
};
} else {
// @ts-expect-error
element.__on_r = () => on_reset(true);
/** @type {any} */ (element)[FORM_RESET_HANDLER] = () => on_reset(true);
}
add_form_reset_listener();

@ -40,7 +40,7 @@ export function bind_this(element_or_component = {}, update, get_value, get_part
parts = get_parts?.() || [];
untrack(() => {
if (element_or_component !== get_value(...parts)) {
if (!is_bound_this(get_value(...parts), element_or_component)) {
update(element_or_component, ...parts);
// If this is an effect rerun (cause: each block context changes), then nullify the binding at
// the previous position if it isn't already taken over by a different effect.

@ -1,4 +1,5 @@
import { to_class } from '../../../shared/attributes.js';
import { CLASS_CACHE } from '../../constants.js';
import { hydrating } from '../hydration.js';
/**
@ -11,8 +12,7 @@ import { hydrating } from '../hydration.js';
* @returns {Record<string, boolean> | undefined}
*/
export function set_class(dom, is_html, value, hash, prev_classes, next_classes) {
// @ts-expect-error need to add __className to patched prototype
var prev = dom.__className;
var prev = /** @type {any} */ (dom)[CLASS_CACHE];
if (
hydrating ||
@ -35,8 +35,7 @@ export function set_class(dom, is_html, value, hash, prev_classes, next_classes)
}
}
// @ts-expect-error need to add __className to patched prototype
dom.__className = value;
/** @type {any} */ (dom)[CLASS_CACHE] = value;
} else if (next_classes && prev_classes !== next_classes) {
for (var key in next_classes) {
var is_present = !!next_classes[key];

@ -237,9 +237,9 @@ export function handle_event_propagation(event) {
});
// This started because of Chromium issue https://chromestatus.com/feature/5128696823545856,
// where removal or moving of of the DOM can cause sync `blur` events to fire, which can cause logic
// where removal or moving of the DOM can cause sync `blur` events to fire, which can cause logic
// to run inside the current `active_reaction`, which isn't what we want at all. However, on reflection,
// it's probably best that all event handled by Svelte have this behaviour, as we don't really want
// it's probably best that all events handled by Svelte have this behaviour, as we don't really want
// an event handler to run in the context of another reaction or effect.
var previous_reaction = active_reaction;
var previous_effect = active_effect;

@ -1,6 +1,7 @@
import { hydrating } from '../hydration.js';
import { clear_text_content, get_first_child } from '../operations.js';
import { queue_micro_task } from '../task.js';
import { FORM_RESET_HANDLER } from '../../constants.js';
/**
* @param {HTMLElement} dom
@ -45,8 +46,7 @@ export function add_form_reset_listener() {
Promise.resolve().then(() => {
if (!evt.defaultPrevented) {
for (const e of /**@type {HTMLFormElement} */ (evt.target).elements) {
// @ts-expect-error
e.__on_r?.();
/** @type {any} */ (e)[FORM_RESET_HANDLER]?.();
}
}
});

@ -1,4 +1,5 @@
import { to_style } from '../../../shared/attributes.js';
import { STYLE_CACHE } from '../../constants.js';
import { hydrating } from '../hydration.js';
/**
@ -28,8 +29,7 @@ function update_styles(dom, prev = {}, next, priority) {
* @param {Record<string, any> | [Record<string, any>, Record<string, any>]} [next_styles]
*/
export function set_style(dom, value, prev_styles, next_styles) {
// @ts-expect-error
var prev = dom.__style;
var prev = /** @type {any} */ (dom)[STYLE_CACHE];
if (hydrating || prev !== value) {
var next_style_attr = to_style(value, next_styles);
@ -42,8 +42,7 @@ export function set_style(dom, value, prev_styles, next_styles) {
}
}
// @ts-expect-error
dom.__style = value;
/** @type {any} */ (dom)[STYLE_CACHE] = value;
} else if (next_styles) {
if (Array.isArray(next_styles)) {
update_styles(dom, prev_styles?.[0], next_styles[0]);

@ -115,10 +115,17 @@ export function animation(element, get_fn, get_params) {
) {
const options = get_fn()(this.element, { from, to }, get_params?.());
animation = animate(this.element, options, undefined, 1, () => {
animation?.abort();
animation = undefined;
});
animation = animate(
this.element,
options,
undefined,
1,
() => {},
() => {
animation?.abort();
animation = undefined;
}
);
}
},
fix() {
@ -239,15 +246,24 @@ export function transition(flags, element, get_fn, get_params) {
intro?.abort();
}
intro = animate(element, get_options(), outro, 1, () => {
dispatch_event(element, 'introend');
// Ensure we cancel the animation to prevent leaking
intro?.abort();
intro = current_options = undefined;
element.style.overflow = overflow;
});
intro = animate(
element,
get_options(),
outro,
1,
() => {
dispatch_event(element, 'introstart');
},
() => {
dispatch_event(element, 'introend');
// Ensure we cancel the animation to prevent leaking
intro?.abort();
intro = current_options = undefined;
element.style.overflow = overflow;
}
);
},
out(fn) {
if (!is_outro) {
@ -258,10 +274,19 @@ export function transition(flags, element, get_fn, get_params) {
element.inert = true;
outro = animate(element, get_options(), intro, 0, () => {
dispatch_event(element, 'outroend');
fn?.();
});
outro = animate(
element,
get_options(),
intro,
0,
() => {
dispatch_event(element, 'outrostart');
},
() => {
dispatch_event(element, 'outroend');
fn?.();
}
);
},
stop: () => {
intro?.abort();
@ -306,10 +331,11 @@ export function transition(flags, element, get_fn, get_params) {
* @param {AnimationConfig | ((opts: { direction: 'in' | 'out' }) => AnimationConfig)} options
* @param {Animation | undefined} counterpart The corresponding intro/outro to this outro/intro
* @param {number} t2 The target `t` value `1` for intro, `0` for outro
* @param {(() => void)} on_begin Called just before beginning the animation
* @param {(() => void)} on_finish Called after successfully completing the animation
* @returns {Animation}
*/
function animate(element, options, counterpart, t2, on_finish) {
function animate(element, options, counterpart, t2, on_begin, on_finish) {
var is_intro = t2 === 1;
if (is_function(options)) {
@ -323,7 +349,7 @@ function animate(element, options, counterpart, t2, on_finish) {
queue_micro_task(() => {
if (aborted) return;
var o = options({ direction: is_intro ? 'in' : 'out' });
a = animate(element, o, counterpart, t2, on_finish);
a = animate(element, o, counterpart, t2, on_begin, on_finish);
});
// ...but we want to do so without using `async`/`await` everywhere, so
@ -342,7 +368,7 @@ function animate(element, options, counterpart, t2, on_finish) {
counterpart?.deactivate();
if (!options?.duration && !options?.delay) {
dispatch_event(element, is_intro ? 'introstart' : 'outrostart');
on_begin();
on_finish();
return {
@ -382,7 +408,7 @@ function animate(element, options, counterpart, t2, on_finish) {
// remove dummy animation from the stack to prevent conflict with main animation
animation.cancel();
dispatch_event(element, is_intro ? 'introstart' : 'outrostart');
on_begin();
// for bidirectional transitions, we start from the current position,
// rather than doing a full intro/outro

@ -5,7 +5,14 @@ import { init_array_prototype_warnings } from '../dev/equality.js';
import { get_descriptor, is_extensible } from '../../shared/utils.js';
import { active_effect } from '../runtime.js';
import { async_mode_flag } from '../../flags/index.js';
import { TEXT_NODE, REACTION_RAN } from '#client/constants';
import {
ATTRIBUTES_CACHE,
CLASS_CACHE,
REACTION_RAN,
STYLE_CACHE,
TEXT_CACHE,
TEXT_NODE
} from '#client/constants';
import { eager_block_effects } from '../reactivity/batch.js';
import { NAMESPACE_HTML } from '../../../constants.js';
@ -48,21 +55,15 @@ export function init_operations() {
if (is_extensible(element_prototype)) {
// the following assignments improve perf of lookups on DOM nodes
// @ts-expect-error
element_prototype.__click = undefined;
// @ts-expect-error
element_prototype.__className = undefined;
// @ts-expect-error
element_prototype.__attributes = null;
// @ts-expect-error
element_prototype.__style = undefined;
/** @type {any} */ (element_prototype)[CLASS_CACHE] = undefined;
/** @type {any} */ (element_prototype)[ATTRIBUTES_CACHE] = null;
/** @type {any} */ (element_prototype)[STYLE_CACHE] = undefined;
// @ts-expect-error
element_prototype.__e = undefined;
}
if (is_extensible(text_prototype)) {
// @ts-expect-error
text_prototype.__t = undefined;
/** @type {any} */ (text_prototype)[TEXT_CACHE] = undefined;
}
if (DEV) {

@ -71,33 +71,35 @@ export function flatten(blockers, sync, async, fn) {
* @param {Source[]} async
*/
function finish(async) {
if ((parent.f & DESTROYED) !== 0) {
return;
}
restore();
try {
fn([...deriveds, ...async]);
} catch (error) {
if ((parent.f & DESTROYED) === 0) {
invoke_error_boundary(error, parent);
}
invoke_error_boundary(error, parent);
}
unset_context();
}
var decrement_pending = increment_pending();
// Fast path: blockers but no async expressions
if (async.length === 0) {
/** @type {Promise<any>} */ (blocker_promise).then(() => finish([]));
/** @type {Promise<any>} */ (blocker_promise).then(() => finish([])).finally(decrement_pending);
return;
}
var decrement_pending = increment_pending();
// Full path: has async expressions
function run() {
Promise.all(async.map((expression) => async_derived(expression)))
.then(finish)
.catch((error) => invoke_error_boundary(error, parent))
.finally(() => decrement_pending());
.finally(decrement_pending);
}
if (blocker_promise) {
@ -180,10 +182,26 @@ export async function save(promise) {
*/
export async function track_reactivity_loss(promise) {
var previous_async_effect = reactivity_loss_tracker;
// Ensure that unrelated reads after an async operation is kicked off don't cause false positives
queueMicrotask(() => {
if (reactivity_loss_tracker === previous_async_effect) {
set_reactivity_loss_tracker(null);
}
});
var value = await promise;
return () => {
set_reactivity_loss_tracker(previous_async_effect);
// While this can result in false negatives it also guards against the more important
// false positives that would occur if this is the last in a chain of async operations,
// and the reactivity_loss_tracker would then stay around until the next async operation happens.
queueMicrotask(() => {
if (reactivity_loss_tracker === previous_async_effect) {
set_reactivity_loss_tracker(null);
}
});
return value;
};
}
@ -213,20 +231,35 @@ export async function* for_await_track_reactivity_loss(iterable) {
throw new TypeError('value is not async iterable');
}
/** Whether the completion of the iterator was "normal", meaning it wasn't ended via `break` or a similar method */
let normal_completion = false;
// eslint-disable-next-line no-useless-assignment
let invoke_return = true;
try {
while (true) {
const { done, value } = (await track_reactivity_loss(iterator.next()))();
if (done) {
normal_completion = true;
invoke_return = false;
break;
}
yield value;
var prev = reactivity_loss_tracker;
try {
yield value;
} catch (e) {
set_reactivity_loss_tracker(prev);
// If the yield throws, we need to call `return` but not return its value, instead rethrow
if (iterator.return !== undefined) {
(await track_reactivity_loss(iterator.return()))();
}
throw e;
}
set_reactivity_loss_tracker(prev);
}
} catch (error) {
invoke_return = false;
throw error;
} finally {
// If the iterator had an abrupt completion and `return` is defined on the iterator, call it and return the value
if (!normal_completion && iterator.return !== undefined) {
// If the iterator had an abrupt completion (break) and `return` is defined on the iterator, call it and return the value
if (invoke_return && iterator.return !== undefined) {
// eslint-disable-next-line no-unsafe-finally
return /** @type {TReturn} */ ((await track_reactivity_loss(iterator.return()))().value);
}
@ -281,16 +314,23 @@ export function run(thunks) {
for (const fn of thunks.slice(1)) {
promise = promise
.then(() => {
if (errored) {
throw errored.error;
}
restore();
if (aborted(active)) {
throw STALE_REACTION;
}
try {
if (errored) {
throw errored.error;
}
restore();
return fn();
if (aborted(active)) {
throw STALE_REACTION;
}
return fn();
} finally {
// We gotta unset context directly in case the function returns a promise, in which case
// unset_context in .finally() would be too late ...
unset_context();
}
})
.catch(handle_error);
@ -299,6 +339,7 @@ export function run(thunks) {
promise.finally(() => {
blocker.settled = true;
// ... but we also need it after such a promise has resolved in case it restores our context
unset_context();
});
}
@ -307,7 +348,7 @@ export function run(thunks) {
// wait one more tick, so that template effects are
// guaranteed to run before `$effect(...)`
.then(() => Promise.resolve())
.finally(() => decrement_pending());
.finally(decrement_pending);
return blockers;
}
@ -331,8 +372,8 @@ export function increment_pending() {
boundary.update_pending_count(1, batch);
batch.increment(blocking, effect);
return (skip = false) => {
return () => {
boundary.update_pending_count(-1, batch);
batch.decrement(blocking, effect, skip);
batch.decrement(blocking, effect);
};
}

@ -15,7 +15,8 @@ import {
ERROR_VALUE,
MANAGED_EFFECT,
REACTION_RAN,
STATE_EAGER_EFFECT
STATE_EAGER_EFFECT,
DESTROYING
} from '#client/constants';
import { async_mode_flag } from '../../flags/index.js';
import { deferred, define_property, includes } from '../../shared/utils.js';
@ -40,14 +41,17 @@ import {
source,
update
} from './sources.js';
import { eager_effect, unlink_effect } from './effects.js';
import { eager_effect, teardown, unlink_effect } from './effects.js';
import { defer_effect } from './utils.js';
import { UNINITIALIZED } from '../../../constants.js';
import { legacy_is_updating_store } from './store.js';
import { invariant } from '../../shared/dev.js';
/** @type {Set<Batch>} */
const batches = new Set();
/** @type {Batch | null} */
let first_batch = null;
/** @type {Batch | null} */
let last_batch = null;
/** @type {Batch | null} */
export let current_batch = null;
@ -91,13 +95,29 @@ export let collected_effects = null;
export let legacy_updates = null;
var flush_count = 0;
var source_stacks = DEV ? new Set() : null;
/** @type {Set<Value>} */
var source_stacks = new Set();
let uid = 1;
export class Batch {
id = uid++;
/** True as soon as `#process` was called */
#started = false;
linked = true;
/** @type {Batch | null} */
#prev = null;
/** @type {Batch | null} */
#next = null;
/** @type {Map<Effect, ReturnType<typeof deferred<any>>>} */
async_deriveds = new Map();
/**
* The current values of any signals that are updated in this batch.
* Tuple format: [value, is_derived] (note: is_derived is false for deriveds, too, if they were overridden via assignment)
@ -136,10 +156,15 @@ export class Batch {
#discard_callbacks = new Set();
/**
* Async effects that are currently in flight
* @type {Map<Effect, number>}
* Callbacks that should run only when a fork is committed.
* @type {Set<(batch: Batch) => void>}
*/
#pending = new Map();
#fork_commit_callbacks = new Set();
/**
* The number of async effects that are currently in flight
*/
#pending = 0;
/**
* Async effects that are currently in flight, _not_ inside a pending boundary
@ -184,35 +209,34 @@ export class Batch {
*/
#skipped_branches = new Map();
/**
* Inverse of #skipped_branches which we need to tell prior batches to unskip them when committing
* @type {Set<Effect>}
*/
#unskipped_branches = new Set();
is_fork = false;
#decrement_queued = false;
/** @type {Set<Batch>} */
#blockers = new Set();
#is_deferred() {
return this.is_fork || this.#blocking_pending.size > 0;
}
if (this.is_fork) return true;
#is_blocked() {
for (const batch of this.#blockers) {
for (const effect of batch.#blocking_pending.keys()) {
var skipped = false;
var e = effect;
for (const effect of this.#blocking_pending.keys()) {
var e = effect;
var skipped = false;
while (e.parent !== null) {
if (this.#skipped_branches.has(e)) {
skipped = true;
break;
}
e = e.parent;
while (e.parent !== null) {
if (this.#skipped_branches.has(e)) {
skipped = true;
break;
}
if (!skipped) {
return true;
}
e = e.parent;
}
if (!skipped) {
return true;
}
}
@ -227,35 +251,39 @@ export class Batch {
if (!this.#skipped_branches.has(effect)) {
this.#skipped_branches.set(effect, { d: [], m: [] });
}
this.#unskipped_branches.delete(effect);
}
/**
* Remove an effect from the #skipped_branches map and reschedule
* any tracked dirty/maybe_dirty child effects
* @param {Effect} effect
* @param {(e: Effect) => void} callback
*/
unskip_effect(effect) {
unskip_effect(effect, callback = (e) => this.schedule(e)) {
var tracked = this.#skipped_branches.get(effect);
if (tracked) {
this.#skipped_branches.delete(effect);
for (var e of tracked.d) {
this.schedule(e);
callback(e);
}
for (e of tracked.m) {
this.schedule(e);
callback(e);
}
}
this.#unskipped_branches.add(effect);
}
#process() {
if (this.#committed) return;
current_batch = this;
this.#started = true;
if (flush_count++ > 1000) {
batches.delete(this);
this.#unlink();
infinite_loop_guard();
}
@ -264,6 +292,14 @@ export class Batch {
mark_reactions(this, source, snapshot.wv, null);
}
if (DEV) {
// track all the values that were updated during this flush,
// so that they can be reset afterwards
for (const value of this.current.keys()) {
source_stacks.add(value);
}
}
// we only reschedule previously-deferred effects if we expect
// to be able to run them after processing the batch
if (!this.#is_deferred()) {
@ -311,61 +347,77 @@ export class Batch {
collected_effects = null;
legacy_updates = null;
if (this.#is_deferred() || this.#is_blocked()) {
// if the batch has outstanding pending work, stash effects and bail
if (this.#is_deferred()) {
this.#defer_effects(render_effects);
this.#defer_effects(effects);
for (const [e, t] of this.#skipped_branches) {
reset_branch(e, t);
}
} else {
if (this.#pending.size === 0) {
batches.delete(this);
if (updates.length > 0) {
/** @type {Batch} */ (/** @type {unknown} */ (current_batch)).#process();
}
// clear effects. Those that are still needed will be rescheduled through unskipping the skipped branches.
this.#dirty_effects.clear();
return;
}
const earlier_batch = this.#find_earlier_batch();
// append/remove branches
for (const fn of this.#commit_callbacks) fn(this);
this.#commit_callbacks.clear();
if (earlier_batch) {
earlier_batch.#merge(this);
return;
}
previous_batch = this;
// clear effects. Those that are still needed will be rescheduled through unskipping the skipped branches.
this.#dirty_effects.clear();
flush_queued_effects(render_effects);
flush_queued_effects(effects);
previous_batch = null;
// append/remove branches
for (const fn of this.#commit_callbacks) fn(this);
this.#commit_callbacks.clear();
this.#deferred?.resolve();
}
previous_batch = this;
flush_queued_effects(render_effects);
flush_queued_effects(effects);
previous_batch = null;
this.#deferred?.resolve();
var next_batch = /** @type {Batch | null} */ (/** @type {unknown} */ (current_batch));
if (this.linked && this.#pending === 0) {
this.#unlink();
}
// Order matters here - we need to commit and THEN continue flushing new batches, not the other way around,
// else we could start flushing a new batch and then, if it has pending work, rebase it right afterwards, which is wrong.
// In sync mode flushSync can cause #commit to wrongfully think that there needs to be a rebase, so we only do it in async mode
// TODO fix the underlying cause, otherwise this will likely regress when non-async mode is removed
if (async_mode_flag && !this.linked) {
this.#commit();
// Rebases can activate other batches or null it out, therefore restore the new one here
current_batch = next_batch;
}
// Edge case: During traversal new branches might create effects that run immediately and set state,
// causing an effect and therefore a root to be scheduled again. We need to traverse the current batch
// once more in that case - most of the time this will just clean up dirty branches.
if (this.#roots.length > 0) {
const batch = (next_batch ??= this);
if (next_batch === null) {
next_batch = this;
this.#link();
}
const batch = next_batch;
batch.#roots.push(...this.#roots.filter((r) => !batch.#roots.includes(r)));
}
active_batch = null;
if (next_batch !== null) {
batches.add(next_batch);
if (DEV) {
for (const source of this.current.keys()) {
/** @type {Set<Source>} */ (source_stacks).add(source);
}
}
next_batch.#process();
}
if (!batches.has(this)) {
this.#commit();
}
}
/**
@ -419,6 +471,83 @@ export class Batch {
}
}
#find_earlier_batch() {
var batch = this.#prev;
while (batch !== null) {
if (!batch.is_fork) {
// if the batches are connected, break
for (const value of this.current.keys()) {
if (batch.current.has(value)) {
return batch;
}
}
}
batch = batch.#prev;
}
return null;
}
/**
* @param {Batch} batch
*/
#merge(batch) {
for (const [source, value] of batch.current) {
var previous = batch.previous.get(source);
if (previous && !this.previous.has(source)) {
this.previous.set(source, previous);
}
this.current.set(source, value);
}
for (const [effect, deferred] of batch.async_deriveds) {
const d = this.async_deriveds.get(effect);
if (d) deferred.promise.then(d.resolve);
}
/**
* mark all effects that depend on `batch.current`, except the
* async effects that we just resolved (TODO unless they depend
* on values in this batch that are NOT in the later batch?).
* Through this we also will populate the correct #skipped_branches,
* oncommit callbacks etc, so we don't need to merge them separately.
* @param {Value} value
*/
const mark = (value) => {
var reactions = value.reactions;
if (reactions === null) return;
for (const reaction of reactions) {
var flags = reaction.f;
if ((flags & DERIVED) !== 0) {
mark(/** @type {Derived} */ (reaction));
} else {
var effect = /** @type {Effect} */ (reaction);
if (flags & (ASYNC | BLOCK_EFFECT) && !this.async_deriveds.has(effect)) {
effect.f ^= ~CLEAN;
this.schedule(effect);
}
}
}
};
for (const source of this.current.keys()) {
mark(source);
}
this.oncommit(() => batch.discard());
batch.#unlink();
current_batch = this;
this.#process();
}
/**
* @param {Effect[]} effects
*/
@ -466,9 +595,11 @@ export class Batch {
}
flush() {
var source_stacks = DEV ? new Set() : null;
try {
if (DEV) {
source_stacks.clear();
}
is_processing = true;
this.#process();
} finally {
@ -484,7 +615,7 @@ export class Batch {
old_values.clear();
if (DEV) {
for (const source of /** @type {Set<Source>} */ (source_stacks)) {
for (const source of source_stacks) {
source.updated = null;
}
}
@ -494,8 +625,9 @@ export class Batch {
discard() {
for (const fn of this.#discard_callbacks) fn(this);
this.#discard_callbacks.clear();
this.#fork_commit_callbacks.clear();
batches.delete(this);
this.#unlink();
}
/**
@ -512,11 +644,13 @@ export class Batch {
if (this.#committed) return;
this.#committed = true;
this.#unlink();
// If there are other pending batches, they now need to be 'rebased' —
// in other words, we re-run block/async effects with the newly
// committed state, unless the batch in question has a more
// recent value for a given source
for (const batch of batches) {
for (let batch = first_batch; batch !== null; batch = batch.#next) {
var is_earlier = batch.id < this.id;
/** @type {Source[]} */
@ -539,6 +673,17 @@ export class Batch {
sources.push(source);
}
if (is_earlier) {
// TODO do we need to restart these in some cases, instead of
// immediately resolving them? Likely not because of how this.apply() works.
for (const [effect, deferred] of this.async_deriveds) {
const d = batch.async_deriveds.get(effect);
if (d) deferred.promise.then(d.resolve);
}
}
if (!batch.#started) continue;
// Re-run async/block effects that depend on distinct values changed in both batches
var others = [...batch.current.keys()].filter((s) => !this.current.has(s));
@ -548,10 +693,25 @@ export class Batch {
batch.discard();
}
} else if (sources.length > 0) {
if (DEV) {
// The microtask queue can contain the batch already scheduled to run right
// after this one is finished, so throwing the invariant would be wrong here.
if (DEV && !batch.#decrement_queued) {
invariant(batch.#roots.length === 0, 'Batch has scheduled roots');
}
// A batch was unskipped in a later batch -> tell prior batches to unskip it, too
if (is_earlier) {
for (const unskipped of this.#unskipped_branches) {
batch.unskip_effect(unskipped, (e) => {
if ((e.f & (BLOCK_EFFECT | ASYNC)) !== 0) {
batch.schedule(e);
} else {
batch.#defer_effects([e]);
}
});
}
}
batch.activate();
/** @type {Set<Value>} */
@ -582,7 +742,8 @@ export class Batch {
}
// Only apply and traverse when we know we triggered async work with marking the effects
if (batch.#roots.length > 0) {
// and know this won't run anyway right afterwards
if (batch.#roots.length > 0 && !batch.#decrement_queued) {
batch.apply();
for (var root of batch.#roots) {
@ -595,17 +756,6 @@ export class Batch {
batch.deactivate();
}
}
for (const batch of batches) {
if (batch.#blockers.has(this)) {
batch.#blockers.delete(this);
if (batch.#blockers.size === 0 && !batch.#is_deferred()) {
batch.activate();
batch.#process();
}
}
}
}
/**
@ -613,8 +763,7 @@ export class Batch {
* @param {Effect} effect
*/
increment(blocking, effect) {
let pending_count = this.#pending.get(effect) ?? 0;
this.#pending.set(effect, pending_count + 1);
this.#pending += 1;
if (blocking) {
let blocking_pending_count = this.#blocking_pending.get(effect) ?? 0;
@ -625,16 +774,9 @@ export class Batch {
/**
* @param {boolean} blocking
* @param {Effect} effect
* @param {boolean} skip - whether to skip updates (because this is triggered by a stale reaction)
*/
decrement(blocking, effect, skip) {
let pending_count = this.#pending.get(effect) ?? 0;
if (pending_count === 1) {
this.#pending.delete(effect);
} else {
this.#pending.set(effect, pending_count - 1);
}
decrement(blocking, effect) {
this.#pending -= 1;
if (blocking) {
let blocking_pending_count = this.#blocking_pending.get(effect) ?? 0;
@ -646,12 +788,15 @@ export class Batch {
}
}
if (this.#decrement_queued || skip) return;
if (this.#decrement_queued) return;
this.#decrement_queued = true;
queue_micro_task(() => {
this.#decrement_queued = false;
this.flush();
if (this.linked) {
this.flush();
}
});
}
@ -676,6 +821,16 @@ export class Batch {
this.#discard_callbacks.add(fn);
}
/** @param {(batch: Batch) => void} fn */
on_fork_commit(fn) {
this.#fork_commit_callbacks.add(fn);
}
run_fork_commit_callbacks() {
for (const fn of this.#fork_commit_callbacks) fn(this);
this.#fork_commit_callbacks.clear();
}
settled() {
return (this.#deferred ??= deferred()).promise;
}
@ -683,20 +838,14 @@ export class Batch {
static ensure() {
if (current_batch === null) {
const batch = (current_batch = new Batch());
batch.#link();
if (!is_processing) {
batches.add(current_batch);
if (!is_flushing_sync) {
queue_micro_task(() => {
if (current_batch !== batch) {
// a flushSync happened in the meantime
return;
}
if (!is_processing && !is_flushing_sync) {
queue_micro_task(() => {
if (!batch.#started) {
batch.flush();
});
}
}
});
}
}
@ -716,24 +865,24 @@ export class Batch {
// we need to override values with the ones in this batch...
this.values = new Map(this.current);
// ...and undo changes belonging to other batches unless they block this one
for (const batch of batches) {
// ...and undo changes belonging to other batches unless they intersect
for (let batch = first_batch; batch !== null; batch = batch.#next) {
if (batch === this || batch.is_fork) continue;
// A batch is blocked on an earlier batch if it overlaps with the earlier batch's changes but is not a superset
// If two batches intersect, the latter batch will be merged into the earlier batch,
// and we should treat them as a single set of changes
var intersects = false;
var differs = false;
if (batch.id < this.id) {
for (const source of batch.current.keys()) {
intersects ||= this.current.has(source);
differs ||= !this.current.has(source);
if (this.current.has(source)) {
intersects = true;
break;
}
}
}
if (intersects && differs) {
this.#blockers.add(batch);
} else {
if (!intersects) {
for (const [value, snapshot] of batch.previous) {
if (!this.values.has(value)) {
this.values.set(value, snapshot);
@ -803,8 +952,39 @@ export class Batch {
this.#roots.push(e);
}
#link() {
if (last_batch === null) {
first_batch = last_batch = this;
} else {
last_batch.#next = this;
this.#prev = last_batch;
}
last_batch = this;
}
#unlink() {
var prev = this.#prev;
var next = this.#next;
if (prev === null) {
first_batch = next;
} else {
prev.#next = next;
}
if (next === null) {
last_batch = prev;
} else {
next.#prev = prev;
}
this.linked = false;
}
}
// TODO Svelte@6 think about removing the callback argument.
/**
* Synchronously flush any pending updates.
* Returns void if no callback is provided, otherwise returns the result of calling the callback.
@ -1046,17 +1226,18 @@ let eager_versions = [];
let running_eager_effect = false;
function eager_flush() {
try {
flushSync(() => {
for (const version of eager_versions) {
update(version);
}
});
} finally {
flushSync(() => {
const eager = eager_versions;
eager_versions = [];
}
for (const version of eager) {
update(version);
}
});
}
/** @type {Map<Reaction, Source<number>>} */
var version_map = new Map();
/**
* Implementation of `$state.eager(fn())`
* @template T
@ -1064,10 +1245,22 @@ function eager_flush() {
* @returns {T}
*/
export function eager(fn) {
var version = source(0);
var initial = true;
var value = /** @type {T} */ (undefined);
if (active_reaction === null) {
return fn();
}
let parent = active_reaction;
let version = version_map.get(parent) ?? source(0);
version_map.set(parent, version);
teardown(() => {
if (parent.f & DESTROYING) version_map.delete(parent);
});
get(version);
if (DEV) {
@ -1196,7 +1389,7 @@ export function fork(fn) {
return;
}
if (!batches.has(batch)) {
if (!batch.linked) {
e.fork_discarded();
}
@ -1215,6 +1408,10 @@ export function fork(fn) {
value.wv = snapshot.wv;
}
batch.activate();
batch.run_fork_commit_callbacks();
batch.deactivate();
// trigger any `$state.eager(...)` expressions with the new state.
// eager effects don't get scheduled like other effects, so we
// can't just encounter them during traversal, we need to
@ -1244,7 +1441,7 @@ export function fork(fn) {
source.wv = increment_write_version();
}
if (!committed && batches.has(batch)) {
if (!committed && batch.linked) {
batch.discard();
}
}
@ -1283,5 +1480,5 @@ export function set_cv(reaction, cv = write_version) {
* Forcibly remove all current batches, to prevent cross-talk between tests
*/
export function clear() {
batches.clear();
first_batch = last_batch = null;
}

@ -1,4 +1,4 @@
/** @import { Derived, Effect, Source } from '#client' */
/** @import { Derived, Effect, Reaction, Source, Value } from '#client' */
/** @import { Batch } from './batch.js'; */
/** @import { Boundary } from '../dom/blocks/boundary.js'; */
import { DEV } from 'esm-env';
@ -12,7 +12,8 @@ import {
DESTROYED,
REACTION_RAN,
CONNECTED,
CLEAN
CLEAN,
INERT
} from '#client/constants';
import {
active_reaction,
@ -22,29 +23,33 @@ import {
push_reaction_value,
update_effect,
remove_reactions,
write_version
write_version,
skipped_deps,
new_deps,
is_destroying_effect,
current_sources
} from '../runtime.js';
import { equals, safe_equals } from './equality.js';
import * as e from '../errors.js';
import * as w from '../warnings.js';
import { async_effect, destroy_effect, destroy_effect_children, teardown } from './effects.js';
import { eager_effects, internal_set, set_eager_effects, source } from './sources.js';
import { eager_effects, internal_set, set_eager_effects, source, state } from './sources.js';
import { get_error } from '../../shared/dev.js';
import { async_mode_flag, tracing_mode_flag } from '../../flags/index.js';
import { component_context } from '../context.js';
import { UNINITIALIZED } from '../../../constants.js';
import { current_batch, get_wv, active_batch, set_cv } from './batch.js';
import { current_batch, get_wv, active_batch, set_cv, previous_batch } from './batch.js';
import { increment_pending, unset_context } from './async.js';
import { deferred, noop } from '../../shared/utils.js';
/**
* This allows us to track 'reactivity loss' that occurs when signals
* are read after a non-context-restoring `await`. Dev-only
* @type {{ effect: Effect, warned: boolean } | null}
* @type {{ effect: Effect, effect_deps: Set<Value>, warned: boolean } | null}
*/
export let reactivity_loss_tracker = null;
/** @param {{ effect: Effect, warned: boolean } | null} v */
/** @param {{ effect: Effect, effect_deps: Set<Value>, warned: boolean } | null} v */
export function set_reactivity_loss_tracker(v) {
reactivity_loss_tracker = v;
}
@ -58,11 +63,6 @@ export const recent_async_deriveds = new Set();
*/
/*#__NO_SIDE_EFFECTS__*/
export function derived(fn) {
var parent_derived =
active_reaction !== null && (active_reaction.f & DERIVED) !== 0
? /** @type {Derived} */ (active_reaction)
: null;
if (active_effect !== null) {
// Since deriveds are evaluated lazily, any effects created inside them are
// created too late to ensure that the parent effect is added to the tree
@ -82,7 +82,7 @@ export function derived(fn) {
rv: 0,
wv: 0,
v: /** @type {V} */ (UNINITIALIZED),
parent: parent_derived ?? active_effect,
parent: active_effect,
ac: null
};
@ -93,6 +93,8 @@ export function derived(fn) {
return signal;
}
export const OBSOLETE = Symbol('obsolete');
/**
* @template V
* @param {() => V | Promise<V>} fn
@ -109,26 +111,23 @@ export function async_derived(fn, label, location) {
}
var promise = /** @type {Promise<V>} */ (/** @type {unknown} */ (undefined));
var signal = source(/** @type {V} */ (UNINITIALIZED));
var signal = state(/** @type {V} */ (UNINITIALIZED));
if (DEV) signal.label = label ?? '{await ...}';
if (DEV) signal.label = label ?? fn.toString();
// only suspend in async deriveds created on initialisation
var should_suspend = !active_reaction;
/** @type {Map<Batch, ReturnType<typeof deferred<V>>>} */
var deferreds = new Map();
/** @type {Set<ReturnType<typeof deferred<V>>>} */
var deferreds = new Set();
async_effect(() => {
var effect = /** @type {Effect} */ (active_effect);
if (DEV) {
reactivity_loss_tracker = {
effect: /** @type {Effect} */ (active_effect),
warned: false
};
reactivity_loss_tracker = { effect, effect_deps: new Set(), warned: false };
}
var effect = /** @type {Effect} */ (active_effect);
/** @type {ReturnType<typeof deferred<V>>} */
var d = deferred();
promise = d.promise;
@ -137,13 +136,37 @@ export function async_derived(fn, label, location) {
// If this code is changed at some point, make sure to still access the then property
// of fn() to read any signals it might access, so that we track them as dependencies.
// We call `unset_context` to undo any `save` calls that happen inside `fn()`
Promise.resolve(fn()).then(d.resolve, d.reject).finally(unset_context);
Promise.resolve(fn())
.then(d.resolve, (e) => {
// if the promise was rejected by the user, via `getAbortSignal`, then
// wait for a subsequent resolution instead of flushing the batch
if (e !== STALE_REACTION) d.reject(e);
})
.finally(unset_context);
} catch (error) {
d.reject(error);
unset_context();
}
if (DEV) {
if (reactivity_loss_tracker) {
// Reused deps from previous run (indices 0 to skipped_deps-1)
// We deliberately only track direct dependencies of the async expression to encourage
// dependencies being directly visible at the point of the expression
if (effect.deps !== null) {
for (let i = 0; i < skipped_deps; i += 1) {
reactivity_loss_tracker.effect_deps.add(effect.deps[i]);
}
}
// New deps discovered this run
if (new_deps !== null) {
for (let i = 0; i < new_deps.length; i += 1) {
reactivity_loss_tracker.effect_deps.add(new_deps[i]);
}
}
}
reactivity_loss_tracker = null;
}
@ -158,18 +181,17 @@ export function async_derived(fn, label, location) {
}
if (/** @type {Boundary} */ (parent.b).is_rendered()) {
deferreds.get(batch)?.reject(STALE_REACTION);
deferreds.delete(batch); // delete to ensure correct order in Map iteration below
batch.async_deriveds.get(effect)?.reject(OBSOLETE);
} else {
// While the boundary is still showing pending, a new run supersedes all older in-flight runs
// for this async expression. Cancel eagerly so resolution cannot commit stale values.
for (const d of deferreds.values()) {
d.reject(STALE_REACTION);
d.reject(OBSOLETE);
}
deferreds.clear();
}
deferreds.set(batch, d);
deferreds.add(d);
batch.async_deriveds.set(effect, d);
}
/**
@ -181,16 +203,10 @@ export function async_derived(fn, label, location) {
reactivity_loss_tracker = null;
}
if (decrement_pending) {
// don't trigger an update if we're only here because
// the promise was superseded before it could resolve
var skip = error === STALE_REACTION;
decrement_pending(skip);
}
decrement_pending?.();
deferreds.delete(d);
if (error === STALE_REACTION || (effect.f & DESTROYED) !== 0) {
return;
}
if (error === OBSOLETE) return;
batch.activate();
@ -206,18 +222,11 @@ export function async_derived(fn, label, location) {
internal_set(signal, value);
// All prior async derived runs are now stale
for (const [b, d] of deferreds) {
deferreds.delete(b);
if (b === batch) break;
d.reject(STALE_REACTION);
}
if (DEV && location !== undefined) {
recent_async_deriveds.add(signal);
setTimeout(() => {
if (recent_async_deriveds.has(signal)) {
if (recent_async_deriveds.has(signal) && (effect.f & DESTROYED) === 0) {
w.await_waterfall(/** @type {string} */ (signal.label), location);
recent_async_deriveds.delete(signal);
}
@ -232,8 +241,8 @@ export function async_derived(fn, label, location) {
});
teardown(() => {
for (const d of deferreds.values()) {
d.reject(STALE_REACTION);
for (const d of deferreds) {
d.reject(OBSOLETE);
}
});
@ -312,23 +321,6 @@ export function destroy_derived_effects(derived) {
*/
export let derived_stack = null;
/**
* @param {Derived} derived
* @returns {Effect | null}
*/
function get_derived_parent_effect(derived) {
var parent = derived.parent;
while (parent !== null) {
if ((parent.f & DERIVED) === 0) {
// The original parent effect might've been destroyed but the derived
// is used elsewhere now - do not return the destroyed effect in that case
return (parent.f & DESTROYED) === 0 ? /** @type {Effect} */ (parent) : null;
}
parent = parent.parent;
}
return null;
}
/**
* @template T
* @param {Derived} derived
@ -337,8 +329,20 @@ function get_derived_parent_effect(derived) {
export function execute_derived(derived) {
var value;
var prev_active_effect = active_effect;
var parent = derived.parent;
set_active_effect(get_derived_parent_effect(derived));
if (
!is_destroying_effect &&
parent !== null &&
derived.v !== UNINITIALIZED && // if it was never evaluated before, it's guaranteed to fail downstream, so we try to execute instead
(parent.f & (DESTROYED | INERT)) !== 0
) {
w.derived_inert();
return derived.v;
}
set_active_effect(parent);
derived_stack ??= [];
@ -396,6 +400,14 @@ export function update_derived(derived) {
if (!derived.equals(value)) {
if (active_batch !== null) {
active_batch.capture(derived, value, write_version);
// We also write to previous_batch because if it exists, it is a sign that we're
// currently in the process of flushing effects. These updates to deriveds may belong
// to the previous batch, not the new one (which can already exist if an earlier
// effect wrote to a source). This can cause bugs when running batch.#commit() later,
// but not adding it to current_batch can, too, so we add it to both.
// See https://github.com/sveltejs/svelte/pull/18117 for more details.
previous_batch?.capture(derived, value, write_version);
} else {
derived.v = value;
derived.wv = write_version;
@ -422,8 +434,8 @@ export function freeze_derived_effects(derived) {
// make it a noop so it doesn't get called again if the derived
// is unfrozen. we don't set it to `null`, because the existence
// of a teardown function is what determines whether the
// effect runs again during unfreezing
e.teardown = noop;
// effect runs again during unfreezing (but not for teardown-only effects)
if (e.fn !== null) e.teardown = noop;
e.ac = null;
remove_reactions(e, 0);
@ -441,7 +453,7 @@ export function unfreeze_derived_effects(derived) {
for (const e of derived.effects) {
// if the effect was previously frozen — indicated by the presence
// of a teardown function — unfreeze it
if (e.teardown) {
if (e.teardown && e.fn !== null) {
update_effect(e);
}
}

@ -41,7 +41,7 @@ import { define_property } from '../../shared/utils.js';
import { get_next_sibling } from '../dom/operations.js';
import { component_context, dev_current_component_function, dev_stack } from '../context.js';
import { Batch, collected_effects, current_batch } from './batch.js';
import { flatten, increment_pending } from './async.js';
import { flatten } from './async.js';
import { without_reactive_context } from '../dom/elements/bindings/shared.js';
/**
@ -388,16 +388,8 @@ export function template_effect(fn, sync = [], async = [], blockers = []) {
* @param {Blocker[]} blockers
*/
export function deferred_template_effect(fn, sync = [], async = [], blockers = []) {
if (async.length > 0 || blockers.length > 0) {
var decrement_pending = increment_pending();
}
flatten(blockers, sync, async, (values) => {
create_effect(EFFECT, () => fn(...values.map(get)));
if (decrement_pending) {
decrement_pending();
}
});
}
@ -646,16 +638,22 @@ function pause_children(effect, transitions, local) {
while (child !== null) {
var sibling = child.next;
var transparent =
(child.f & EFFECT_TRANSPARENT) !== 0 ||
// If this is a branch effect without a block effect parent,
// it means the parent block effect was pruned. In that case,
// transparency information was transferred to the branch effect.
((child.f & BRANCH_EFFECT) !== 0 && (effect.f & BLOCK_EFFECT) !== 0);
// TODO we don't need to call pause_children recursively with a linked list in place
// it's slightly more involved though as we have to account for `transparent` changing
// through the tree.
pause_children(child, transitions, transparent ? local : false);
// If this child is a root effect, then it will become an independent root when its parent
// is destroyed, it should therefore not become inert nor partake in transitions.
if ((child.f & ROOT_EFFECT) === 0) {
var transparent =
(child.f & EFFECT_TRANSPARENT) !== 0 ||
// If this is a branch effect without a block effect parent,
// it means the parent block effect was pruned. In that case,
// transparency information was transferred to the branch effect.
((child.f & BRANCH_EFFECT) !== 0 && (effect.f & BLOCK_EFFECT) !== 0);
// TODO we don't need to call pause_children recursively with a linked list in place
// it's slightly more involved though as we have to account for `transparent` changing
// through the tree.
pause_children(child, transitions, transparent ? local : false);
}
child = sibling;
}
}

@ -1,4 +1,4 @@
/** @import { Effect, Source } from './types.js' */
/** @import { Derived, Effect, Source } from './types.js' */
import { DEV } from 'esm-env';
import {
PROPS_IS_BINDABLE,
@ -283,8 +283,14 @@ export function prop(props, key, flags, fallback) {
var fallback_value = /** @type {V} */ (fallback);
var fallback_dirty = true;
var fallback_signal = /** @type {Derived<V> | undefined} */ (undefined);
var get_fallback = () => {
if (lazy && runes) {
fallback_signal ??= derived(/** @type {() => V} */ (fallback));
return get(fallback_signal);
}
if (fallback_dirty) {
fallback_dirty = false;

@ -27,7 +27,8 @@ import {
ASYNC,
WAS_MARKED,
CONNECTED,
STATE_EAGER_EFFECT
STATE_EAGER_EFFECT,
REACTION_IS_UPDATING
} from '#client/constants';
import * as e from '../errors.js';
import { legacy_mode_flag, tracing_mode_flag } from '../../flags/index.js';
@ -49,7 +50,7 @@ import { proxy } from '../proxy.js';
import { execute_derived } from './deriveds.js';
import { UNINITIALIZED } from '../../../constants.js';
/** @type {Set<any>} */
/** @type {Set<Effect>} */
export let eager_effects = new Set();
/** @type {Map<Source, any>} */
@ -258,7 +259,18 @@ export function flush_eager_effects() {
eager_effects_deferred = false;
for (const effect of eager_effects) {
if ((effect.f & STATE_EAGER_EFFECT) !== 0 || is_dirty(effect)) {
let dirty;
try {
dirty = (effect.f & STATE_EAGER_EFFECT) !== 0 || is_dirty(effect);
} catch {
// Dirty-checking can evaluate derived dependencies and throw in cases where
// parent effects are about to destroy this eager effect. Run the effect so
// its own error handling can deal with transient failures.
dirty = true;
}
if (dirty) {
update_effect(effect);
}
}
@ -326,17 +338,16 @@ export function mark_reactions(batch, signal, wv, updated_during_traversal) {
// In legacy mode, skip the current effect to prevent infinite loops
if (!runes && reaction === active_effect) continue;
// Inspect effects need to run immediately, so that the stack trace makes sense
if (DEV && (flags & EAGER_EFFECT) !== 0) {
eager_effects.add(reaction);
continue;
}
// TODO ideally this would work, but I think we need to `apply()` before `mark_reactions`.
// Or pass `batch` in as an argument?
// if (wv <= get_cv(reaction)) continue;
if ((flags & DERIVED) !== 0) {
if ((flags & EAGER_EFFECT) !== 0) {
// Eager effects need to run immediately:
// - for $inspect so that the stack trace makes sense
// - for $state.eager because they might be without an effect parent
eager_effects.add(/** @type {Effect} */ (reaction));
} else if ((flags & DERIVED) !== 0) {
var derived = /** @type {Derived} */ (reaction);
if (wv > get_cv(derived)) {

@ -21,7 +21,7 @@ export let legacy_is_updating_store = false;
*/
let is_store_binding = false;
let IS_UNMOUNTED = Symbol();
let IS_UNMOUNTED = Symbol('unmounted');
/**
* Gets the current value of a store. If the store isn't subscribed to yet, it will create a proxy

@ -53,8 +53,8 @@ export interface Derived<V = unknown> extends Value<V>, Reaction {
fn: () => V;
/** Effects created inside this signal. Used to destroy those effects when the derived reruns or is cleaned up */
effects: null | Effect[];
/** Parent effect or derived */
parent: Effect | Derived | null;
/** Parent effect */
parent: Effect | null;
}
export interface EffectNodes {

@ -23,7 +23,7 @@ import * as w from './warnings.js';
import * as e from './errors.js';
import { assign_nodes } from './dom/template.js';
import { is_passive_event } from '../../utils.js';
import { COMMENT_NODE, STATE_SYMBOL } from './constants.js';
import { COMMENT_NODE, STATE_SYMBOL, TEXT_CACHE } from './constants.js';
import { boundary } from './dom/blocks/boundary.js';
/**
@ -46,10 +46,9 @@ export function set_should_intro(value) {
export function set_text(text, value) {
// For objects, we apply string coercion (which might make things like $state array references in the template reactive) before diffing
var str = value == null ? '' : typeof value === 'object' ? `${value}` : value;
// @ts-expect-error
if (str !== (text.__t ??= text.nodeValue)) {
// @ts-expect-error
text.__t = str;
// prettier-ignore
if (str !== (/** @type {any} */ (text)[TEXT_CACHE] ??= text.nodeValue)) {
/** @type {any} */ (text)[TEXT_CACHE] = str;
text.nodeValue = `${str}`;
}
}

@ -562,9 +562,15 @@ export function get(signal) {
}
}
} else {
// we're adding a dependency outside the init/update cycle
// (i.e. after an `await`)
(active_reaction.deps ??= []).push(signal);
// We're adding a dependency outside the init/update cycle (i.e. after an `await`).
// We have to deduplicate deps/reactions in this case or remove_reactions could
// disconnect deps/reactions that are actually still in use (if skip_deps says
// "disconnect all after this index" and some of the signals are also present in
// list prior to the cutoff index, i.e. that should be kept).
active_reaction.deps ??= [];
if (!includes.call(active_reaction.deps, signal)) {
active_reaction.deps.push(signal);
}
var reactions = signal.reactions;
@ -582,7 +588,8 @@ export function get(signal) {
!untracking &&
reactivity_loss_tracker &&
!reactivity_loss_tracker.warned &&
(reactivity_loss_tracker.effect.f & REACTION_IS_UPDATING) === 0
(reactivity_loss_tracker.effect.f & REACTION_IS_UPDATING) === 0 &&
!reactivity_loss_tracker.effect_deps.has(signal)
) {
reactivity_loss_tracker.warned = true;

@ -74,6 +74,17 @@ export function console_log_state(method) {
}
}
/**
* Reading a derived belonging to a now-destroyed effect may result in stale values
*/
export function derived_inert() {
if (DEV) {
console.warn(`%c[svelte] derived_inert\n%cReading a derived belonging to a now-destroyed effect may result in stale values\nhttps://svelte.dev/e/derived_inert`, bold, normal);
} else {
console.warn(`https://svelte.dev/e/derived_inert`);
}
}
/**
* %handler% should be a function. Did you mean to %suggestion%?
* @param {string} handler

@ -3,7 +3,6 @@ import { async_mode_flag } from '../flags/index.js';
import { get_render_context } from './render-context.js';
import * as e from './errors.js';
import * as devalue from 'devalue';
import { get_stack } from '../shared/dev.js';
import { DEV } from 'esm-env';
import { get_user_code_location } from './dev.js';
@ -65,7 +64,13 @@ function encode(key, value, unresolved) {
const placeholder = `"${uid++}"`;
const p = value
.then((v) => {
entry.serialized = entry.serialized.replace(placeholder, `r(${uneval(v)})`);
entry.serialized = entry.serialized.replace(
placeholder,
// use the function form here to prevent any string replacement characters from being interpreted
// in `v`, as it's potentially user-controlled and therefore potentially malicious.
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/replace#specifying_a_string_as_the_replacement
() => `r(${uneval(v)})`
);
})
.catch((devalue_error) =>
e.hydratable_serialization_failed(

@ -0,0 +1,33 @@
import { afterAll, beforeAll, expect, test } from 'vitest';
import { Renderer } from './renderer.js';
import type { Component } from 'svelte';
import { disable_async_mode_flag, enable_async_mode_flag } from '../flags/index.js';
import { hydratable } from './hydratable.js';
beforeAll(() => {
enable_async_mode_flag();
});
afterAll(() => {
disable_async_mode_flag();
});
test('treats replacement tokens in hydratable promise values as literals', async () => {
const component = (renderer: Renderer) => {
hydratable('key', () => Promise.resolve(`$'`));
renderer.child(async () => {
await Promise.resolve();
});
renderer.push('ok');
};
const { head } = await Renderer.render(component as unknown as Component);
const script_match = head.match(/<script(?:\s[^>]*)?>([\s\S]*)<\/script>/);
expect(script_match, 'expected hydratable script in head output').toBeTruthy();
const script_content = script_match![1];
expect(script_content).toContain('const h = (window.__svelte ??= {}).h ??= new Map();');
expect(script_content).toContain('r("$\'")');
expect(script_content).toMatch(/\[\s*"key"\s*,\s*r\("\$'"\)\s*\]/);
});

@ -151,7 +151,7 @@ export function attributes(attrs, css_hash, classes, styles, flags = 0) {
// omit functions, internal svelte properties and invalid attribute names
if (typeof attrs[name] === 'function') continue;
if (name[0] === '$' && name[1] === '$') continue; // faster than name.startsWith('$$')
if (INVALID_ATTR_NAME_CHAR_REGEX.test(name)) continue;
if (name === '' || INVALID_ATTR_NAME_CHAR_REGEX.test(name)) continue;
var value = attrs[name];
var lower = name.toLowerCase();

@ -715,7 +715,12 @@ export class Renderer {
const { context, failed, transformError } = item.#boundary;
set_ssr_context(context);
let transformed = await transformError(error);
let promise = transformError(error);
set_ssr_context(null);
let transformed = await promise;
set_ssr_context(context);
// Render the failed snippet instead of the partial children content
const failed_renderer = new Renderer(item.global, item);

@ -4,7 +4,7 @@ import { tag } from '../internal/client/dev/tracing.js';
import { get } from '../internal/client/runtime.js';
import { get_current_url } from './url.js';
export const REPLACE = Symbol();
export const REPLACE = Symbol('replace');
/**
* A reactive version of the built-in [`URLSearchParams`](https://developer.mozilla.org/en-US/docs/Web/API/URLSearchParams) object.

@ -491,7 +491,7 @@ export function is_raw_text_element(name) {
// Rejects strings containing whitespace, quotes, angle brackets, slashes, equals,
// or other characters that could break out of a tag-name token and enable markup injection.
export const REGEX_VALID_TAG_NAME =
/^[a-zA-Z][a-zA-Z0-9]*(-[a-zA-Z0-9.\-_\u00B7\u00C0-\u00D6\u00D8-\u00F6\u00F8-\u037D\u037F-\u1FFF\u200C-\u200D\u203F-\u2040\u2070-\u218F\u2C00-\u2FEF\u3001-\uD7FF\uF900-\uFDCF\uFDF0-\uFFFD\u{10000}-\u{EFFFF}]+)*$/u;
/^[a-zA-Z][a-zA-Z0-9]*(-[a-zA-Z0-9.\-_\u00B7\u00C0-\u00D6\u00D8-\u00F6\u00F8-\u037D\u037F-\u1FFF\u200C-\u200D\u203F-\u2040\u2070-\u218F\u2C00-\u2FEF\u3001-\uD7FF\uF900-\uFDCF\uFDF0-\uFFFD\u{10000}-\u{EFFFF}]*)?$/u;
/**
* Prevent devtools trying to make `location` a clickable link by inserting a zero-width space

@ -0,0 +1,50 @@
import { expect, test } from 'vitest';
import { REGEX_VALID_TAG_NAME } from './utils';
test('REGEX_VALID_TAG_NAME accepts common HTML tag names', () => {
const common_html_tag_names = ['div', 'span', 'button', 'input', 'svg', 'math', 'a'];
for (const tag_name of common_html_tag_names) {
expect(REGEX_VALID_TAG_NAME.test(tag_name)).toBe(true);
}
});
test('REGEX_VALID_TAG_NAME accepts basic custom element names', () => {
const valid_custom_tag_names = ['my-element', 'x-foo', 'todo-item', 'my-element2'];
for (const tag_name of valid_custom_tag_names) {
expect(REGEX_VALID_TAG_NAME.test(tag_name)).toBe(true);
}
});
test('REGEX_VALID_TAG_NAME accepts spec-allowed custom element characters', () => {
const valid_custom_tag_names = [
'x-foo.bar',
'x-foo_bar',
'x-foo\u00B7bar',
'x-foo\u00FCbar',
'x-foo\u{1F600}bar',
'x-'
];
for (const tag_name of valid_custom_tag_names) {
expect(REGEX_VALID_TAG_NAME.test(tag_name)).toBe(true);
}
});
test('REGEX_VALID_TAG_NAME rejects invalid tag names', () => {
const invalid_tag_names = ['', '1', 'x\u00FC', '-x-foo', '1foo', 'x-foo bar', 'x-foo/', 'x-foo>'];
for (const tag_name of invalid_tag_names) {
expect(REGEX_VALID_TAG_NAME.test(tag_name)).toBe(false);
}
});
test('REGEX_VALID_TAG_NAME no ReDoS', () => {
const before = performance.now();
REGEX_VALID_TAG_NAME.test('a-----------------------------------!');
const after = performance.now();
if (after - before > 10) {
throw new Error(`REGEX_VALID_TAG_NAME is vulnerable to ReDoS`);
}
});

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

@ -0,0 +1,4 @@
p.svelte-xyz {
padding: 0 /* it's a comment */ 1em;
}

@ -0,0 +1,7 @@
<p>red</p>
<style>
p {
padding: 0 /* it's a comment */ 1em;
}
</style>

@ -201,7 +201,7 @@ export const async_mode = process.env.SVELTE_NO_ASYNC !== 'true';
* @param {any[]} logs
*/
export function normalise_inspect_logs(logs) {
/** @type {string[]} */
/** @type {any[]} */
const normalised = [];
for (const log of logs) {

@ -0,0 +1,61 @@
{
"css": null,
"js": [],
"start": 0,
"end": 11,
"type": "Root",
"fragment": {
"type": "Fragment",
"nodes": [
{
"type": "ExpressionTag",
"start": 0,
"end": 11,
"expression": {
"type": "Literal",
"start": 7,
"end": 9,
"loc": {
"start": {
"line": 1,
"column": 7
},
"end": {
"line": 1,
"column": 9
}
},
"value": 42,
"raw": "42",
"leadingComments": [
{
"type": "Block",
"value": "",
"start": 2,
"end": 6
}
]
}
}
]
},
"options": null,
"comments": [
{
"type": "Block",
"value": "",
"start": 2,
"end": 6,
"loc": {
"start": {
"line": 1,
"column": 2
},
"end": {
"line": 1,
"column": 6
}
}
}
]
}

@ -0,0 +1,7 @@
<style>
@keyframes foo {
0% { left: 0px; }
50% { left: 50px; }
100% { left: 100px; }
}
</style>

@ -0,0 +1,13 @@
<style>
@keyframes foo {
0% {
left: 0px;
}
50% {
left: 50px;
}
100% {
left: 100px;
}
}
</style>

@ -19,7 +19,7 @@
from {
opacity: 0;
}
50%% {
50% {
opacity: 0.5;
}
to {

@ -0,0 +1 @@
<svelte:body onmousemove={handleMousemove} />

@ -0,0 +1 @@
<svelte:body onmousemove={handleMousemove}></svelte:body>

@ -0,0 +1,23 @@
import { assert_ok, test } from '../../assert';
export default test({
async test({ assert, target, waitUntil, window }) {
const form = target.querySelector('form');
const button = target.querySelector('button');
const [i1, i2, i3] = target.querySelectorAll('input');
assert_ok(form);
assert_ok(button);
assert.equal(form.id, 'initial-form');
assert.equal(form.className, 'first');
assert.equal(window.getComputedStyle(form).backgroundColor, 'rgb(255, 0, 0)');
button.click();
await waitUntil(() => form.id === 'updated-form');
assert.equal(form.id, 'updated-form');
assert.equal(form.className, 'second');
assert.equal(i3.id, '', 'input clobbered form');
assert.equal(window.getComputedStyle(form).backgroundColor, 'rgb(0, 0, 255)');
}
});

@ -0,0 +1,20 @@
<script>
let form_attributes = $state({ id: 'initial-form' });
let class_name = $state('first');
let background_color = $state('rgb(255, 0, 0)');
function update() {
form_attributes = { id: 'updated-form' };
class_name = 'second';
background_color = 'rgb(0, 0, 255)';
}
</script>
<button onclick={update}>update</button>
<form {...form_attributes} class={class_name} style:background-color={background_color}>
<!-- the once non-symbol-ified hidden properties we used -->
<input {...{ name: '__className' }} value="x" />
<input {...{ name: '__style' }} value="y" />
<input {...{ name: '__attributes' }} value="z" />
</form>

@ -0,0 +1,8 @@
<svelte:options customElement={{
tag: "my-inner",
props: { value: { reflect: true }}
}} />
<script>
export let value;
</script>
{value}

@ -0,0 +1,20 @@
import { tick } from 'svelte';
import { test } from '../../test';
export default test({
async test({ assert, target }) {
await tick();
const [btn] = target.querySelectorAll('button');
btn.click();
await tick();
assert.htmlEqual(
target.innerHTML,
`
<button>inc</button>
<my-inner value="2"></my-inner>
2
`
);
}
});

@ -0,0 +1,12 @@
<script>
import "./Inner.svelte"
let count = 1;
</script>
<button on:click={() => count++}>inc</button>
<!-- updating value prop will cause a flushSync -->
<my-inner value={count}></my-inner>
<!-- updating count will cause an internal_set -->
{#each [count] as row}
{row}
{/each}

@ -60,6 +60,8 @@ export interface RuntimeTest<Props extends Record<string, any> = Record<string,
id_prefix?: string;
before_test?: () => void;
after_test?: () => void;
/** If true, flushSync() will not be called before invoking test() */
skip_initial_flushSync?: boolean;
test?: (args: {
variant: 'dom' | 'hydrate';
assert: Assert;
@ -505,7 +507,7 @@ async function run_test_variant(
try {
if (config.test) {
flushSync();
if (!config.skip_initial_flushSync) flushSync();
if (variant === 'hydrate' && cwd.includes('async-')) {
// wait for pending boundaries to render
@ -543,7 +545,7 @@ async function run_test_variant(
}
} finally {
if (runes) {
unmount(instance);
await unmount(instance);
} else {
instance.$destroy();
}

@ -0,0 +1,23 @@
import { tick } from 'svelte';
import { test } from '../../test';
export default test({
async test({ assert, target }) {
await tick();
const [increment, shift] = target.querySelectorAll('button');
increment.click();
await tick();
assert.htmlEqual(
target.innerHTML,
`<button>clicks: 0 - 0 - 0</button> <button>shift</button> <p>true - true</p>`
);
shift.click();
await tick();
assert.htmlEqual(
target.innerHTML,
`<button>clicks: 1 - 1 - 1</button> <button>shift</button> <p>false - false</p>`
);
}
});

@ -0,0 +1,22 @@
<script>
let count = $state(0);
const delayedCount = $derived(await push(count));
const derivedCount = $derived(count);
let resolvers = [];
function push(value) {
if (!value) return value;
const { promise, resolve } = Promise.withResolvers();
resolvers.push(() => resolve(value));
return promise;
}
</script>
<button onclick={() => count += 1}>
clicks: {count} - {delayedCount} - {derivedCount}
</button>
<button onclick={() => resolvers.shift()?.()}>shift</button>
<p>{$state.eager(count) !== count} - {$state.eager(derivedCount) !== derivedCount}</p>

@ -0,0 +1,31 @@
import { flushSync } from 'svelte';
import { test } from '../../test';
export default test({
test({ assert, raf, target, logs }) {
let divs = target.querySelectorAll('div');
divs.forEach((div) => {
// @ts-expect-error
div.getBoundingClientRect = function () {
// @ts-expect-error
const index = [...this.parentNode.children].indexOf(this);
const top = index * 30;
return {
left: 0,
right: 100,
top,
bottom: top + 20
};
};
});
const [btn] = target.querySelectorAll('button');
flushSync(() => btn.click());
raf.tick(1);
assert.deepEqual(logs, []);
raf.tick(100);
assert.deepEqual(logs, []);
}
});

@ -0,0 +1,19 @@
<script>
import { flip } from "svelte/animate";
let numbers = $state([0,1]);
</script>
<button onclick={() => numbers.reverse()}>reverse</button>
{#each numbers as num (num)}
<div
onintrostart={() => console.log("intro start")}
onoutrostart={() => console.log("outro start")}
onintroend={() => console.log("intro end")}
onoutroend={() => console.log("outro end")}
animate:flip={{ duration: 100 }}
>
{num}
</div>
{/each}

@ -0,0 +1,24 @@
import { tick } from 'svelte';
import { test } from '../../test';
export default test({
async test({ assert, target }) {
await tick();
const [incremet, resolve] = target.querySelectorAll('button');
incremet.click();
await tick();
incremet.click();
await tick();
resolve.click();
await tick();
resolve.click();
await tick();
resolve.click();
await tick();
resolve.click();
await tick();
assert.htmlEqual(target.innerHTML, '<button>increment</button> <button>resolve</button> 4 4 1');
}
});

@ -0,0 +1,22 @@
<script>
let count = $state(0);
const queue = [];
function push(v) {
if (v === 0) return v;
return new Promise((fulfil) => queue.push(() => fulfil(v)));
}
async function request(v) {
const result = $derived(await push(v));
return result + count;
}
</script>
<button onclick={() => count++}>increment</button>
<button onclick={() => queue.shift()?.()}>resolve</button>
{#await request(count) then result}{result}{/await}
{#await await push(count) + count then result}{result}{/await}
{#await await 1 then result}{result}{/await}

@ -0,0 +1,30 @@
import { tick } from 'svelte';
import { test } from '../../test';
export default test({
async test({ assert, target }) {
await tick();
const [increment, shift, middle] = target.querySelectorAll('button');
const [div] = target.querySelectorAll('div');
increment.click();
await tick();
increment.click();
await tick();
increment.click();
await tick();
middle.click(); // resolve the second increment which will make the if block go away and the first batch discarded
await tick();
assert.htmlEqual(div.innerHTML, '2 2');
shift.click();
await tick();
shift.click();
await tick();
shift.click();
await tick();
shift.click();
await tick();
assert.htmlEqual(div.innerHTML, '3 3');
}
});

@ -0,0 +1,21 @@
<script>
let a = $state(0);
const deferred = [];
function delay(value) {
if (!value) return value;
return new Promise((resolve) => deferred.push(() => resolve(value)));
}
</script>
<div>
{a} {await delay(a)}
{#if a < 2}
{await delay(a)}
{/if}
</div>
<button onclick={() => {a++;}}>a++</button>
<button onclick={() => deferred.shift()?.()}>shift</button>
<button onclick={() => deferred[2]()}>middle</button>

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

Loading…
Cancel
Save