Merge remote-tracking branch 'origin' into elliott/resources

pull/17124/head
Elliott Johnson 2 weeks ago
commit 8667dab837

@ -166,6 +166,21 @@ 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`.
## `$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).
In some cases, you may want to update the UI as soon as the state changes. For example, you might want to update a navigation bar when the user clicks on a link, so that they get visual feedback while waiting for the new page to load. To do this, use `$state.eager(value)`:
```svelte
<nav>
<a href="/" aria-current={$state.eager(pathname) === '/' ? 'page' : null}>home</a>
<a href="/about" aria-current={$state.eager(pathname) === '/about' ? 'page' : null}>about</a>
</nav>
```
Use this feature sparingly, and only to provide feedback in response to user action — in general, allowing Svelte to coordinate updates will provide a better user experience.
## Passing state into functions
JavaScript is a _pass-by-value_ language — when you call a function, the arguments are the _values_ rather than the _variables_. In other words:

@ -18,6 +18,8 @@ The `$inspect` rune is roughly equivalent to `console.log`, with the exception t
<input bind:value={message} />
```
On updates, a stack trace will be printed, making it easy to find the origin of a state change (unless you're in the playground, due to technical limitations).
## $inspect(...).with
`$inspect` returns a property `with`, which you can invoke with a callback, which will then be invoked instead of `console.log`. The first argument to the callback is either `"init"` or `"update"`; subsequent arguments are the values passed to `$inspect` ([demo](/playground/untitled#H4sIAAAAAAAACkVQ24qDMBD9lSEUqlTqPlsj7ON-w7pQG8c2VCchmVSK-O-bKMs-DefKYRYx6BG9qL4XQd2EohKf1opC8Nsm4F84MkbsTXAqMbVXTltuWmp5RAZlAjFIOHjuGLOP_BKVqB00eYuKs82Qn2fNjyxLtcWeyUE2sCRry3qATQIpJRyD7WPVMf9TW-7xFu53dBcoSzAOrsqQNyOe2XUKr0Xi5kcMvdDB2wSYO-I9vKazplV1-T-d6ltgNgSG1KjVUy7ZtmdbdjqtzRcphxMS1-XubOITJtPrQWMvKnYB15_1F7KKadA_AQAA)):
@ -36,13 +38,6 @@ The `$inspect` rune is roughly equivalent to `console.log`, with the exception t
<button onclick={() => count++}>Increment</button>
```
A convenient way to find the origin of some change is to pass `console.trace` to `with`:
```js
// @errors: 2304
$inspect(stuff).with(console.trace);
```
## $inspect.trace(...)
This rune, added in 5.14, causes the surrounding function to be _traced_ in development. Any time the function re-runs as part of an [effect]($effect) or a [derived]($derived), information will be printed to the console about which pieces of reactive state caused the effect to fire.

@ -135,6 +135,54 @@ If a `<svelte:boundary>` with a `pending` snippet is encountered during SSR, tha
> [!NOTE] In the future, we plan to add a streaming implementation that renders the content in the background.
## Forking
The [`fork(...)`](svelte#fork) API, added in 5.42, makes it possible to run `await` expressions that you _expect_ to happen in the near future. This is mainly intended for frameworks like SvelteKit to implement preloading when (for example) users signal an intent to navigate.
```svelte
<script>
import { fork } from 'svelte';
import Menu from './Menu.svelte';
let open = $state(false);
/** @type {import('svelte').Fork | null} */
let pending = null;
function preload() {
pending ??= fork(() => {
open = true;
});
}
function discard() {
pending?.discard();
pending = null;
}
</script>
<button
onfocusin={preload}
onfocusout={discard}
onpointerenter={preload}
onpointerleave={discard}
onclick={() => {
pending?.commit();
pending = null;
// in case `pending` didn't exist
// (if it did, this is a no-op)
open = true;
}}
>open menu</button>
{#if open}
<!-- any async work inside this component will start
as soon as the fork is created -->
<Menu onclose={() => open = false} />
{/if}
```
## Caveats
As an experimental feature, the details of how `await` is handled (and related APIs like `$effect.pending()`) are subject to breaking changes outside of a semver major release, though we intend to keep such changes to a bare minimum.

@ -130,6 +130,12 @@ $effect(() => {
Often when encountering this issue, the value in question shouldn't be state (for example, if you are pushing to a `logs` array in an effect, make `logs` a normal array rather than `$state([])`). In the rare cases where you really _do_ need to write to state in an effect — [which you should avoid]($effect#When-not-to-use-$effect) — you can read the state with [untrack](svelte#untrack) to avoid adding it as a dependency.
### experimental_async_fork
```
Cannot use `fork(...)` unless the `experimental.async` compiler option is `true`
```
### flush_sync_in_effect
```
@ -140,6 +146,18 @@ The `flushSync()` function can be used to flush any pending effects synchronousl
This restriction only applies when using the `experimental.async` option, which will be active by default in Svelte 6.
### fork_discarded
```
Cannot commit a fork that was already discarded
```
### fork_timing
```
Cannot create a fork inside an effect or when state changes are pending
```
### get_abort_signal_outside_reaction
```

@ -199,7 +199,7 @@ Cyclical dependency detected: %cycle%
### const_tag_invalid_reference
```
The `{@const %name% = ...}` declaration is not available in this snippet
The `{@const %name% = ...}` declaration is not available in this snippet
```
The following is an error:
@ -453,6 +453,12 @@ This turned out to be buggy and unpredictable, particularly when working with de
{/each}
```
### each_key_without_as
```
An `{#each ...}` block without an `as` clause cannot have a key
```
### effect_invalid_placement
```

@ -1,5 +1,133 @@
# svelte
## 5.43.3
### Patch Changes
- fix: ensure fork always accesses correct values ([#17098](https://github.com/sveltejs/svelte/pull/17098))
- fix: change title only after any pending work has completed ([#17061](https://github.com/sveltejs/svelte/pull/17061))
- fix: preserve symbols when creating derived rest properties ([#17096](https://github.com/sveltejs/svelte/pull/17096))
## 5.43.2
### Patch Changes
- fix: treat each blocks with async dependencies as uncontrolled ([#17077](https://github.com/sveltejs/svelte/pull/17077))
## 5.43.1
### Patch Changes
- fix: transform `$bindable` after `await` expressions ([#17066](https://github.com/sveltejs/svelte/pull/17066))
## 5.43.0
### Minor Changes
- feat: out-of-order rendering ([#17038](https://github.com/sveltejs/svelte/pull/17038))
### Patch Changes
- fix: settle batch after DOM updates ([#17054](https://github.com/sveltejs/svelte/pull/17054))
## 5.42.3
### Patch Changes
- fix: handle `<svelte:head>` rendered asynchronously ([#17052](https://github.com/sveltejs/svelte/pull/17052))
- fix: don't restore batch in `#await` ([#17051](https://github.com/sveltejs/svelte/pull/17051))
## 5.42.2
### Patch Changes
- fix: better error message for global variable assignments ([#17036](https://github.com/sveltejs/svelte/pull/17036))
- chore: tweak memoizer logic ([#17042](https://github.com/sveltejs/svelte/pull/17042))
## 5.42.1
### Patch Changes
- fix: ignore fork `discard()` after `commit()` ([#17034](https://github.com/sveltejs/svelte/pull/17034))
## 5.42.0
### Minor Changes
- feat: experimental `fork` API ([#17004](https://github.com/sveltejs/svelte/pull/17004))
### Patch Changes
- fix: always allow `setContext` before first await in component ([#17031](https://github.com/sveltejs/svelte/pull/17031))
- fix: less confusing names for inspect errors ([#17026](https://github.com/sveltejs/svelte/pull/17026))
## 5.41.4
### Patch Changes
- fix: take into account static blocks when determining transition locality ([#17018](https://github.com/sveltejs/svelte/pull/17018))
- fix: coordinate mount of snippets with await expressions ([#17021](https://github.com/sveltejs/svelte/pull/17021))
- fix: better optimization of await expressions ([#17025](https://github.com/sveltejs/svelte/pull/17025))
- fix: flush pending changes after rendering `failed` snippet ([#16995](https://github.com/sveltejs/svelte/pull/16995))
## 5.41.3
### Patch Changes
- chore: exclude vite optimized deps from stack traces ([#17008](https://github.com/sveltejs/svelte/pull/17008))
- perf: skip repeatedly traversing the same derived ([#17016](https://github.com/sveltejs/svelte/pull/17016))
## 5.41.2
### Patch Changes
- fix: keep batches alive until all async work is complete ([#16971](https://github.com/sveltejs/svelte/pull/16971))
- fix: don't preserve reactivity context across function boundaries ([#17002](https://github.com/sveltejs/svelte/pull/17002))
- fix: make `$inspect` logs come from the callsite ([#17001](https://github.com/sveltejs/svelte/pull/17001))
- fix: ensure guards (eg. if, each, key) run before their contents ([#16930](https://github.com/sveltejs/svelte/pull/16930))
## 5.41.1
### Patch Changes
- fix: place `let:` declarations before `{@const}` declarations ([#16985](https://github.com/sveltejs/svelte/pull/16985))
- fix: improve `each_key_without_as` error ([#16983](https://github.com/sveltejs/svelte/pull/16983))
- chore: centralise branch management ([#16977](https://github.com/sveltejs/svelte/pull/16977))
## 5.41.0
### Minor Changes
- feat: add `$state.eager(value)` rune ([#16849](https://github.com/sveltejs/svelte/pull/16849))
### Patch Changes
- fix: preserve `<select>` state while focused ([#16958](https://github.com/sveltejs/svelte/pull/16958))
- chore: run boundary async effects in the context of the current batch ([#16968](https://github.com/sveltejs/svelte/pull/16968))
- fix: error if `each` block has `key` but no `as` clause ([#16966](https://github.com/sveltejs/svelte/pull/16966))
## 5.40.2
### Patch Changes
- fix: add hydration markers in `pending` branch of SSR boundary ([#16965](https://github.com/sveltejs/svelte/pull/16965))
## 5.40.1
### Patch Changes

@ -100,6 +100,10 @@ $effect(() => {
Often when encountering this issue, the value in question shouldn't be state (for example, if you are pushing to a `logs` array in an effect, make `logs` a normal array rather than `$state([])`). In the rare cases where you really _do_ need to write to state in an effect — [which you should avoid]($effect#When-not-to-use-$effect) — you can read the state with [untrack](svelte#untrack) to avoid adding it as a dependency.
## experimental_async_fork
> Cannot use `fork(...)` unless the `experimental.async` compiler option is `true`
## flush_sync_in_effect
> Cannot use `flushSync` inside an effect
@ -108,6 +112,14 @@ The `flushSync()` function can be used to flush any pending effects synchronousl
This restriction only applies when using the `experimental.async` option, which will be active by default in Svelte 6.
## fork_discarded
> Cannot commit a fork that was already discarded
## fork_timing
> Cannot create a fork inside an effect or when state changes are pending
## get_abort_signal_outside_reaction
> `getAbortSignal()` can only be called inside an effect or derived

@ -126,7 +126,7 @@
## const_tag_invalid_reference
> The `{@const %name% = ...}` declaration is not available in this snippet
> The `{@const %name% = ...}` declaration is not available in this snippet
The following is an error:
@ -179,6 +179,10 @@ The same applies to components:
> `%type%` name cannot be empty
## each_key_without_as
> An `{#each ...}` block without an `as` clause cannot have a key
## element_invalid_closing_tag
> `</%name%>` attempted to close an element that was not open

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

@ -95,6 +95,18 @@ declare namespace $state {
: never
: never;
/**
* Returns the latest `value`, even if the rest of the UI is suspending
* while async work (such as data loading) completes.
*
* ```svelte
* <nav>
* <a href="/" aria-current={$state.eager(pathname) === '/' ? 'page' : null}>home</a>
* <a href="/about" aria-current={$state.eager(pathname) === '/about' ? 'page' : null}>about</a>
* </nav>
* ```
*/
export function eager<T>(value: T): T;
/**
* Declares state that is _not_ made deeply reactive instead of mutating it,
* you must reassign it.

@ -986,13 +986,13 @@ export function const_tag_invalid_placement(node) {
}
/**
* The `{@const %name% = ...}` declaration is not available in this snippet
* The `{@const %name% = ...}` declaration is not available in this snippet
* @param {null | number | NodeLike} node
* @param {string} name
* @returns {never}
*/
export function const_tag_invalid_reference(node, name) {
e(node, 'const_tag_invalid_reference', `The \`{@const ${name} = ...}\` declaration is not available in this snippet \nhttps://svelte.dev/e/const_tag_invalid_reference`);
e(node, 'const_tag_invalid_reference', `The \`{@const ${name} = ...}\` declaration is not available in this snippet\nhttps://svelte.dev/e/const_tag_invalid_reference`);
}
/**
@ -1023,6 +1023,15 @@ export function directive_missing_name(node, type) {
e(node, 'directive_missing_name', `\`${type}\` name cannot be empty\nhttps://svelte.dev/e/directive_missing_name`);
}
/**
* An `{#each ...}` block without an `as` clause cannot have a key
* @param {null | number | NodeLike} node
* @returns {never}
*/
export function each_key_without_as(node) {
e(node, 'each_key_without_as', `An \`{#each ...}\` block without an \`as\` clause cannot have a key\nhttps://svelte.dev/e/each_key_without_as`);
}
/**
* `</%name%>` attempted to close an element that was not open
* @param {null | number | NodeLike} node

@ -9,11 +9,10 @@ import { decode_character_references } from '../utils/html.js';
import * as e from '../../../errors.js';
import * as w from '../../../warnings.js';
import { create_fragment } from '../utils/create.js';
import { create_attribute, create_expression_metadata, is_element_node } from '../../nodes.js';
import { create_attribute, ExpressionMetadata, is_element_node } from '../../nodes.js';
import { get_attribute_expression, is_expression_attribute } from '../../../utils/ast.js';
import { closing_tag_omitted } from '../../../../html-tree-validation.js';
import { list } from '../../../utils/string.js';
import { regex_whitespace } from '../../patterns.js';
const regex_invalid_unquoted_attribute_value = /^(\/>|[\s"'=<>`])/;
const regex_closing_textarea_tag = /^<\/textarea(\s[^>]*)?>/i;
@ -297,7 +296,7 @@ export default function element(parser) {
element.tag = get_attribute_expression(definition);
}
element.metadata.expression = create_expression_metadata();
element.metadata.expression = new ExpressionMetadata();
}
if (is_top_level_script_or_style) {
@ -508,7 +507,7 @@ function read_attribute(parser) {
end: parser.index,
expression,
metadata: {
expression: create_expression_metadata()
expression: new ExpressionMetadata()
}
};
@ -528,7 +527,7 @@ function read_attribute(parser) {
end: parser.index,
expression,
metadata: {
expression: create_expression_metadata()
expression: new ExpressionMetadata()
}
};
@ -568,7 +567,7 @@ function read_attribute(parser) {
name
},
metadata: {
expression: create_expression_metadata()
expression: new ExpressionMetadata()
}
};
@ -628,7 +627,7 @@ function read_attribute(parser) {
modifiers: /** @type {Array<'important'>} */ (modifiers),
value,
metadata: {
expression: create_expression_metadata()
expression: new ExpressionMetadata()
}
};
}
@ -658,7 +657,7 @@ function read_attribute(parser) {
name: directive_name,
expression,
metadata: {
expression: create_expression_metadata()
expression: new ExpressionMetadata()
}
};
@ -824,7 +823,7 @@ function read_sequence(parser, done, location) {
end: parser.index,
expression,
metadata: {
expression: create_expression_metadata()
expression: new ExpressionMetadata()
}
};

@ -3,7 +3,7 @@
/** @import { Parser } from '../index.js' */
import { walk } from 'zimmerframe';
import * as e from '../../../errors.js';
import { create_expression_metadata } from '../../nodes.js';
import { ExpressionMetadata } from '../../nodes.js';
import { parse_expression_at } from '../acorn.js';
import read_pattern from '../read/context.js';
import read_expression, { get_loose_identifier } from '../read/expression.js';
@ -42,7 +42,7 @@ export default function tag(parser) {
end: parser.index,
expression,
metadata: {
expression: create_expression_metadata()
expression: new ExpressionMetadata()
}
});
}
@ -65,7 +65,7 @@ function open(parser) {
consequent: create_fragment(),
alternate: null,
metadata: {
expression: create_expression_metadata()
expression: new ExpressionMetadata()
}
});
@ -249,7 +249,7 @@ function open(parser) {
then: null,
catch: null,
metadata: {
expression: create_expression_metadata()
expression: new ExpressionMetadata()
}
});
@ -334,7 +334,7 @@ function open(parser) {
expression,
fragment: create_fragment(),
metadata: {
expression: create_expression_metadata()
expression: new ExpressionMetadata()
}
});
@ -477,7 +477,7 @@ function next(parser) {
consequent: create_fragment(),
alternate: null,
metadata: {
expression: create_expression_metadata()
expression: new ExpressionMetadata()
}
});
@ -643,7 +643,7 @@ function special(parser) {
end: parser.index,
expression,
metadata: {
expression: create_expression_metadata()
expression: new ExpressionMetadata()
}
});
@ -721,7 +721,7 @@ function special(parser) {
end: parser.index - 1
},
metadata: {
expression: create_expression_metadata()
expression: new ExpressionMetadata()
}
});
}
@ -748,7 +748,7 @@ function special(parser) {
end: parser.index,
expression: /** @type {AST.RenderTag['expression']} */ (expression),
metadata: {
expression: create_expression_metadata(),
expression: new ExpressionMetadata(),
dynamic: false,
arguments: [],
path: [],

@ -1,4 +1,4 @@
/** @import { Expression, Node, Program } from 'estree' */
/** @import * as ESTree from 'estree' */
/** @import { Binding, AST, ValidatedCompileOptions, ValidatedModuleCompileOptions } from '#compiler' */
/** @import { AnalysisState, Visitors } from './types' */
/** @import { Analysis, ComponentAnalysis, Js, ReactiveStatement, Template } from '../types' */
@ -6,7 +6,12 @@ import { walk } from 'zimmerframe';
import { parse } from '../1-parse/acorn.js';
import * as e from '../../errors.js';
import * as w from '../../warnings.js';
import { extract_identifiers } from '../../utils/ast.js';
import {
extract_identifiers,
has_await_expression,
object,
unwrap_pattern
} from '../../utils/ast.js';
import * as b from '#compiler/builders';
import { Scope, ScopeRoot, create_scopes, get_rune, set_scope } from '../scope.js';
import check_graph_for_cycles from './utils/check_graph_for_cycles.js';
@ -206,7 +211,7 @@ const visitors = {
* @returns {Js}
*/
function js(script, root, allow_reactive_declarations, parent) {
/** @type {Program} */
/** @type {ESTree.Program} */
const ast = script?.content ?? {
type: 'Program',
sourceType: 'module',
@ -289,7 +294,7 @@ export function analyze_module(source, options) {
});
walk(
/** @type {Node} */ (ast),
/** @type {ESTree.Node} */ (ast),
{
scope,
scopes,
@ -306,7 +311,7 @@ export function analyze_module(source, options) {
fragment: null,
parent_element: null,
reactive_statement: null,
in_derived: false
derived_function_depth: -1
},
visitors
);
@ -347,7 +352,7 @@ export function analyze_component(root, source, options) {
const store_name = name.slice(1);
const declaration = instance.scope.get(store_name);
const init = /** @type {Node | undefined} */ (declaration?.initial);
const init = /** @type {ESTree.Node | undefined} */ (declaration?.initial);
// If we're not in legacy mode through the compiler option, assume the user
// is referencing a rune and not a global store.
@ -407,7 +412,7 @@ export function analyze_component(root, source, options) {
/** @type {number} */ (node.start) > /** @type {number} */ (module.ast.start) &&
/** @type {number} */ (node.end) < /** @type {number} */ (module.ast.end) &&
// const state = $state(0) is valid
get_rune(/** @type {Node} */ (path.at(-1)), module.scope) === null
get_rune(/** @type {ESTree.Node} */ (path.at(-1)), module.scope) === null
) {
e.store_invalid_subscription(node);
}
@ -543,7 +548,13 @@ export function analyze_component(root, source, options) {
snippet_renderers: new Map(),
snippets: new Set(),
async_deriveds: new Set(),
pickled_awaits: new Set()
pickled_awaits: new Set(),
instance_body: {
sync: [],
async: [],
declarations: [],
hoisted: []
}
};
if (!runes) {
@ -636,7 +647,7 @@ export function analyze_component(root, source, options) {
// @ts-expect-error
_: set_scope,
Identifier(node, context) {
const parent = /** @type {Expression} */ (context.path.at(-1));
const parent = /** @type {ESTree.Expression} */ (context.path.at(-1));
if (is_reference(node, parent)) {
const binding = context.state.scope.get(node.name);
@ -676,6 +687,194 @@ export function analyze_component(root, source, options) {
}
}
/**
* @param {ESTree.Node} expression
* @param {Scope} scope
* @param {Set<Binding>} touched
* @param {Set<ESTree.Node>} seen
*/
const touch = (expression, scope, touched, seen = new Set()) => {
if (seen.has(expression)) return;
seen.add(expression);
walk(
expression,
{ scope },
{
ImportDeclaration(node) {},
Identifier(node, context) {
const parent = /** @type {ESTree.Node} */ (context.path.at(-1));
if (is_reference(node, parent)) {
const binding = context.state.scope.get(node.name);
if (binding) {
touched.add(binding);
for (const assignment of binding.assignments) {
touch(assignment.value, assignment.scope, touched, seen);
}
}
}
}
}
);
};
/**
* @param {ESTree.Node} node
* @param {Set<ESTree.Node>} seen
* @param {Set<Binding>} reads
* @param {Set<Binding>} writes
*/
const trace_references = (node, reads, writes, seen = new Set()) => {
if (seen.has(node)) return;
seen.add(node);
/**
* @param {ESTree.Pattern} node
* @param {Scope} scope
*/
function update(node, scope) {
for (const pattern of unwrap_pattern(node)) {
const node = object(pattern);
if (!node) return;
const binding = scope.get(node.name);
if (!binding) return;
writes.add(binding);
}
}
walk(
node,
{ scope: instance.scope },
{
_(node, context) {
const scope = scopes.get(node);
if (scope) {
context.next({ scope });
} else {
context.next();
}
},
AssignmentExpression(node, context) {
update(node.left, context.state.scope);
},
UpdateExpression(node, context) {
update(
/** @type {ESTree.Identifier | ESTree.MemberExpression} */ (node.argument),
context.state.scope
);
},
CallExpression(node, context) {
// for now, assume everything touched by the callee ends up mutating the object
// TODO optimise this better
// special case — no need to peek inside effects as they only run once async work has completed
const rune = get_rune(node, context.state.scope);
if (rune === '$effect') return;
/** @type {Set<Binding>} */
const touched = new Set();
touch(node, context.state.scope, touched);
for (const b of touched) {
writes.add(b);
}
},
// don't look inside functions until they are called
ArrowFunctionExpression(_, context) {},
FunctionDeclaration(_, context) {},
FunctionExpression(_, context) {},
Identifier(node, context) {
const parent = /** @type {ESTree.Node} */ (context.path.at(-1));
if (is_reference(node, parent)) {
const binding = context.state.scope.get(node.name);
if (binding) {
reads.add(binding);
}
}
}
}
);
};
let awaited = false;
// TODO this should probably be attached to the scope?
var promises = b.id('$$promises');
/**
* @param {ESTree.Identifier} id
* @param {ESTree.Expression} blocker
*/
function push_declaration(id, blocker) {
analysis.instance_body.declarations.push(id);
const binding = /** @type {Binding} */ (instance.scope.get(id.name));
binding.blocker = blocker;
}
for (let node of instance.ast.body) {
if (node.type === 'ImportDeclaration') {
analysis.instance_body.hoisted.push(node);
continue;
}
if (node.type === 'ExportDefaultDeclaration' || node.type === 'ExportAllDeclaration') {
// these can't exist inside `<script>` but TypeScript doesn't know that
continue;
}
if (node.type === 'ExportNamedDeclaration') {
if (node.declaration) {
node = node.declaration;
} else {
continue;
}
}
const has_await = has_await_expression(node);
awaited ||= has_await;
if (awaited && node.type !== 'FunctionDeclaration') {
/** @type {Set<Binding>} */
const reads = new Set(); // TODO we're not actually using this yet
/** @type {Set<Binding>} */
const writes = new Set();
trace_references(node, reads, writes);
const blocker = b.member(promises, b.literal(analysis.instance_body.async.length), true);
for (const binding of writes) {
binding.blocker = blocker;
}
if (node.type === 'VariableDeclaration') {
for (const declarator of node.declarations) {
for (const id of extract_identifiers(declarator.id)) {
push_declaration(id, blocker);
}
// one declarator per declaration, makes things simpler
analysis.instance_body.async.push({
node: declarator,
has_await
});
}
} else if (node.type === 'ClassDeclaration') {
push_declaration(node.id, blocker);
analysis.instance_body.async.push({ node, has_await });
} else {
analysis.instance_body.async.push({ node, has_await });
}
} else {
analysis.instance_body.sync.push(node);
}
}
if (analysis.runes) {
const props_refs = module.scope.references.get('$$props');
if (props_refs) {
@ -703,7 +902,7 @@ export function analyze_component(root, source, options) {
state_fields: new Map(),
function_depth: scope.function_depth,
reactive_statement: null,
in_derived: false
derived_function_depth: -1
};
walk(/** @type {AST.SvelteNode} */ (ast), state, visitors);
@ -771,7 +970,7 @@ export function analyze_component(root, source, options) {
expression: null,
state_fields: new Map(),
function_depth: scope.function_depth,
in_derived: false
derived_function_depth: -1
};
walk(/** @type {AST.SvelteNode} */ (ast), state, visitors);

@ -1,6 +1,7 @@
import type { Scope } from '../scope.js';
import type { ComponentAnalysis, ReactiveStatement } from '../types.js';
import type { AST, ExpressionMetadata, StateField, ValidatedCompileOptions } from '#compiler';
import type { AST, StateField, ValidatedCompileOptions } from '#compiler';
import type { ExpressionMetadata } from '../nodes.js';
export interface AnalysisState {
scope: Scope;
@ -29,9 +30,9 @@ export interface AnalysisState {
reactive_statement: null | ReactiveStatement;
/**
* True if we're directly inside a `$derived(...)` expression (but not `$derived.by(...)`)
* Set when we're inside a `$derived(...)` expression (but not `$derived.by(...)`) or `@const`
*/
in_derived: boolean;
derived_function_depth: number;
}
export type Context<State extends AnalysisState = AnalysisState> = import('zimmerframe').Context<

@ -1,12 +1,7 @@
/** @import { ArrowFunctionExpression, Expression, FunctionDeclaration, FunctionExpression } from 'estree' */
/** @import { AST, DelegatedEvent } from '#compiler' */
/** @import { AST } from '#compiler' */
/** @import { Context } from '../types' */
import { cannot_be_set_statically, is_capture_event, is_delegated } from '../../../../utils.js';
import {
get_attribute_chunks,
get_attribute_expression,
is_event_attribute
} from '../../../utils/ast.js';
import { cannot_be_set_statically, can_delegate_event } from '../../../../utils.js';
import { get_attribute_chunks, is_event_attribute } from '../../../utils/ast.js';
import { mark_subtree_dynamic } from './shared/fragment.js';
/**
@ -64,181 +59,8 @@ export function Attribute(node, context) {
context.state.analysis.uses_event_attributes = true;
}
const expression = get_attribute_expression(node);
const delegated_event = get_delegated_event(node.name.slice(2), expression, context);
if (delegated_event !== null) {
if (delegated_event.hoisted) {
delegated_event.function.metadata.hoisted = true;
}
node.metadata.delegated = delegated_event;
}
}
}
}
/** @type {DelegatedEvent} */
const unhoisted = { hoisted: false };
/**
* Checks if given event attribute can be delegated/hoisted and returns the corresponding info if so
* @param {string} event_name
* @param {Expression | null} handler
* @param {Context} context
* @returns {null | DelegatedEvent}
*/
function get_delegated_event(event_name, handler, context) {
// Handle delegated event handlers. Bail out if not a delegated event.
if (!handler || !is_delegated(event_name)) {
return null;
}
// If we are not working with a RegularElement, then bail out.
const element = context.path.at(-1);
if (element?.type !== 'RegularElement') {
return null;
}
/** @type {FunctionExpression | FunctionDeclaration | ArrowFunctionExpression | null} */
let target_function = null;
let binding = null;
if (element.metadata.has_spread) {
// event attribute becomes part of the dynamic spread array
return unhoisted;
}
if (handler.type === 'ArrowFunctionExpression' || handler.type === 'FunctionExpression') {
target_function = handler;
} else if (handler.type === 'Identifier') {
binding = context.state.scope.get(handler.name);
if (context.state.analysis.module.scope.references.has(handler.name)) {
// If a binding with the same name is referenced in the module scope (even if not declared there), bail out
return unhoisted;
}
if (binding != null) {
for (const { path } of binding.references) {
const parent = path.at(-1);
if (parent === undefined) return unhoisted;
const grandparent = path.at(-2);
/** @type {AST.RegularElement | null} */
let element = null;
/** @type {string | null} */
let event_name = null;
if (parent.type === 'OnDirective') {
element = /** @type {AST.RegularElement} */ (grandparent);
event_name = parent.name;
} else if (
parent.type === 'ExpressionTag' &&
grandparent?.type === 'Attribute' &&
is_event_attribute(grandparent)
) {
element = /** @type {AST.RegularElement} */ (path.at(-3));
const attribute = /** @type {AST.Attribute} */ (grandparent);
event_name = get_attribute_event_name(attribute.name);
}
if (element && event_name) {
if (
element.type !== 'RegularElement' ||
element.metadata.has_spread ||
!is_delegated(event_name)
) {
return unhoisted;
}
} else if (parent.type !== 'FunctionDeclaration' && parent.type !== 'VariableDeclarator') {
return unhoisted;
}
}
node.metadata.delegated =
parent?.type === 'RegularElement' && can_delegate_event(node.name.slice(2));
}
// If the binding is exported, bail out
if (context.state.analysis.exports.find((node) => node.name === handler.name)) {
return unhoisted;
}
if (binding?.is_function()) {
target_function = binding.initial;
}
}
// If we can't find a function, or the function has multiple parameters, bail out
if (target_function == null || target_function.params.length > 1) {
return unhoisted;
}
const visited_references = new Set();
const scope = target_function.metadata.scope;
for (const [reference] of scope.references) {
// Bail out if the arguments keyword is used or $host is referenced
if (reference === 'arguments' || reference === '$host') return unhoisted;
// Bail out if references a store subscription
if (scope.get(`$${reference}`)?.kind === 'store_sub') return unhoisted;
const binding = scope.get(reference);
const local_binding = context.state.scope.get(reference);
// if the function access a snippet that can't be hoisted we bail out
if (
local_binding !== null &&
local_binding.initial?.type === 'SnippetBlock' &&
!local_binding.initial.metadata.can_hoist
) {
return unhoisted;
}
// If we are referencing a binding that is shadowed in another scope then bail out (unless it's declared within the function).
if (
local_binding !== null &&
binding !== null &&
local_binding.node !== binding.node &&
scope.declarations.get(reference) !== binding
) {
return unhoisted;
}
// If we have multiple references to the same store using $ prefix, bail out.
if (
binding !== null &&
binding.kind === 'store_sub' &&
visited_references.has(reference.slice(1))
) {
return unhoisted;
}
// If we reference the index within an each block, then bail out.
if (binding !== null && binding.initial?.type === 'EachBlock') return unhoisted;
if (
binding !== null &&
// Bail out if the binding is a rest param
(binding.declaration_kind === 'rest_param' ||
// Bail out if we reference anything from the EachBlock (for now) that mutates in non-runes mode,
(((!context.state.analysis.runes && binding.kind === 'each') ||
// or any normal not reactive bindings that are mutated.
binding.kind === 'normal') &&
binding.updated))
) {
return unhoisted;
}
visited_references.add(reference);
}
return { hoisted: true, function: target_function };
}
/**
* @param {string} event_name
*/
function get_attribute_event_name(event_name) {
event_name = event_name.slice(2);
if (is_capture_event(event_name)) {
event_name = event_name.slice(0, -7);
}
return event_name;
}

@ -10,13 +10,13 @@ import * as e from '../../../errors.js';
export function AwaitExpression(node, context) {
const tla = context.state.ast_type === 'instance' && context.state.function_depth === 1;
// preserve context for
// a) top-level await and
// b) awaits that precede other expressions in template or `$derived(...)`
// preserve context for awaits that precede other expressions in template or `$derived(...)`
if (
tla ||
(is_reactive_expression(context.path, context.state.in_derived) &&
!is_last_evaluated_expression(context.path, node))
is_reactive_expression(
context.path,
context.state.derived_function_depth === context.state.function_depth
) &&
!is_last_evaluated_expression(context.path, node)
) {
context.state.analysis.pickled_awaits.add(node);
}
@ -53,9 +53,7 @@ export function AwaitExpression(node, context) {
* @param {boolean} in_derived
*/
export function is_reactive_expression(path, in_derived) {
if (in_derived) {
return true;
}
if (in_derived) return true;
let i = path.length;
@ -67,6 +65,7 @@ export function is_reactive_expression(path, in_derived) {
parent.type === 'FunctionExpression' ||
parent.type === 'FunctionDeclaration'
) {
// No reactive expression found between function and await
return false;
}
@ -83,11 +82,16 @@ export function is_reactive_expression(path, in_derived) {
* @param {AST.SvelteNode[]} path
* @param {Expression | SpreadElement | Property} node
*/
export function is_last_evaluated_expression(path, node) {
function is_last_evaluated_expression(path, node) {
let i = path.length;
while (i--) {
const parent = /** @type {Expression | Property | SpreadElement} */ (path[i]);
const parent = path[i];
if (parent.type === 'ConstTag') {
// {@const ...} tags are treated as deriveds and its contents should all get the preserve-reactivity treatment
return false;
}
// @ts-expect-error we could probably use a neater/more robust mechanism
if (parent.metadata) {
@ -138,6 +142,9 @@ export function is_last_evaluated_expression(path, node) {
if (node !== parent.expressions.at(-1)) return false;
break;
case 'VariableDeclarator':
return true;
default:
return false;
}

@ -172,6 +172,7 @@ export function BindDirective(node, context) {
}
const binding = context.state.scope.get(left.name);
node.metadata.binding = binding;
if (assignee.type === 'Identifier') {
// reassignment

@ -7,7 +7,7 @@ import { get_parent } from '../../../utils/ast.js';
import { is_pure, is_safe_identifier } from './shared/utils.js';
import { dev, locate_node, source } from '../../../state.js';
import * as b from '#compiler/builders';
import { create_expression_metadata } from '../../nodes.js';
import { ExpressionMetadata } from '../../nodes.js';
/**
* @param {CallExpression} node
@ -226,6 +226,13 @@ export function CallExpression(node, context) {
break;
}
case '$state.eager':
if (node.arguments.length !== 1) {
e.rune_invalid_arguments_length(node, rune, 'exactly one argument');
}
break;
case '$state.snapshot':
if (node.arguments.length !== 1) {
e.rune_invalid_arguments_length(node, rune, 'exactly one argument');
@ -236,12 +243,12 @@ export function CallExpression(node, context) {
// `$inspect(foo)` or `$derived(foo) should not trigger the `static-state-reference` warning
if (rune === '$derived') {
const expression = create_expression_metadata();
const expression = new ExpressionMetadata();
context.next({
...context.state,
function_depth: context.state.function_depth + 1,
in_derived: true,
derived_function_depth: context.state.function_depth + 1,
expression
});

@ -38,6 +38,8 @@ export function ConstTag(node, context) {
context.visit(declaration.init, {
...context.state,
expression: node.metadata.expression,
in_derived: true
// We're treating this like a $derived under the hood
function_depth: context.state.function_depth + 1,
derived_function_depth: context.state.function_depth + 1
});
}

@ -1,3 +1,4 @@
/** @import { Expression } from 'estree' */
/** @import { AST, Binding } from '#compiler' */
/** @import { Context } from '../types' */
/** @import { Scope } from '../../scope' */
@ -28,6 +29,10 @@ export function EachBlock(node, context) {
node.key.type !== 'Identifier' || !node.index || node.key.name !== node.index;
}
if (node.metadata.keyed && !node.context) {
e.each_key_without_as(/** @type {Expression} */ (node.key));
}
// evaluate expression in parent scope
context.visit(node.expression, {
...context.state,

@ -5,7 +5,7 @@ import * as e from '../../../errors.js';
import { validate_opening_tag } from './shared/utils.js';
import { mark_subtree_dynamic } from './shared/fragment.js';
import { is_resolved_snippet } from './shared/snippets.js';
import { create_expression_metadata } from '../../nodes.js';
import { ExpressionMetadata } from '../../nodes.js';
/**
* @param {AST.RenderTag} node
@ -57,7 +57,7 @@ export function RenderTag(node, context) {
context.visit(callee, { ...context.state, expression: node.metadata.expression });
for (const arg of expression.arguments) {
const metadata = create_expression_metadata();
const metadata = new ExpressionMetadata();
node.metadata.arguments.push(metadata);
context.visit(arg, {

@ -81,8 +81,13 @@ export function SnippetBlock(node, context) {
function can_hoist_snippet(scope, scopes, visited = new Set()) {
for (const [reference] of scope.references) {
const binding = scope.get(reference);
if (!binding) continue;
if (!binding || binding.scope.function_depth === 0) {
if (binding.blocker) {
return false;
}
if (binding.scope.function_depth === 0) {
continue;
}

@ -64,12 +64,6 @@ export function VariableDeclarator(node, context) {
}
}
if (rune === '$derived') {
context.visit(node.id);
context.visit(/** @type {Expression} */ (node.init), { ...context.state, in_derived: true });
return;
}
if (rune === '$props') {
if (node.id.type !== 'ObjectPattern' && node.id.type !== 'Identifier') {
e.props_invalid_identifier(node);

@ -6,13 +6,6 @@
* @param {Context} context
*/
export function visit_function(node, context) {
// TODO retire this in favour of a more general solution based on bindings
node.metadata = {
hoisted: false,
hoisted_params: [],
scope: context.state.scope
};
if (context.state.expression) {
for (const [name] of context.state.scope.references) {
const binding = context.state.scope.get(name);

@ -22,7 +22,10 @@ export function validate_assignment(node, argument, context) {
const binding = context.state.scope.get(argument.name);
if (context.state.analysis.runes) {
if (binding?.node === context.state.analysis.props_id) {
if (
context.state.analysis.props_id != null &&
binding?.node === context.state.analysis.props_id
) {
e.constant_assignment(node, '$props.id()');
}

@ -33,7 +33,6 @@ import { FunctionExpression } from './visitors/FunctionExpression.js';
import { HtmlTag } from './visitors/HtmlTag.js';
import { Identifier } from './visitors/Identifier.js';
import { IfBlock } from './visitors/IfBlock.js';
import { ImportDeclaration } from './visitors/ImportDeclaration.js';
import { KeyBlock } from './visitors/KeyBlock.js';
import { LabeledStatement } from './visitors/LabeledStatement.js';
import { LetDirective } from './visitors/LetDirective.js';
@ -111,7 +110,6 @@ const visitors = {
HtmlTag,
Identifier,
IfBlock,
ImportDeclaration,
KeyBlock,
LabeledStatement,
LetDirective,
@ -153,7 +151,7 @@ export function client_component(analysis, options) {
scope: analysis.module.scope,
scopes: analysis.module.scopes,
is_instance: false,
hoisted: [b.import_all('$', 'svelte/internal/client')],
hoisted: [b.import_all('$', 'svelte/internal/client'), ...analysis.instance_body.hoisted],
node: /** @type {any} */ (null), // populated by the root node
legacy_reactive_imports: [],
legacy_reactive_statements: new Map(),
@ -172,6 +170,7 @@ export function client_component(analysis, options) {
// these are set inside the `Fragment` visitor, and cannot be used until then
init: /** @type {any} */ (null),
consts: /** @type {any} */ (null),
let_directives: /** @type {any} */ (null),
update: /** @type {any} */ (null),
after_update: /** @type {any} */ (null),
template: /** @type {any} */ (null),
@ -369,39 +368,22 @@ export function client_component(analysis, options) {
analysis.reactive_statements.size > 0 ||
component_returned_object.length > 0;
if (analysis.instance.has_await) {
if (should_inject_context && component_returned_object.length > 0) {
component_block.body.push(b.var('$$exports'));
}
const body = b.block([
...store_setup,
...state.instance_level_snippets,
.../** @type {ESTree.Statement[]} */ (instance.body),
...(should_inject_context && component_returned_object.length > 0
? [b.stmt(b.assignment('=', b.id('$$exports'), b.object(component_returned_object)))]
: []),
b.if(b.call('$.aborted'), b.return()),
.../** @type {ESTree.Statement[]} */ (template.body)
]);
component_block.body.push(b.stmt(b.call(`$.async_body`, b.arrow([], body, true))));
} else {
component_block.body.push(
...state.instance_level_snippets,
.../** @type {ESTree.Statement[]} */ (instance.body)
);
if (should_inject_context && component_returned_object.length > 0) {
component_block.body.push(b.var('$$exports', b.object(component_returned_object)));
}
component_block.body.unshift(...store_setup);
component_block.body.push(
...state.instance_level_snippets,
.../** @type {ESTree.Statement[]} */ (instance.body)
);
if (!analysis.runes && analysis.needs_context) {
component_block.body.push(b.stmt(b.call('$.init', analysis.immutable ? b.true : undefined)));
}
if (should_inject_context && component_returned_object.length > 0) {
component_block.body.push(b.var('$$exports', b.object(component_returned_object)));
}
component_block.body.unshift(...store_setup);
component_block.body.push(.../** @type {ESTree.Statement[]} */ (template.body));
if (!analysis.runes && analysis.needs_context) {
component_block.body.push(b.stmt(b.call('$.init', analysis.immutable ? b.true : undefined)));
}
component_block.body.push(.../** @type {ESTree.Statement[]} */ (template.body));
if (analysis.needs_mutation_validation) {
component_block.body.unshift(
b.var('$$ownership_validator', b.call('$.create_ownership_validator', b.id('$$props')))

@ -21,9 +21,6 @@ export interface ClientTransformState extends TransformState {
*/
readonly in_constructor: boolean;
/** `true` if we're transforming the contents of `<script>` */
readonly is_instance: boolean;
readonly transform: Record<
string,
{
@ -54,6 +51,8 @@ export interface ComponentClientTransformState extends ClientTransformState {
readonly after_update: Statement[];
/** Transformed `{@const }` declarations */
readonly consts: Statement[];
/** Transformed `let:` directives */
readonly let_directives: Statement[];
/** Memoized expressions */
readonly memoizer: Memoizer;
/** The HTML template string */

@ -1,6 +1,6 @@
/** @import { ArrowFunctionExpression, AssignmentExpression, BlockStatement, Expression, FunctionDeclaration, FunctionExpression, Identifier, Node, Pattern, UpdateExpression } from 'estree' */
/** @import { BlockStatement, Expression, Identifier } from 'estree' */
/** @import { Binding } from '#compiler' */
/** @import { ClientTransformState, ComponentClientTransformState, ComponentContext } from './types.js' */
/** @import { ClientTransformState, ComponentClientTransformState } from './types.js' */
/** @import { Analysis } from '../../types.js' */
/** @import { Scope } from '../../scope.js' */
import * as b from '#compiler/builders';
@ -12,9 +12,6 @@ import {
PROPS_IS_UPDATED,
PROPS_IS_BINDABLE
} from '../../../../constants.js';
import { dev } from '../../../state.js';
import { walk } from 'zimmerframe';
import { validate_mutation } from './visitors/shared/utils.js';
/**
* @param {Binding} binding
@ -46,125 +43,6 @@ export function build_getter(node, state) {
return node;
}
/**
* @param {FunctionDeclaration | FunctionExpression | ArrowFunctionExpression} node
* @param {ComponentContext} context
* @returns {Pattern[]}
*/
function get_hoisted_params(node, context) {
const scope = context.state.scope;
/** @type {Identifier[]} */
const params = [];
/**
* We only want to push if it's not already present to avoid name clashing
* @param {Identifier} id
*/
function push_unique(id) {
if (!params.find((param) => param.name === id.name)) {
params.push(id);
}
}
for (const [reference] of scope.references) {
let binding = scope.get(reference);
if (binding !== null && !scope.declarations.has(reference) && binding.initial !== node) {
if (binding.kind === 'store_sub') {
// We need both the subscription for getting the value and the store for updating
push_unique(b.id(binding.node.name));
binding = /** @type {Binding} */ (scope.get(binding.node.name.slice(1)));
}
let expression = context.state.transform[reference]?.read(b.id(binding.node.name));
if (
// If it's a destructured derived binding, then we can extract the derived signal reference and use that.
// TODO this code is bad, we need to kill it
expression != null &&
typeof expression !== 'function' &&
expression.type === 'MemberExpression' &&
expression.object.type === 'CallExpression' &&
expression.object.callee.type === 'Identifier' &&
expression.object.callee.name === '$.get' &&
expression.object.arguments[0].type === 'Identifier'
) {
push_unique(b.id(expression.object.arguments[0].name));
} else if (
// If we are referencing a simple $$props value, then we need to reference the object property instead
(binding.kind === 'prop' || binding.kind === 'bindable_prop') &&
!is_prop_source(binding, context.state)
) {
push_unique(b.id('$$props'));
} else if (
// imports don't need to be hoisted
binding.declaration_kind !== 'import'
) {
// create a copy to remove start/end tags which would mess up source maps
push_unique(b.id(binding.node.name));
// rest props are often accessed through the $$props object for optimization reasons,
// but we can't know if the delegated event handler will use it, so we need to add both as params
if (binding.kind === 'rest_prop' && context.state.analysis.runes) {
push_unique(b.id('$$props'));
}
}
}
}
if (dev) {
// this is a little hacky, but necessary for ownership validation
// to work inside hoisted event handlers
/**
* @param {AssignmentExpression | UpdateExpression} node
* @param {{ next: () => void, stop: () => void }} context
*/
function visit(node, { next, stop }) {
if (validate_mutation(node, /** @type {any} */ (context), node) !== node) {
params.push(b.id('$$ownership_validator'));
stop();
} else {
next();
}
}
walk(/** @type {Node} */ (node), null, {
AssignmentExpression: visit,
UpdateExpression: visit
});
}
return params;
}
/**
* @param {FunctionDeclaration | FunctionExpression | ArrowFunctionExpression} node
* @param {ComponentContext} context
* @returns {Pattern[]}
*/
export function build_hoisted_params(node, context) {
const hoisted_params = get_hoisted_params(node, context);
node.metadata.hoisted_params = hoisted_params;
/** @type {Pattern[]} */
const params = [];
if (node.params.length === 0) {
if (hoisted_params.length > 0) {
// For the event object
params.push(b.id(context.state.scope.generate('_')));
}
} else {
for (const param of node.params) {
params.push(/** @type {Pattern} */ (context.visit(param)));
}
}
params.push(...hoisted_params);
return params;
}
/**
* @param {Binding} binding
* @param {ComponentClientTransformState} state

@ -243,18 +243,29 @@ export function BindDirective(node, context) {
}
}
const defer =
node.name !== 'this' &&
parent.type === 'RegularElement' &&
parent.attributes.find((a) => a.type === 'UseDirective');
let statement = defer ? b.stmt(b.call('$.effect', b.thunk(call))) : b.stmt(call);
// TODO this doesn't account for function bindings
if (node.metadata.binding?.blocker) {
statement = b.stmt(
b.call(b.member(node.metadata.binding.blocker, b.id('then')), b.thunk(b.block([statement])))
);
}
// Bindings need to happen after attribute updates, therefore after the render effect, and in order with events/actions.
// bind:this is a special case as it's one-way and could influence the render effect.
if (node.name === 'this') {
context.state.init.push(b.stmt(call));
context.state.init.push(statement);
} else {
const has_use =
parent.type === 'RegularElement' && parent.attributes.find((a) => a.type === 'UseDirective');
if (has_use) {
context.state.init.push(b.stmt(b.call('$.effect', b.thunk(call))));
if (defer) {
context.state.init.push(statement);
} else {
context.state.after_update.push(b.stmt(call));
context.state.after_update.push(statement);
}
}
}

@ -3,8 +3,8 @@
import { dev, is_ignored } from '../../../../state.js';
import * as b from '#compiler/builders';
import { get_rune } from '../../../scope.js';
import { transform_inspect_rune } from '../../utils.js';
import { should_proxy } from '../utils.js';
import { get_inspect_args } from '../../utils.js';
/**
* @param {CallExpression} node
@ -49,6 +49,12 @@ export function CallExpression(node, context) {
return b.call('$.derived', rune === '$derived' ? b.thunk(fn) : fn);
}
case '$state.eager':
return b.call(
'$.eager',
b.thunk(/** @type {Expression} */ (context.visit(node.arguments[0])))
);
case '$state.snapshot':
return b.call(
'$.snapshot',
@ -56,6 +62,17 @@ export function CallExpression(node, context) {
is_ignored(node, 'state_snapshot_uncloneable') && b.true
);
case '$effect':
case '$effect.pre': {
const callee = rune === '$effect' ? '$.user_effect' : '$.user_pre_effect';
const func = /** @type {Expression} */ (context.visit(node.arguments[0]));
const expr = b.call(callee, /** @type {Expression} */ (func));
expr.callee.loc = node.callee.loc; // ensure correct mapping
return expr;
}
case '$effect.root':
return b.call(
'$.effect_root',
@ -63,11 +80,11 @@ export function CallExpression(node, context) {
);
case '$effect.pending':
return b.call('$.pending');
return b.call('$.eager', b.thunk(b.call('$.pending')));
case '$inspect':
case '$inspect().with':
return transform_inspect_rune(node, context);
return transform_inspect_rune(rune, node, context);
}
if (
@ -98,3 +115,21 @@ export function CallExpression(node, context) {
context.next();
}
/**
* @param {'$inspect' | '$inspect().with'} rune
* @param {CallExpression} node
* @param {Context} context
*/
function transform_inspect_rune(rune, node, context) {
if (!dev) return b.empty;
const { args, inspector } = get_inspect_args(rune, node, context.visit);
// by passing an arrow function, the log appears to come from the `$inspect` callsite
// rather than the `inspect.js` file containing the utility
const id = b.id('$$args');
const fn = b.arrow([b.rest(id)], b.call(inspector, b.spread(id)));
return b.call('$.inspect', b.thunk(b.array(args)), fn, rune === '$inspect' && b.true);
}

@ -312,9 +312,10 @@ export function EachBlock(node, context) {
declarations.push(b.let(node.index, index));
}
const { has_await } = node.metadata.expression;
const get_collection = b.thunk(collection, has_await);
const thunk = has_await ? b.thunk(b.call('$.get', b.id('$$collection'))) : get_collection;
const is_async = node.metadata.expression.is_async();
const get_collection = b.thunk(collection, node.metadata.expression.has_await);
const thunk = is_async ? b.thunk(b.call('$.get', b.id('$$collection'))) : get_collection;
const render_args = [b.id('$$anchor'), item];
if (uses_index || collection_id) render_args.push(index);
@ -341,12 +342,13 @@ export function EachBlock(node, context) {
statements.unshift(b.stmt(b.call('$.validate_each_keys', thunk, key_function)));
}
if (has_await) {
if (is_async) {
context.state.init.push(
b.stmt(
b.call(
'$.async',
context.state.node,
node.metadata.expression.blockers(),
b.array([get_collection]),
b.arrow([context.state.node, b.id('$$collection')], b.block(statements))
)

@ -1,4 +1,4 @@
/** @import { Expression, ExpressionStatement } from 'estree' */
/** @import { ExpressionStatement } from 'estree' */
/** @import { ComponentContext } from '../types' */
import * as b from '#compiler/builders';
import { get_rune } from '../../../scope.js';
@ -11,16 +11,6 @@ export function ExpressionStatement(node, context) {
if (node.expression.type === 'CallExpression') {
const rune = get_rune(node.expression, context.state.scope);
if (rune === '$effect' || rune === '$effect.pre') {
const callee = rune === '$effect' ? '$.user_effect' : '$.user_pre_effect';
const func = /** @type {Expression} */ (context.visit(node.expression.arguments[0]));
const expr = b.call(callee, /** @type {Expression} */ (func));
expr.callee.loc = node.expression.callee.loc; // ensure correct mapping
return b.stmt(expr);
}
if (rune === '$inspect.trace') {
return b.empty;
}

@ -63,6 +63,7 @@ export function Fragment(node, context) {
...context.state,
init: [],
consts: [],
let_directives: [],
update: [],
after_update: [],
memoizer: new Memoizer(),
@ -150,7 +151,7 @@ export function Fragment(node, context) {
}
}
body.push(...state.consts);
body.push(...state.let_directives, ...state.consts);
if (has_await) {
body.push(b.if(b.call('$.aborted'), b.return()));
@ -177,7 +178,11 @@ export function Fragment(node, context) {
}
if (has_await) {
return b.block([b.stmt(b.call('$.async_body', b.arrow([], b.block(body), true)))]);
return b.block([
b.stmt(
b.call('$.async_body', b.id('$$anchor'), b.arrow([b.id('$$anchor')], b.block(body), true))
)
]);
} else {
return b.block(body);
}

@ -1,7 +1,5 @@
/** @import { FunctionDeclaration } from 'estree' */
/** @import { ComponentContext } from '../types' */
import { build_hoisted_params } from '../utils.js';
import * as b from '#compiler/builders';
/**
* @param {FunctionDeclaration} node
@ -10,14 +8,5 @@ import * as b from '#compiler/builders';
export function FunctionDeclaration(node, context) {
const state = { ...context.state, in_constructor: false, in_derived: false };
if (node.metadata?.hoisted === true) {
const params = build_hoisted_params(node, context);
const body = context.visit(node.body, state);
context.state.hoisted.push(/** @type {FunctionDeclaration} */ ({ ...node, params, body }));
return b.empty;
}
context.next(state);
}

@ -11,9 +11,10 @@ import { build_expression } from './shared/utils.js';
export function HtmlTag(node, context) {
context.state.template.push_comment();
const { has_await } = node.metadata.expression;
const is_async = node.metadata.expression.is_async();
const expression = build_expression(context, node.expression, node.metadata.expression);
const html = has_await ? b.call('$.get', b.id('$$html')) : expression;
const html = is_async ? b.call('$.get', b.id('$$html')) : expression;
const is_svg = context.state.metadata.namespace === 'svg';
const is_mathml = context.state.metadata.namespace === 'mathml';
@ -30,13 +31,14 @@ export function HtmlTag(node, context) {
);
// push into init, so that bindings run afterwards, which might trigger another run and override hydration
if (node.metadata.expression.has_await) {
if (is_async) {
context.state.init.push(
b.stmt(
b.call(
'$.async',
context.state.node,
b.array([b.thunk(expression, true)]),
node.metadata.expression.blockers(),
b.array([b.thunk(expression, node.metadata.expression.has_await)]),
b.arrow([context.state.node, b.id('$$html')], b.block([statement]))
)
)

@ -25,9 +25,10 @@ export function IfBlock(node, context) {
statements.push(b.var(alternate_id, b.arrow([b.id('$$anchor')], alternate)));
}
const { has_await } = node.metadata.expression;
const is_async = node.metadata.expression.is_async();
const expression = build_expression(context, node.test, node.metadata.expression);
const test = has_await ? b.call('$.get', b.id('$$condition')) : expression;
const test = is_async ? b.call('$.get', b.id('$$condition')) : expression;
/** @type {Expression[]} */
const args = [
@ -71,13 +72,14 @@ export function IfBlock(node, context) {
statements.push(add_svelte_meta(b.call('$.if', ...args), node, 'if'));
if (has_await) {
if (is_async) {
context.state.init.push(
b.stmt(
b.call(
'$.async',
context.state.node,
b.array([b.thunk(expression, true)]),
node.metadata.expression.blockers(),
b.array([b.thunk(expression, node.metadata.expression.has_await)]),
b.arrow([context.state.node, b.id('$$condition')], b.block(statements))
)
)

@ -1,16 +0,0 @@
/** @import { ImportDeclaration } from 'estree' */
/** @import { ComponentContext } from '../types' */
import * as b from '#compiler/builders';
/**
* @param {ImportDeclaration} node
* @param {ComponentContext} context
*/
export function ImportDeclaration(node, context) {
if ('hoisted' in context.state) {
context.state.hoisted.push(node);
return b.empty;
}
context.next();
}

@ -11,10 +11,10 @@ import { build_expression, add_svelte_meta } from './shared/utils.js';
export function KeyBlock(node, context) {
context.state.template.push_comment();
const { has_await } = node.metadata.expression;
const is_async = node.metadata.expression.is_async();
const expression = build_expression(context, node.expression, node.metadata.expression);
const key = b.thunk(has_await ? b.call('$.get', b.id('$$key')) : expression);
const key = b.thunk(is_async ? b.call('$.get', b.id('$$key')) : expression);
const body = /** @type {Expression} */ (context.visit(node.fragment));
let statement = add_svelte_meta(
@ -23,12 +23,13 @@ export function KeyBlock(node, context) {
'key'
);
if (has_await) {
if (is_async) {
statement = b.stmt(
b.call(
'$.async',
context.state.node,
b.array([b.thunk(expression, true)]),
node.metadata.expression.blockers(),
b.array([b.thunk(expression, node.metadata.expression.has_await)]),
b.arrow([context.state.node, b.id('$$key')], b.block([statement]))
)
);

@ -21,22 +21,24 @@ export function LetDirective(node, context) {
};
}
return b.const(
name,
b.call(
'$.derived',
b.thunk(
b.block([
b.let(
/** @type {Expression} */ (node.expression).type === 'ObjectExpression'
? // @ts-expect-error types don't match, but it can't contain spread elements and the structure is otherwise fine
b.object_pattern(node.expression.properties)
: // @ts-expect-error types don't match, but it can't contain spread elements and the structure is otherwise fine
b.array_pattern(node.expression.elements),
b.member(b.id('$$slotProps'), node.name)
),
b.return(b.object(bindings.map((binding) => b.init(binding.node.name, binding.node))))
])
context.state.let_directives.push(
b.const(
name,
b.call(
'$.derived',
b.thunk(
b.block([
b.let(
/** @type {Expression} */ (node.expression).type === 'ObjectExpression'
? // @ts-expect-error types don't match, but it can't contain spread elements and the structure is otherwise fine
b.object_pattern(node.expression.properties)
: // @ts-expect-error types don't match, but it can't contain spread elements and the structure is otherwise fine
b.array_pattern(node.expression.elements),
b.member(b.id('$$slotProps'), node.name)
),
b.return(b.object(bindings.map((binding) => b.init(binding.node.name, binding.node))))
])
)
)
)
);
@ -46,6 +48,8 @@ export function LetDirective(node, context) {
read: (node) => b.call('$.get', node)
};
return b.const(name, create_derived(context.state, b.member(b.id('$$slotProps'), node.name)));
context.state.let_directives.push(
b.const(name, create_derived(context.state, b.member(b.id('$$slotProps'), node.name)))
);
}
}

@ -1,14 +1,15 @@
/** @import { Expression, ImportDeclaration, MemberExpression, Program } from 'estree' */
/** @import { Expression, ImportDeclaration, MemberExpression, Node, Program } from 'estree' */
/** @import { ComponentContext } from '../types' */
import { build_getter, is_prop_source } from '../utils.js';
import * as b from '#compiler/builders';
import { add_state_transformers } from './shared/declarations.js';
import { transform_body } from '../../shared/transform-async.js';
/**
* @param {Program} _
* @param {Program} node
* @param {ComponentContext} context
*/
export function Program(_, context) {
export function Program(node, context) {
if (!context.state.analysis.runes) {
context.state.transform['$$props'] = {
read: (node) => ({ ...node, name: '$$sanitized_props' })
@ -137,5 +138,16 @@ export function Program(_, context) {
add_state_transformers(context);
if (context.state.is_instance) {
return {
...node,
body: transform_body(
context.state.analysis.instance_body,
b.id('$.run'),
(node) => /** @type {Node} */ (context.visit(node))
)
};
}
context.next();
}

@ -11,7 +11,7 @@ import {
import { is_ignored } from '../../../../state.js';
import { is_event_attribute, is_text_attribute } from '../../../../utils/ast.js';
import * as b from '#compiler/builders';
import { create_attribute, is_custom_element_node } from '../../../nodes.js';
import { create_attribute, ExpressionMetadata, is_custom_element_node } from '../../../nodes.js';
import { clean_nodes, determine_namespace_for_children } from '../../utils.js';
import { build_getter } from '../utils.js';
import {
@ -106,7 +106,7 @@ export function RegularElement(node, context) {
case 'LetDirective':
// visit let directives before everything else, to set state
lets.push(/** @type {ExpressionStatement} */ (context.visit(attribute)));
context.visit(attribute, { ...context.state, let_directives: lets });
break;
case 'OnDirective':
@ -267,10 +267,7 @@ export function RegularElement(node, context) {
const { value, has_state } = build_attribute_value(
attribute.value,
context,
(value, metadata) =>
metadata.has_call || metadata.has_await
? context.state.memoizer.add(value, metadata.has_await)
: value
(value, metadata) => context.state.memoizer.add(value, metadata)
);
const update = build_element_attribute_update(node, node_id, name, value, attributes);
@ -487,11 +484,25 @@ function setup_select_synchronization(value_binding, context) {
);
}
/**
* @param {ExpressionMetadata} target
* @param {ExpressionMetadata} source
*/
function merge_metadata(target, source) {
target.has_assignment ||= source.has_assignment;
target.has_await ||= source.has_await;
target.has_call ||= source.has_call;
target.has_member_expression ||= source.has_member_expression;
target.has_state ||= source.has_state;
for (const r of source.references) target.references.add(r);
for (const b of source.dependencies) target.dependencies.add(b);
}
/**
* @param {AST.ClassDirective[]} class_directives
* @param {ComponentContext} context
* @param {Memoizer} memoizer
* @return {ObjectExpression | Identifier}
*/
export function build_class_directives_object(
class_directives,
@ -499,26 +510,25 @@ export function build_class_directives_object(
memoizer = context.state.memoizer
) {
let properties = [];
let has_call_or_state = false;
let has_await = false;
const metadata = new ExpressionMetadata();
for (const d of class_directives) {
merge_metadata(metadata, d.metadata.expression);
const expression = /** @type Expression */ (context.visit(d.expression));
properties.push(b.init(d.name, expression));
has_call_or_state ||= d.metadata.expression.has_call || d.metadata.expression.has_state;
has_await ||= d.metadata.expression.has_await;
}
const directives = b.object(properties);
return has_call_or_state || has_await ? memoizer.add(directives, has_await) : directives;
return memoizer.add(directives, metadata);
}
/**
* @param {AST.StyleDirective[]} style_directives
* @param {ComponentContext} context
* @param {Memoizer} memoizer
* @return {ObjectExpression | ArrayExpression | Identifier}}
*/
export function build_style_directives_object(
style_directives,
@ -528,10 +538,11 @@ export function build_style_directives_object(
const normal = b.object([]);
const important = b.object([]);
let has_call_or_state = false;
let has_await = false;
const metadata = new ExpressionMetadata();
for (const d of style_directives) {
merge_metadata(metadata, d.metadata.expression);
const expression =
d.value === true
? build_getter(b.id(d.name), context.state)
@ -539,14 +550,11 @@ export function build_style_directives_object(
const object = d.modifiers.includes('important') ? important : normal;
object.properties.push(b.init(d.name, expression));
has_call_or_state ||= d.metadata.expression.has_call || d.metadata.expression.has_state;
has_await ||= d.metadata.expression.has_await;
}
const directives = important.properties.length ? b.array([normal, important]) : normal;
return has_call_or_state || has_await ? memoizer.add(directives, has_await) : directives;
return memoizer.add(directives, metadata);
}
/**
@ -675,7 +683,7 @@ function build_element_special_value_attribute(
element === 'select' && attribute.value !== true && !is_text_attribute(attribute);
const { value, has_state } = build_attribute_value(attribute.value, context, (value, metadata) =>
metadata.has_call || metadata.has_await ? state.memoizer.add(value, metadata.has_await) : value
state.memoizer.add(value, metadata)
);
const evaluated = context.state.scope.evaluate(value);

@ -22,11 +22,11 @@ export function RenderTag(node, context) {
for (let i = 0; i < call.arguments.length; i++) {
const arg = /** @type {Expression} */ (call.arguments[i]);
const metadata = node.metadata.arguments[i];
let expression = build_expression(context, arg, metadata);
const memoized = memoizer.add(expression, metadata);
if (metadata.has_await || metadata.has_call) {
expression = b.call('$.get', memoizer.add(expression, metadata.has_await));
if (expression !== memoized) {
expression = b.call('$.get', memoized);
}
args.push(b.thunk(expression));
@ -71,13 +71,15 @@ export function RenderTag(node, context) {
}
const async_values = memoizer.async_values();
const blockers = memoizer.blockers();
if (async_values) {
if (async_values || blockers) {
context.state.init.push(
b.stmt(
b.call(
'$.async',
context.state.node,
blockers,
memoizer.async_values(),
b.arrow([context.state.node, ...memoizer.async_ids()], b.block(statements))
)

@ -35,7 +35,7 @@ export function SlotElement(node, context) {
context,
(value, metadata) =>
metadata.has_call || metadata.has_await
? b.call('$.get', memoizer.add(value, metadata.has_await))
? b.call('$.get', memoizer.add(value, metadata))
: value
);
@ -49,7 +49,7 @@ export function SlotElement(node, context) {
}
}
} else if (attribute.type === 'LetDirective') {
lets.push(/** @type {ExpressionStatement} */ (context.visit(attribute)));
context.visit(attribute, { ...context.state, let_directives: lets });
}
}
@ -74,13 +74,15 @@ export function SlotElement(node, context) {
);
const async_values = memoizer.async_values();
const blockers = memoizer.blockers();
if (async_values) {
if (async_values || blockers) {
context.state.init.push(
b.stmt(
b.call(
'$.async',
context.state.node,
blockers,
async_values,
b.arrow([context.state.node, ...memoizer.async_ids()], b.block(statements))
)

@ -93,10 +93,10 @@ export function SvelteElement(node, context) {
);
}
const { has_await } = node.metadata.expression;
const is_async = node.metadata.expression.is_async();
const expression = /** @type {Expression} */ (context.visit(node.tag));
const get_tag = b.thunk(has_await ? b.call('$.get', b.id('$$tag')) : expression);
const get_tag = b.thunk(is_async ? b.call('$.get', b.id('$$tag')) : expression);
/** @type {Statement[]} */
const inner = inner_context.state.init;
@ -139,13 +139,14 @@ export function SvelteElement(node, context) {
)
);
if (has_await) {
if (is_async) {
context.state.init.push(
b.stmt(
b.call(
'$.async',
context.state.node,
b.array([b.thunk(expression, true)]),
node.metadata.expression.blockers(),
b.array([b.thunk(expression, node.metadata.expression.has_await)]),
b.arrow([context.state.node, b.id('$$tag')], b.block(statements))
)
)

@ -9,7 +9,7 @@
export function SvelteFragment(node, context) {
for (const attribute of node.attributes) {
if (attribute.type === 'LetDirective') {
context.state.init.push(/** @type {ExpressionStatement} */ (context.visit(attribute)));
context.visit(attribute);
}
}

@ -2,6 +2,8 @@
/** @import { AST } from '#compiler' */
/** @import { ComponentContext } from '../types' */
import * as b from '#compiler/builders';
import { hash } from '../../../../../utils.js';
import { filename } from '../../../../state.js';
/**
* @param {AST.SvelteHead} node
@ -13,6 +15,7 @@ export function SvelteHead(node, context) {
b.stmt(
b.call(
'$.head',
b.literal(hash(filename)),
b.arrow([b.id('$$anchor')], /** @type {BlockStatement} */ (context.visit(node.fragment)))
)
)

@ -1,16 +1,19 @@
/** @import { AST } from '#compiler' */
/** @import { ComponentContext } from '../types' */
import * as b from '#compiler/builders';
import { build_template_chunk } from './shared/utils.js';
import { build_template_chunk, Memoizer } from './shared/utils.js';
/**
* @param {AST.TitleElement} node
* @param {ComponentContext} context
*/
export function TitleElement(node, context) {
const memoizer = new Memoizer();
const { has_state, value } = build_template_chunk(
/** @type {any} */ (node.fragment.nodes),
context
context,
context.state,
(value, metadata) => memoizer.add(value, metadata)
);
const evaluated = context.state.scope.evaluate(value);
@ -26,9 +29,21 @@ export function TitleElement(node, context) {
)
);
// Always in an $effect so it only changes the title once async work is done
if (has_state) {
context.state.update.push(statement);
context.state.after_update.push(
b.stmt(
b.call(
'$.template_effect',
b.arrow(memoizer.apply(), b.block([statement])),
memoizer.sync_values(),
memoizer.async_values(),
memoizer.blockers(),
b.true
)
)
);
} else {
context.state.init.push(statement);
context.state.after_update.push(b.stmt(b.call('$.effect', b.thunk(b.block([statement])))));
}
}

@ -7,7 +7,6 @@ import * as b from '#compiler/builders';
import * as assert from '../../../../utils/assert.js';
import { get_rune } from '../../../scope.js';
import { get_prop_source, is_prop_source, is_state_source, should_proxy } from '../utils.js';
import { is_hoisted_function } from '../../utils.js';
import { get_value } from './shared/declarations.js';
/**
@ -32,13 +31,6 @@ export function VariableDeclaration(node, context) {
rune === '$state.snapshot' ||
rune === '$host'
) {
if (init != null && is_hoisted_function(init)) {
context.state.hoisted.push(
b.const(declarator.id, /** @type {Expression} */ (context.visit(init)))
);
continue;
}
declarations.push(/** @type {VariableDeclarator} */ (context.visit(declarator)));
continue;
}
@ -201,18 +193,25 @@ export function VariableDeclaration(node, context) {
/** @type {CallExpression} */ (init)
);
// for now, only wrap async derived in $.save if it's not
// a top-level instance derived. TODO in future maybe we
// can dewaterfall all of them?
const should_save = context.state.is_instance && context.state.scope.function_depth > 1;
if (declarator.id.type === 'Identifier') {
let expression = /** @type {Expression} */ (context.visit(value));
if (is_async) {
const location = dev && !is_ignored(init, 'await_waterfall') && locate_node(init);
/** @type {Expression} */
let call = b.call(
'$.async_derived',
b.thunk(expression, true),
location ? b.literal(location) : undefined
);
call = save(call);
call = should_save ? save(call) : b.await(call);
if (dev) call = b.call('$.tag', call, b.literal(declarator.id.name));
declarations.push(b.declarator(declarator.id, call));
@ -232,18 +231,22 @@ export function VariableDeclaration(node, context) {
if (rune !== '$derived' || init.arguments[0].type !== 'Identifier') {
const id = b.id(context.state.scope.generate('$$d'));
/** @type {Expression} */
let call = b.call('$.derived', rune === '$derived' ? b.thunk(expression) : expression);
rhs = b.call('$.get', id);
if (is_async) {
const location = dev && !is_ignored(init, 'await_waterfall') && locate_node(init);
call = b.call(
'$.async_derived',
b.thunk(expression, true),
location ? b.literal(location) : undefined
);
call = save(call);
call = should_save ? save(call) : b.await(call);
}
if (dev) {
@ -295,16 +298,6 @@ export function VariableDeclaration(node, context) {
const has_props = bindings.some((binding) => binding.kind === 'bindable_prop');
if (!has_state && !has_props) {
const init = declarator.init;
if (init != null && is_hoisted_function(init)) {
context.state.hoisted.push(
b.const(declarator.id, /** @type {Expression} */ (context.visit(init)))
);
continue;
}
declarations.push(/** @type {VariableDeclarator} */ (context.visit(declarator)));
continue;
}

@ -16,12 +16,12 @@ import { determine_slot } from '../../../../../utils/slot.js';
* @returns {Statement}
*/
export function build_component(node, component_name, context) {
/**
* @type {Expression}
*/
/** @type {Expression} */
const anchor = context.state.node;
/** @type {Array<Property[] | Expression>} */
const props_and_spreads = [];
/** @type {Array<() => void>} */
const delayed_props = [];
@ -101,7 +101,7 @@ export function build_component(node, component_name, context) {
if (slot_scope_applies_to_itself) {
for (const attribute of node.attributes) {
if (attribute.type === 'LetDirective') {
lets.push(/** @type {ExpressionStatement} */ (context.visit(attribute)));
context.visit(attribute, { ...context.state, let_directives: lets });
}
}
}
@ -109,7 +109,7 @@ export function build_component(node, component_name, context) {
for (const attribute of node.attributes) {
if (attribute.type === 'LetDirective') {
if (!slot_scope_applies_to_itself) {
lets.push(/** @type {ExpressionStatement} */ (context.visit(attribute, states.default)));
context.visit(attribute, { ...states.default, let_directives: lets });
}
} else if (attribute.type === 'OnDirective') {
if (!attribute.expression) {
@ -129,14 +129,16 @@ export function build_component(node, component_name, context) {
(events[attribute.name] ||= []).push(handler);
} else if (attribute.type === 'SpreadAttribute') {
const expression = /** @type {Expression} */ (context.visit(attribute));
const memoized_expression = memoizer.add(expression, attribute.metadata.expression);
const is_memoized = expression !== memoized_expression;
if (attribute.metadata.expression.has_state || attribute.metadata.expression.has_await) {
if (
is_memoized ||
attribute.metadata.expression.has_state ||
attribute.metadata.expression.has_await
) {
props_and_spreads.push(
b.thunk(
attribute.metadata.expression.has_await || attribute.metadata.expression.has_call
? b.call('$.get', memoizer.add(expression, attribute.metadata.expression.has_await))
: expression
)
b.thunk(is_memoized ? b.call('$.get', memoized_expression) : expression)
);
} else {
props_and_spreads.push(expression);
@ -147,10 +149,10 @@ export function build_component(node, component_name, context) {
b.init(
attribute.name,
build_attribute_value(attribute.value, context, (value, metadata) => {
const memoized = memoizer.add(value, metadata);
// TODO put the derived in the local block
return metadata.has_call || metadata.has_await
? b.call('$.get', memoizer.add(value, metadata.has_await))
: value;
return value !== memoized ? b.call('$.get', memoized) : value;
}).value
)
);
@ -184,9 +186,9 @@ export function build_component(node, component_name, context) {
);
});
return should_wrap_in_derived
? b.call('$.get', memoizer.add(value, metadata.has_await))
: value;
const memoized = memoizer.add(value, metadata, should_wrap_in_derived);
return value !== memoized ? b.call('$.get', memoized) : value;
}
);
@ -497,12 +499,14 @@ export function build_component(node, component_name, context) {
memoizer.apply();
const async_values = memoizer.async_values();
const blockers = memoizer.blockers();
if (async_values) {
if (async_values || blockers) {
return b.stmt(
b.call(
'$.async',
anchor,
blockers,
async_values,
b.arrow([b.id('$$anchor'), ...memoizer.async_ids()], b.block(statements))
)

@ -1,5 +1,5 @@
/** @import { Expression, Identifier, ObjectExpression } from 'estree' */
/** @import { AST, ExpressionMetadata } from '#compiler' */
/** @import { AST } from '#compiler' */
/** @import { ComponentContext } from '../../types' */
import { escape_html } from '../../../../../../escaping.js';
import { normalize_attribute } from '../../../../../../utils.js';
@ -8,6 +8,7 @@ import { is_event_attribute } from '../../../../../utils/ast.js';
import * as b from '#compiler/builders';
import { build_class_directives_object, build_style_directives_object } from '../RegularElement.js';
import { build_expression, build_template_chunk, Memoizer } from './utils.js';
import { ExpressionMetadata } from '../../../../nodes.js';
/**
* @param {Array<AST.Attribute | AST.SpreadAttribute>} attributes
@ -35,7 +36,7 @@ export function build_attribute_effect(
for (const attribute of attributes) {
if (attribute.type === 'Attribute') {
const { value } = build_attribute_value(attribute.value, context, (value, metadata) =>
metadata.has_call || metadata.has_await ? memoizer.add(value, metadata.has_await) : value
memoizer.add(value, metadata)
);
if (
@ -52,9 +53,7 @@ export function build_attribute_effect(
} else {
let value = /** @type {Expression} */ (context.visit(attribute));
if (attribute.metadata.expression.has_call || attribute.metadata.expression.has_await) {
value = memoizer.add(value, attribute.metadata.expression.has_await);
}
value = memoizer.add(value, attribute.metadata.expression);
values.push(b.spread(value));
}
@ -90,6 +89,7 @@ export function build_attribute_effect(
b.arrow(ids, b.object(values)),
memoizer.sync_values(),
memoizer.async_values(),
memoizer.blockers(),
element.metadata.scoped &&
context.state.analysis.css.hash !== '' &&
b.literal(context.state.analysis.css.hash),
@ -155,9 +155,7 @@ export function build_set_class(element, node_id, attribute, class_directives, c
value = b.call('$.clsx', value);
}
return metadata.has_call || metadata.has_await
? context.state.memoizer.add(value, metadata.has_await)
: value;
return context.state.memoizer.add(value, metadata);
});
/** @type {Identifier | undefined} */
@ -166,7 +164,7 @@ export function build_set_class(element, node_id, attribute, class_directives, c
/** @type {ObjectExpression | Identifier | undefined} */
let prev;
/** @type {ObjectExpression | Identifier | undefined} */
/** @type {Expression | undefined} */
let next;
if (class_directives.length) {
@ -227,7 +225,7 @@ export function build_set_class(element, node_id, attribute, class_directives, c
*/
export function build_set_style(node_id, attribute, style_directives, context) {
let { value, has_state } = build_attribute_value(attribute.value, context, (value, metadata) =>
metadata.has_call ? context.state.memoizer.add(value, metadata.has_await) : value
context.state.memoizer.add(value, metadata)
);
/** @type {Identifier | undefined} */

@ -1,9 +1,10 @@
/** @import { Expression } from 'estree' */
/** @import { AST, ExpressionMetadata } from '#compiler' */
/** @import { AST } from '#compiler' */
/** @import { ComponentContext } from '../../types' */
import { is_capture_event, is_passive_event } from '../../../../../../utils.js';
import { dev, locator } from '../../../../../state.js';
import * as b from '#compiler/builders';
import { ExpressionMetadata } from '../../../../nodes.js';
/**
* @param {AST.Attribute} node
@ -26,40 +27,12 @@ export function visit_event_attribute(node, context) {
let handler = build_event_handler(tag.expression, tag.metadata.expression, context);
if (node.metadata.delegated) {
let delegated_assignment;
if (!context.state.events.has(event_name)) {
context.state.events.add(event_name);
}
// Hoist function if we can, otherwise we leave the function as is
if (node.metadata.delegated.hoisted) {
if (node.metadata.delegated.function === tag.expression) {
const func_name = context.state.scope.root.unique('on_' + event_name);
context.state.hoisted.push(b.var(func_name, handler));
handler = func_name;
}
const hoisted_params = /** @type {Expression[]} */ (
node.metadata.delegated.function.metadata.hoisted_params
);
// When we hoist a function we assign an array with the function and all
// hoisted closure params.
if (hoisted_params) {
const args = [handler, ...hoisted_params];
delegated_assignment = b.array(args);
} else {
delegated_assignment = handler;
}
} else {
delegated_assignment = handler;
}
context.state.init.push(
b.stmt(
b.assignment('=', b.member(context.state.node, '__' + event_name), delegated_assignment)
)
b.stmt(b.assignment('=', b.member(context.state.node, '__' + event_name), handler))
);
} else {
const statement = b.stmt(

@ -105,7 +105,7 @@ export function process_children(nodes, initial, is_element, context) {
is_element &&
// In case it's wrapped in async the async logic will want to skip sibling nodes up until the end, hence we cannot make this controlled
// TODO switch this around and instead optimize for elements with a single block child and not require extra comments (neither for async nor normally)
!(node.body.metadata.has_await || node.metadata.expression.has_await)
!(node.body.metadata.has_await || node.metadata.expression.is_async())
) {
node.metadata.is_controlled = true;
} else {

@ -1,14 +1,11 @@
/** @import { ArrowFunctionExpression, FunctionExpression, Node } from 'estree' */
/** @import { ComponentContext } from '../../types' */
import { build_hoisted_params } from '../../utils.js';
/**
* @param {ArrowFunctionExpression | FunctionExpression} node
* @param {ComponentContext} context
*/
export const visit_function = (node, context) => {
const metadata = node.metadata;
let state = { ...context.state, in_constructor: false, in_derived: false };
if (node.type === 'FunctionExpression') {
@ -16,15 +13,5 @@ export const visit_function = (node, context) => {
state.in_constructor = parent.type === 'MethodDefinition' && parent.kind === 'constructor';
}
if (metadata?.hoisted === true) {
const params = build_hoisted_params(node, context);
return /** @type {FunctionExpression} */ ({
...node,
params,
body: context.visit(node.body, state)
});
}
context.next(state);
};

@ -1,5 +1,5 @@
/** @import { AssignmentExpression, Expression, Identifier, MemberExpression, SequenceExpression, Literal, Super, UpdateExpression, ExpressionStatement } from 'estree' */
/** @import { AST, ExpressionMetadata } from '#compiler' */
/** @import { AST } from '#compiler' */
/** @import { ComponentClientTransformState, ComponentContext, Context } from '../../types' */
import { walk } from 'zimmerframe';
import { object } from '../../../../../utils/ast.js';
@ -9,6 +9,7 @@ import { regex_is_valid_identifier } from '../../../../patterns.js';
import is_reference from 'is-reference';
import { dev, is_ignored, locator, component_name } from '../../../../../state.js';
import { build_getter } from '../../utils.js';
import { ExpressionMetadata } from '../../../../nodes.js';
/**
* A utility for extracting complex expressions (such as call expressions)
@ -21,14 +22,32 @@ export class Memoizer {
/** @type {Array<{ id: Identifier, expression: Expression }>} */
#async = [];
/** @type {Set<Expression>} */
#blockers = new Set();
/**
* @param {Expression} expression
* @param {boolean} has_await
* @param {ExpressionMetadata} metadata
* @param {boolean} memoize_if_state
*/
add(expression, has_await) {
add(expression, metadata, memoize_if_state = false) {
for (const binding of metadata.dependencies) {
if (binding.blocker) {
this.#blockers.add(binding.blocker);
}
}
const should_memoize =
metadata.has_call || metadata.has_await || (memoize_if_state && metadata.has_state);
if (!should_memoize) {
// no memoization required
return expression;
}
const id = b.id('#'); // filled in later
(has_await ? this.#async : this.#sync).push({ id, expression });
(metadata.has_await ? this.#async : this.#sync).push({ id, expression });
return id;
}
@ -40,6 +59,10 @@ export class Memoizer {
});
}
blockers() {
return this.#blockers.size > 0 ? b.array([...this.#blockers]) : undefined;
}
deriveds(runes = true) {
return this.#sync.map((memo) =>
b.let(memo.id, b.call(runes ? '$.derived' : '$.derived_safe_equal', b.thunk(memo.expression)))
@ -72,8 +95,7 @@ export function build_template_chunk(
values,
context,
state = context.state,
memoize = (value, metadata) =>
metadata.has_call || metadata.has_await ? state.memoizer.add(value, metadata.has_await) : value
memoize = (value, metadata) => state.memoizer.add(value, metadata)
) {
/** @type {Expression[]} */
const expressions = [];
@ -176,7 +198,8 @@ export function build_render_statement(state) {
: b.block(state.update)
),
memoizer.sync_values(),
memoizer.async_values()
memoizer.async_values(),
memoizer.blockers()
)
);
}

@ -1,4 +1,4 @@
/** @import { Program, Property, Statement, VariableDeclarator } from 'estree' */
/** @import * as ESTree from 'estree' */
/** @import { AST, ValidatedCompileOptions, ValidatedModuleCompileOptions } from '#compiler' */
/** @import { ComponentServerTransformState, ComponentVisitors, ServerTransformState, Visitors } from './types.js' */
/** @import { Analysis, ComponentAnalysis } from '../../types.js' */
@ -25,6 +25,7 @@ import { IfBlock } from './visitors/IfBlock.js';
import { KeyBlock } from './visitors/KeyBlock.js';
import { LabeledStatement } from './visitors/LabeledStatement.js';
import { MemberExpression } from './visitors/MemberExpression.js';
import { Program } from './visitors/Program.js';
import { PropertyDefinition } from './visitors/PropertyDefinition.js';
import { RegularElement } from './visitors/RegularElement.js';
import { RenderTag } from './visitors/RenderTag.js';
@ -40,7 +41,7 @@ import { TitleElement } from './visitors/TitleElement.js';
import { UpdateExpression } from './visitors/UpdateExpression.js';
import { VariableDeclaration } from './visitors/VariableDeclaration.js';
import { SvelteBoundary } from './visitors/SvelteBoundary.js';
import { call_component_renderer, create_async_block } from './visitors/shared/utils.js';
import { call_component_renderer } from './visitors/shared/utils.js';
/** @type {Visitors} */
const global_visitors = {
@ -53,6 +54,7 @@ const global_visitors = {
Identifier,
LabeledStatement,
MemberExpression,
Program,
PropertyDefinition,
UpdateExpression,
VariableDeclaration
@ -86,7 +88,7 @@ const template_visitors = {
/**
* @param {ComponentAnalysis} analysis
* @param {ValidatedCompileOptions} options
* @returns {Program}
* @returns {ESTree.Program}
*/
export function server_component(analysis, options) {
/** @type {ComponentServerTransformState} */
@ -95,7 +97,7 @@ export function server_component(analysis, options) {
options,
scope: analysis.module.scope,
scopes: analysis.module.scopes,
hoisted: [b.import_all('$', 'svelte/internal/server')],
hoisted: [b.import_all('$', 'svelte/internal/server'), ...analysis.instance_body.hoisted],
legacy_reactive_statements: new Map(),
// these are set inside the `Fragment` visitor, and cannot be used until then
init: /** @type {any} */ (null),
@ -103,17 +105,18 @@ export function server_component(analysis, options) {
namespace: options.namespace,
preserve_whitespace: options.preserveWhitespace,
state_fields: new Map(),
skip_hydration_boundaries: false
skip_hydration_boundaries: false,
is_instance: false
};
const module = /** @type {Program} */ (
const module = /** @type {ESTree.Program} */ (
walk(/** @type {AST.SvelteNode} */ (analysis.module.ast), state, global_visitors)
);
const instance = /** @type {Program} */ (
const instance = /** @type {ESTree.Program} */ (
walk(
/** @type {AST.SvelteNode} */ (analysis.instance.ast),
{ ...state, scopes: analysis.instance.scopes },
{ ...state, scopes: analysis.instance.scopes, is_instance: true },
{
...global_visitors,
ImportDeclaration(node) {
@ -131,7 +134,7 @@ export function server_component(analysis, options) {
)
);
const template = /** @type {Program} */ (
const template = /** @type {ESTree.Program} */ (
walk(
/** @type {AST.SvelteNode} */ (analysis.template.ast),
{ ...state, scopes: analysis.template.scopes },
@ -140,7 +143,7 @@ export function server_component(analysis, options) {
)
);
/** @type {VariableDeclarator[]} */
/** @type {ESTree.VariableDeclarator[]} */
const legacy_reactive_declarations = [];
for (const [node] of analysis.reactive_statements) {
@ -192,7 +195,7 @@ export function server_component(analysis, options) {
b.function_declaration(
b.id('$$render_inner'),
[b.id('$$renderer')],
b.block(/** @type {Statement[]} */ (rest))
b.block(/** @type {ESTree.Statement[]} */ (rest))
),
b.do_while(
b.unary('!', b.id('$$settled')),
@ -219,7 +222,7 @@ export function server_component(analysis, options) {
// Propagate values of bound props upwards if they're undefined in the parent and have a value.
// Don't do this as part of the props retrieval because people could eagerly mutate the prop in the instance script.
/** @type {Property[]} */
/** @type {ESTree.Property[]} */
const props = [];
for (const [name, binding] of analysis.instance.scope.declarations) {
@ -239,14 +242,10 @@ export function server_component(analysis, options) {
}
let component_block = b.block([
.../** @type {Statement[]} */ (instance.body),
.../** @type {Statement[]} */ (template.body)
.../** @type {ESTree.Statement[]} */ (instance.body),
.../** @type {ESTree.Statement[]} */ (template.body)
]);
if (analysis.instance.has_await) {
component_block = b.block([create_async_block(component_block)]);
}
// trick esrap into including comments
component_block.loc = instance.loc;
@ -395,7 +394,7 @@ export function server_component(analysis, options) {
/**
* @param {Analysis} analysis
* @param {ValidatedModuleCompileOptions} options
* @returns {Program}
* @returns {ESTree.Program}
*/
export function server_module(analysis, options) {
/** @type {ServerTransformState} */
@ -408,10 +407,11 @@ export function server_module(analysis, options) {
// to be present for `javascript_visitors_legacy` and so is included in module
// transform state as well as component transform state
legacy_reactive_statements: new Map(),
state_fields: new Map()
state_fields: new Map(),
is_instance: false
};
const module = /** @type {Program} */ (
const module = /** @type {ESTree.Program} */ (
walk(/** @type {AST.SvelteNode} */ (analysis.module.ast), state, global_visitors)
);

@ -25,8 +25,12 @@ export function AwaitBlock(node, context) {
)
);
if (node.metadata.expression.has_await) {
statement = create_async_block(b.block([statement]));
if (node.metadata.expression.is_async()) {
statement = create_async_block(
b.block([statement]),
node.metadata.expression.blockers(),
node.metadata.expression.has_await
);
}
context.state.template.push(statement, block_close);

@ -1,9 +1,9 @@
/** @import { CallExpression, Expression } from 'estree' */
/** @import { CallExpression, Expression, MemberExpression } from 'estree' */
/** @import { Context } from '../types.js' */
import { is_ignored } from '../../../../state.js';
import { dev, is_ignored } from '../../../../state.js';
import * as b from '#compiler/builders';
import { get_rune } from '../../../scope.js';
import { transform_inspect_rune } from '../../utils.js';
import { get_inspect_args } from '../../utils.js';
/**
* @param {CallExpression} node
@ -12,7 +12,14 @@ import { transform_inspect_rune } from '../../utils.js';
export function CallExpression(node, context) {
const rune = get_rune(node, context.state.scope);
if (rune === '$host') {
if (
rune === '$host' ||
rune === '$effect' ||
rune === '$effect.pre' ||
rune === '$inspect.trace'
) {
// we will only encounter `$effect` etc if they are top-level statements in the <script>
// following an `await`, otherwise they are removed by the ExpressionStatement visitor
return b.void0;
}
@ -38,6 +45,10 @@ export function CallExpression(node, context) {
return b.call('$.derived', rune === '$derived' ? b.thunk(fn) : fn);
}
if (rune === '$state.eager') {
return node.arguments[0];
}
if (rune === '$state.snapshot') {
return b.call(
'$.snapshot',
@ -47,7 +58,13 @@ export function CallExpression(node, context) {
}
if (rune === '$inspect' || rune === '$inspect().with') {
return transform_inspect_rune(node, context);
if (!dev) return b.empty;
const { args, inspector } = get_inspect_args(rune, node, context.visit);
return rune === '$inspect'
? b.call(inspector, b.literal('$inspect('), ...args, b.literal(')'))
: b.call(inspector, b.literal('init'), ...args);
}
context.next();

@ -34,7 +34,11 @@ export function EachBlock(node, context) {
const new_body = /** @type {BlockStatement} */ (context.visit(node.body)).body;
each.push(...(node.body.metadata.has_await ? [create_async_block(b.block(new_body))] : new_body));
if (node.body)
each.push(
// TODO get rid of fragment.has_await
...(node.body.metadata.has_await ? [create_async_block(b.block(new_body))] : new_body)
);
const for_loop = b.for(
b.declaration('let', [
@ -65,8 +69,15 @@ export function EachBlock(node, context) {
block.body.push(for_loop);
}
if (node.metadata.expression.has_await) {
state.template.push(create_async_block(block), block_close);
if (node.metadata.expression.is_async()) {
state.template.push(
create_async_block(
block,
node.metadata.expression.blockers(),
node.metadata.expression.has_await
),
block_close
);
} else {
state.template.push(...block.body, block_close);
}

@ -2,6 +2,7 @@
/** @import { AST } from '#compiler' */
/** @import { ComponentContext } from '../types.js' */
import * as b from '#compiler/builders';
import { create_push } from './shared/utils.js';
/**
* @param {AST.HtmlTag} node
@ -10,9 +11,6 @@ import * as b from '#compiler/builders';
export function HtmlTag(node, context) {
const expression = /** @type {Expression} */ (context.visit(node.expression));
const call = b.call('$.html', expression);
context.state.template.push(
node.metadata.expression.has_await
? b.stmt(b.call('$$renderer.push', b.thunk(call, true)))
: call
);
context.state.template.push(create_push(call, node.metadata.expression, true));
}

@ -23,12 +23,20 @@ export function IfBlock(node, context) {
/** @type {Statement} */
let statement = b.if(test, consequent, alternate);
if (
const is_async = node.metadata.expression.is_async();
const has_await =
node.metadata.expression.has_await ||
// TODO get rid of this stuff
node.consequent.metadata.has_await ||
node.alternate?.metadata.has_await
) {
statement = create_async_block(b.block([statement]));
node.alternate?.metadata.has_await;
if (is_async || has_await) {
statement = create_async_block(
b.block([statement]),
node.metadata.expression.blockers(),
!!has_await
);
}
context.state.template.push(statement, block_close);

@ -1,16 +1,22 @@
/** @import { BlockStatement } from 'estree' */
/** @import { AST } from '#compiler' */
/** @import { ComponentContext } from '../types.js' */
import { empty_comment } from './shared/utils.js';
import { block_close, block_open, empty_comment } from './shared/utils.js';
/**
* @param {AST.KeyBlock} node
* @param {ComponentContext} context
*/
export function KeyBlock(node, context) {
const is_async = node.metadata.expression.is_async();
if (is_async) context.state.template.push(block_open);
context.state.template.push(
empty_comment,
/** @type {BlockStatement} */ (context.visit(node.fragment)),
empty_comment
);
if (is_async) context.state.template.push(block_close);
}

@ -0,0 +1,25 @@
/** @import { Node, Program } from 'estree' */
/** @import { Context, ComponentServerTransformState } from '../types' */
import * as b from '#compiler/builders';
import { transform_body } from '../../shared/transform-async.js';
/**
* @param {Program} node
* @param {Context} context
*/
export function Program(node, context) {
if (context.state.is_instance) {
const state = /** @type {ComponentServerTransformState} */ (context.state);
return {
...node,
body: transform_body(
state.analysis.instance_body,
b.id('$$renderer.run'),
(node) => /** @type {Node} */ (context.visit(node))
)
};
}
context.next();
}

@ -12,7 +12,8 @@ import {
process_children,
build_template,
create_child_block,
PromiseOptimiser
PromiseOptimiser,
create_async_block
} from './shared/utils.js';
/**
@ -202,13 +203,19 @@ export function RegularElement(node, context) {
state.template.push(b.stmt(b.call('$.pop_element')));
}
if (optimiser.expressions.length > 0) {
context.state.template.push(
create_child_block(
b.block([optimiser.apply(), ...state.init, ...build_template(state.template)]),
true
)
if (optimiser.is_async()) {
let statement = create_child_block(
b.block([optimiser.apply(), ...state.init, ...build_template(state.template)]),
true
);
const blockers = optimiser.blockers();
if (blockers.elements.length > 0) {
statement = create_async_block(b.block([statement]), blockers, false, false);
}
context.state.template.push(statement);
} else {
context.state.init.push(...state.init);
context.state.template.push(...state.template);

@ -3,32 +3,48 @@
/** @import { ComponentContext } from '../types.js' */
import { unwrap_optional } from '../../../../utils/ast.js';
import * as b from '#compiler/builders';
import { empty_comment } from './shared/utils.js';
import { create_async_block, empty_comment, PromiseOptimiser } from './shared/utils.js';
/**
* @param {AST.RenderTag} node
* @param {ComponentContext} context
*/
export function RenderTag(node, context) {
const optimiser = new PromiseOptimiser();
const callee = unwrap_optional(node.expression).callee;
const raw_args = unwrap_optional(node.expression).arguments;
const snippet_function = /** @type {Expression} */ (context.visit(callee));
const snippet_function = optimiser.transform(
/** @type {Expression} */ (context.visit(callee)),
node.metadata.expression
);
const snippet_args = raw_args.map((arg) => {
return /** @type {Expression} */ (context.visit(arg));
const snippet_args = raw_args.map((arg, i) => {
return optimiser.transform(
/** @type {Expression} */ (context.visit(arg)),
node.metadata.arguments[i]
);
});
context.state.template.push(
b.stmt(
(node.expression.type === 'CallExpression' ? b.call : b.maybe_call)(
snippet_function,
b.id('$$renderer'),
...snippet_args
)
let statement = b.stmt(
(node.expression.type === 'CallExpression' ? b.call : b.maybe_call)(
snippet_function,
b.id('$$renderer'),
...snippet_args
)
);
if (optimiser.is_async()) {
statement = create_async_block(
b.block([optimiser.apply(), statement]),
optimiser.blockers(),
optimiser.has_await
);
}
context.state.template.push(statement);
if (!context.state.skip_hydration_boundaries) {
context.state.template.push(empty_comment);
}

@ -65,10 +65,13 @@ export function SlotElement(node, context) {
fallback
);
const statement =
optimiser.expressions.length > 0
? create_async_block(b.block([optimiser.apply(), b.stmt(slot)]))
: b.stmt(slot);
const statement = optimiser.is_async()
? create_async_block(
b.block([optimiser.apply(), b.stmt(slot)]),
optimiser.blockers(),
optimiser.has_await
)
: b.stmt(slot);
context.state.template.push(block_open, statement, block_close);
}

@ -49,7 +49,7 @@ export function SvelteBoundary(node, context) {
context.state.template.push(
b.if(
callee,
b.block([b.stmt(pending)]),
b.block(build_template([block_open_else, b.stmt(pending), block_close])),
b.block(build_template([block_open, statement, block_close]))
)
);

@ -6,7 +6,12 @@ import { dev, locator } from '../../../../state.js';
import * as b from '#compiler/builders';
import { determine_namespace_for_children } from '../../utils.js';
import { build_element_attributes } from './shared/element.js';
import { build_template, create_child_block, PromiseOptimiser } from './shared/utils.js';
import {
build_template,
create_async_block,
create_child_block,
PromiseOptimiser
} from './shared/utils.js';
/**
* @param {AST.SvelteElement} node
@ -39,11 +44,14 @@ export function SvelteElement(node, context) {
const optimiser = new PromiseOptimiser();
/** @type {Statement[]} */
let statements = [];
build_element_attributes(node, { ...context, state }, optimiser.transform);
if (dev) {
const location = /** @type {Location} */ (locator(node.start));
context.state.template.push(
statements.push(
b.stmt(
b.call(
'$.push_element',
@ -74,9 +82,21 @@ export function SvelteElement(node, context) {
statement = create_child_block(b.block([optimiser.apply(), statement]), true);
}
context.state.template.push(statement);
statements.push(statement);
if (dev) {
context.state.template.push(b.stmt(b.call('$.pop_element')));
statements.push(b.stmt(b.call('$.pop_element')));
}
if (node.metadata.expression.is_async()) {
statements = [
create_async_block(
b.block(statements),
node.metadata.expression.blockers(),
node.metadata.expression.has_await
)
];
}
context.state.template.push(...statements);
}

@ -2,6 +2,8 @@
/** @import { AST } from '#compiler' */
/** @import { ComponentContext } from '../types.js' */
import * as b from '#compiler/builders';
import { hash } from '../../../../../utils.js';
import { filename } from '../../../../state.js';
/**
* @param {AST.SvelteHead} node
@ -11,6 +13,13 @@ export function SvelteHead(node, context) {
const block = /** @type {BlockStatement} */ (context.visit(node.fragment));
context.state.template.push(
b.stmt(b.call('$.head', b.id('$$renderer'), b.arrow([b.id('$$renderer')], block)))
b.stmt(
b.call(
'$.head',
b.literal(hash(filename)),
b.id('$$renderer'),
b.arrow([b.id('$$renderer')], block)
)
)
);
}

@ -5,8 +5,7 @@ import {
empty_comment,
build_attribute_value,
create_async_block,
PromiseOptimiser,
build_template
PromiseOptimiser
} from './utils.js';
import * as b from '#compiler/builders';
import { is_element_node } from '../../../../nodes.js';
@ -323,8 +322,14 @@ export function build_inline_component(node, expression, context) {
);
}
if (optimiser.expressions.length > 0) {
statement = create_async_block(b.block([optimiser.apply(), statement]));
const is_async = optimiser.is_async();
if (is_async) {
statement = create_async_block(
b.block([optimiser.apply(), statement]),
optimiser.blockers(),
optimiser.has_await
);
}
if (dynamic && custom_css_props.length === 0) {
@ -334,6 +339,7 @@ export function build_inline_component(node, expression, context) {
context.state.template.push(statement);
if (
!is_async &&
!context.state.skip_hydration_boundaries &&
custom_css_props.length === 0 &&
optimiser.expressions.length === 0

@ -1,13 +1,9 @@
/** @import { ArrayExpression, Expression, Literal, ObjectExpression } from 'estree' */
/** @import { AST, ExpressionMetadata } from '#compiler' */
/** @import { AST } from '#compiler' */
/** @import { ComponentContext, ComponentServerTransformState } from '../../types.js' */
import { is_event_attribute, is_text_attribute } from '../../../../../utils/ast.js';
import { binding_properties } from '../../../../bindings.js';
import {
create_attribute,
create_expression_metadata,
is_custom_element_node
} from '../../../../nodes.js';
import { create_attribute, ExpressionMetadata, is_custom_element_node } from '../../../../nodes.js';
import { regex_starts_with_newline } from '../../../../patterns.js';
import * as b from '#compiler/builders';
import {
@ -160,7 +156,7 @@ export function build_element_attributes(node, context, transform) {
build_attribute_value(value_attribute.value, context, transform)
),
metadata: {
expression: create_expression_metadata()
expression: new ExpressionMetadata()
}
}
])
@ -174,7 +170,7 @@ export function build_element_attributes(node, context, transform) {
end: -1,
expression,
metadata: {
expression: create_expression_metadata()
expression: new ExpressionMetadata()
}
}
])

@ -1,5 +1,5 @@
/** @import { AssignmentOperator, Expression, Identifier, Node, Statement, BlockStatement } from 'estree' */
/** @import { AST, ExpressionMetadata } from '#compiler' */
/** @import { Expression, Identifier, Node, Statement, BlockStatement, ArrayExpression } from 'estree' */
/** @import { AST } from '#compiler' */
/** @import { ComponentContext, ServerTransformState } from '../../types.js' */
import { escape_html } from '../../../../../../escaping.js';
@ -12,7 +12,8 @@ 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 } from '../../../../../utils/ast.js';
import { has_await_expression } 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 */
export const block_open = b.literal(BLOCK_OPEN);
@ -76,12 +77,11 @@ export function process_children(nodes, { visit, state }) {
}
for (const node of nodes) {
if (node.type === 'ExpressionTag' && node.metadata.expression.has_await) {
if (node.type === 'ExpressionTag' && node.metadata.expression.is_async()) {
flush();
const visited = /** @type {Expression} */ (visit(node.expression));
state.template.push(
b.stmt(b.call('$$renderer.push', b.thunk(b.call('$.escape', visited), true)))
);
const expression = /** @type {Expression} */ (visit(node.expression));
state.template.push(create_push(b.call('$.escape', expression), node.metadata.expression));
} else if (node.type === 'Text' || node.type === 'Comment' || node.type === 'ExpressionTag') {
sequence.push(node);
} else {
@ -274,9 +274,50 @@ export function create_child_block(body, async) {
/**
* Creates a `$$renderer.async(...)` expression statement
* @param {BlockStatement | Expression} body
* @param {ArrayExpression} blockers
* @param {boolean} has_await
* @param {boolean} needs_hydration_markers
*/
export function create_async_block(
body,
blockers = b.array([]),
has_await = true,
needs_hydration_markers = true
) {
return b.stmt(
b.call(
needs_hydration_markers ? '$$renderer.async_block' : '$$renderer.async',
blockers,
b.arrow([b.id('$$renderer')], body, has_await)
)
);
}
/**
* @param {Expression} expression
* @param {ExpressionMetadata} metadata
* @param {boolean} needs_hydration_markers
* @returns {Expression | Statement}
*/
export function create_async_block(body) {
return b.stmt(b.call('$$renderer.async', b.arrow([b.id('$$renderer')], body, true)));
export function create_push(expression, metadata, needs_hydration_markers = false) {
if (metadata.is_async()) {
let statement = b.stmt(b.call('$$renderer.push', b.thunk(expression, metadata.has_await)));
const blockers = metadata.blockers();
if (blockers.elements.length > 0) {
statement = create_async_block(
b.block([statement]),
blockers,
false,
needs_hydration_markers
);
}
return statement;
}
return expression;
}
/**
@ -290,17 +331,36 @@ export function call_component_renderer(body, component_fn_id) {
);
}
/**
* A utility for optimising promises in templates. Without it code like
* `<Component foo={await fetch()} bar={await other()} />` would be transformed
* into two blocking promises, with it it's using `Promise.all` to await them.
* It also keeps track of blocking promises, i.e. those that need to be resolved before continuing.
*/
export class PromiseOptimiser {
/** @type {Expression[]} */
expressions = [];
has_await = false;
/** @type {Set<Expression>} */
#blockers = new Set();
/**
*
* @param {Expression} expression
* @param {ExpressionMetadata} metadata
*/
transform = (expression, metadata) => {
for (const binding of metadata.dependencies) {
if (binding.blocker) {
this.#blockers.add(binding.blocker);
}
}
if (metadata.has_await) {
this.has_await = true;
const length = this.expressions.push(expression);
return b.id(`$$${length - 1}`);
}
@ -309,13 +369,17 @@ export class PromiseOptimiser {
};
apply() {
if (this.expressions.length === 0) {
return b.empty;
}
if (this.expressions.length === 1) {
return b.const('$$0', this.expressions[0]);
}
const promises = b.array(
this.expressions.map((expression) => {
return expression.type === 'AwaitExpression' && !has_await(expression.argument)
return expression.type === 'AwaitExpression' && !has_await_expression(expression.argument)
? expression.argument
: b.call(b.thunk(expression, true));
})
@ -326,4 +390,12 @@ export class PromiseOptimiser {
b.await(b.call('Promise.all', promises))
);
}
blockers() {
return b.array([...this.#blockers]);
}
is_async() {
return this.expressions.length > 0 || this.#blockers.size > 0;
}
}

@ -0,0 +1,102 @@
/** @import * as ESTree from 'estree' */
/** @import { ComponentAnalysis } from '../../types' */
import * as b from '#compiler/builders';
/**
* Transforms the body of the instance script in such a way that await expressions are made non-blocking as much as possible.
*
* Example Transformation:
* ```js
* let x = 1;
* let data = await fetch('/api');
* let y = data.value;
* ```
* becomes:
* ```js
* let x = 1;
* var data, y;
* var $$promises = $.run([
* () => data = await fetch('/api'),
* () => y = data.value
* ]);
* ```
* where `$$promises` is an array of promises that are resolved in the order they are declared,
* and which expressions in the template can await on like `await $$promises[0]` which means they
* wouldn't have to wait for e.g. `$$promises[1]` to resolve.
*
* @param {ComponentAnalysis['instance_body']} instance_body
* @param {ESTree.Expression} runner
* @param {(node: ESTree.Node) => ESTree.Node} transform
* @returns {Array<ESTree.Statement | ESTree.VariableDeclaration>}
*/
export function transform_body(instance_body, runner, transform) {
// Any sync statements before the first await expression
const statements = instance_body.sync.map(
(node) => /** @type {ESTree.Statement | ESTree.VariableDeclaration} */ (transform(node))
);
// Declarations for the await expressions (they will asign to them; need to be hoisted to be available in whole instance scope)
if (instance_body.declarations.length > 0) {
statements.push(
b.declaration(
'var',
instance_body.declarations.map((id) => b.declarator(id))
)
);
}
// Thunks for the await expressions
if (instance_body.async.length > 0) {
const thunks = instance_body.async.map((s) => {
if (s.node.type === 'VariableDeclarator') {
const visited = /** @type {ESTree.VariableDeclaration} */ (
transform(b.var(s.node.id, s.node.init))
);
if (visited.declarations.length === 1) {
return b.thunk(
b.assignment('=', visited.declarations[0].id, visited.declarations[0].init ?? b.void0),
s.has_await
);
}
// if we have multiple declarations, it indicates destructuring
return b.thunk(
b.block([
b.var(visited.declarations[0].id, visited.declarations[0].init),
...visited.declarations
.slice(1)
.map((d) => b.stmt(b.assignment('=', d.id, d.init ?? b.void0)))
]),
s.has_await
);
}
if (s.node.type === 'ClassDeclaration') {
return b.thunk(
b.assignment(
'=',
s.node.id,
/** @type {ESTree.ClassExpression} */ ({ ...s.node, type: 'ClassExpression' })
),
s.has_await
);
}
if (s.node.type === 'ExpressionStatement') {
const expression = /** @type {ESTree.Expression} */ (transform(s.node.expression));
return expression.type === 'AwaitExpression'
? b.thunk(expression, true)
: b.thunk(b.unary('void', expression), s.has_await);
}
return b.thunk(b.block([/** @type {ESTree.Statement} */ (transform(s.node))]), s.has_await);
});
// TODO get the `$$promises` ID from scope
statements.push(b.var('$$promises', b.call(runner, b.array(thunks))));
}
return statements;
}

@ -8,5 +8,8 @@ export interface TransformState {
readonly scope: Scope;
readonly scopes: Map<AST.SvelteNode, Scope>;
/** `true` if we're transforming the contents of `<script>` */
readonly is_instance: boolean;
readonly state_fields: Map<string, StateField>;
}

@ -1,36 +1,17 @@
/** @import { Context } from 'zimmerframe' */
/** @import { TransformState } from './types.js' */
/** @import { AST, Binding, Namespace, ValidatedCompileOptions } from '#compiler' */
/** @import { Node, Expression, CallExpression } from 'estree' */
/** @import { Node, Expression, CallExpression, MemberExpression } from 'estree' */
import {
regex_ends_with_whitespaces,
regex_not_whitespace,
regex_starts_with_newline,
regex_starts_with_whitespaces
} from '../patterns.js';
import * as b from '#compiler/builders';
import * as e from '../../errors.js';
import { walk } from 'zimmerframe';
import { extract_identifiers } from '../../utils/ast.js';
import check_graph_for_cycles from '../2-analyze/utils/check_graph_for_cycles.js';
import is_reference from 'is-reference';
import { set_scope } from '../scope.js';
import { dev } from '../../state.js';
/**
* @param {Node} node
* @returns {boolean}
*/
export function is_hoisted_function(node) {
if (
node.type === 'ArrowFunctionExpression' ||
node.type === 'FunctionExpression' ||
node.type === 'FunctionDeclaration'
) {
return node.metadata?.hoisted === true;
}
return false;
}
/**
* Match Svelte 4 behaviour by sorting ConstTag nodes in topological order
@ -452,30 +433,19 @@ export function determine_namespace_for_children(node, namespace) {
}
/**
* @template {TransformState} T
* @param {'$inspect' | '$inspect().with'} rune
* @param {CallExpression} node
* @param {Context<any, T>} context
* @param {(node: AST.SvelteNode) => AST.SvelteNode} visit
*/
export function transform_inspect_rune(node, context) {
const { state, visit } = context;
const as_fn = state.options.generate === 'client';
if (!dev) return b.empty;
if (node.callee.type === 'MemberExpression') {
const raw_inspect_args = /** @type {CallExpression} */ (node.callee.object).arguments;
const inspect_args =
/** @type {Array<Expression>} */
(raw_inspect_args.map((arg) => visit(arg)));
const with_arg = /** @type {Expression} */ (visit(node.arguments[0]));
return b.call(
'$.inspect',
as_fn ? b.thunk(b.array(inspect_args)) : b.array(inspect_args),
with_arg
);
} else {
const arg = node.arguments.map((arg) => /** @type {Expression} */ (visit(arg)));
return b.call('$.inspect', as_fn ? b.thunk(b.array(arg)) : b.array(arg));
}
export function get_inspect_args(rune, node, visit) {
const call =
rune === '$inspect'
? node
: /** @type {CallExpression} */ (/** @type {MemberExpression} */ (node.callee).object);
return {
args: call.arguments.map((arg) => /** @type {Expression} */ (visit(arg))),
inspector:
rune === '$inspect' ? 'console.log' : /** @type {Expression} */ (visit(node.arguments[0]))
};
}

@ -1,5 +1,6 @@
/** @import { Expression, PrivateIdentifier } from 'estree' */
/** @import { AST, ExpressionMetadata } from '#compiler' */
/** @import { AST, Binding } from '#compiler' */
import * as b from '#compiler/builders';
/**
* All nodes that can appear elsewhere than the top level, have attributes and can contain children
@ -59,25 +60,61 @@ export function create_attribute(name, start, end, value) {
name,
value,
metadata: {
delegated: null,
delegated: false,
needs_clsx: false
}
};
}
export class ExpressionMetadata {
/** True if the expression references state directly, or _might_ (via member/call expressions) */
has_state = false;
/**
* @returns {ExpressionMetadata}
*/
export function create_expression_metadata() {
return {
dependencies: new Set(),
references: new Set(),
has_state: false,
has_call: false,
has_member_expression: false,
has_assignment: false,
has_await: false
};
/** True if the expression involves a call expression (often, it will need to be wrapped in a derived) */
has_call = false;
/** True if the expression contains `await` */
has_await = false;
/** True if the expression includes a member expression */
has_member_expression = false;
/** True if the expression includes an assignment or an update */
has_assignment = false;
/**
* All the bindings that are referenced eagerly (not inside functions) in this expression
* @type {Set<Binding>}
*/
dependencies = new Set();
/**
* True if the expression references state directly, or _might_ (via member/call expressions)
* @type {Set<Binding>}
*/
references = new Set();
/** @type {null | Set<Expression>} */
#blockers = null;
#get_blockers() {
if (!this.#blockers) {
this.#blockers = new Set();
for (const d of this.dependencies) {
if (d.blocker) this.#blockers.add(d.blocker);
}
}
return this.#blockers;
}
blockers() {
return b.array([...this.#get_blockers()]);
}
is_async() {
return this.has_await || this.#get_blockers().size > 0;
}
}
/**

@ -3,7 +3,7 @@
/** @import { AST, BindingKind, DeclarationKind } from '#compiler' */
import is_reference from 'is-reference';
import { walk } from 'zimmerframe';
import { create_expression_metadata } from './nodes.js';
import { ExpressionMetadata } from './nodes.js';
import * as b from '#compiler/builders';
import * as e from '../errors.js';
import {
@ -108,6 +108,9 @@ export class Binding {
/** @type {Array<{ node: Identifier; path: AST.SvelteNode[] }>} */
references = [];
/** @type {Array<{ value: Expression; scope: Scope }>} */
assignments = [];
/**
* For `legacy_reactive`: its reactive dependencies
* @type {Binding[]}
@ -129,6 +132,15 @@ export class Binding {
mutated = false;
reassigned = false;
/**
* Instance-level declarations may follow (or contain) a top-level `await`. In these cases,
* any reads that occur in the template must wait for the corresponding promise to resolve
* otherwise the initial value will not have been assigned
* TODO the blocker is set during transform which feels a bit grubby
* @type {Expression | null}
*/
blocker = null;
/**
*
* @param {Scope} scope
@ -143,6 +155,10 @@ export class Binding {
this.initial = initial;
this.kind = kind;
this.declaration_kind = declaration_kind;
if (initial) {
this.assignments.push({ value: /** @type {Expression} */ (initial), scope });
}
}
get updated() {
@ -859,7 +875,7 @@ export function create_scopes(ast, root, allow_reactive_declarations, parent) {
/** @type {[Scope, { node: Identifier; path: AST.SvelteNode[] }][]} */
const references = [];
/** @type {[Scope, Pattern | MemberExpression][]} */
/** @type {[Scope, Pattern | MemberExpression, Expression][]} */
const updates = [];
/**
@ -1047,12 +1063,13 @@ export function create_scopes(ast, root, allow_reactive_declarations, parent) {
// updates
AssignmentExpression(node, { state, next }) {
updates.push([state.scope, node.left]);
updates.push([state.scope, node.left, node.right]);
next();
},
UpdateExpression(node, { state, next }) {
updates.push([state.scope, /** @type {Identifier | MemberExpression} */ (node.argument)]);
const expression = /** @type {Identifier | MemberExpression} */ (node.argument);
updates.push([state.scope, expression, expression]);
next();
},
@ -1201,7 +1218,7 @@ export function create_scopes(ast, root, allow_reactive_declarations, parent) {
if (node.fallback) visit(node.fallback, { scope });
node.metadata = {
expression: create_expression_metadata(),
expression: new ExpressionMetadata(),
keyed: false,
contains_group_binding: false,
index: scope.root.unique('$$index'),
@ -1273,10 +1290,11 @@ export function create_scopes(ast, root, allow_reactive_declarations, parent) {
},
BindDirective(node, context) {
updates.push([
context.state.scope,
/** @type {Identifier | MemberExpression} */ (node.expression)
]);
if (node.expression.type !== 'SequenceExpression') {
const expression = /** @type {Identifier | MemberExpression} */ (node.expression);
updates.push([context.state.scope, expression, expression]);
}
context.next();
},
@ -1311,7 +1329,7 @@ export function create_scopes(ast, root, allow_reactive_declarations, parent) {
scope.reference(node, path);
}
for (const [scope, node] of updates) {
for (const [scope, node, value] of updates) {
for (const expression of unwrap_pattern(node)) {
const left = object(expression);
const binding = left && scope.get(left.name);
@ -1319,6 +1337,7 @@ export function create_scopes(ast, root, allow_reactive_declarations, parent) {
if (binding !== null && left !== binding.node) {
if (left === expression) {
binding.reassigned = true;
binding.assignments.push({ value, scope });
} else {
binding.mutated = true;
}

@ -3,11 +3,19 @@ import type {
AwaitExpression,
CallExpression,
ClassBody,
ClassDeclaration,
FunctionDeclaration,
Identifier,
LabeledStatement,
Program
ModuleDeclaration,
Pattern,
Program,
Statement,
VariableDeclaration,
VariableDeclarator
} from 'estree';
import type { Scope, ScopeRoot } from './scope.js';
import type { ExpressionMetadata } from './nodes.js';
export interface Js {
ast: Program;
@ -27,6 +35,14 @@ export interface ReactiveStatement {
dependencies: Binding[];
}
export interface AwaitedDeclaration {
id: Identifier;
has_await: boolean;
pattern: Pattern;
metadata: ExpressionMetadata;
updated_by: Set<Identifier>;
}
/**
* Analysis common to modules and components
*/
@ -108,30 +124,13 @@ export interface ComponentAnalysis extends Analysis {
* Every snippet that is declared locally
*/
snippets: Set<AST.SnippetBlock>;
}
declare module 'estree' {
interface ArrowFunctionExpression {
metadata: {
hoisted: boolean;
hoisted_params: Pattern[];
scope: Scope;
};
}
interface FunctionExpression {
metadata: {
hoisted: boolean;
hoisted_params: Pattern[];
scope: Scope;
};
}
interface FunctionDeclaration {
metadata: {
hoisted: boolean;
hoisted_params: Pattern[];
scope: Scope;
};
}
/**
* Pre-transformed `<script>` block
*/
instance_body: {
hoisted: Array<Statement | ModuleDeclaration>;
sync: Array<Statement | ModuleDeclaration | VariableDeclaration>;
async: Array<{ node: Statement | VariableDeclarator; has_await: boolean }>;
declarations: Array<Identifier>;
};
}

@ -294,23 +294,6 @@ export type DeclarationKind =
| 'using'
| 'await using';
export interface ExpressionMetadata {
/** All the bindings that are referenced eagerly (not inside functions) in this expression */
dependencies: Set<Binding>;
/** All the bindings that are referenced inside this expression, including inside functions */
references: Set<Binding>;
/** True if the expression references state directly, or _might_ (via member/call expressions) */
has_state: boolean;
/** True if the expression involves a call expression (often, it will need to be wrapped in a derived) */
has_call: boolean;
/** True if the expression contains `await` */
has_await: boolean;
/** True if the expression includes a member expression */
has_member_expression: boolean;
/** True if the expression includes an assignment or an update */
has_assignment: boolean;
}
export interface StateField {
type: StateCreationRuneName;
node: PropertyDefinition | AssignmentExpression;

@ -1,12 +1,10 @@
import type { Binding, ExpressionMetadata } from '#compiler';
import type { Binding } from '#compiler';
import type {
ArrayExpression,
ArrowFunctionExpression,
VariableDeclaration,
VariableDeclarator,
Expression,
FunctionDeclaration,
FunctionExpression,
Identifier,
MemberExpression,
Node,
@ -19,6 +17,7 @@ import type {
} from 'estree';
import type { Scope } from '../phases/scope';
import type { _CSS } from './css';
import type { ExpressionMetadata } from '../phases/nodes';
/**
* - `html` the default, for e.g. `<div>` or `<span>`
@ -27,13 +26,6 @@ import type { _CSS } from './css';
*/
export type Namespace = 'html' | 'svg' | 'mathml';
export type DelegatedEvent =
| {
hoisted: true;
function: ArrowFunctionExpression | FunctionExpression | FunctionDeclaration;
}
| { hoisted: false };
export namespace AST {
export interface BaseNode {
type: string;
@ -56,6 +48,7 @@ export namespace AST {
* Whether or not we need to traverse into the fragment during mount/hydrate
*/
dynamic: boolean;
/** @deprecated we should get rid of this in favour of the `$$renderer.run` mechanism */
has_await: boolean;
};
}
@ -214,6 +207,7 @@ export namespace AST {
expression: Identifier | MemberExpression | SequenceExpression;
/** @internal */
metadata: {
binding?: Binding | null;
binding_group_name: Identifier;
parent_each_blocks: EachBlock[];
};
@ -531,7 +525,7 @@ export namespace AST {
/** @internal */
metadata: {
/** May be set if this is an event attribute */
delegated: null | DelegatedEvent;
delegated: boolean;
/** May be `true` if this is a `class` attribute that needs `clsx` */
needs_clsx: boolean;
};

@ -1,4 +1,4 @@
/** @import { AST } from '#compiler' */
/** @import { AST, Scope } from '#compiler' */
/** @import * as ESTree from 'estree' */
import { walk } from 'zimmerframe';
import * as b from '#compiler/builders';
@ -611,16 +611,20 @@ export function build_assignment_value(operator, left, right) {
}
/**
* @param {ESTree.Expression} expression
* @param {ESTree.Node} node
*/
export function has_await(expression) {
export function has_await_expression(node) {
let has_await = false;
walk(expression, null, {
walk(node, null, {
AwaitExpression(_node, context) {
has_await = true;
context.stop();
}
},
// don't traverse into these
FunctionDeclaration() {},
FunctionExpression() {},
ArrowFunctionExpression() {}
});
return has_await;

@ -2,7 +2,7 @@
import { walk } from 'zimmerframe';
import { regex_is_valid_identifier } from '../phases/patterns.js';
import { sanitize_template_string } from './sanitize_template_string.js';
import { has_await } from './ast.js';
import { has_await_expression } from './ast.js';
/**
* @param {Array<ESTree.Expression | ESTree.SpreadElement | null>} elements
@ -42,8 +42,7 @@ export function arrow(params, body, async = false) {
body,
expression: body.type !== 'BlockStatement',
generator: false,
async,
metadata: /** @type {any} */ (null) // should not be used by codegen
async
};
}
@ -83,6 +82,17 @@ export function block(body) {
return { type: 'BlockStatement', body };
}
/**
* @param {ESTree.Identifier | null} id
* @param {ESTree.ClassBody} body
* @param {ESTree.Expression | null} [superClass]
* @param {ESTree.Decorator[]} [decorators]
* @returns {ESTree.ClassExpression}
*/
export function class_expression(id, body, superClass, decorators = []) {
return { type: 'ClassExpression', body, superClass, decorators };
}
/**
* @param {string} name
* @param {ESTree.Statement} body
@ -185,7 +195,7 @@ export function declaration(kind, declarations) {
/**
* @param {ESTree.Pattern | string} pattern
* @param {ESTree.Expression} [init]
* @param {ESTree.Expression | null} [init]
* @returns {ESTree.VariableDeclarator}
*/
export function declarator(pattern, init) {
@ -237,8 +247,7 @@ export function function_declaration(id, params, body, async = false) {
params,
body,
generator: false,
async,
metadata: /** @type {any} */ (null) // should not be used by codegen
async
};
}
@ -451,7 +460,7 @@ export function thunk(expression, async = false) {
export function unthunk(expression) {
// optimize `async () => await x()`, but not `async () => await x(await y)`
if (expression.async && expression.body.type === 'AwaitExpression') {
if (!has_await(expression.body.argument)) {
if (!has_await_expression(expression.body.argument)) {
return unthunk(arrow(expression.params, expression.body.argument));
}
}
@ -522,7 +531,7 @@ const this_instance = {
/**
* @param {string | ESTree.Pattern} pattern
* @param { ESTree.Expression} [init]
* @param {ESTree.Expression | null} [init]
* @returns {ESTree.VariableDeclaration}
*/
function let_builder(pattern, init) {
@ -531,7 +540,7 @@ function let_builder(pattern, init) {
/**
* @param {string | ESTree.Pattern} pattern
* @param { ESTree.Expression} init
* @param {ESTree.Expression | null} init
* @returns {ESTree.VariableDeclaration}
*/
function const_builder(pattern, init) {
@ -540,7 +549,7 @@ function const_builder(pattern, init) {
/**
* @param {string | ESTree.Pattern} pattern
* @param { ESTree.Expression} [init]
* @param {ESTree.Expression | null} [init]
* @returns {ESTree.VariableDeclaration}
*/
function var_builder(pattern, init) {
@ -595,8 +604,7 @@ function function_builder(id, params, body, async = false) {
params,
body,
generator: false,
async,
metadata: /** @type {any} */ (null) // should not be used by codegen
async
};
}

@ -241,7 +241,7 @@ function init_update_callbacks(context) {
return (l.u ??= { a: [], b: [], m: [] });
}
export { flushSync } from './internal/client/reactivity/batch.js';
export { flushSync, fork } from './internal/client/reactivity/batch.js';
export {
createContext,
getContext,

@ -33,6 +33,10 @@ export function unmount() {
e.lifecycle_function_unavailable('unmount');
}
export function fork() {
e.lifecycle_function_unavailable('fork');
}
export async function tick() {}
export async function settled() {}

@ -352,4 +352,20 @@ export type MountOptions<Props extends Record<string, any> = Record<string, any>
props: Props;
});
/**
* Represents work that is happening off-screen, such as data being preloaded
* in anticipation of the user navigating
* @since 5.42
*/
export interface Fork {
/**
* Commit the fork. The promise will resolve once the state change has been applied
*/
commit(): Promise<void>;
/**
* Discard the fork
*/
discard(): void;
}
export * from './index-client.js';

@ -1,3 +1,4 @@
// General flags
export const DERIVED = 1 << 1;
export const EFFECT = 1 << 2;
export const RENDER_EFFECT = 1 << 3;
@ -5,21 +6,35 @@ export const BLOCK_EFFECT = 1 << 4;
export const BRANCH_EFFECT = 1 << 5;
export const ROOT_EFFECT = 1 << 6;
export const BOUNDARY_EFFECT = 1 << 7;
export const UNOWNED = 1 << 8;
export const DISCONNECTED = 1 << 9;
export const CLEAN = 1 << 10;
export const DIRTY = 1 << 11;
export const MAYBE_DIRTY = 1 << 12;
export const INERT = 1 << 13;
export const DESTROYED = 1 << 14;
// Flags exclusive to effects
/** Set once an effect that should run synchronously has run */
export const EFFECT_RAN = 1 << 15;
/** 'Transparent' effects do not create a transition boundary */
/**
* 'Transparent' effects do not create a transition boundary.
* This is on a block effect 99% of the time but may also be on a branch effect if its parent block effect was pruned
*/
export const EFFECT_TRANSPARENT = 1 << 16;
export const INSPECT_EFFECT = 1 << 17;
export const EAGER_EFFECT = 1 << 17;
export const HEAD_EFFECT = 1 << 18;
export const EFFECT_PRESERVED = 1 << 19;
export const USER_EFFECT = 1 << 20;
// Flags exclusive to deriveds
export const UNOWNED = 1 << 8;
export const DISCONNECTED = 1 << 9;
/**
* Tells that we marked this derived and its reactions as visited during the "mark as (maybe) dirty"-phase.
* Will be lifted during execution of the derived and during checking its dirty state (both are necessary
* because a derived might be checked but not executed).
*/
export const WAS_MARKED = 1 << 15;
// Flags used for async
export const REACTION_IS_UPDATING = 1 << 21;
export const ASYNC = 1 << 22;

@ -130,7 +130,11 @@ export function setContext(key, context) {
if (async_mode_flag) {
var flags = /** @type {Effect} */ (active_effect).f;
var valid = !active_reaction && (flags & BRANCH_EFFECT) !== 0 && (flags & EFFECT_RAN) === 0;
var valid =
!active_reaction &&
(flags & BRANCH_EFFECT) !== 0 &&
// pop() runs synchronously, so this indicates we're setting context after an await
!(/** @type {ComponentContext} */ (component_context).i);
if (!valid) {
e.set_context_after_init();
@ -175,6 +179,7 @@ export function getAllContexts() {
export function push(props, runes = false, fn) {
component_context = {
p: component_context,
i: false,
c: null,
e: null,
s: props,
@ -210,6 +215,8 @@ export function pop(component) {
context.x = component;
}
context.i = true;
component_context = context.p;
if (DEV) {

@ -1,14 +1,15 @@
import { UNINITIALIZED } from '../../../constants.js';
import { snapshot } from '../../shared/clone.js';
import { inspect_effect, render_effect, validate_effect } from '../reactivity/effects.js';
import { eager_effect, render_effect, validate_effect } from '../reactivity/effects.js';
import { untrack } from '../runtime.js';
import { get_stack } from './tracing.js';
/**
* @param {() => any[]} get_value
* @param {Function} [inspector]
* @param {Function} inspector
* @param {boolean} show_stack
*/
// eslint-disable-next-line no-console
export function inspect(get_value, inspector = console.log) {
export function inspect(get_value, inspector, show_stack = false) {
validate_effect('$inspect');
let initial = true;
@ -18,7 +19,7 @@ export function inspect(get_value, inspector = console.log) {
// stack traces. As a consequence, reading the value might result
// in an error (an `$inspect(object.property)` will run before the
// `{#if object}...{/if}` that contains it)
inspect_effect(() => {
eager_effect(() => {
try {
var value = get_value();
} catch (e) {
@ -28,7 +29,25 @@ export function inspect(get_value, inspector = console.log) {
var snap = snapshot(value, true, true);
untrack(() => {
inspector(initial ? 'init' : 'update', ...snap);
if (show_stack) {
inspector(...snap);
if (!initial) {
const stack = get_stack('$inspect(...)');
// eslint-disable-next-line no-console
if (stack) {
// eslint-disable-next-line no-console
console.groupCollapsed('stack trace');
// eslint-disable-next-line no-console
console.log(stack);
// eslint-disable-next-line no-console
console.groupEnd();
}
}
} else {
inspector(initial ? 'init' : 'update', ...snap);
}
});
initial = false;

@ -134,7 +134,16 @@ export function trace(label, fn) {
* @returns {Error & { stack: string } | null}
*/
export function get_stack(label) {
// @ts-ignore stackTraceLimit doesn't exist everywhere
const limit = Error.stackTraceLimit;
// @ts-ignore
Error.stackTraceLimit = Infinity;
let error = Error();
// @ts-ignore
Error.stackTraceLimit = limit;
const stack = error.stack;
if (!stack) return null;
@ -144,16 +153,20 @@ export function get_stack(label) {
for (let i = 0; i < lines.length; i++) {
const line = lines[i];
const posixified = line.replaceAll('\\', '/');
if (line === 'Error') {
continue;
}
if (line.includes('validate_each_keys')) {
return null;
}
if (line.includes('svelte/src/internal')) {
if (posixified.includes('svelte/src/internal') || posixified.includes('node_modules/.vite')) {
continue;
}
new_lines.push(line);
}
@ -166,8 +179,7 @@ export function get_stack(label) {
});
define_property(error, 'name', {
// 'Error' suffix is required for stack traces to be rendered properly
value: `${label}Error`
value: label
});
return /** @type {Error & { stack: string }} */ (error);

@ -1,5 +1,6 @@
/** @import { TemplateNode, Value } from '#client' */
import { flatten } from '../../reactivity/async.js';
import { Batch, current_batch } from '../../reactivity/batch.js';
import { get } from '../../runtime.js';
import {
hydrate_next,
@ -13,13 +14,17 @@ import { get_boundary } from './boundary.js';
/**
* @param {TemplateNode} node
* @param {Array<Promise<void>>} blockers
* @param {Array<() => Promise<any>>} expressions
* @param {(anchor: TemplateNode, ...deriveds: Value[]) => void} fn
*/
export function async(node, expressions, fn) {
export function async(node, blockers = [], expressions = [], fn) {
var boundary = get_boundary();
var batch = /** @type {Batch} */ (current_batch);
var blocking = !boundary.is_pending();
boundary.update_pending_count(1);
batch.increment(blocking);
var was_hydrating = hydrating;
@ -31,7 +36,7 @@ export function async(node, expressions, fn) {
set_hydrate_node(end);
}
flatten([], expressions, (values) => {
flatten(blockers, [], expressions, (values) => {
if (was_hydrating) {
set_hydrating(true);
set_hydrate_node(previous_hydrate_node);
@ -43,11 +48,12 @@ export function async(node, expressions, fn) {
fn(node, ...values);
} finally {
boundary.update_pending_count(-1);
}
if (was_hydrating) {
set_hydrating(false);
}
if (was_hydrating) {
set_hydrating(false);
boundary.update_pending_count(-1);
batch.decrement(blocking);
}
});
}

@ -1,12 +1,9 @@
/** @import { Effect, Source, TemplateNode } from '#client' */
import { DEV } from 'esm-env';
/** @import { Source, TemplateNode } from '#client' */
import { is_promise } from '../../../shared/utils.js';
import { block, branch, pause_effect, resume_effect } from '../../reactivity/effects.js';
import { block } from '../../reactivity/effects.js';
import { internal_set, mutable_source, source } from '../../reactivity/sources.js';
import { set_active_effect, set_active_reaction } from '../../runtime.js';
import {
hydrate_next,
hydrate_node,
hydrating,
skip_nodes,
set_hydrate_node,
@ -14,15 +11,10 @@ import {
} from '../hydration.js';
import { queue_micro_task } from '../task.js';
import { HYDRATION_START_ELSE, UNINITIALIZED } from '../../../../constants.js';
import {
component_context,
dev_stack,
is_runes,
set_component_context,
set_dev_current_component_function,
set_dev_stack
} from '../../context.js';
import { flushSync, is_flushing_sync } from '../../reactivity/batch.js';
import { is_runes } from '../../context.js';
import { Batch, flushSync, is_flushing_sync } from '../../reactivity/batch.js';
import { BranchManager } from './branches.js';
import { capture, unset_context } from '../../reactivity/async.js';
const PENDING = 0;
const THEN = 1;
@ -33,7 +25,7 @@ const CATCH = 2;
/**
* @template V
* @param {TemplateNode} node
* @param {(() => Promise<V>)} get_input
* @param {(() => any)} get_input
* @param {null | ((anchor: Node) => void)} pending_fn
* @param {null | ((anchor: Node, value: Source<V>) => void)} then_fn
* @param {null | ((anchor: Node, error: unknown) => void)} catch_fn
@ -44,149 +36,98 @@ export function await_block(node, get_input, pending_fn, then_fn, catch_fn) {
hydrate_next();
}
var anchor = node;
var runes = is_runes();
var active_component_context = component_context;
/** @type {any} */
var component_function = DEV ? component_context?.function : null;
var dev_original_stack = DEV ? dev_stack : null;
/** @type {V | Promise<V> | typeof UNINITIALIZED} */
var input = UNINITIALIZED;
/** @type {Effect | null} */
var pending_effect;
/** @type {Effect | null} */
var then_effect;
/** @type {Effect | null} */
var catch_effect;
var input_source = runes
? source(/** @type {V} */ (undefined))
: mutable_source(/** @type {V} */ (undefined), false, false);
var error_source = runes ? source(undefined) : mutable_source(undefined, false, false);
var resolved = false;
/**
* @param {AwaitState} state
* @param {boolean} restore
*/
function update(state, restore) {
resolved = true;
if (restore) {
set_active_effect(effect);
set_active_reaction(effect); // TODO do we need both?
set_component_context(active_component_context);
if (DEV) {
set_dev_current_component_function(component_function);
set_dev_stack(dev_original_stack);
}
}
try {
if (state === PENDING && pending_fn) {
if (pending_effect) resume_effect(pending_effect);
else pending_effect = branch(() => pending_fn(anchor));
}
if (state === THEN && then_fn) {
if (then_effect) resume_effect(then_effect);
else then_effect = branch(() => then_fn(anchor, input_source));
}
if (state === CATCH && catch_fn) {
if (catch_effect) resume_effect(catch_effect);
else catch_effect = branch(() => catch_fn(anchor, error_source));
}
if (state !== PENDING && pending_effect) {
pause_effect(pending_effect, () => (pending_effect = null));
}
if (state !== THEN && then_effect) {
pause_effect(then_effect, () => (then_effect = null));
}
if (state !== CATCH && catch_effect) {
pause_effect(catch_effect, () => (catch_effect = null));
}
} finally {
if (restore) {
if (DEV) {
set_dev_current_component_function(null);
set_dev_stack(null);
}
set_component_context(null);
set_active_reaction(null);
set_active_effect(null);
var v = /** @type {V} */ (UNINITIALIZED);
var value = runes ? source(v) : mutable_source(v, false, false);
var error = runes ? source(v) : mutable_source(v, false, false);
// without this, the DOM does not update until two ticks after the promise
// resolves, which is unexpected behaviour (and somewhat irksome to test)
if (!is_flushing_sync) flushSync();
}
}
}
var branches = new BranchManager(node);
var effect = block(() => {
if (input === (input = get_input())) return;
block(() => {
var input = get_input();
var destroyed = false;
/** Whether or not there was a hydration mismatch. Needs to be a `let` or else it isn't treeshaken out */
// @ts-ignore coercing `anchor` to a `Comment` causes TypeScript and Prettier to fight
let mismatch = hydrating && is_promise(input) === (anchor.data === HYDRATION_START_ELSE);
// @ts-ignore coercing `node` to a `Comment` causes TypeScript and Prettier to fight
let mismatch = hydrating && is_promise(input) === (node.data === HYDRATION_START_ELSE);
if (mismatch) {
// Hydration mismatch: remove everything inside the anchor and start fresh
anchor = skip_nodes();
set_hydrate_node(anchor);
set_hydrate_node(skip_nodes());
set_hydrating(false);
mismatch = true;
}
if (is_promise(input)) {
var promise = input;
var restore = capture();
var resolved = false;
/**
* @param {() => void} fn
*/
const resolve = (fn) => {
if (destroyed) return;
resolved = true;
// We don't want to restore the previous batch here; {#await} blocks don't follow the async logic
// we have elsewhere, instead pending/resolve/fail states are each their own batch so to speak.
restore(false);
// Make sure we have a batch, since the branch manager expects one to exist
Batch.ensure();
if (hydrating) {
// `restore()` could set `hydrating` to `true`, which we very much
// don't want — we want to restore everything _except_ this
set_hydrating(false);
}
resolved = false;
try {
fn();
} finally {
unset_context();
promise.then(
(value) => {
if (promise !== input) return;
// we technically could use `set` here since it's on the next microtick
// but let's use internal_set for consistency and just to be safe
internal_set(input_source, value);
update(THEN, true);
// without this, the DOM does not update until two ticks after the promise
// resolves, which is unexpected behaviour (and somewhat irksome to test)
if (!is_flushing_sync) flushSync();
}
};
input.then(
(v) => {
resolve(() => {
internal_set(value, v);
branches.ensure(THEN, then_fn && ((target) => then_fn(target, value)));
});
},
(error) => {
if (promise !== input) return;
// we technically could use `set` here since it's on the next microtick
// but let's use internal_set for consistency and just to be safe
internal_set(error_source, error);
update(CATCH, true);
if (!catch_fn) {
// Rethrow the error if no catch block exists
throw error_source.v;
}
(e) => {
resolve(() => {
internal_set(error, e);
branches.ensure(THEN, catch_fn && ((target) => catch_fn(target, error)));
if (!catch_fn) {
// Rethrow the error if no catch block exists
throw error.v;
}
});
}
);
if (hydrating) {
if (pending_fn) {
pending_effect = branch(() => pending_fn(anchor));
}
branches.ensure(PENDING, pending_fn);
} else {
// Wait a microtask before checking if we should show the pending state as
// the promise might have resolved by the next microtask.
// the promise might have resolved by then
queue_micro_task(() => {
if (!resolved) update(PENDING, true);
if (!resolved) {
resolve(() => {
branches.ensure(PENDING, pending_fn);
});
}
});
}
} else {
internal_set(input_source, input);
update(THEN, false);
internal_set(value, input);
branches.ensure(THEN, then_fn && ((target) => then_fn(target, value)));
}
if (mismatch) {
@ -194,11 +135,8 @@ export function await_block(node, get_input, pending_fn, then_fn, catch_fn) {
set_hydrating(true);
}
// Set the input to something else, in order to disable the promise callbacks
return () => (input = UNINITIALIZED);
return () => {
destroyed = true;
};
});
if (hydrating) {
anchor = hydrate_node;
}
}

@ -8,7 +8,13 @@ import {
import { HYDRATION_START_ELSE } from '../../../../constants.js';
import { component_context, set_component_context } from '../../context.js';
import { handle_error, invoke_error_boundary } from '../../error-handling.js';
import { block, branch, destroy_effect, pause_effect } from '../../reactivity/effects.js';
import {
block,
branch,
destroy_effect,
move_effect,
pause_effect
} from '../../reactivity/effects.js';
import {
active_effect,
active_reaction,
@ -24,15 +30,15 @@ import {
skip_nodes,
set_hydrate_node
} from '../hydration.js';
import { get_next_sibling } from '../operations.js';
import { queue_micro_task } from '../task.js';
import * as e from '../../errors.js';
import * as w from '../../warnings.js';
import { DEV } from 'esm-env';
import { Batch, effect_pending_updates } from '../../reactivity/batch.js';
import { 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';
import { create_text } from '../operations.js';
/**
* @typedef {{
@ -87,6 +93,9 @@ export class Boundary {
/** @type {DocumentFragment | null} */
#offscreen_fragment = null;
/** @type {TemplateNode | null} */
#pending_anchor = null;
#local_pending_count = 0;
#pending_count = 0;
@ -101,12 +110,6 @@ export class Boundary {
*/
#effect_pending = null;
#effect_pending_update = () => {
if (this.#effect_pending) {
internal_set(this.#effect_pending, this.#local_pending_count);
}
};
#effect_pending_subscriber = createSubscriber(() => {
this.#effect_pending = source(this.#local_pending_count);
@ -150,8 +153,10 @@ export class Boundary {
this.#hydrate_resolved_content();
}
} else {
var anchor = this.#get_anchor();
try {
this.#main_effect = branch(() => children(this.#anchor));
this.#main_effect = branch(() => children(anchor));
} catch (error) {
this.error(error);
}
@ -162,6 +167,10 @@ export class Boundary {
this.#pending = false;
}
}
return () => {
this.#pending_anchor?.remove();
};
}, flags);
if (hydrating) {
@ -189,9 +198,11 @@ export class Boundary {
this.#pending_effect = branch(() => pending(this.#anchor));
Batch.enqueue(() => {
var anchor = this.#get_anchor();
this.#main_effect = this.#run(() => {
Batch.ensure();
return branch(() => this.#children(this.#anchor));
return branch(() => this.#children(anchor));
});
if (this.#pending_count > 0) {
@ -206,6 +217,19 @@ export class Boundary {
});
}
#get_anchor() {
var anchor = this.#anchor;
if (this.#pending) {
this.#pending_anchor = create_text();
this.#anchor.before(this.#pending_anchor);
anchor = this.#pending_anchor;
}
return anchor;
}
/**
* Returns `true` if the effect exists inside a boundary whose pending snippet is shown
* @returns {boolean}
@ -247,6 +271,7 @@ export class Boundary {
if (this.#main_effect !== null) {
this.#offscreen_fragment = document.createDocumentFragment();
this.#offscreen_fragment.append(/** @type {TemplateNode} */ (this.#pending_anchor));
move_effect(this.#main_effect, this.#offscreen_fragment);
}
@ -285,13 +310,6 @@ export class Boundary {
this.#anchor.before(this.#offscreen_fragment);
this.#offscreen_fragment = null;
}
// TODO this feels like a little bit of a kludge, but until we
// overhaul the boundary/batch relationship it's probably
// the most pragmatic solution available to us
queue_micro_task(() => {
Batch.ensure().flush();
});
}
}
@ -305,7 +323,10 @@ export class Boundary {
this.#update_pending_count(d);
this.#local_pending_count += d;
effect_pending_updates.add(this.#effect_pending_update);
if (this.#effect_pending) {
internal_set(this.#effect_pending, this.#local_pending_count);
}
}
get_effect_pending() {
@ -403,6 +424,7 @@ export class Boundary {
if (failed) {
queue_micro_task(() => {
this.#failed_effect = this.#run(() => {
Batch.ensure();
this.#is_creating_fallback = true;
try {
@ -425,24 +447,6 @@ export class Boundary {
}
}
/**
*
* @param {Effect} effect
* @param {DocumentFragment} fragment
*/
function move_effect(effect, fragment) {
var node = effect.nodes_start;
var end = effect.nodes_end;
while (node !== null) {
/** @type {TemplateNode | null} */
var next = node === end ? null : /** @type {TemplateNode} */ (get_next_sibling(node));
fragment.append(node);
node = next;
}
}
export function get_boundary() {
return /** @type {Boundary} */ (/** @type {Effect} */ (active_effect).b);
}

@ -0,0 +1,200 @@
/** @import { Effect, TemplateNode } from '#client' */
import { Batch, current_batch } from '../../reactivity/batch.js';
import {
branch,
destroy_effect,
move_effect,
pause_effect,
resume_effect
} from '../../reactivity/effects.js';
import { hydrate_node, hydrating } from '../hydration.js';
import { create_text, should_defer_append } from '../operations.js';
/**
* @typedef {{ effect: Effect, fragment: DocumentFragment }} Branch
*/
/**
* @template Key
*/
export class BranchManager {
/** @type {TemplateNode} */
anchor;
/** @type {Map<Batch, Key>} */
#batches = new Map();
/** @type {Map<Key, Effect>} */
#onscreen = new Map();
/** @type {Map<Key, Branch>} */
#offscreen = new Map();
/**
* Whether to pause (i.e. outro) on change, or destroy immediately.
* This is necessary for `<svelte:element>`
*/
#transition = true;
/**
* @param {TemplateNode} anchor
* @param {boolean} transition
*/
constructor(anchor, transition = true) {
this.anchor = anchor;
this.#transition = transition;
}
#commit = () => {
var batch = /** @type {Batch} */ (current_batch);
// if this batch was made obsolete, bail
if (!this.#batches.has(batch)) return;
var key = /** @type {Key} */ (this.#batches.get(batch));
var onscreen = this.#onscreen.get(key);
if (onscreen) {
// effect is already in the DOM — abort any current outro
resume_effect(onscreen);
} else {
// effect is currently offscreen. put it in the DOM
var offscreen = this.#offscreen.get(key);
if (offscreen) {
this.#onscreen.set(key, offscreen.effect);
this.#offscreen.delete(key);
// remove the anchor...
/** @type {TemplateNode} */ (offscreen.fragment.lastChild).remove();
// ...and append the fragment
this.anchor.before(offscreen.fragment);
onscreen = offscreen.effect;
}
}
for (const [b, k] of this.#batches) {
this.#batches.delete(b);
if (b === batch) {
// keep values for newer batches
break;
}
const offscreen = this.#offscreen.get(k);
if (offscreen) {
// for older batches, destroy offscreen effects
// as they will never be committed
destroy_effect(offscreen.effect);
this.#offscreen.delete(k);
}
}
// outro/destroy all onscreen effects...
for (const [k, effect] of this.#onscreen) {
// ...except the one that was just committed
if (k === key) continue;
const on_destroy = () => {
const keys = Array.from(this.#batches.values());
if (keys.includes(k)) {
// keep the effect offscreen, as another batch will need it
var fragment = document.createDocumentFragment();
move_effect(effect, fragment);
fragment.append(create_text()); // TODO can we avoid this?
this.#offscreen.set(k, { effect, fragment });
} else {
destroy_effect(effect);
}
this.#onscreen.delete(k);
};
if (this.#transition || !onscreen) {
pause_effect(effect, on_destroy, false);
} else {
on_destroy();
}
}
};
/**
* @param {Batch} batch
*/
#discard = (batch) => {
this.#batches.delete(batch);
const keys = Array.from(this.#batches.values());
for (const [k, branch] of this.#offscreen) {
if (!keys.includes(k)) {
destroy_effect(branch.effect);
this.#offscreen.delete(k);
}
}
};
/**
*
* @param {any} key
* @param {null | ((target: TemplateNode) => void)} fn
*/
ensure(key, fn) {
var batch = /** @type {Batch} */ (current_batch);
var defer = should_defer_append();
if (fn && !this.#onscreen.has(key) && !this.#offscreen.has(key)) {
if (defer) {
var fragment = document.createDocumentFragment();
var target = create_text();
fragment.append(target);
this.#offscreen.set(key, {
effect: branch(() => fn(target)),
fragment
});
} else {
this.#onscreen.set(
key,
branch(() => fn(this.anchor))
);
}
}
this.#batches.set(batch, key);
if (defer) {
for (const [k, effect] of this.#onscreen) {
if (k === key) {
batch.skipped_effects.delete(effect);
} else {
batch.skipped_effects.add(effect);
}
}
for (const [k, branch] of this.#offscreen) {
if (k === key) {
batch.skipped_effects.delete(branch.effect);
} else {
batch.skipped_effects.add(branch.effect);
}
}
batch.oncommit(this.#commit);
batch.ondiscard(this.#discard);
} else {
if (hydrating) {
this.anchor = hydrate_node;
}
this.#commit();
}
}
}

@ -310,7 +310,7 @@ export function each(node, flags, get_collection, get_key, render_fn, fallback_f
}
}
batch.add_callback(commit);
batch.oncommit(commit);
} else {
commit();
}

@ -1,19 +1,16 @@
/** @import { Effect, TemplateNode } from '#client' */
/** @import { Batch } from '../../reactivity/batch.js'; */
/** @import { TemplateNode } from '#client' */
import { EFFECT_TRANSPARENT } from '#client/constants';
import {
hydrate_next,
hydrate_node,
hydrating,
read_hydration_instruction,
skip_nodes,
set_hydrate_node,
set_hydrating
} from '../hydration.js';
import { block, branch, pause_effect, resume_effect } from '../../reactivity/effects.js';
import { HYDRATION_START_ELSE, UNINITIALIZED } from '../../../../constants.js';
import { create_text, should_defer_append } from '../operations.js';
import { current_batch } from '../../reactivity/batch.js';
import { block } from '../../reactivity/effects.js';
import { HYDRATION_START_ELSE } from '../../../../constants.js';
import { BranchManager } from './branches.js';
// TODO reinstate https://github.com/sveltejs/svelte/pull/15250
@ -28,122 +25,46 @@ export function if_block(node, fn, elseif = false) {
hydrate_next();
}
var anchor = node;
/** @type {Effect | null} */
var consequent_effect = null;
/** @type {Effect | null} */
var alternate_effect = null;
/** @type {typeof UNINITIALIZED | boolean | null} */
var condition = UNINITIALIZED;
var branches = new BranchManager(node);
var flags = elseif ? EFFECT_TRANSPARENT : 0;
var has_branch = false;
const set_branch = (/** @type {(anchor: Node) => void} */ fn, flag = true) => {
has_branch = true;
update_branch(flag, fn);
};
/** @type {DocumentFragment | null} */
var offscreen_fragment = null;
function commit() {
if (offscreen_fragment !== null) {
// remove the anchor
/** @type {Text} */ (offscreen_fragment.lastChild).remove();
anchor.before(offscreen_fragment);
offscreen_fragment = null;
}
var active = condition ? consequent_effect : alternate_effect;
var inactive = condition ? alternate_effect : consequent_effect;
if (active) {
resume_effect(active);
}
if (inactive) {
pause_effect(inactive, () => {
if (condition) {
alternate_effect = null;
} else {
consequent_effect = null;
}
});
}
}
const update_branch = (
/** @type {boolean | null} */ new_condition,
/** @type {null | ((anchor: Node) => void)} */ fn
) => {
if (condition === (condition = new_condition)) return;
/** Whether or not there was a hydration mismatch. Needs to be a `let` or else it isn't treeshaken out */
let mismatch = false;
/**
* @param {boolean} condition,
* @param {null | ((anchor: Node) => void)} fn
*/
function update_branch(condition, fn) {
if (hydrating) {
const is_else = read_hydration_instruction(anchor) === HYDRATION_START_ELSE;
const is_else = read_hydration_instruction(node) === HYDRATION_START_ELSE;
if (!!condition === is_else) {
if (condition === is_else) {
// Hydration mismatch: remove everything inside the anchor and start fresh.
// This could happen with `{#if browser}...{/if}`, for example
anchor = skip_nodes();
var anchor = skip_nodes();
set_hydrate_node(anchor);
set_hydrating(false);
mismatch = true;
}
}
branches.anchor = anchor;
var defer = should_defer_append();
var target = anchor;
if (defer) {
offscreen_fragment = document.createDocumentFragment();
offscreen_fragment.append((target = create_text()));
}
set_hydrating(false);
branches.ensure(condition, fn);
set_hydrating(true);
if (condition) {
consequent_effect ??= fn && branch(() => fn(target));
} else {
alternate_effect ??= fn && branch(() => fn(target));
return;
}
}
if (defer) {
var batch = /** @type {Batch} */ (current_batch);
var active = condition ? consequent_effect : alternate_effect;
var inactive = condition ? alternate_effect : consequent_effect;
if (active) batch.skipped_effects.delete(active);
if (inactive) batch.skipped_effects.add(inactive);
branches.ensure(condition, fn);
}
batch.add_callback(commit);
} else {
commit();
}
block(() => {
var has_branch = false;
if (mismatch) {
// continue in hydration mode
set_hydrating(true);
}
};
fn((fn, flag = true) => {
has_branch = true;
update_branch(flag, fn);
});
block(() => {
has_branch = false;
fn(set_branch);
if (!has_branch) {
update_branch(null, null);
update_branch(false, null);
}
}, flags);
if (hydrating) {
anchor = hydrate_node;
}
}

@ -1,12 +1,8 @@
/** @import { Effect, TemplateNode } from '#client' */
/** @import { Batch } from '../../reactivity/batch.js'; */
import { UNINITIALIZED } from '../../../../constants.js';
import { block, branch, pause_effect } from '../../reactivity/effects.js';
import { not_equal, safe_not_equal } from '../../reactivity/equality.js';
/** @import { TemplateNode } from '#client' */
import { is_runes } from '../../context.js';
import { hydrate_next, hydrate_node, hydrating } from '../hydration.js';
import { create_text, should_defer_append } from '../operations.js';
import { current_batch } from '../../reactivity/batch.js';
import { block } from '../../reactivity/effects.js';
import { hydrate_next, hydrating } from '../hydration.js';
import { BranchManager } from './branches.js';
/**
* @template V
@ -20,60 +16,18 @@ export function key(node, get_key, render_fn) {
hydrate_next();
}
var anchor = node;
var branches = new BranchManager(node);
/** @type {V | typeof UNINITIALIZED} */
var key = UNINITIALIZED;
/** @type {Effect} */
var effect;
/** @type {Effect} */
var pending_effect;
/** @type {DocumentFragment | null} */
var offscreen_fragment = null;
var changed = is_runes() ? not_equal : safe_not_equal;
function commit() {
if (effect) {
pause_effect(effect);
}
if (offscreen_fragment !== null) {
// remove the anchor
/** @type {Text} */ (offscreen_fragment.lastChild).remove();
anchor.before(offscreen_fragment);
offscreen_fragment = null;
}
effect = pending_effect;
}
var legacy = !is_runes();
block(() => {
if (changed(key, (key = get_key()))) {
var target = anchor;
var defer = should_defer_append();
if (defer) {
offscreen_fragment = document.createDocumentFragment();
offscreen_fragment.append((target = create_text()));
}
pending_effect = branch(() => render_fn(target));
var key = get_key();
if (defer) {
/** @type {Batch} */ (current_batch).add_callback(commit);
} else {
commit();
}
// key blocks in Svelte <5 had stupid semantics
if (legacy && key !== null && typeof key === 'object') {
key = /** @type {V} */ ({});
}
});
if (hydrating) {
anchor = hydrate_node;
}
branches.ensure(key, render_fn);
});
}

@ -1,8 +1,8 @@
/** @import { Snippet } from 'svelte' */
/** @import { Effect, TemplateNode } from '#client' */
/** @import { TemplateNode } from '#client' */
/** @import { Getters } from '#shared' */
import { EFFECT_TRANSPARENT, ELEMENT_NODE } from '#client/constants';
import { branch, block, destroy_effect, teardown } from '../../reactivity/effects.js';
import { block, teardown } from '../../reactivity/effects.js';
import {
dev_current_component_function,
set_dev_current_component_function
@ -14,8 +14,8 @@ import * as w from '../../warnings.js';
import * as e from '../../errors.js';
import { DEV } from 'esm-env';
import { get_first_child, get_next_sibling } from '../operations.js';
import { noop } from '../../../shared/utils.js';
import { prevent_snippet_stringification } from '../../../shared/validate.js';
import { BranchManager } from './branches.js';
/**
* @template {(node: TemplateNode, ...args: any[]) => void} SnippetFn
@ -25,33 +25,17 @@ import { prevent_snippet_stringification } from '../../../shared/validate.js';
* @returns {void}
*/
export function snippet(node, get_snippet, ...args) {
var anchor = node;
/** @type {SnippetFn | null | undefined} */
// @ts-ignore
var snippet = noop;
/** @type {Effect | null} */
var snippet_effect;
var branches = new BranchManager(node);
block(() => {
if (snippet === (snippet = get_snippet())) return;
if (snippet_effect) {
destroy_effect(snippet_effect);
snippet_effect = null;
}
const snippet = get_snippet() ?? null;
if (DEV && snippet == null) {
e.invalid_snippet();
}
snippet_effect = branch(() => /** @type {SnippetFn} */ (snippet)(anchor, ...args));
branches.ensure(snippet, snippet && ((anchor) => snippet(anchor, ...args)));
}, EFFECT_TRANSPARENT);
if (hydrating) {
anchor = hydrate_node;
}
}
/**

@ -1,10 +1,8 @@
/** @import { TemplateNode, Dom, Effect } from '#client' */
/** @import { Batch } from '../../reactivity/batch.js'; */
/** @import { TemplateNode, Dom } from '#client' */
import { EFFECT_TRANSPARENT } from '#client/constants';
import { block, branch, pause_effect } from '../../reactivity/effects.js';
import { current_batch } from '../../reactivity/batch.js';
import { hydrate_next, hydrate_node, hydrating } from '../hydration.js';
import { create_text, should_defer_append } from '../operations.js';
import { block } from '../../reactivity/effects.js';
import { hydrate_next, hydrating } from '../hydration.js';
import { BranchManager } from './branches.js';
/**
* @template P
@ -19,64 +17,10 @@ export function component(node, get_component, render_fn) {
hydrate_next();
}
var anchor = node;
/** @type {C} */
var component;
/** @type {Effect | null} */
var effect;
/** @type {DocumentFragment | null} */
var offscreen_fragment = null;
/** @type {Effect | null} */
var pending_effect = null;
function commit() {
if (effect) {
pause_effect(effect);
effect = null;
}
if (offscreen_fragment) {
// remove the anchor
/** @type {Text} */ (offscreen_fragment.lastChild).remove();
anchor.before(offscreen_fragment);
offscreen_fragment = null;
}
effect = pending_effect;
pending_effect = null;
}
var branches = new BranchManager(node);
block(() => {
if (component === (component = get_component())) return;
var defer = should_defer_append();
if (component) {
var target = anchor;
if (defer) {
offscreen_fragment = document.createDocumentFragment();
offscreen_fragment.append((target = create_text()));
if (effect) {
/** @type {Batch} */ (current_batch).skipped_effects.add(effect);
}
}
pending_effect = branch(() => render_fn(target, component));
}
if (defer) {
/** @type {Batch} */ (current_batch).add_callback(commit);
} else {
commit();
}
var component = get_component() ?? null;
branches.ensure(component, component && ((target) => render_fn(target, component)));
}, EFFECT_TRANSPARENT);
if (hydrating) {
anchor = hydrate_node;
}
}

@ -8,13 +8,7 @@ import {
set_hydrating
} from '../hydration.js';
import { create_text, get_first_child } from '../operations.js';
import {
block,
branch,
destroy_effect,
pause_effect,
resume_effect
} from '../../reactivity/effects.js';
import { block, teardown } from '../../reactivity/effects.js';
import { set_should_intro } from '../../render.js';
import { current_each_item, set_current_each_item } from './each.js';
import { active_effect } from '../../runtime.js';
@ -23,6 +17,7 @@ import { DEV } from 'esm-env';
import { EFFECT_TRANSPARENT, ELEMENT_NODE } from '#client/constants';
import { assign_nodes } from '../template.js';
import { is_raw_text_element } from '../../../../utils.js';
import { BranchManager } from './branches.js';
/**
* @param {Comment | Element} node
@ -42,12 +37,6 @@ export function element(node, get_tag, is_svg, render_fn, get_namespace, locatio
var filename = DEV && location && component_context?.function[FILENAME];
/** @type {string | null} */
var tag;
/** @type {string | null} */
var current_tag;
/** @type {null | Element} */
var element = null;
@ -58,9 +47,6 @@ export function element(node, get_tag, is_svg, render_fn, get_namespace, locatio
var anchor = /** @type {TemplateNode} */ (hydrating ? hydrate_node : node);
/** @type {Effect | null} */
var effect;
/**
* The keyed `{#each ...}` item block, if any, that this element is inside.
* We track this so we can set it when changing the element, allowing any
@ -68,36 +54,24 @@ export function element(node, get_tag, is_svg, render_fn, get_namespace, locatio
*/
var each_item_block = current_each_item;
var branches = new BranchManager(anchor, false);
block(() => {
const next_tag = get_tag() || null;
var ns = get_namespace ? get_namespace() : is_svg || next_tag === 'svg' ? NAMESPACE_SVG : null;
// Assumption: Noone changes the namespace but not the tag (what would that even mean?)
if (next_tag === tag) return;
// See explanation of `each_item_block` above
var previous_each_item = current_each_item;
set_current_each_item(each_item_block);
if (effect) {
if (next_tag === null) {
// start outro
pause_effect(effect, () => {
effect = null;
current_tag = null;
});
} else if (next_tag === current_tag) {
// same tag as is currently rendered — abort outro
resume_effect(effect);
} else {
// tag is changing — destroy immediately, render contents without intro transitions
destroy_effect(effect);
set_should_intro(false);
}
if (next_tag === null) {
branches.ensure(null, null);
set_should_intro(true);
return;
}
if (next_tag && next_tag !== current_tag) {
effect = branch(() => {
branches.ensure(next_tag, (anchor) => {
// See explanation of `each_item_block` above
var previous_each_item = current_each_item;
set_current_each_item(each_item_block);
if (next_tag) {
element = hydrating
? /** @type {Element} */ (element)
: ns
@ -149,16 +123,31 @@ export function element(node, get_tag, is_svg, render_fn, get_namespace, locatio
/** @type {Effect} */ (active_effect).nodes_end = element;
anchor.before(element);
});
}
}
set_current_each_item(previous_each_item);
if (hydrating) {
set_hydrate_node(anchor);
}
});
tag = next_tag;
if (tag) current_tag = tag;
// revert to the default state after the effect has been created
set_should_intro(true);
set_current_each_item(previous_each_item);
return () => {
if (next_tag) {
// if we're in this callback because we're re-running the effect,
// disable intros (unless no element is currently displayed)
set_should_intro(false);
}
};
}, EFFECT_TRANSPARENT);
teardown(() => {
set_should_intro(true);
});
if (was_hydrating) {
set_hydrating(true);
set_hydrate_node(anchor);

@ -3,22 +3,13 @@ import { hydrate_node, hydrating, set_hydrate_node, set_hydrating } from '../hyd
import { create_text, get_first_child, get_next_sibling } from '../operations.js';
import { block } from '../../reactivity/effects.js';
import { COMMENT_NODE, HEAD_EFFECT } from '#client/constants';
import { HYDRATION_START } from '../../../../constants.js';
/**
* @type {Node | undefined}
*/
let head_anchor;
export function reset_head_anchor() {
head_anchor = undefined;
}
/**
* @param {string} hash
* @param {(anchor: Node) => void} render_fn
* @returns {void}
*/
export function head(render_fn) {
export function head(hash, render_fn) {
// The head function may be called after the first hydration pass and ssr comment nodes may still be present,
// therefore we need to skip that when we detect that we're not in hydration mode.
let previous_hydrate_node = null;
@ -30,15 +21,13 @@ export function head(render_fn) {
if (hydrating) {
previous_hydrate_node = hydrate_node;
// There might be multiple head blocks in our app, so we need to account for each one needing independent hydration.
if (head_anchor === undefined) {
head_anchor = /** @type {TemplateNode} */ (get_first_child(document.head));
}
var head_anchor = /** @type {TemplateNode} */ (get_first_child(document.head));
// There might be multiple head blocks in our app, and they could have been
// rendered in an arbitrary order — find one corresponding to this component
while (
head_anchor !== null &&
(head_anchor.nodeType !== COMMENT_NODE ||
/** @type {Comment} */ (head_anchor).data !== HYDRATION_START)
(head_anchor.nodeType !== COMMENT_NODE || /** @type {Comment} */ (head_anchor).data !== hash)
) {
head_anchor = /** @type {TemplateNode} */ (get_next_sibling(head_anchor));
}
@ -48,7 +37,10 @@ export function head(render_fn) {
if (head_anchor === null) {
set_hydrating(false);
} else {
head_anchor = set_hydrate_node(/** @type {TemplateNode} */ (get_next_sibling(head_anchor)));
var start = /** @type {TemplateNode} */ (get_next_sibling(head_anchor));
head_anchor.remove(); // in case this component is repeated
set_hydrate_node(start);
}
}
@ -61,7 +53,6 @@ export function head(render_fn) {
} finally {
if (was_hydrating) {
set_hydrating(true);
head_anchor = hydrate_node; // so that next head block starts from the correct node
set_hydrate_node(/** @type {TemplateNode} */ (previous_hydrate_node));
}
}

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

Loading…
Cancel
Save