Merge branch 'main' into defer-effects-in-pending-boundary

defer-effects-in-pending-boundary
Rich Harris 1 day ago
commit a1be776de4

@ -1,5 +0,0 @@
---
'svelte': patch
---
fix: flush effects scheduled during boundary's pending phase

@ -15,7 +15,6 @@ jobs:
contents: read # to clone the repo
steps:
- name: monitor action permissions
uses: GitHubSecurityLab/actions-permissions/monitor@v1
- name: check user authorization # user needs triage permission
uses: actions/github-script@v7
id: check-permissions

@ -14,7 +14,6 @@ jobs:
name: 'Update comment'
runs-on: ubuntu-latest
steps:
- uses: GitHubSecurityLab/actions-permissions/monitor@v1
- name: Download artifact
uses: actions/download-artifact@v4
with:

@ -17,7 +17,6 @@ jobs:
name: Release
runs-on: ubuntu-latest
steps:
- uses: GitHubSecurityLab/actions-permissions/monitor@v1
- name: Checkout Repo
uses: actions/checkout@v4
with:

@ -15,6 +15,7 @@ packages/svelte/src/internal/client/warnings.js
packages/svelte/src/internal/shared/errors.js
packages/svelte/src/internal/shared/warnings.js
packages/svelte/src/internal/server/errors.js
packages/svelte/src/internal/server/warnings.js
packages/svelte/tests/migrate/samples/*/output.svelte
packages/svelte/tests/**/*.svelte
packages/svelte/tests/**/_expected*

@ -9,7 +9,7 @@ The [Open Source Guides](https://opensource.guide/) website has a collection of
## Get involved
There are many ways to contribute to Svelte, and many of them do not involve writing any code. Here's a few ideas to get started:
There are many ways to contribute to Svelte, and many of them do not involve writing any code. Here are a few ideas to get started:
- Simply start using Svelte. Go through the [Getting Started](https://svelte.dev/docs#getting-started) guide. Does everything work as expected? If not, we're always looking for improvements. Let us know by [opening an issue](#reporting-new-issues).
- Look through the [open issues](https://github.com/sveltejs/svelte/issues). A good starting point would be issues tagged [good first issue](https://github.com/sveltejs/svelte/issues?q=is%3Aissue+is%3Aopen+label%3A%22good+first+issue%22). Provide workarounds, ask for clarification, or suggest labels. Help [triage issues](#triaging-issues-and-pull-requests).
@ -90,9 +90,9 @@ A good test plan has the exact commands you ran and their output, provides scree
#### Writing tests
All tests are located in `/test` folder.
All tests are located in the `/tests` folder.
Test samples are kept in `/test/xxx/samples` folder.
Test samples are kept in `/tests/xxx/samples` folders.
#### Running tests

@ -312,6 +312,27 @@ Reactive `$state(...)` proxies and the values they proxy have different identiti
To resolve this, ensure you're comparing values where both values were created with `$state(...)`, or neither were. Note that `$state.raw(...)` will _not_ create a state proxy.
### state_proxy_unmount
```
Tried to unmount a state proxy, rather than a component
```
`unmount` was called with a state proxy:
```js
import { mount, unmount } from 'svelte';
import Component from './Component.svelte';
let target = document.body;
// ---cut---
let component = $state(mount(Component, { target }));
// later...
unmount(component);
```
Avoid using `$state` here. If `component` _does_ need to be reactive for some reason, use `$state.raw` instead.
### svelte_boundary_reset_noop
```

@ -1,5 +1,19 @@
<!-- This file is generated by scripts/process-messages/index.js. Do not edit! -->
### await_invalid
```
Encountered asynchronous work while rendering synchronously.
```
You (or the framework you're using) called [`render(...)`](svelte-server#render) with a component containing an `await` expression. Either `await` the result of `render` or wrap the `await` (or the component containing it) in a [`<svelte:boundary>`](svelte-boundary) with a `pending` snippet.
### html_deprecated
```
The `html` property of server render results has been deprecated. Use `body` instead.
```
### lifecycle_function_unavailable
```

@ -0,0 +1,9 @@
<!-- This file is generated by scripts/process-messages/index.js. Do not edit! -->
### experimental_async_ssr
```
Attempted to use asynchronous rendering without `experimental.async` enabled
```
Set `experimental.async: true` in your compiler options (usually in `svelte.config.js`) to use async server rendering. This render ran synchronously.

@ -1,25 +1,5 @@
<!-- This file is generated by scripts/process-messages/index.js. Do not edit! -->
### await_outside_boundary
```
Cannot await outside a `<svelte:boundary>` with a `pending` snippet
```
The `await` keyword can only appear in a `$derived(...)` or template expression, or at the top level of a component's `<script>` block, if it is inside a [`<svelte:boundary>`](/docs/svelte/svelte-boundary) that has a `pending` snippet:
```svelte
<svelte:boundary>
<p>{await getData()}</p>
{#snippet pending()}
<p>loading...</p>
{/snippet}
</svelte:boundary>
```
This restriction may be lifted in a future version of Svelte.
### invalid_default_snippet
```

@ -94,6 +94,7 @@ export default [
'packages/svelte/src/internal/client/errors.js',
'packages/svelte/src/internal/client/warnings.js',
'packages/svelte/src/internal/shared/warnings.js',
'packages/svelte/src/internal/server/warnings.js',
'packages/svelte/compiler/index.js',
// stuff we don't want to lint
'benchmarking/**',

@ -30,6 +30,7 @@
"@sveltejs/eslint-config": "^8.3.3",
"@svitejs/changesets-changelog-github-compact": "^1.1.0",
"@types/node": "^20.11.5",
"@types/picomatch": "^4.0.2",
"@vitest/coverage-v8": "^2.1.9",
"eslint": "^9.9.1",
"eslint-plugin-lube": "^0.4.3",

@ -1,5 +1,51 @@
# svelte
## 5.39.4
### Patch Changes
- fix: restore hydration state after `await` in `<script>` ([#16806](https://github.com/sveltejs/svelte/pull/16806))
## 5.39.3
### Patch Changes
- fix: remove outer hydration markers ([#16800](https://github.com/sveltejs/svelte/pull/16800))
- fix: async hydration ([#16797](https://github.com/sveltejs/svelte/pull/16797))
## 5.39.2
### Patch Changes
- fix: preserve SSR context when block expressions contain `await` ([#16791](https://github.com/sveltejs/svelte/pull/16791))
- chore: bump some devDependencies ([#16787](https://github.com/sveltejs/svelte/pull/16787))
## 5.39.1
### Patch Changes
- fix: issue `state_proxy_unmount` warning when unmounting a state proxy ([#16747](https://github.com/sveltejs/svelte/pull/16747))
- fix: add `then` to class component `render` output ([#16783](https://github.com/sveltejs/svelte/pull/16783))
## 5.39.0
### Minor Changes
- feat: experimental async SSR ([#16748](https://github.com/sveltejs/svelte/pull/16748))
### Patch Changes
- fix: correctly SSR hidden="until-found" ([#16773](https://github.com/sveltejs/svelte/pull/16773))
## 5.38.10
### Patch Changes
- fix: flush effects scheduled during boundary's pending phase ([#16738](https://github.com/sveltejs/svelte/pull/16738))
## 5.38.9
### Patch Changes

@ -272,6 +272,25 @@ To silence the warning, ensure that `value`:
To resolve this, ensure you're comparing values where both values were created with `$state(...)`, or neither were. Note that `$state.raw(...)` will _not_ create a state proxy.
## state_proxy_unmount
> Tried to unmount a state proxy, rather than a component
`unmount` was called with a state proxy:
```js
import { mount, unmount } from 'svelte';
import Component from './Component.svelte';
let target = document.body;
// ---cut---
let component = $state(mount(Component, { target }));
// later...
unmount(component);
```
Avoid using `$state` here. If `component` _does_ need to be reactive for some reason, use `$state.raw` instead.
## svelte_boundary_reset_noop
> A `<svelte:boundary>` `reset` function only resets the boundary the first time it is called

@ -0,0 +1,15 @@
## await_invalid
> Encountered asynchronous work while rendering synchronously.
You (or the framework you're using) called [`render(...)`](svelte-server#render) with a component containing an `await` expression. Either `await` the result of `render` or wrap the `await` (or the component containing it) in a [`<svelte:boundary>`](svelte-boundary) with a `pending` snippet.
## html_deprecated
> The `html` property of server render results has been deprecated. Use `body` instead.
## lifecycle_function_unavailable
> `%name%(...)` is not available on the server
Certain methods such as `mount` cannot be invoked while running in a server context. Avoid calling them eagerly, i.e. not during render.

@ -1,5 +0,0 @@
## lifecycle_function_unavailable
> `%name%(...)` is not available on the server
Certain methods such as `mount` cannot be invoked while running in a server context. Avoid calling them eagerly, i.e. not during render.

@ -0,0 +1,5 @@
## experimental_async_ssr
> Attempted to use asynchronous rendering without `experimental.async` enabled
Set `experimental.async: true` in your compiler options (usually in `svelte.config.js`) to use async server rendering. This render ran synchronously.

@ -1,21 +1,3 @@
## await_outside_boundary
> Cannot await outside a `<svelte:boundary>` with a `pending` snippet
The `await` keyword can only appear in a `$derived(...)` or template expression, or at the top level of a component's `<script>` block, if it is inside a [`<svelte:boundary>`](/docs/svelte/svelte-boundary) that has a `pending` snippet:
```svelte
<svelte:boundary>
<p>{await getData()}</p>
{#snippet pending()}
<p>loading...</p>
{/snippet}
</svelte:boundary>
```
This restriction may be lifted in a future version of Svelte.
## invalid_default_snippet
> Cannot use `{@render children(...)}` if the parent component uses `let:` directives. Consider using a named snippet instead

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

@ -401,6 +401,7 @@ function run() {
transform('client-warnings', 'src/internal/client/warnings.js');
transform('client-errors', 'src/internal/client/errors.js');
transform('server-warnings', 'src/internal/server/warnings.js');
transform('server-errors', 'src/internal/server/errors.js');
transform('shared-errors', 'src/internal/shared/errors.js');
transform('shared-warnings', 'src/internal/shared/warnings.js');

@ -0,0 +1,20 @@
import { DEV } from 'esm-env';
var bold = 'font-weight: bold';
var normal = 'font-weight: normal';
/**
* MESSAGE
* @param {string} PARAMETER
*/
export function CODE(PARAMETER) {
if (DEV) {
console.warn(
`%c[svelte] ${'CODE'}\n%c${MESSAGE}\nhttps://svelte.dev/e/${'CODE'}`,
bold,
normal
);
} else {
console.warn(`https://svelte.dev/e/${'CODE'}`);
}
}

@ -177,7 +177,8 @@ export default function element(parser) {
mathml: false,
scoped: false,
has_spread: false,
path: []
path: [],
synthetic_value_node: null
}
}
: /** @type {AST.ElementLike} */ ({

@ -278,7 +278,8 @@ export function analyze_module(source, options) {
tracing: false,
async_deriveds: new Set(),
comments,
classes: new Map()
classes: new Map(),
pickled_awaits: new Set()
};
state.adjust({
@ -304,7 +305,8 @@ export function analyze_module(source, options) {
options: /** @type {ValidatedCompileOptions} */ (options),
fragment: null,
parent_element: null,
reactive_statement: null
reactive_statement: null,
in_derived: false
},
visitors
);
@ -540,7 +542,8 @@ export function analyze_component(root, source, options) {
source,
snippet_renderers: new Map(),
snippets: new Set(),
async_deriveds: new Set()
async_deriveds: new Set(),
pickled_awaits: new Set()
};
if (!runes) {
@ -699,7 +702,8 @@ export function analyze_component(root, source, options) {
expression: null,
state_fields: new Map(),
function_depth: scope.function_depth,
reactive_statement: null
reactive_statement: null,
in_derived: false
};
walk(/** @type {AST.SvelteNode} */ (ast), state, visitors);
@ -766,7 +770,8 @@ export function analyze_component(root, source, options) {
component_slots: new Set(),
expression: null,
state_fields: new Map(),
function_depth: scope.function_depth
function_depth: scope.function_depth,
in_derived: false
};
walk(/** @type {AST.SvelteNode} */ (ast), state, visitors);

@ -27,6 +27,11 @@ export interface AnalysisState {
// legacy stuff
reactive_statement: null | ReactiveStatement;
/**
* True if we're directly inside a `$derived(...)` expression (but not `$derived.by(...)`)
*/
in_derived: boolean;
}
export type Context<State extends AnalysisState = AnalysisState> = import('zimmerframe').Context<

@ -1,5 +1,6 @@
/** @import { AwaitExpression } from 'estree' */
/** @import { AwaitExpression, Expression, SpreadElement, Property } from 'estree' */
/** @import { Context } from '../types' */
/** @import { AST } from '#compiler' */
import * as e from '../../../errors.js';
/**
@ -7,16 +8,25 @@ import * as e from '../../../errors.js';
* @param {Context} context
*/
export function AwaitExpression(node, context) {
let suspend = context.state.ast_type === 'instance' && context.state.function_depth === 1;
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(...)`
if (
tla ||
(is_reactive_expression(context.path, context.state.in_derived) &&
!is_last_evaluated_expression(context.path, node))
) {
context.state.analysis.pickled_awaits.add(node);
}
let suspend = tla;
if (context.state.expression) {
context.state.expression.has_await = true;
if (
context.state.fragment &&
// TODO there's probably a better way to do this
context.path.some((node) => node.type === 'ConstTag')
) {
if (context.state.fragment && context.path.some((node) => node.type === 'ConstTag')) {
context.state.fragment.metadata.has_await = true;
}
@ -37,3 +47,101 @@ export function AwaitExpression(node, context) {
context.next();
}
/**
* @param {AST.SvelteNode[]} path
* @param {boolean} in_derived
*/
export function is_reactive_expression(path, in_derived) {
if (in_derived) {
return true;
}
let i = path.length;
while (i--) {
const parent = path[i];
if (
parent.type === 'ArrowFunctionExpression' ||
parent.type === 'FunctionExpression' ||
parent.type === 'FunctionDeclaration'
) {
return false;
}
// @ts-expect-error we could probably use a neater/more robust mechanism
if (parent.metadata) {
return true;
}
}
return false;
}
/**
* @param {AST.SvelteNode[]} path
* @param {Expression | SpreadElement | Property} node
*/
export function is_last_evaluated_expression(path, node) {
let i = path.length;
while (i--) {
const parent = /** @type {Expression | Property | SpreadElement} */ (path[i]);
// @ts-expect-error we could probably use a neater/more robust mechanism
if (parent.metadata) {
return true;
}
switch (parent.type) {
case 'ArrayExpression':
if (node !== parent.elements.at(-1)) return false;
break;
case 'AssignmentExpression':
case 'BinaryExpression':
case 'LogicalExpression':
if (node === parent.left) return false;
break;
case 'CallExpression':
case 'NewExpression':
if (node !== parent.arguments.at(-1)) return false;
break;
case 'ConditionalExpression':
if (node === parent.test) return false;
break;
case 'MemberExpression':
if (parent.computed && node === parent.object) return false;
break;
case 'ObjectExpression':
if (node !== parent.properties.at(-1)) return false;
break;
case 'Property':
if (node === parent.key) return false;
break;
case 'SequenceExpression':
if (node !== parent.expressions.at(-1)) return false;
break;
case 'TaggedTemplateExpression':
if (node !== parent.quasi.expressions.at(-1)) return false;
break;
case 'TemplateLiteral':
if (node !== parent.expressions.at(-1)) return false;
break;
default:
return false;
}
node = parent;
}
}

@ -241,6 +241,7 @@ export function CallExpression(node, context) {
context.next({
...context.state,
function_depth: context.state.function_depth + 1,
in_derived: true,
expression
});

@ -35,5 +35,9 @@ export function ConstTag(node, context) {
const declaration = node.declaration.declarations[0];
context.visit(declaration.id);
context.visit(declaration.init, { ...context.state, expression: node.metadata.expression });
context.visit(declaration.init, {
...context.state,
expression: node.metadata.expression,
in_derived: true
});
}

@ -70,7 +70,7 @@ export function RegularElement(node, context) {
)
) {
const child = node.fragment.nodes[0];
node.attributes.push(create_attribute('value', child.start, child.end, [child]));
node.metadata.synthetic_value_node = child;
}
const binding = context.state.scope.get(node.name);

@ -49,6 +49,12 @@ 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);

@ -166,7 +166,6 @@ export function client_component(analysis, options) {
state_fields: new Map(),
transform: {},
in_constructor: false,
in_derived: false,
instance_level_snippets: [],
module_level_snippets: [],
@ -712,7 +711,6 @@ export function client_module(analysis, options) {
state_fields: new Map(),
transform: {},
in_constructor: false,
in_derived: false,
is_instance: false
};

@ -6,8 +6,7 @@ import type {
Expression,
AssignmentExpression,
UpdateExpression,
VariableDeclaration,
Declaration
VariableDeclaration
} from 'estree';
import type { AST, Namespace, ValidatedCompileOptions } from '#compiler';
import type { TransformState } from '../types.js';
@ -22,11 +21,6 @@ export interface ClientTransformState extends TransformState {
*/
readonly in_constructor: boolean;
/**
* True if we're directly inside a `$derived(...)` expression (but not `$derived.by(...)`)
*/
readonly in_derived: boolean;
/** `true` if we're transforming the contents of `<script>` */
readonly is_instance: boolean;

@ -4,7 +4,7 @@
/** @import { Analysis } from '../../types.js' */
/** @import { Scope } from '../../scope.js' */
import * as b from '#compiler/builders';
import { is_simple_expression } from '../../../utils/ast.js';
import { is_simple_expression, save } from '../../../utils/ast.js';
import {
PROPS_IS_LAZY_INITIAL,
PROPS_IS_IMMUTABLE,
@ -296,7 +296,7 @@ export function create_derived(state, expression, async = false) {
const thunk = b.thunk(expression, async);
if (async) {
return b.call(b.await(b.call('$.save', b.call('$.async_derived', thunk))));
return save(b.call('$.async_derived', thunk));
} else {
return b.call(state.analysis.runes ? '$.derived' : '$.derived_safe_equal', thunk);
}

@ -1,6 +1,7 @@
/** @import { AwaitExpression, Expression, Property, SpreadElement } from 'estree' */
/** @import { AwaitExpression, Expression } from 'estree' */
/** @import { Context } from '../types' */
import { dev, is_ignored } from '../../../../state.js';
import { save } from '../../../../utils/ast.js';
import * as b from '../../../../utils/builders.js';
/**
@ -10,13 +11,8 @@ import * as b from '../../../../utils/builders.js';
export function AwaitExpression(node, context) {
const argument = /** @type {Expression} */ (context.visit(node.argument));
const tla = context.state.is_instance && context.state.scope.function_depth === 1;
// preserve context for
// a) top-level await and
// b) awaits that precede other expressions in template or `$derived(...)`
if (tla || (is_reactive_expression(context) && !is_last_evaluated_expression(context, node))) {
return b.call(b.await(b.call('$.save', argument)));
if (context.state.analysis.pickled_awaits.has(node)) {
return save(argument);
}
// in dev, note which values are read inside a reactive expression,
@ -27,100 +23,3 @@ export function AwaitExpression(node, context) {
return argument === node.argument ? node : { ...node, argument };
}
/**
* @param {Context} context
*/
function is_reactive_expression(context) {
if (context.state.in_derived) {
return true;
}
let i = context.path.length;
while (i--) {
const parent = context.path[i];
if (
parent.type === 'ArrowFunctionExpression' ||
parent.type === 'FunctionExpression' ||
parent.type === 'FunctionDeclaration'
) {
return false;
}
// @ts-expect-error we could probably use a neater/more robust mechanism
if (parent.metadata) {
return true;
}
}
return false;
}
/**
* @param {Context} context
* @param {Expression | SpreadElement | Property} node
*/
function is_last_evaluated_expression(context, node) {
let i = context.path.length;
while (i--) {
const parent = /** @type {Expression | Property | SpreadElement} */ (context.path[i]);
// @ts-expect-error we could probably use a neater/more robust mechanism
if (parent.metadata) {
return true;
}
switch (parent.type) {
case 'ArrayExpression':
if (node !== parent.elements.at(-1)) return false;
break;
case 'AssignmentExpression':
case 'BinaryExpression':
case 'LogicalExpression':
if (node === parent.left) return false;
break;
case 'CallExpression':
case 'NewExpression':
if (node !== parent.arguments.at(-1)) return false;
break;
case 'ConditionalExpression':
if (node === parent.test) return false;
break;
case 'MemberExpression':
if (parent.computed && node === parent.object) return false;
break;
case 'ObjectExpression':
if (node !== parent.properties.at(-1)) return false;
break;
case 'Property':
if (node === parent.key) return false;
break;
case 'SequenceExpression':
if (node !== parent.expressions.at(-1)) return false;
break;
case 'TaggedTemplateExpression':
if (node !== parent.quasi.expressions.at(-1)) return false;
break;
case 'TemplateLiteral':
if (node !== parent.expressions.at(-1)) return false;
break;
default:
return false;
}
node = parent;
}
}

@ -44,9 +44,7 @@ export function CallExpression(node, context) {
case '$derived':
case '$derived.by': {
let fn = /** @type {Expression} */ (
context.visit(node.arguments[0], { ...context.state, in_derived: rune === '$derived' })
);
let fn = /** @type {Expression} */ (context.visit(node.arguments[0]));
return b.call('$.derived', rune === '$derived' ? b.thunk(fn) : fn);
}

@ -16,11 +16,7 @@ export function ConstTag(node, context) {
const declaration = node.declaration.declarations[0];
// TODO we can almost certainly share some code with $derived(...)
if (declaration.id.type === 'Identifier') {
const init = build_expression(
{ ...context, state: { ...context.state, in_derived: true } },
declaration.init,
node.metadata.expression
);
const init = build_expression(context, declaration.init, node.metadata.expression);
let expression = create_derived(context.state, init, node.metadata.expression.has_await);
@ -51,8 +47,7 @@ export function ConstTag(node, context) {
const child_state = /** @type {ComponentContext['state']} */ ({
...context.state,
transform,
in_derived: true
transform
});
// TODO optimise the simple `{ x } = y` case — we can just return `y`

@ -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 { is_custom_element_node } from '../../../nodes.js';
import { create_attribute, is_custom_element_node } from '../../../nodes.js';
import { clean_nodes, determine_namespace_for_children } from '../../utils.js';
import { build_getter } from '../utils.js';
import {
@ -406,10 +406,24 @@ export function RegularElement(node, context) {
}
if (!has_spread && needs_special_value_handling) {
for (const attribute of /** @type {AST.Attribute[]} */ (attributes)) {
if (attribute.name === 'value') {
build_element_special_value_attribute(node.name, node_id, attribute, context);
break;
if (node.metadata.synthetic_value_node) {
const synthetic_node = node.metadata.synthetic_value_node;
const synthetic_attribute = create_attribute(
'value',
synthetic_node.start,
synthetic_node.end,
[synthetic_node]
);
// this node is an `option` that didn't have a `value` attribute, but had
// a single-expression child, so we treat the value of that expression as
// the value of the option
build_element_special_value_attribute(node.name, node_id, synthetic_attribute, context, true);
} else {
for (const attribute of /** @type {AST.Attribute[]} */ (attributes)) {
if (attribute.name === 'value') {
build_element_special_value_attribute(node.name, node_id, attribute, context);
break;
}
}
}
}
@ -645,8 +659,15 @@ function build_custom_element_attribute_update_assignment(node_id, attribute, co
* @param {Identifier} node_id
* @param {AST.Attribute} attribute
* @param {ComponentContext} context
* @param {boolean} [synthetic] - true if this should not sync to the DOM
*/
function build_element_special_value_attribute(element, node_id, attribute, context) {
function build_element_special_value_attribute(
element,
node_id,
attribute,
context,
synthetic = false
) {
const state = context.state;
const is_select_with_value =
// attribute.metadata.dynamic would give false negatives because even if the value does not change,
@ -660,7 +681,7 @@ function build_element_special_value_attribute(element, node_id, attribute, cont
const evaluated = context.state.scope.evaluate(value);
const assignment = b.assignment('=', b.member(node_id, '__value'), value);
const inner_assignment = b.assignment(
const set_value_assignment = b.assignment(
'=',
b.member(node_id, 'value'),
evaluated.is_defined ? assignment : b.logical('??', assignment, b.literal(''))
@ -669,14 +690,16 @@ function build_element_special_value_attribute(element, node_id, attribute, cont
const update = b.stmt(
is_select_with_value
? b.sequence([
inner_assignment,
set_value_assignment,
// This ensures a one-way street to the DOM in case it's <select {value}>
// and not <select bind:value>. We need it in addition to $.init_select
// because the select value is not reflected as an attribute, so the
// mutation observer wouldn't notice.
b.call('$.select_option', node_id, value)
])
: inner_assignment
: synthetic
? assignment
: set_value_assignment
);
if (has_state) {

@ -2,7 +2,7 @@
/** @import { Binding } from '#compiler' */
/** @import { ComponentContext } from '../types' */
import { dev, is_ignored, locate_node } from '../../../../state.js';
import { extract_paths } from '../../../../utils/ast.js';
import { extract_paths, save } from '../../../../utils/ast.js';
import * as b from '#compiler/builders';
import * as assert from '../../../../utils/assert.js';
import { get_rune } from '../../../scope.js';
@ -202,12 +202,7 @@ export function VariableDeclaration(node, context) {
);
if (declarator.id.type === 'Identifier') {
let expression = /** @type {Expression} */ (
context.visit(value, {
...context.state,
in_derived: rune === '$derived'
})
);
let expression = /** @type {Expression} */ (context.visit(value));
if (is_async) {
const location = dev && !is_ignored(init, 'await_waterfall') && locate_node(init);
@ -217,7 +212,7 @@ export function VariableDeclaration(node, context) {
location ? b.literal(location) : undefined
);
call = b.call(b.await(b.call('$.save', call)));
call = save(call);
if (dev) call = b.call('$.tag', call, b.literal(declarator.id.name));
declarations.push(b.declarator(declarator.id, call));
@ -231,12 +226,7 @@ export function VariableDeclaration(node, context) {
}
} else {
const init = /** @type {CallExpression} */ (declarator.init);
let expression = /** @type {Expression} */ (
context.visit(value, {
...context.state,
in_derived: rune === '$derived'
})
);
let expression = /** @type {Expression} */ (context.visit(value));
let rhs = value;
@ -253,7 +243,7 @@ export function VariableDeclaration(node, context) {
b.thunk(expression, true),
location ? b.literal(location) : undefined
);
call = b.call(b.await(b.call('$.save', call)));
call = save(call);
}
if (dev) {

@ -130,7 +130,7 @@ export function build_component(node, component_name, context) {
} else if (attribute.type === 'SpreadAttribute') {
const expression = /** @type {Expression} */ (context.visit(attribute));
if (attribute.metadata.expression.has_state) {
if (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

@ -6,7 +6,7 @@ import { walk } from 'zimmerframe';
import { set_scope } from '../../scope.js';
import { extract_identifiers } from '../../../utils/ast.js';
import * as b from '#compiler/builders';
import { dev, filename } from '../../../state.js';
import { component_name, dev, filename } from '../../../state.js';
import { render_stylesheet } from '../css/index.js';
import { AssignmentExpression } from './visitors/AssignmentExpression.js';
import { AwaitBlock } from './visitors/AwaitBlock.js';
@ -40,6 +40,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';
/** @type {Visitors} */
const global_visitors = {
@ -187,23 +188,21 @@ export function server_component(analysis, options) {
template.body = [
...snippets,
b.let('$$settled', b.true),
b.let('$$inner_payload'),
b.let('$$inner_renderer'),
b.function_declaration(
b.id('$$render_inner'),
[b.id('$$payload')],
[b.id('$$renderer')],
b.block(/** @type {Statement[]} */ (rest))
),
b.do_while(
b.unary('!', b.id('$$settled')),
b.block([
b.stmt(b.assignment('=', b.id('$$settled'), b.true)),
b.stmt(
b.assignment('=', b.id('$$inner_payload'), b.call('$.copy_payload', b.id('$$payload')))
),
b.stmt(b.call('$$render_inner', b.id('$$inner_payload')))
b.stmt(b.assignment('=', b.id('$$inner_renderer'), b.call('$$renderer.copy'))),
b.stmt(b.call('$$render_inner', b.id('$$inner_renderer')))
])
),
b.stmt(b.call('$.assign_payload', b.id('$$payload'), b.id('$$inner_payload')))
b.stmt(b.call('$$renderer.subsume', b.id('$$inner_renderer')))
];
}
@ -239,26 +238,31 @@ export function server_component(analysis, options) {
template.body.push(b.stmt(b.call('$.bind_props', b.id('$$props'), b.object(props))));
}
const component_block = b.block([
let component_block = b.block([
.../** @type {Statement[]} */ (instance.body),
.../** @type {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;
if (analysis.props_id) {
// need to be placed on first line of the component for hydration
component_block.body.unshift(
b.const(analysis.props_id, b.call('$.props_id', b.id('$$payload')))
b.const(analysis.props_id, b.call('$.props_id', b.id('$$renderer')))
);
}
let should_inject_context = dev || analysis.needs_context;
if (should_inject_context) {
component_block.body.unshift(b.stmt(b.call('$.push', dev && b.id(analysis.name))));
component_block.body.push(b.stmt(b.call('$.pop')));
component_block = b.block([
call_component_renderer(component_block, dev && b.id(component_name))
]);
}
if (analysis.uses_rest_props) {
@ -297,7 +301,7 @@ export function server_component(analysis, options) {
const code = b.literal(render_stylesheet(analysis.source, analysis, options).code);
body.push(b.const('$$css', b.object([b.init('hash', hash), b.init('code', code)])));
component_block.body.unshift(b.stmt(b.call('$$payload.css.add', b.id('$$css'))));
component_block.body.unshift(b.stmt(b.call('$$renderer.global.css.add', b.id('$$css'))));
}
let should_inject_props =
@ -311,7 +315,7 @@ export function server_component(analysis, options) {
const component_function = b.function_declaration(
b.id(analysis.name),
should_inject_props ? [b.id('$$payload'), b.id('$$props')] : [b.id('$$payload')],
should_inject_props ? [b.id('$$renderer'), b.id('$$props')] : [b.id('$$renderer')],
component_block
);
@ -377,6 +381,10 @@ export function server_component(analysis, options) {
);
}
if (options.experimental.async) {
body.unshift(b.imports([], 'svelte/internal/flags/async'));
}
return {
type: 'Program',
sourceType: 'module',

@ -1,29 +1,33 @@
/** @import { BlockStatement, Expression, Pattern } from 'estree' */
/** @import { BlockStatement, Expression, Pattern, Statement } from 'estree' */
/** @import { AST } from '#compiler' */
/** @import { ComponentContext } from '../types.js' */
import * as b from '#compiler/builders';
import { block_close } from './shared/utils.js';
import { block_close, create_async_block } from './shared/utils.js';
/**
* @param {AST.AwaitBlock} node
* @param {ComponentContext} context
*/
export function AwaitBlock(node, context) {
context.state.template.push(
b.stmt(
b.call(
'$.await',
b.id('$$payload'),
/** @type {Expression} */ (context.visit(node.expression)),
b.thunk(
node.pending ? /** @type {BlockStatement} */ (context.visit(node.pending)) : b.block([])
),
b.arrow(
node.value ? [/** @type {Pattern} */ (context.visit(node.value))] : [],
node.then ? /** @type {BlockStatement} */ (context.visit(node.then)) : b.block([])
)
/** @type {Statement} */
let statement = b.stmt(
b.call(
'$.await',
b.id('$$renderer'),
/** @type {Expression} */ (context.visit(node.expression)),
b.thunk(
node.pending ? /** @type {BlockStatement} */ (context.visit(node.pending)) : b.block([])
),
b.arrow(
node.value ? [/** @type {Pattern} */ (context.visit(node.value))] : [],
node.then ? /** @type {BlockStatement} */ (context.visit(node.then)) : b.block([])
)
),
block_close
)
);
if (node.metadata.expression.has_await) {
statement = create_async_block(b.block([statement]));
}
context.state.template.push(statement, block_close);
}

@ -1,25 +1,40 @@
/** @import { AwaitExpression } from 'estree' */
/** @import { Context } from '../types.js' */
import * as b from '../../../../utils/builders.js';
/** @import { AwaitExpression, Expression } from 'estree' */
/** @import { Context } from '../types' */
import { save } from '../../../../utils/ast.js';
/**
* @param {AwaitExpression} node
* @param {Context} context
*/
export function AwaitExpression(node, context) {
// if `await` is inside a function, or inside `<script module>`,
// allow it, otherwise error
if (
context.state.scope.function_depth === 0 ||
context.path.some(
(node) =>
node.type === 'ArrowFunctionExpression' ||
node.type === 'FunctionDeclaration' ||
node.type === 'FunctionExpression'
)
) {
return context.next();
const argument = /** @type {Expression} */ (context.visit(node.argument));
if (context.state.analysis.pickled_awaits.has(node)) {
return save(argument);
}
// we also need to restore context after block expressions
let i = context.path.length;
while (i--) {
const parent = context.path[i];
if (
parent.type === 'ArrowFunctionExpression' ||
parent.type === 'FunctionExpression' ||
parent.type === 'FunctionDeclaration'
) {
break;
}
// @ts-ignore
if (parent.metadata) {
if (parent.type !== 'ExpressionTag' && parent.type !== 'Fragment') {
return save(argument);
}
break;
}
}
return b.call('$.await_outside_boundary');
return argument === node.argument ? node : { ...node, argument };
}

@ -1,9 +1,8 @@
/** @import { BlockStatement, Expression, Statement } from 'estree' */
/** @import { AST } from '#compiler' */
/** @import { ComponentContext } from '../types.js' */
import { BLOCK_OPEN_ELSE } from '../../../../../internal/server/hydration.js';
import * as b from '#compiler/builders';
import { block_close, block_open } from './shared/utils.js';
import { block_close, block_open, block_open_else, create_async_block } from './shared/utils.js';
/**
* @param {AST.EachBlock} node
@ -18,7 +17,9 @@ export function EachBlock(node, context) {
each_node_meta.contains_group_binding || !node.index ? each_node_meta.index : b.id(node.index);
const array_id = state.scope.root.unique('each_array');
state.init.push(b.const(array_id, b.call('$.ensure_array_like', collection)));
/** @type {Statement} */
let block = b.block([b.const(array_id, b.call('$.ensure_array_like', collection))]);
/** @type {Statement[]} */
const each = [];
@ -44,23 +45,27 @@ export function EachBlock(node, context) {
);
if (node.fallback) {
const open = b.stmt(b.call(b.member(b.id('$$payload.out'), b.id('push')), block_open));
const open = b.stmt(b.call(b.id('$$renderer.push'), block_open));
const fallback = /** @type {BlockStatement} */ (context.visit(node.fallback));
fallback.body.unshift(
b.stmt(b.call(b.member(b.id('$$payload.out'), b.id('push')), b.literal(BLOCK_OPEN_ELSE)))
);
fallback.body.unshift(b.stmt(b.call(b.id('$$renderer.push'), block_open_else)));
state.template.push(
block.body.push(
b.if(
b.binary('!==', b.member(array_id, 'length'), b.literal(0)),
b.block([open, for_loop]),
fallback
),
block_close
)
);
} else {
state.template.push(block_open, for_loop, block_close);
state.template.push(block_open);
block.body.push(for_loop);
}
if (node.metadata.expression.has_await) {
state.template.push(create_async_block(block), block_close);
} else {
state.template.push(...block.body, block_close);
}
}

@ -1,9 +1,8 @@
/** @import { BlockStatement, Expression } from 'estree' */
/** @import { BlockStatement, Expression, Statement } from 'estree' */
/** @import { AST } from '#compiler' */
/** @import { ComponentContext } from '../types.js' */
import { BLOCK_OPEN_ELSE } from '../../../../../internal/server/hydration.js';
import * as b from '#compiler/builders';
import { block_close, block_open } from './shared/utils.js';
import { block_close, block_open, block_open_else, create_async_block } from './shared/utils.js';
/**
* @param {AST.IfBlock} node
@ -17,13 +16,16 @@ export function IfBlock(node, context) {
? /** @type {BlockStatement} */ (context.visit(node.alternate))
: b.block([]);
consequent.body.unshift(
b.stmt(b.call(b.member(b.id('$$payload.out'), b.id('push')), block_open))
);
consequent.body.unshift(b.stmt(b.call(b.id('$$renderer.push'), block_open)));
alternate.body.unshift(
b.stmt(b.call(b.member(b.id('$$payload.out'), b.id('push')), b.literal(BLOCK_OPEN_ELSE)))
);
alternate.body.unshift(b.stmt(b.call(b.id('$$renderer.push'), block_open_else)));
context.state.template.push(b.if(test, consequent, alternate), block_close);
/** @type {Statement} */
let statement = b.if(test, consequent, alternate);
if (node.metadata.expression.has_await) {
statement = create_async_block(b.block([statement]));
}
context.state.template.push(statement, block_close);
}

@ -8,7 +8,13 @@ import { dev, locator } from '../../../../state.js';
import * as b from '#compiler/builders';
import { clean_nodes, determine_namespace_for_children } from '../../utils.js';
import { build_element_attributes, build_spread_object } from './shared/element.js';
import { process_children, build_template, build_attribute_value } from './shared/utils.js';
import {
process_children,
build_template,
build_attribute_value,
create_child_block,
PromiseOptimiser
} from './shared/utils.js';
/**
* @param {AST.RegularElement} node
@ -22,21 +28,38 @@ export function RegularElement(node, context) {
...context.state,
namespace,
preserve_whitespace:
context.state.preserve_whitespace || node.name === 'pre' || node.name === 'textarea'
context.state.preserve_whitespace || node.name === 'pre' || node.name === 'textarea',
init: [],
template: []
};
const node_is_void = is_void(node.name);
context.state.template.push(b.literal(`<${node.name}`));
const body = build_element_attributes(node, { ...context, state });
context.state.template.push(b.literal(node_is_void ? '/>' : '>')); // add `/>` for XHTML compliance
const optimiser = new PromiseOptimiser();
state.template.push(b.literal(`<${node.name}`));
const body = build_element_attributes(node, { ...context, state }, optimiser.transform);
state.template.push(b.literal(node_is_void ? '/>' : '>')); // add `/>` for XHTML compliance
if ((node.name === 'script' || node.name === 'style') && node.fragment.nodes.length === 1) {
context.state.template.push(
state.template.push(
b.literal(/** @type {AST.Text} */ (node.fragment.nodes[0]).data),
b.literal(`</${node.name}>`)
);
// TODO this is a real edge case, would be good to DRY this out
if (optimiser.expressions.length > 0) {
context.state.template.push(
create_child_block(
b.block([optimiser.apply(), ...state.init, ...build_template(state.template)]),
true
)
);
} else {
context.state.init.push(...state.init);
context.state.template.push(...state.template);
}
return;
}
@ -63,7 +86,7 @@ export function RegularElement(node, context) {
b.stmt(
b.call(
'$.push_element',
b.id('$$payload'),
b.id('$$renderer'),
b.literal(node.name),
b.literal(location.line),
b.literal(location.column)
@ -72,84 +95,92 @@ export function RegularElement(node, context) {
);
}
let select_with_value = false;
if (node.name === 'select') {
const value = node.attributes.find(
(attribute) =>
(attribute.type === 'Attribute' || attribute.type === 'BindDirective') &&
attribute.name === 'value'
);
if (node.attributes.some((attribute) => attribute.type === 'SpreadAttribute')) {
select_with_value = true;
state.template.push(
b.stmt(
b.assignment(
'=',
b.id('$$payload.select_value'),
b.member(
build_spread_object(
node,
node.attributes.filter(
(attribute) =>
attribute.type === 'Attribute' ||
attribute.type === 'BindDirective' ||
attribute.type === 'SpreadAttribute'
),
context
),
'value',
false,
true
)
)
)
);
} else if (value) {
select_with_value = true;
const left = b.id('$$payload.select_value');
if (value.type === 'Attribute') {
state.template.push(
b.stmt(b.assignment('=', left, build_attribute_value(value.value, context)))
);
} else if (value.type === 'BindDirective') {
state.template.push(
b.stmt(
b.assignment(
'=',
left,
value.expression.type === 'SequenceExpression'
? /** @type {Expression} */ (context.visit(b.call(value.expression.expressions[0])))
: /** @type {Expression} */ (context.visit(value.expression))
)
)
);
}
}
}
if (
node.name === 'option' &&
!node.attributes.some(
node.name === 'select' &&
node.attributes.some(
(attribute) =>
attribute.type === 'SpreadAttribute' ||
((attribute.type === 'Attribute' || attribute.type === 'BindDirective') &&
attribute.name === 'value')
attribute.name === 'value') ||
attribute.type === 'SpreadAttribute'
)
) {
const attributes = build_spread_object(
node,
node.attributes.filter(
(attribute) =>
attribute.type === 'Attribute' ||
attribute.type === 'BindDirective' ||
attribute.type === 'SpreadAttribute'
),
context,
optimiser.transform
);
const inner_state = { ...state, template: [], init: [] };
process_children(trimmed, { ...context, state: inner_state });
state.template.push(
b.stmt(
b.call(
'$.valueless_option',
b.id('$$payload'),
b.thunk(b.block([...inner_state.init, ...build_template(inner_state.template)]))
)
)
const fn = b.arrow(
[b.id('$$renderer')],
b.block([...state.init, ...build_template(inner_state.template)])
);
} else if (body !== null) {
const statement = b.stmt(b.call('$$renderer.select', attributes, fn));
if (optimiser.expressions.length > 0) {
context.state.template.push(
create_child_block(b.block([optimiser.apply(), ...state.init, statement]), true)
);
} else {
context.state.template.push(...state.init, statement);
}
return;
}
if (node.name === 'option') {
const attributes = build_spread_object(
node,
node.attributes.filter(
(attribute) =>
attribute.type === 'Attribute' ||
attribute.type === 'BindDirective' ||
attribute.type === 'SpreadAttribute'
),
context,
optimiser.transform
);
let body;
if (node.metadata.synthetic_value_node) {
body = optimiser.transform(
node.metadata.synthetic_value_node.expression,
node.metadata.synthetic_value_node.metadata.expression
);
} else {
const inner_state = { ...state, template: [], init: [] };
process_children(trimmed, { ...context, state: inner_state });
body = b.arrow(
[b.id('$$renderer')],
b.block([...state.init, ...build_template(inner_state.template)])
);
}
const statement = b.stmt(b.call('$$renderer.option', attributes, body));
if (optimiser.expressions.length > 0) {
context.state.template.push(
create_child_block(b.block([optimiser.apply(), ...state.init, statement]), true)
);
} else {
context.state.template.push(...state.init, statement);
}
return;
}
if (body !== null) {
// if this is a `<textarea>` value or a contenteditable binding, we only add
// the body if the attribute/binding is falsy
const inner_state = { ...state, template: [], init: [] };
@ -174,10 +205,6 @@ export function RegularElement(node, context) {
process_children(trimmed, { ...context, state });
}
if (select_with_value) {
state.template.push(b.stmt(b.assignment('=', b.id('$$payload.select_value'), b.void0)));
}
if (!node_is_void) {
state.template.push(b.literal(`</${node.name}>`));
}
@ -185,4 +212,16 @@ export function RegularElement(node, context) {
if (dev) {
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
)
);
} else {
context.state.init.push(...state.init);
context.state.template.push(...state.template);
}
}

@ -23,7 +23,7 @@ export function RenderTag(node, context) {
b.stmt(
(node.expression.type === 'CallExpression' ? b.call : b.maybe_call)(
snippet_function,
b.id('$$payload'),
b.id('$$renderer'),
...snippet_args
)
)

@ -2,7 +2,13 @@
/** @import { AST } from '#compiler' */
/** @import { ComponentContext } from '../types.js' */
import * as b from '#compiler/builders';
import { empty_comment, build_attribute_value } from './shared/utils.js';
import {
build_attribute_value,
PromiseOptimiser,
create_async_block,
block_open,
block_close
} from './shared/utils.js';
/**
* @param {AST.SlotElement} node
@ -15,13 +21,22 @@ export function SlotElement(node, context) {
/** @type {Expression[]} */
const spreads = [];
const optimiser = new PromiseOptimiser();
let name = b.literal('default');
for (const attribute of node.attributes) {
if (attribute.type === 'SpreadAttribute') {
spreads.push(/** @type {Expression} */ (context.visit(attribute)));
let expression = /** @type {Expression} */ (context.visit(attribute));
spreads.push(optimiser.transform(expression, attribute.metadata.expression));
} else if (attribute.type === 'Attribute') {
const value = build_attribute_value(attribute.value, context, false, true);
const value = build_attribute_value(
attribute.value,
context,
optimiser.transform,
false,
true
);
if (attribute.name === 'name') {
name = /** @type {Literal} */ (value);
@ -43,12 +58,17 @@ export function SlotElement(node, context) {
const slot = b.call(
'$.slot',
b.id('$$payload'),
b.id('$$renderer'),
b.id('$$props'),
name,
props_expression,
fallback
);
context.state.template.push(empty_comment, b.stmt(slot), empty_comment);
const statement =
optimiser.expressions.length > 0
? create_async_block(b.block([optimiser.apply(), b.stmt(slot)]))
: b.stmt(slot);
context.state.template.push(block_open, statement, block_close);
}

@ -11,7 +11,7 @@ import * as b from '#compiler/builders';
export function SnippetBlock(node, context) {
let fn = b.function_declaration(
node.expression,
[b.id('$$payload'), ...node.parameters],
[b.id('$$renderer'), ...node.parameters],
/** @type {BlockStatement} */ (context.visit(node.body))
);
@ -21,7 +21,7 @@ export function SnippetBlock(node, context) {
const statements = node.metadata.can_hoist ? context.state.hoisted : context.state.init;
if (dev) {
fn.body.body.unshift(b.stmt(b.call('$.validate_snippet_args', b.id('$$payload'))));
fn.body.body.unshift(b.stmt(b.call('$.validate_snippet_args', b.id('$$renderer'))));
statements.push(b.stmt(b.call('$.prevent_snippet_stringification', fn.id)));
}

@ -1,17 +1,14 @@
/** @import { BlockStatement } from 'estree' */
/** @import { AST } from '#compiler' */
/** @import { ComponentContext } from '../types' */
import { BLOCK_CLOSE, BLOCK_OPEN } from '../../../../../internal/server/hydration.js';
import * as b from '#compiler/builders';
import { build_attribute_value } from './shared/utils.js';
import { block_close, block_open, block_open_else, build_attribute_value } from './shared/utils.js';
/**
* @param {AST.SvelteBoundary} node
* @param {ComponentContext} context
*/
export function SvelteBoundary(node, context) {
context.state.template.push(b.literal(BLOCK_OPEN));
// if this has a `pending` snippet, render it
const pending_attribute = /** @type {AST.Attribute} */ (
node.attributes.find((node) => node.type === 'Attribute' && node.name === 'pending')
@ -23,16 +20,23 @@ export function SvelteBoundary(node, context) {
)
);
if (pending_attribute) {
const value = build_attribute_value(pending_attribute.value, context, false, true);
context.state.template.push(b.call(value, b.id('$$payload')));
} else if (pending_snippet) {
context.state.template.push(
/** @type {BlockStatement} */ (context.visit(pending_snippet.body))
);
if (pending_attribute || pending_snippet) {
const pending = pending_attribute
? b.call(
build_attribute_value(
pending_attribute.value,
context,
(expression) => expression,
false,
true
),
b.id('$$renderer')
)
: /** @type {BlockStatement} */ (context.visit(pending_snippet.body));
context.state.template.push(block_open_else, pending, block_close);
} else {
context.state.template.push(/** @type {BlockStatement} */ (context.visit(node.fragment)));
const block = /** @type {BlockStatement} */ (context.visit(node.fragment));
context.state.template.push(block_open, block, block_close);
}
context.state.template.push(b.literal(BLOCK_CLOSE));
}

@ -1,12 +1,12 @@
/** @import { Location } from 'locate-character' */
/** @import { BlockStatement, Expression } from 'estree' */
/** @import { BlockStatement, Expression, Statement } from 'estree' */
/** @import { AST } from '#compiler' */
/** @import { ComponentContext } from '../types.js' */
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 } from './shared/utils.js';
import { build_template, create_child_block, PromiseOptimiser } from './shared/utils.js';
/**
* @param {AST.SvelteElement} node
@ -37,7 +37,9 @@ export function SvelteElement(node, context) {
init: []
};
build_element_attributes(node, { ...context, state });
const optimiser = new PromiseOptimiser();
build_element_attributes(node, { ...context, state }, optimiser.transform);
if (dev) {
const location = /** @type {Location} */ (locator(node.start));
@ -45,7 +47,7 @@ export function SvelteElement(node, context) {
b.stmt(
b.call(
'$.push_element',
b.id('$$payload'),
b.id('$$renderer'),
tag,
b.literal(location.line),
b.literal(location.column)
@ -57,18 +59,23 @@ export function SvelteElement(node, context) {
const attributes = b.block([...state.init, ...build_template(state.template)]);
const children = /** @type {BlockStatement} */ (context.visit(node.fragment, state));
context.state.template.push(
b.stmt(
b.call(
'$.element',
b.id('$$payload'),
tag,
attributes.body.length > 0 && b.thunk(attributes),
children.body.length > 0 && b.thunk(children)
)
/** @type {Statement} */
let statement = b.stmt(
b.call(
'$.element',
b.id('$$renderer'),
tag,
attributes.body.length > 0 && b.thunk(attributes),
children.body.length > 0 && b.thunk(children)
)
);
if (optimiser.expressions.length > 0) {
statement = create_child_block(b.block([optimiser.apply(), statement]), true);
}
context.state.template.push(statement);
if (dev) {
context.state.template.push(b.stmt(b.call('$.pop_element')));
}

@ -11,6 +11,6 @@ export function SvelteHead(node, context) {
const block = /** @type {BlockStatement} */ (context.visit(node.fragment));
context.state.template.push(
b.stmt(b.call('$.head', b.id('$$payload'), b.arrow([b.id('$$payload')], block)))
b.stmt(b.call('$.head', b.id('$$renderer'), b.arrow([b.id('$$renderer')], block)))
);
}

@ -13,5 +13,9 @@ export function TitleElement(node, context) {
process_children(node.fragment.nodes, { ...context, state: { ...context.state, template } });
template.push(b.literal('</title>'));
context.state.init.push(...build_template(template, b.id('$$payload.title'), '='));
context.state.init.push(
b.stmt(
b.call('$$renderer.title', b.arrow([b.id('$$renderer')], b.block(build_template(template))))
)
);
}

@ -1,7 +1,13 @@
/** @import { BlockStatement, Expression, Pattern, Property, SequenceExpression, Statement } from 'estree' */
/** @import { AST } from '#compiler' */
/** @import { ComponentContext } from '../../types.js' */
import { empty_comment, build_attribute_value } from './utils.js';
import {
empty_comment,
build_attribute_value,
create_async_block,
PromiseOptimiser,
build_template
} from './utils.js';
import * as b from '#compiler/builders';
import { is_element_node } from '../../../../nodes.js';
import { dev } from '../../../../../state.js';
@ -72,16 +78,26 @@ export function build_inline_component(node, expression, context) {
}
}
const optimiser = new PromiseOptimiser();
for (const attribute of node.attributes) {
if (attribute.type === 'LetDirective') {
if (!slot_scope_applies_to_itself) {
lets.default.push(attribute);
}
} else if (attribute.type === 'SpreadAttribute') {
props_and_spreads.push(/** @type {Expression} */ (context.visit(attribute)));
let expression = /** @type {Expression} */ (context.visit(attribute));
props_and_spreads.push(optimiser.transform(expression, attribute.metadata.expression));
} else if (attribute.type === 'Attribute') {
const value = build_attribute_value(
attribute.value,
context,
optimiser.transform,
false,
true
);
if (attribute.name.startsWith('--')) {
const value = build_attribute_value(attribute.value, context, false, true);
custom_css_props.push(b.init(attribute.name, value));
continue;
}
@ -90,7 +106,6 @@ export function build_inline_component(node, expression, context) {
has_children_prop = true;
}
const value = build_attribute_value(attribute.value, context, false, true);
push_prop(b.prop('init', b.key(attribute.name), value));
} else if (attribute.type === 'BindDirective' && attribute.name !== 'this') {
if (attribute.expression.type === 'SequenceExpression') {
@ -201,7 +216,7 @@ export function build_inline_component(node, expression, context) {
if (block.body.length === 0) continue;
/** @type {Pattern[]} */
const params = [b.id('$$payload')];
const params = [b.id('$$renderer')];
if (lets[slot_name].length > 0) {
const pattern = b.object_pattern(
@ -278,7 +293,7 @@ export function build_inline_component(node, expression, context) {
let statement = b.stmt(
(node.type === 'SvelteComponent' ? b.maybe_call : b.call)(
expression,
b.id('$$payload'),
b.id('$$renderer'),
props_expression
)
);
@ -291,27 +306,33 @@ export function build_inline_component(node, expression, context) {
node.type === 'SvelteComponent' || (node.type === 'Component' && node.metadata.dynamic);
if (custom_css_props.length > 0) {
context.state.template.push(
b.stmt(
b.call(
'$.css_props',
b.id('$$payload'),
b.literal(context.state.namespace === 'svg' ? false : true),
b.object(custom_css_props),
b.thunk(b.block([statement])),
dynamic && b.true
)
statement = b.stmt(
b.call(
'$.css_props',
b.id('$$renderer'),
b.literal(context.state.namespace === 'svg' ? false : true),
b.object(custom_css_props),
b.thunk(b.block([statement])),
dynamic && b.true
)
);
} else {
if (dynamic) {
context.state.template.push(empty_comment);
}
}
context.state.template.push(statement);
if (optimiser.expressions.length > 0) {
statement = create_async_block(b.block([optimiser.apply(), statement]));
}
if (!context.state.skip_hydration_boundaries) {
context.state.template.push(empty_comment);
}
if (dynamic && custom_css_props.length === 0) {
context.state.template.push(empty_comment);
}
context.state.template.push(statement);
if (
!context.state.skip_hydration_boundaries &&
custom_css_props.length === 0 &&
optimiser.expressions.length === 0
) {
context.state.template.push(empty_comment);
}
}

@ -1,5 +1,5 @@
/** @import { ArrayExpression, Expression, Literal, ObjectExpression } from 'estree' */
/** @import { AST } from '#compiler' */
/** @import { AST, ExpressionMetadata } 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';
@ -30,8 +30,9 @@ const WHITESPACE_INSENSITIVE_ATTRIBUTES = ['class', 'style'];
* their output to be the child content instead. In this case, an object is returned.
* @param {AST.RegularElement | AST.SvelteElement} node
* @param {import('zimmerframe').Context<AST.SvelteNode, ComponentServerTransformState>} context
* @param {(expression: Expression, metadata: ExpressionMetadata) => Expression} transform
*/
export function build_element_attributes(node, context) {
export function build_element_attributes(node, context, transform) {
/** @type {Array<AST.Attribute | AST.SpreadAttribute>} */
const attributes = [];
@ -62,7 +63,8 @@ export function build_element_attributes(node, context) {
// also see related code in analysis phase
attribute.value[0].data = '\n' + attribute.value[0].data;
}
content = b.call('$.escape', build_attribute_value(attribute.value, context));
content = b.call('$.escape', build_attribute_value(attribute.value, context, transform));
} else if (node.name !== 'select') {
// omit value attribute for select elements, it's irrelevant for the initially selected value and has no
// effect on the selected value after the user interacts with the select element (the value _property_ does, but not the attribute)
@ -150,12 +152,12 @@ export function build_element_attributes(node, context) {
expression: is_checkbox
? b.call(
b.member(attribute.expression, 'includes'),
build_attribute_value(value_attribute.value, context)
build_attribute_value(value_attribute.value, context, transform)
)
: b.binary(
'===',
attribute.expression,
build_attribute_value(value_attribute.value, context)
build_attribute_value(value_attribute.value, context, transform)
),
metadata: {
expression: create_expression_metadata()
@ -202,30 +204,14 @@ export function build_element_attributes(node, context) {
}
if (has_spread) {
build_element_spread_attributes(node, attributes, style_directives, class_directives, context);
if (node.name === 'option') {
context.state.template.push(
b.call(
'$.maybe_selected',
b.id('$$payload'),
b.member(
build_spread_object(
node,
node.attributes.filter(
(attribute) =>
attribute.type === 'Attribute' ||
attribute.type === 'BindDirective' ||
attribute.type === 'SpreadAttribute'
),
context
),
'value',
false,
true
)
)
);
}
build_element_spread_attributes(
node,
attributes,
style_directives,
class_directives,
context,
transform
);
} else {
const css_hash = node.metadata.scoped ? context.state.analysis.css.hash : null;
@ -240,6 +226,7 @@ export function build_element_attributes(node, context) {
build_attribute_value(
attribute.value,
context,
transform,
WHITESPACE_INSENSITIVE_ATTRIBUTES.includes(name)
)
).value;
@ -260,22 +247,13 @@ export function build_element_attributes(node, context) {
);
}
if (node.name === 'option' && name === 'value') {
context.state.template.push(
b.call(
'$.maybe_selected',
b.id('$$payload'),
literal_value != null ? b.literal(/** @type {any} */ (literal_value)) : b.void0
)
);
}
continue;
}
const value = build_attribute_value(
attribute.value,
context,
transform,
WHITESPACE_INSENSITIVE_ATTRIBUTES.includes(name)
);
@ -286,18 +264,16 @@ export function build_element_attributes(node, context) {
}
context.state.template.push(b.literal(` ${name}="${escape_html(value.value, true)}"`));
} else if (name === 'class') {
context.state.template.push(build_attr_class(class_directives, value, context, css_hash));
context.state.template.push(
build_attr_class(class_directives, value, context, css_hash, transform)
);
} else if (name === 'style') {
context.state.template.push(build_attr_style(style_directives, value, context));
context.state.template.push(build_attr_style(style_directives, value, context, transform));
} else {
context.state.template.push(
b.call('$.attr', b.literal(name), value, is_boolean_attribute(name) && b.true)
);
}
if (name === 'value' && node.name === 'option') {
context.state.template.push(b.call('$.maybe_selected', b.id('$$payload'), value));
}
}
}
@ -328,17 +304,20 @@ function get_attribute_name(element, attribute) {
* @param {AST.RegularElement | AST.SvelteElement} element
* @param {Array<AST.Attribute | AST.SpreadAttribute | AST.BindDirective>} attributes
* @param {ComponentContext} context
* @param {(expression: Expression, metadata: ExpressionMetadata) => Expression} transform
*/
export function build_spread_object(element, attributes, context) {
return b.object(
export function build_spread_object(element, attributes, context, transform) {
const object = b.object(
attributes.map((attribute) => {
if (attribute.type === 'Attribute') {
const name = get_attribute_name(element, attribute);
const value = build_attribute_value(
attribute.value,
context,
transform,
WHITESPACE_INSENSITIVE_ATTRIBUTES.includes(name)
);
return b.prop('init', b.key(name), value);
} else if (attribute.type === 'BindDirective') {
const name = get_attribute_name(element, attribute);
@ -346,12 +325,20 @@ export function build_spread_object(element, attributes, context) {
attribute.expression.type === 'SequenceExpression'
? b.call(attribute.expression.expressions[0])
: /** @type {Expression} */ (context.visit(attribute.expression));
return b.prop('init', b.key(name), value);
}
return b.spread(/** @type {Expression} */ (context.visit(attribute)));
return b.spread(
transform(
/** @type {Expression} */ (context.visit(attribute)),
attribute.metadata.expression
)
);
})
);
return object;
}
/**
@ -361,39 +348,48 @@ export function build_spread_object(element, attributes, context) {
* @param {AST.StyleDirective[]} style_directives
* @param {AST.ClassDirective[]} class_directives
* @param {ComponentContext} context
* @param {(expression: Expression, metadata: ExpressionMetadata) => Expression} transform
*/
function build_element_spread_attributes(
element,
attributes,
style_directives,
class_directives,
context
context,
transform
) {
let classes;
let styles;
let flags = 0;
let has_await = false;
if (class_directives.length) {
const properties = class_directives.map((directive) =>
b.init(
const properties = class_directives.map((directive) => {
has_await ||= directive.metadata.expression.has_await;
return b.init(
directive.name,
directive.expression.type === 'Identifier' && directive.expression.name === directive.name
? b.id(directive.name)
: /** @type {Expression} */ (context.visit(directive.expression))
)
);
);
});
classes = b.object(properties);
}
if (style_directives.length > 0) {
const properties = style_directives.map((directive) =>
b.init(
const properties = style_directives.map((directive) => {
has_await ||= directive.metadata.expression.has_await;
return b.init(
directive.name,
directive.value === true
? b.id(directive.name)
: build_attribute_value(directive.value, context, true)
)
);
: build_attribute_value(directive.value, context, transform, true)
);
});
styles = b.object(properties);
}
@ -406,15 +402,18 @@ function build_element_spread_attributes(
flags |= ELEMENT_IS_INPUT;
}
const object = build_spread_object(element, attributes, context);
const object = build_spread_object(element, attributes, context, transform);
const css_hash =
element.metadata.scoped && context.state.analysis.css.hash
? b.literal(context.state.analysis.css.hash)
: b.null;
: undefined;
const args = [object, css_hash, classes, styles, flags ? b.literal(flags) : undefined];
context.state.template.push(b.call('$.spread_attributes', ...args));
let call = b.call('$.attributes', ...args);
context.state.template.push(call);
}
/**
@ -423,8 +422,9 @@ function build_element_spread_attributes(
* @param {Expression} expression
* @param {ComponentContext} context
* @param {string | null} hash
* @param {(expression: Expression, metadata: ExpressionMetadata) => Expression} transform
*/
function build_attr_class(class_directives, expression, context, hash) {
function build_attr_class(class_directives, expression, context, hash, transform) {
/** @type {ObjectExpression | undefined} */
let directives;
@ -434,7 +434,10 @@ function build_attr_class(class_directives, expression, context, hash) {
b.prop(
'init',
b.literal(directive.name),
/** @type {Expression} */ (context.visit(directive.expression, context.state))
transform(
/** @type {Expression} */ (context.visit(directive.expression, context.state)),
directive.metadata.expression
)
)
)
);
@ -457,9 +460,10 @@ function build_attr_class(class_directives, expression, context, hash) {
*
* @param {AST.StyleDirective[]} style_directives
* @param {Expression} expression
* @param {ComponentContext} context
* @param {ComponentContext} context,
* @param {(expression: Expression, metadata: ExpressionMetadata) => Expression} transform
*/
function build_attr_style(style_directives, expression, context) {
function build_attr_style(style_directives, expression, context, transform) {
/** @type {ArrayExpression | ObjectExpression | undefined} */
let directives;
@ -471,7 +475,7 @@ function build_attr_style(style_directives, expression, context) {
const expression =
directive.value === true
? b.id(directive.name)
: build_attribute_value(directive.value, context, true);
: build_attribute_value(directive.value, context, transform, true);
let name = directive.name;
if (name[0] !== '-' || name[1] !== '-') {

@ -1,20 +1,25 @@
/** @import { AssignmentOperator, Expression, Identifier, Node, Statement } from 'estree' */
/** @import { AST } from '#compiler' */
/** @import { AssignmentOperator, Expression, Identifier, Node, Statement, BlockStatement } from 'estree' */
/** @import { AST, ExpressionMetadata } from '#compiler' */
/** @import { ComponentContext, ServerTransformState } from '../../types.js' */
import { escape_html } from '../../../../../../escaping.js';
import {
BLOCK_CLOSE,
BLOCK_OPEN,
BLOCK_OPEN_ELSE,
EMPTY_COMMENT
} from '../../../../../../internal/server/hydration.js';
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';
/** 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);
/** Opens an if/each block, so that we can remove nodes in the case of a mismatch */
export const block_open_else = b.literal(BLOCK_OPEN_ELSE);
/** Closes an if/each block, so that we can remove nodes in the case of a mismatch. Also serves as an anchor for these blocks */
export const block_close = b.literal(BLOCK_CLOSE);
@ -32,6 +37,10 @@ export function process_children(nodes, { visit, state }) {
let sequence = [];
function flush() {
if (sequence.length === 0) {
return;
}
let quasi = b.quasi('', false);
const quasis = [quasi];
@ -63,26 +72,25 @@ export function process_children(nodes, { visit, state }) {
}
state.template.push(b.template(quasis, expressions));
sequence = [];
}
for (let i = 0; i < nodes.length; i += 1) {
const node = nodes[i];
if (node.type === 'Text' || node.type === 'Comment' || node.type === 'ExpressionTag') {
for (const node of nodes) {
if (node.type === 'ExpressionTag' && node.metadata.expression.has_await) {
flush();
const visited = /** @type {Expression} */ (visit(node.expression));
state.template.push(
b.stmt(b.call('$$renderer.push', b.thunk(b.call('$.escape', visited), true)))
);
} else if (node.type === 'Text' || node.type === 'Comment' || node.type === 'ExpressionTag') {
sequence.push(node);
} else {
if (sequence.length > 0) {
flush();
sequence = [];
}
flush();
visit(node, { ...state });
}
}
if (sequence.length > 0) {
flush();
}
flush();
}
/**
@ -95,11 +103,9 @@ function is_statement(node) {
/**
* @param {Array<Statement | Expression>} template
* @param {Identifier} out
* @param {AssignmentOperator | 'push'} operator
* @returns {Statement[]}
*/
export function build_template(template, out = b.id('$$payload.out'), operator = 'push') {
export function build_template(template) {
/** @type {string[]} */
let strings = [];
@ -110,32 +116,18 @@ export function build_template(template, out = b.id('$$payload.out'), operator =
const statements = [];
const flush = () => {
if (operator === 'push') {
statements.push(
b.stmt(
b.call(
b.member(out, b.id('push')),
b.template(
strings.map((cooked, i) => b.quasi(cooked, i === strings.length - 1)),
expressions
)
)
)
);
} else {
statements.push(
b.stmt(
b.assignment(
operator,
out,
b.template(
strings.map((cooked, i) => b.quasi(cooked, i === strings.length - 1)),
expressions
)
statements.push(
b.stmt(
b.call(
b.id('$$renderer.push'),
b.template(
strings.map((cooked, i) => b.quasi(cooked, i === strings.length - 1)),
expressions
)
)
);
}
)
);
strings = [];
expressions = [];
};
@ -178,6 +170,7 @@ export function build_template(template, out = b.id('$$payload.out'), operator =
*
* @param {AST.Attribute['value']} value
* @param {ComponentContext} context
* @param {(expression: Expression, metadata: ExpressionMetadata) => Expression} transform
* @param {boolean} trim_whitespace
* @param {boolean} is_component
* @returns {Expression}
@ -185,6 +178,7 @@ export function build_template(template, out = b.id('$$payload.out'), operator =
export function build_attribute_value(
value,
context,
transform,
trim_whitespace = false,
is_component = false
) {
@ -203,7 +197,10 @@ export function build_attribute_value(
return b.literal(is_component ? data : escape_html(data, true));
}
return /** @type {Expression} */ (context.visit(chunk.expression));
return transform(
/** @type {Expression} */ (context.visit(chunk.expression)),
chunk.metadata.expression
);
}
let quasi = b.quasi('', false);
@ -221,7 +218,13 @@ export function build_attribute_value(
: node.data;
} else {
expressions.push(
b.call('$.stringify', /** @type {Expression} */ (context.visit(node.expression)))
b.call(
'$.stringify',
transform(
/** @type {Expression} */ (context.visit(node.expression)),
node.metadata.expression
)
)
);
quasi = b.quasi('', i + 1 === value.length);
@ -257,3 +260,70 @@ export function build_getter(node, state) {
return node;
}
/**
* Creates a `$$renderer.child(...)` expression statement
* @param {BlockStatement | Expression} body
* @param {boolean} async
* @returns {Statement}
*/
export function create_child_block(body, async) {
return b.stmt(b.call('$$renderer.child', b.arrow([b.id('$$renderer')], body, async)));
}
/**
* Creates a `$$renderer.async(...)` expression statement
* @param {BlockStatement | Expression} body
*/
export function create_async_block(body) {
return b.stmt(b.call('$$renderer.async', b.arrow([b.id('$$renderer')], body, true)));
}
/**
* @param {BlockStatement | Expression} body
* @param {Identifier | false} component_fn_id
* @returns {Statement}
*/
export function call_component_renderer(body, component_fn_id) {
return b.stmt(
b.call('$$renderer.component', b.arrow([b.id('$$renderer')], body, false), component_fn_id)
);
}
export class PromiseOptimiser {
/** @type {Expression[]} */
expressions = [];
/**
*
* @param {Expression} expression
* @param {ExpressionMetadata} metadata
*/
transform = (expression, metadata) => {
if (metadata.has_await) {
const length = this.expressions.push(expression);
return b.id(`$$${length - 1}`);
}
return expression;
};
apply() {
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)
? expression.argument
: b.call(b.thunk(expression, true));
})
);
return b.const(
b.array_pattern(this.expressions.map((_, i) => b.id(`$$${i}`))),
b.await(b.call('Promise.all', promises))
);
}
}

@ -1,10 +1,10 @@
import type { AST, Binding, StateField } from '#compiler';
import type {
AwaitExpression,
CallExpression,
ClassBody,
Identifier,
LabeledStatement,
Node,
Program
} from 'estree';
import type { Scope, ScopeRoot } from './scope.js';
@ -47,6 +47,8 @@ export interface Analysis {
/** A set of deriveds that contain `await` expressions */
async_deriveds: Set<CallExpression>;
/** Awaits needing context preservation */
pickled_awaits: Set<AwaitExpression>;
}
export interface ComponentAnalysis extends Analysis {

@ -283,11 +283,16 @@ export type DeclarationKind =
| 'var'
| 'let'
| 'const'
| 'using'
| 'await using'
| 'function'
| 'import'
| 'param'
| 'rest_param'
| 'synthetic';
| 'synthetic'
// TODO not yet implemented, but needed for TypeScript reasons
| 'using'
| 'await using';
export interface ExpressionMetadata {
/** All the bindings that are referenced eagerly (not inside functions) in this expression */

@ -344,6 +344,8 @@ export namespace AST {
has_spread: boolean;
scoped: boolean;
path: SvelteNode[];
/** Synthetic value attribute for <option> with single expression child, used for client-only handling */
synthetic_value_node: ExpressionTag | null;
};
}

@ -609,3 +609,27 @@ export function build_assignment_value(operator, left, right) {
? b.logical(/** @type {ESTree.LogicalOperator} */ (operator.slice(0, -1)), left, right)
: b.binary(/** @type {ESTree.BinaryOperator} */ (operator.slice(0, -1)), left, right);
}
/**
* @param {ESTree.Expression} expression
*/
export function has_await(expression) {
let has_await = false;
walk(expression, null, {
AwaitExpression(_node, context) {
has_await = true;
context.stop();
}
});
return has_await;
}
/**
* Turns `await ...` to `(await $.save(...))()`
* @param {ESTree.Expression} expression
*/
export function save(expression) {
return b.call(b.await(b.call('$.save', expression)));
}

@ -2,6 +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';
/**
* @param {Array<ESTree.Expression | ESTree.SpreadElement | null>} elements
@ -363,7 +364,14 @@ export function prop(kind, key, value, computed = false) {
* @returns {ESTree.PropertyDefinition}
*/
export function prop_def(key, value, computed = false, is_static = false) {
return { type: 'PropertyDefinition', key, value, computed, static: is_static };
return {
type: 'PropertyDefinition',
decorators: [],
key,
value,
computed,
static: is_static
};
}
/**
@ -443,16 +451,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') {
let has_await = false;
walk(expression.body.argument, null, {
AwaitExpression(_node, context) {
has_await = true;
context.stop();
}
});
if (!has_await) {
if (!has_await(expression.body.argument)) {
return unthunk(arrow(expression.params, expression.body.argument));
}
}
@ -573,6 +572,7 @@ function for_builder(init, test, update, body) {
export function method(kind, key, params, body, computed = false, is_static = false) {
return {
type: 'MethodDefinition',
decorators: [],
key,
kind,
value: function_builder(null, params, block(body)),
@ -618,6 +618,7 @@ function if_builder(test, consequent, alternate) {
export function import_all(as, source) {
return {
type: 'ImportDeclaration',
attributes: [],
source: literal(source),
specifiers: [import_namespace(as)]
};
@ -631,6 +632,7 @@ export function import_all(as, source) {
export function imports(parts, source) {
return {
type: 'ImportDeclaration',
attributes: [],
source: literal(source),
specifiers: parts.map((p) => ({
type: 'ImportSpecifier',

@ -1,12 +1,12 @@
/** @import { Component } from '#server' */
import { current_component } from './internal/server/context.js';
/** @import { SSRContext } from '#server' */
/** @import { Renderer } from './internal/server/renderer.js' */
import { ssr_context } from './internal/server/context.js';
import { noop } from './internal/shared/utils.js';
import * as e from './internal/server/errors.js';
/** @param {() => void} fn */
export function onDestroy(fn) {
var context = /** @type {Component} */ (current_component);
(context.d ??= []).push(fn);
/** @type {Renderer} */ (/** @type {SSRContext} */ (ssr_context).r).on_destroy(fn);
}
export {

@ -1,6 +1,14 @@
/** @import { TemplateNode, Value } from '#client' */
import { flatten } from '../../reactivity/async.js';
import { get } from '../../runtime.js';
import {
hydrate_next,
hydrate_node,
hydrating,
set_hydrate_node,
set_hydrating,
skip_nodes
} from '../hydration.js';
import { get_boundary } from './boundary.js';
/**
@ -13,7 +21,22 @@ export function async(node, expressions, fn) {
boundary.update_pending_count(1);
var was_hydrating = hydrating;
if (was_hydrating) {
hydrate_next();
var previous_hydrate_node = hydrate_node;
var end = skip_nodes(false);
set_hydrate_node(end);
}
flatten([], expressions, (values) => {
if (was_hydrating) {
set_hydrating(true);
set_hydrate_node(previous_hydrate_node);
}
try {
// get values eagerly to avoid creating blocks if they reject
for (const d of values) get(d);
@ -22,5 +45,9 @@ export function async(node, expressions, fn) {
} finally {
boundary.update_pending_count(-1);
}
if (was_hydrating) {
set_hydrating(false);
}
});
}

@ -8,7 +8,7 @@ import {
hydrate_next,
hydrate_node,
hydrating,
remove_nodes,
skip_nodes,
set_hydrate_node,
set_hydrating
} from '../hydration.js';
@ -140,7 +140,7 @@ export function await_block(node, get_input, pending_fn, then_fn, catch_fn) {
if (mismatch) {
// Hydration mismatch: remove everything inside the anchor and start fresh
anchor = remove_nodes();
anchor = skip_nodes();
set_hydrate_node(anchor);
set_hydrating(false);

@ -1,5 +1,11 @@
/** @import { Effect, Source, TemplateNode, } from '#client' */
import { BOUNDARY_EFFECT, EFFECT_PRESERVED, EFFECT_TRANSPARENT } from '#client/constants';
import {
BOUNDARY_EFFECT,
COMMENT_NODE,
EFFECT_PRESERVED,
EFFECT_TRANSPARENT
} from '#client/constants';
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';
@ -15,7 +21,7 @@ import {
hydrate_node,
hydrating,
next,
remove_nodes,
skip_nodes,
set_hydrate_node
} from '../hydration.js';
import { get_next_sibling } from '../operations.js';
@ -57,8 +63,8 @@ export class Boundary {
/** @type {TemplateNode} */
#anchor;
/** @type {TemplateNode} */
#hydrate_open;
/** @type {TemplateNode | null} */
#hydrate_open = hydrating ? hydrate_node : null;
/** @type {BoundaryProps} */
#props;
@ -83,6 +89,7 @@ export class Boundary {
#local_pending_count = 0;
#pending_count = 0;
#is_creating_fallback = false;
/**
@ -122,8 +129,6 @@ export class Boundary {
this.#props = props;
this.#children = children;
this.#hydrate_open = hydrate_node;
this.parent = /** @type {Effect} */ (active_effect).b;
this.#pending = !!this.#props.pending;
@ -132,34 +137,18 @@ export class Boundary {
/** @type {Effect} */ (active_effect).b = this;
if (hydrating) {
const comment = this.#hydrate_open;
hydrate_next();
}
const pending = this.#props.pending;
if (hydrating && pending) {
this.#pending_effect = branch(() => pending(this.#anchor));
// future work: when we have some form of async SSR, we will
// need to use hydration boundary comments to report whether
// the pending or main block was rendered for a given
// boundary, and hydrate accordingly
Batch.enqueue(() => {
this.#main_effect = this.#run(() => {
Batch.ensure();
return branch(() => this.#children(this.#anchor));
});
if (this.#pending_count > 0) {
this.#show_pending_snippet();
} else {
pause_effect(/** @type {Effect} */ (this.#pending_effect), () => {
this.#pending_effect = null;
});
const server_rendered_pending =
/** @type {Comment} */ (comment).nodeType === COMMENT_NODE &&
/** @type {Comment} */ (comment).data === HYDRATION_START_ELSE;
this.#pending = false;
}
});
if (server_rendered_pending) {
this.#hydrate_pending_content();
} else {
this.#hydrate_resolved_content();
}
} else {
try {
this.#main_effect = branch(() => children(this.#anchor));
@ -180,6 +169,43 @@ export class Boundary {
}
}
#hydrate_resolved_content() {
try {
this.#main_effect = branch(() => this.#children(this.#anchor));
} catch (error) {
this.error(error);
}
// Since server rendered resolved content, we never show pending state
// Even if client-side async operations are still running, the content is already displayed
this.#pending = false;
}
#hydrate_pending_content() {
const pending = this.#props.pending;
if (!pending) {
return;
}
this.#pending_effect = branch(() => pending(this.#anchor));
Batch.enqueue(() => {
this.#main_effect = this.#run(() => {
Batch.ensure();
return branch(() => this.#children(this.#anchor));
});
if (this.#pending_count > 0) {
this.#show_pending_snippet();
} else {
pause_effect(/** @type {Effect} */ (this.#pending_effect), () => {
this.#pending_effect = null;
});
this.#pending = false;
}
});
}
/**
* Returns `true` if the effect exists inside a boundary whose pending snippet is shown
* @returns {boolean}
@ -238,10 +264,10 @@ export class Boundary {
if (!this.has_pending_snippet()) {
if (this.parent) {
this.parent.#update_pending_count(d);
return;
}
e.await_outside_boundary();
// if there's no parent, we're in a scope with no pending snippet
return;
}
this.#pending_count += d;
@ -307,9 +333,9 @@ export class Boundary {
}
if (hydrating) {
set_hydrate_node(this.#hydrate_open);
set_hydrate_node(/** @type {TemplateNode} */ (this.#hydrate_open));
next();
set_hydrate_node(remove_nodes());
set_hydrate_node(skip_nodes());
}
var did_reset = false;
@ -330,7 +356,7 @@ export class Boundary {
// If the failure happened while flushing effects, current_batch can be null
Batch.ensure();
this.#pending_count = 0;
this.#local_pending_count = 0;
if (this.#failed_effect !== null) {
pause_effect(this.#failed_effect, () => {
@ -338,7 +364,9 @@ export class Boundary {
});
}
this.#pending = true;
// we intentionally do not try to find the nearest pending boundary. If this boundary has one, we'll render it on reset
// but it would be really weird to show the parent's boundary on a child reset.
this.#pending = this.has_pending_snippet();
this.#main_effect = this.#run(() => {
this.#is_creating_fallback = false;
@ -409,13 +437,7 @@ function move_effect(effect, fragment) {
}
export function get_boundary() {
const boundary = /** @type {Effect} */ (active_effect).b;
if (boundary === null) {
e.await_outside_boundary();
}
return boundary;
return /** @type {Boundary} */ (/** @type {Effect} */ (active_effect).b);
}
export function pending() {

@ -14,7 +14,7 @@ import {
hydrate_node,
hydrating,
read_hydration_instruction,
remove_nodes,
skip_nodes,
set_hydrate_node,
set_hydrating
} from '../hydration.js';
@ -209,7 +209,7 @@ export function each(node, flags, get_collection, get_key, render_fn, fallback_f
if (is_else !== (length === 0)) {
// hydration mismatch — remove the server-rendered DOM and start over
anchor = remove_nodes();
anchor = skip_nodes();
set_hydrate_node(anchor);
set_hydrating(false);
@ -259,7 +259,7 @@ export function each(node, flags, get_collection, get_key, render_fn, fallback_f
// remove excess nodes
if (length > 0) {
set_hydrate_node(remove_nodes());
set_hydrate_node(skip_nodes());
}
}

@ -6,7 +6,7 @@ import {
hydrate_node,
hydrating,
read_hydration_instruction,
remove_nodes,
skip_nodes,
set_hydrate_node,
set_hydrating
} from '../hydration.js';
@ -93,7 +93,7 @@ export function if_block(node, fn, elseif = false) {
if (!!condition === is_else) {
// Hydration mismatch: remove everything inside the anchor and start fresh.
// This could happen with `{#if browser}...{/if}`, for example
anchor = remove_nodes();
anchor = skip_nodes();
set_hydrate_node(anchor);
set_hydrating(false);

@ -81,9 +81,10 @@ export function next(count = 1) {
}
/**
* Removes all nodes starting at `hydrate_node` up until the next hydration end comment
* Skips or removes (depending on {@link remove}) all nodes starting at `hydrate_node` up until the next hydration end comment
* @param {boolean} remove
*/
export function remove_nodes() {
export function skip_nodes(remove = true) {
var depth = 0;
var node = hydrate_node;
@ -100,7 +101,7 @@ export function remove_nodes() {
}
var next = /** @type {TemplateNode} */ (get_next_sibling(node));
node.remove();
if (remove) node.remove();
node = next;
}
}

@ -130,11 +130,11 @@ export function child(node, is_text) {
/**
* Don't mark this as side-effect-free, hydration needs to walk all nodes
* @param {DocumentFragment | TemplateNode[]} fragment
* @param {boolean} is_text
* @param {DocumentFragment | TemplateNode | TemplateNode[]} fragment
* @param {boolean} [is_text]
* @returns {Node | null}
*/
export function first_child(fragment, is_text) {
export function first_child(fragment, is_text = false) {
if (!hydrating) {
// when not hydrating, `fragment` is a `DocumentFragment` (the result of calling `open_frag`)
var first = /** @type {DocumentFragment} */ (get_first_child(/** @type {Node} */ (fragment)));

@ -316,6 +316,9 @@ export function text(value = '') {
return node;
}
/**
* @returns {TemplateNode | DocumentFragment}
*/
export function comment() {
// we're not delegating to `template` here for performance reasons
if (hydrating) {

@ -103,7 +103,7 @@ export {
save,
track_reactivity_loss
} from './reactivity/async.js';
export { flushSync as flush, suspend } from './reactivity/batch.js';
export { flushSync as flush } from './reactivity/batch.js';
export {
async_derived,
user_derived as derived,

@ -11,7 +11,7 @@ import {
set_active_effect,
set_active_reaction
} from '../runtime.js';
import { current_batch, suspend } from './batch.js';
import { Batch, current_batch } from './batch.js';
import {
async_derived,
current_async_effect,
@ -20,6 +20,14 @@ import {
set_from_async_derived
} from './deriveds.js';
import { aborted } from './effects.js';
import {
hydrate_next,
hydrate_node,
hydrating,
set_hydrate_node,
set_hydrating,
skip_nodes
} from '../dom/hydration.js';
/**
*
@ -39,7 +47,8 @@ export function flatten(sync, async, fn) {
var parent = /** @type {Effect} */ (active_effect);
var restore = capture();
var boundary = get_boundary();
var was_hydrating = hydrating;
Promise.all(async.map((expression) => async_derived(expression)))
.then((result) => {
@ -56,11 +65,15 @@ export function flatten(sync, async, fn) {
}
}
if (was_hydrating) {
set_hydrating(false);
}
batch?.deactivate();
unset_context();
})
.catch((error) => {
boundary.error(error);
invoke_error_boundary(error, parent);
});
}
@ -75,12 +88,23 @@ function capture() {
var previous_component_context = component_context;
var previous_batch = current_batch;
var was_hydrating = hydrating;
if (was_hydrating) {
var previous_hydrate_node = hydrate_node;
}
return function restore() {
set_active_effect(previous_effect);
set_active_reaction(previous_reaction);
set_component_context(previous_component_context);
previous_batch?.activate();
if (was_hydrating) {
set_hydrating(true);
set_hydrate_node(previous_hydrate_node);
}
if (DEV) {
set_from_async_derived(null);
}
@ -178,16 +202,52 @@ export function unset_context() {
* @param {() => Promise<void>} fn
*/
export async function async_body(fn) {
var unsuspend = suspend();
var boundary = get_boundary();
var batch = /** @type {Batch} */ (current_batch);
var pending = boundary.is_pending();
boundary.update_pending_count(1);
if (!pending) batch.increment();
var active = /** @type {Effect} */ (active_effect);
var was_hydrating = hydrating;
var next_hydrate_node = undefined;
if (was_hydrating) {
hydrate_next();
next_hydrate_node = skip_nodes(false);
}
try {
await fn();
var promise = fn();
} finally {
if (next_hydrate_node) {
set_hydrate_node(next_hydrate_node);
hydrate_next();
}
}
try {
await promise;
} catch (error) {
if (!aborted(active)) {
invoke_error_boundary(error, active);
}
} finally {
unsuspend();
if (was_hydrating) {
set_hydrating(false);
}
boundary.update_pending_count(-1);
if (pending) {
batch.flush();
} else {
batch.activate();
batch.decrement();
}
unset_context();
}
}

@ -10,12 +10,10 @@ import {
INERT,
RENDER_EFFECT,
ROOT_EFFECT,
USER_EFFECT,
MAYBE_DIRTY
} from '#client/constants';
import { async_mode_flag } from '../../flags/index.js';
import { deferred, define_property } from '../../shared/utils.js';
import { get_boundary } from '../dom/blocks/boundary.js';
import {
active_effect,
is_dirty,
@ -30,7 +28,6 @@ import { DEV } from 'esm-env';
import { invoke_error_boundary } from '../error-handling.js';
import { old_values } from './sources.js';
import { unlink_effect } from './effects.js';
import { unset_context } from './async.js';
/** @type {Set<Batch>} */
const batches = new Set();
@ -653,28 +650,6 @@ export function schedule_effect(signal) {
queued_root_effects.push(effect);
}
export function suspend() {
var boundary = get_boundary();
var batch = /** @type {Batch} */ (current_batch);
var pending = boundary.is_pending();
boundary.update_pending_count(1);
if (!pending) batch.increment();
return function unsuspend() {
boundary.update_pending_count(-1);
if (!pending) {
batch.activate();
batch.decrement();
} else {
batch.flush();
}
unset_context();
};
}
/**
* Forcibly remove all current batches, to prevent cross-talk between tests
*/

@ -11,7 +11,7 @@ import {
import { HYDRATION_END, HYDRATION_ERROR, HYDRATION_START } from '../../constants.js';
import { active_effect } from './runtime.js';
import { push, pop, component_context } from './context.js';
import { component_root, branch } from './reactivity/effects.js';
import { component_root } from './reactivity/effects.js';
import {
hydrate_next,
hydrate_node,
@ -30,7 +30,8 @@ import * as w from './warnings.js';
import * as e from './errors.js';
import { assign_nodes } from './dom/template.js';
import { is_passive_event } from '../../utils.js';
import { COMMENT_NODE } from './constants.js';
import { COMMENT_NODE, STATE_SYMBOL } from './constants.js';
import { boundary } from './dom/blocks/boundary.js';
/**
* This is normally true block effects should run their intro transitions
@ -119,19 +120,9 @@ export function hydrate(component, options) {
set_hydrating(true);
set_hydrate_node(/** @type {Comment} */ (anchor));
hydrate_next();
const instance = _mount(component, { ...options, anchor });
if (
hydrate_node === null ||
hydrate_node.nodeType !== COMMENT_NODE ||
/** @type {Comment} */ (hydrate_node).data !== HYDRATION_END
) {
w.hydration_mismatch();
throw HYDRATION_ERROR;
}
set_hydrating(false);
return /** @type {Exports} */ (instance);
@ -218,35 +209,50 @@ function _mount(Component, { target, anchor, props = {}, events, context, intro
var unmount = component_root(() => {
var anchor_node = anchor ?? target.appendChild(create_text());
branch(() => {
if (context) {
push({});
var ctx = /** @type {ComponentContext} */ (component_context);
ctx.c = context;
}
if (events) {
// We can't spread the object or else we'd lose the state proxy stuff, if it is one
/** @type {any} */ (props).$$events = events;
}
boundary(
/** @type {TemplateNode} */ (anchor_node),
{
pending: () => {}
},
(anchor_node) => {
if (context) {
push({});
var ctx = /** @type {ComponentContext} */ (component_context);
ctx.c = context;
}
if (hydrating) {
assign_nodes(/** @type {TemplateNode} */ (anchor_node), null);
}
if (events) {
// We can't spread the object or else we'd lose the state proxy stuff, if it is one
/** @type {any} */ (props).$$events = events;
}
should_intro = intro;
// @ts-expect-error the public typings are not what the actual function looks like
component = Component(anchor_node, props) || {};
should_intro = true;
if (hydrating) {
assign_nodes(/** @type {TemplateNode} */ (anchor_node), null);
}
if (hydrating) {
/** @type {Effect} */ (active_effect).nodes_end = hydrate_node;
}
should_intro = intro;
// @ts-expect-error the public typings are not what the actual function looks like
component = Component(anchor_node, props) || {};
should_intro = true;
if (hydrating) {
/** @type {Effect} */ (active_effect).nodes_end = hydrate_node;
if (
hydrate_node === null ||
hydrate_node.nodeType !== COMMENT_NODE ||
/** @type {Comment} */ (hydrate_node).data !== HYDRATION_END
) {
w.hydration_mismatch();
throw HYDRATION_ERROR;
}
}
if (context) {
pop();
if (context) {
pop();
}
}
});
);
return () => {
for (var event_name of registered_events) {
@ -309,7 +315,11 @@ export function unmount(component, options) {
}
if (DEV) {
w.lifecycle_double_unmount();
if (STATE_SYMBOL in component) {
w.state_proxy_unmount();
} else {
w.lifecycle_double_unmount();
}
}
return Promise.resolve();

@ -224,6 +224,17 @@ export function state_proxy_equality_mismatch(operator) {
}
}
/**
* Tried to unmount a state proxy, rather than a component
*/
export function state_proxy_unmount() {
if (DEV) {
console.warn(`%c[svelte] state_proxy_unmount\n%cTried to unmount a state proxy, rather than a component\nhttps://svelte.dev/e/state_proxy_unmount`, bold, normal);
} else {
console.warn(`https://svelte.dev/e/state_proxy_unmount`);
}
}
/**
* A `<svelte:boundary>` `reset` function only resets the boundary the first time it is called
*/

@ -1,5 +1,5 @@
/** @import { Snippet } from 'svelte' */
/** @import { Payload } from '../payload' */
/** @import { Renderer } from '../renderer' */
/** @import { Getters } from '#shared' */
/**
@ -13,9 +13,9 @@
*/
export function createRawSnippet(fn) {
// @ts-expect-error the types are a lie
return (/** @type {Payload} */ payload, /** @type {Params} */ ...args) => {
return (/** @type {Renderer} */ renderer, /** @type {Params} */ ...args) => {
var getters = /** @type {Getters<Params>} */ (args.map((value) => () => value));
payload.out.push(
renderer.push(
fn(...getters)
.render()
.trim()

@ -1,10 +1,14 @@
/** @import { Component } from '#server' */
/** @import { SSRContext } from '#server' */
import { DEV } from 'esm-env';
import { on_destroy } from './index.js';
import * as e from './errors.js';
/** @type {Component | null} */
export var current_component = null;
/** @type {SSRContext | null} */
export var ssr_context = null;
/** @param {SSRContext | null} v */
export function set_ssr_context(v) {
ssr_context = v;
}
/**
* @template T
@ -47,42 +51,35 @@ export function getAllContexts() {
* @returns {Map<unknown, unknown>}
*/
function get_or_init_context_map(name) {
if (current_component === null) {
if (ssr_context === null) {
e.lifecycle_outside_component(name);
}
return (current_component.c ??= new Map(get_parent_context(current_component) || undefined));
return (ssr_context.c ??= new Map(get_parent_context(ssr_context) || undefined));
}
/**
* @param {Function} [fn]
*/
export function push(fn) {
current_component = { p: current_component, c: null, d: null };
ssr_context = { p: ssr_context, c: null, r: null };
if (DEV) {
// component function
current_component.function = fn;
ssr_context.function = fn;
ssr_context.element = ssr_context.p?.element;
}
}
export function pop() {
var component = /** @type {Component} */ (current_component);
var ondestroy = component.d;
if (ondestroy) {
on_destroy.push(...ondestroy);
}
current_component = component.p;
ssr_context = /** @type {SSRContext} */ (ssr_context).p;
}
/**
* @param {Component} component_context
* @param {SSRContext} ssr_context
* @returns {Map<unknown, unknown> | null}
*/
function get_parent_context(component_context) {
let parent = component_context.p;
function get_parent_context(ssr_context) {
let parent = ssr_context.p;
while (parent !== null) {
const context_map = parent.c;
@ -94,3 +91,22 @@ function get_parent_context(component_context) {
return null;
}
/**
* Wraps an `await` expression in such a way that the component context that was
* active before the expression evaluated can be reapplied afterwards
* `await a + b()` becomes `(await $.save(a))() + b()`, meaning `b()` will have access
* to the context of its component.
* @template T
* @param {Promise<T>} promise
* @returns {Promise<() => T>}
*/
export async function save(promise) {
var previous_context = ssr_context;
var value = await promise;
return () => {
ssr_context = previous_context;
return value;
};
}

@ -1,36 +1,35 @@
/** @import { Component } from '#server' */
/** @import { SSRContext } from '#server' */
import { FILENAME } from '../../constants.js';
import {
is_tag_valid_with_ancestor,
is_tag_valid_with_parent
} from '../../html-tree-validation.js';
import { current_component } from './context.js';
import { set_ssr_context, ssr_context } from './context.js';
import * as e from './errors.js';
import { HeadPayload, Payload } from './payload.js';
import { Renderer } from './renderer.js';
// TODO move this
/**
* @typedef {{
* tag: string;
* parent: null | Element;
* filename: null | string;
* parent: undefined | Element;
* filename: undefined | string;
* line: number;
* column: number;
* }} Element
*/
/**
* @type {Element | null}
* This is exported so that it can be cleared between tests
* @type {Set<string>}
*/
let parent = null;
/** @type {Set<string>} */
let seen;
export let seen;
/**
* @param {Payload} payload
* @param {Renderer} renderer
* @param {string} message
*/
function print_error(payload, message) {
function print_error(renderer, message) {
message =
`node_invalid_placement_ssr: ${message}\n\n` +
'This can cause content to shift around as the browser repairs the HTML, and will likely result in a `hydration_mismatch` warning.';
@ -40,28 +39,22 @@ function print_error(payload, message) {
// eslint-disable-next-line no-console
console.error(message);
payload.head.out.push(`<script>console.error(${JSON.stringify(message)})</script>`);
}
export function reset_elements() {
let old_parent = parent;
parent = null;
return () => {
parent = old_parent;
};
renderer.head((r) => r.push(`<script>console.error(${JSON.stringify(message)})</script>`));
}
/**
* @param {Payload} payload
* @param {Renderer} renderer
* @param {string} tag
* @param {number} line
* @param {number} column
*/
export function push_element(payload, tag, line, column) {
var filename = /** @type {Component} */ (current_component).function[FILENAME];
var child = { tag, parent, filename, line, column };
export function push_element(renderer, tag, line, column) {
var context = /** @type {SSRContext} */ (ssr_context);
var filename = context.function[FILENAME];
var parent = context.element;
var element = { tag, parent, filename, line, column };
if (parent !== null) {
if (parent !== undefined) {
var ancestor = parent.parent;
var ancestors = [parent.tag];
@ -71,7 +64,7 @@ export function push_element(payload, tag, line, column) {
: undefined;
const message = is_tag_valid_with_parent(tag, parent.tag, child_loc, parent_loc);
if (message) print_error(payload, message);
if (message) print_error(renderer, message);
while (ancestor != null) {
ancestors.push(ancestor.tag);
@ -80,27 +73,27 @@ export function push_element(payload, tag, line, column) {
: undefined;
const message = is_tag_valid_with_ancestor(tag, ancestors, child_loc, ancestor_loc);
if (message) print_error(payload, message);
if (message) print_error(renderer, message);
ancestor = ancestor.parent;
}
}
parent = child;
set_ssr_context({ ...context, p: context, element });
}
export function pop_element() {
parent = /** @type {Element} */ (parent).parent;
set_ssr_context(/** @type {SSRContext} */ (ssr_context).p);
}
/**
* @param {Payload} payload
* @param {Renderer} renderer
*/
export function validate_snippet_args(payload) {
export function validate_snippet_args(renderer) {
if (
typeof payload !== 'object' ||
// for some reason typescript consider the type of payload as never after the first instanceof
!(payload instanceof Payload || /** @type {any} */ (payload) instanceof HeadPayload)
typeof renderer !== 'object' ||
// for some reason typescript consider the type of renderer as never after the first instanceof
!(renderer instanceof Renderer)
) {
e.invalid_snippet_arguments();
}

@ -2,6 +2,30 @@
export * from '../shared/errors.js';
/**
* Encountered asynchronous work while rendering synchronously.
* @returns {never}
*/
export function await_invalid() {
const error = new Error(`await_invalid\nEncountered asynchronous work while rendering synchronously.\nhttps://svelte.dev/e/await_invalid`);
error.name = 'Svelte error';
throw error;
}
/**
* The `html` property of server render results has been deprecated. Use `body` instead.
* @returns {never}
*/
export function html_deprecated() {
const error = new Error(`html_deprecated\nThe \`html\` property of server render results has been deprecated. Use \`body\` instead.\nhttps://svelte.dev/e/html_deprecated`);
error.name = 'Svelte error';
throw error;
}
/**
* `%name%(...)` is not available on the server
* @param {string} name

@ -1,6 +1,7 @@
/** @import { ComponentType, SvelteComponent } from 'svelte' */
/** @import { Component, RenderOutput } from '#server' */
/** @import { ComponentType, SvelteComponent, Component } from 'svelte' */
/** @import { RenderOutput } from '#server' */
/** @import { Store } from '#shared' */
/** @import { AccumulatedContent } from './renderer.js' */
export { FILENAME, HMR } from '../../constants.js';
import { attr, clsx, to_class, to_style } from '../shared/attributes.js';
import { is_promise, noop } from '../shared/utils.js';
@ -13,13 +14,10 @@ import {
} from '../../constants.js';
import { escape_html } from '../../escaping.js';
import { DEV } from 'esm-env';
import { current_component, pop, push } from './context.js';
import { EMPTY_COMMENT, BLOCK_CLOSE, BLOCK_OPEN, BLOCK_OPEN_ELSE } from './hydration.js';
import { validate_store } from '../shared/validate.js';
import { is_boolean_attribute, is_raw_text_element, is_void } from '../../utils.js';
import { reset_elements } from './dev.js';
import { Payload } from './payload.js';
import { abort } from './abort-signal.js';
import { Renderer } from './renderer.js';
// https://html.spec.whatwg.org/multipage/syntax.html#attributes-2
// https://infra.spec.whatwg.org/#noncharacter
@ -27,150 +25,96 @@ const INVALID_ATTR_NAME_CHAR_REGEX =
/[\s'">/=\u{FDD0}-\u{FDEF}\u{FFFE}\u{FFFF}\u{1FFFE}\u{1FFFF}\u{2FFFE}\u{2FFFF}\u{3FFFE}\u{3FFFF}\u{4FFFE}\u{4FFFF}\u{5FFFE}\u{5FFFF}\u{6FFFE}\u{6FFFF}\u{7FFFE}\u{7FFFF}\u{8FFFE}\u{8FFFF}\u{9FFFE}\u{9FFFF}\u{AFFFE}\u{AFFFF}\u{BFFFE}\u{BFFFF}\u{CFFFE}\u{CFFFF}\u{DFFFE}\u{DFFFF}\u{EFFFE}\u{EFFFF}\u{FFFFE}\u{FFFFF}\u{10FFFE}\u{10FFFF}]/u;
/**
* @param {Payload} payload
* @param {Renderer} renderer
* @param {string} tag
* @param {() => void} attributes_fn
* @param {() => void} children_fn
* @returns {void}
*/
export function element(payload, tag, attributes_fn = noop, children_fn = noop) {
payload.out.push('<!---->');
export function element(renderer, tag, attributes_fn = noop, children_fn = noop) {
renderer.push('<!---->');
if (tag) {
payload.out.push(`<${tag}`);
renderer.push(`<${tag}`);
attributes_fn();
payload.out.push(`>`);
renderer.push(`>`);
if (!is_void(tag)) {
children_fn();
if (!is_raw_text_element(tag)) {
payload.out.push(EMPTY_COMMENT);
renderer.push(EMPTY_COMMENT);
}
payload.out.push(`</${tag}>`);
renderer.push(`</${tag}>`);
}
}
payload.out.push('<!---->');
renderer.push('<!---->');
}
/**
* Array of `onDestroy` callbacks that should be called at the end of the server render function
* @type {Function[]}
*/
export let on_destroy = [];
/**
* Only available on the server and when compiling with the `server` option.
* Takes a component and returns an object with `body` and `head` properties on it, which you can use to populate the HTML when server-rendering your app.
* @template {Record<string, any>} Props
* @param {import('svelte').Component<Props> | ComponentType<SvelteComponent<Props>>} component
* @param {Component<Props> | ComponentType<SvelteComponent<Props>>} component
* @param {{ props?: Omit<Props, '$$slots' | '$$events'>; context?: Map<any, any>; idPrefix?: string }} [options]
* @returns {RenderOutput}
*/
export function render(component, options = {}) {
try {
const payload = new Payload(options.idPrefix ? options.idPrefix + '-' : '');
const prev_on_destroy = on_destroy;
on_destroy = [];
payload.out.push(BLOCK_OPEN);
let reset_reset_element;
if (DEV) {
// prevent parent/child element state being corrupted by a bad render
reset_reset_element = reset_elements();
}
if (options.context) {
push();
/** @type {Component} */ (current_component).c = options.context;
}
// @ts-expect-error
component(payload, options.props ?? {}, {}, {});
if (options.context) {
pop();
}
if (reset_reset_element) {
reset_reset_element();
}
payload.out.push(BLOCK_CLOSE);
for (const cleanup of on_destroy) cleanup();
on_destroy = prev_on_destroy;
let head = payload.head.out.join('') + payload.head.title;
for (const { hash, code } of payload.css) {
head += `<style id="${hash}">${code}</style>`;
}
const body = payload.out.join('');
return {
head,
html: body,
body: body
};
} finally {
abort();
}
return Renderer.render(/** @type {Component<Props>} */ (component), options);
}
/**
* @param {Payload} payload
* @param {(head_payload: Payload['head']) => void} fn
* @param {Renderer} renderer
* @param {(renderer: Renderer) => Promise<void> | void} fn
* @returns {void}
*/
export function head(payload, fn) {
const head_payload = payload.head;
head_payload.out.push(BLOCK_OPEN);
fn(head_payload);
head_payload.out.push(BLOCK_CLOSE);
export function head(renderer, fn) {
renderer.head((renderer) => {
renderer.push(BLOCK_OPEN);
renderer.child(fn);
renderer.push(BLOCK_CLOSE);
});
}
/**
* @param {Payload} payload
* @param {Renderer} renderer
* @param {boolean} is_html
* @param {Record<string, string>} props
* @param {() => void} component
* @param {boolean} dynamic
* @returns {void}
*/
export function css_props(payload, is_html, props, component, dynamic = false) {
export function css_props(renderer, is_html, props, component, dynamic = false) {
const styles = style_object_to_string(props);
if (is_html) {
payload.out.push(`<svelte-css-wrapper style="display: contents; ${styles}">`);
renderer.push(`<svelte-css-wrapper style="display: contents; ${styles}">`);
} else {
payload.out.push(`<g style="${styles}">`);
renderer.push(`<g style="${styles}">`);
}
if (dynamic) {
payload.out.push('<!---->');
renderer.push('<!---->');
}
component();
if (is_html) {
payload.out.push(`<!----></svelte-css-wrapper>`);
renderer.push(`<!----></svelte-css-wrapper>`);
} else {
payload.out.push(`<!----></g>`);
renderer.push(`<!----></g>`);
}
}
/**
* @param {Record<string, unknown>} attrs
* @param {string | null} css_hash
* @param {string} [css_hash]
* @param {Record<string, boolean>} [classes]
* @param {Record<string, string>} [styles]
* @param {number} [flags]
* @returns {string}
*/
export function spread_attributes(attrs, css_hash, classes, styles, flags = 0) {
export function attributes(attrs, css_hash, classes, styles, flags = 0) {
if (styles) {
attrs.style = to_style(attrs.style, styles);
}
@ -360,14 +304,14 @@ export function unsubscribe_stores(store_values) {
}
/**
* @param {Payload} payload
* @param {Renderer} renderer
* @param {Record<string, any>} $$props
* @param {string} name
* @param {Record<string, unknown>} slot_props
* @param {null | (() => void)} fallback_fn
* @returns {void}
*/
export function slot(payload, $$props, name, slot_props, fallback_fn) {
export function slot(renderer, $$props, name, slot_props, fallback_fn) {
var slot_fn = $$props.$$slots?.[name];
// Interop: Can use snippets to fill slots
if (slot_fn === true) {
@ -375,7 +319,7 @@ export function slot(payload, $$props, name, slot_props, fallback_fn) {
}
if (slot_fn !== undefined) {
slot_fn(payload, slot_props);
slot_fn(renderer, slot_props);
} else {
fallback_fn?.();
}
@ -443,21 +387,21 @@ export function bind_props(props_parent, props_now) {
/**
* @template V
* @param {Payload} payload
* @param {Renderer} renderer
* @param {Promise<V>} promise
* @param {null | (() => void)} pending_fn
* @param {(value: V) => void} then_fn
* @returns {void}
*/
function await_block(payload, promise, pending_fn, then_fn) {
function await_block(renderer, promise, pending_fn, then_fn) {
if (is_promise(promise)) {
payload.out.push(BLOCK_OPEN);
renderer.push(BLOCK_OPEN);
promise.then(null, noop);
if (pending_fn !== null) {
pending_fn();
}
} else if (then_fn !== null) {
payload.out.push(BLOCK_OPEN_ELSE);
renderer.push(BLOCK_OPEN_ELSE);
then_fn(promise);
}
}
@ -499,12 +443,12 @@ export function once(get_value) {
/**
* Create an unique ID
* @param {Payload} payload
* @param {Renderer} renderer
* @returns {string}
*/
export function props_id(payload) {
const uid = payload.uid();
payload.out.push('<!--#' + uid + '-->');
export function props_id(renderer) {
const uid = renderer.global.uid();
renderer.push('<!--#' + uid + '-->');
return uid;
}
@ -512,12 +456,10 @@ export { attr, clsx };
export { html } from './blocks/html.js';
export { push, pop } from './context.js';
export { save } from './context.js';
export { push_element, pop_element, validate_snippet_args } from './dev.js';
export { assign_payload, copy_payload } from './payload.js';
export { snapshot } from '../shared/clone.js';
export { fallback, to_array } from '../shared/utils.js';
@ -531,8 +473,6 @@ export {
export { escape_html as escape };
export { await_outside_boundary } from '../shared/errors.js';
/**
* @template T
* @param {()=>T} fn
@ -553,33 +493,3 @@ export function derived(fn) {
return updated_value;
};
}
/**
*
* @param {Payload} payload
* @param {*} value
*/
export function maybe_selected(payload, value) {
return value === payload.select_value ? ' selected' : '';
}
/**
* @param {Payload} payload
* @param {() => void} children
* @returns {void}
*/
export function valueless_option(payload, children) {
var i = payload.out.length;
children();
var body = payload.out.slice(i).join('');
if (body.replace(/<!---->/g, '') === payload.select_value) {
// replace '>' with ' selected>' (closing tag will be added later)
var last_item = payload.out[i - 1];
payload.out[i - 1] = last_item.slice(0, -1) + ' selected>';
// Remove the old items after position i and add the body as a single item
payload.out.splice(i, payload.out.length - i, body);
}
}

@ -1,80 +0,0 @@
export class HeadPayload {
/** @type {Set<{ hash: string; code: string }>} */
css = new Set();
/** @type {string[]} */
out = [];
uid = () => '';
title = '';
constructor(
/** @type {Set<{ hash: string; code: string }>} */ css = new Set(),
/** @type {string[]} */ out = [],
title = '',
uid = () => ''
) {
this.css = css;
this.out = out;
this.title = title;
this.uid = uid;
}
}
export class Payload {
/** @type {Set<{ hash: string; code: string }>} */
css = new Set();
/** @type {string[]} */
out = [];
uid = () => '';
select_value = undefined;
head = new HeadPayload();
constructor(id_prefix = '') {
this.uid = props_id_generator(id_prefix);
this.head.uid = this.uid;
}
}
/**
* Used in legacy mode to handle bindings
* @param {Payload} to_copy
* @returns {Payload}
*/
export function copy_payload({ out, css, head, uid }) {
const payload = new Payload();
payload.out = [...out];
payload.css = new Set(css);
payload.uid = uid;
payload.head = new HeadPayload();
payload.head.out = [...head.out];
payload.head.css = new Set(head.css);
payload.head.title = head.title;
payload.head.uid = head.uid;
return payload;
}
/**
* Assigns second payload to first
* @param {Payload} p1
* @param {Payload} p2
* @returns {void}
*/
export function assign_payload(p1, p2) {
p1.out = [...p2.out];
p1.css = p2.css;
p1.head = p2.head;
p1.uid = p2.uid;
}
/**
* Creates an ID generator
* @param {string} prefix
* @returns {() => string}
*/
function props_id_generator(prefix) {
let uid = 1;
return () => `${prefix}s${uid++}`;
}

@ -0,0 +1,611 @@
/** @import { Component } from 'svelte' */
/** @import { RenderOutput, SSRContext, SyncRenderOutput } from './types.js' */
import { async_mode_flag } from '../flags/index.js';
import { abort } from './abort-signal.js';
import { pop, push, set_ssr_context, ssr_context } from './context.js';
import * as e from './errors.js';
import * as w from './warnings.js';
import { BLOCK_CLOSE, BLOCK_OPEN } from './hydration.js';
import { attributes } from './index.js';
/** @typedef {'head' | 'body'} RendererType */
/** @typedef {{ [key in RendererType]: string }} AccumulatedContent */
/**
* @template T
* @typedef {T | Promise<T>} MaybePromise<T>
*/
/**
* @typedef {string | Renderer} RendererItem
*/
/**
* Renderers are basically a tree of `string | Renderer`s, where each `Renderer` in the tree represents
* work that may or may not have completed. A renderer can be {@link collect}ed to aggregate the
* content from itself and all of its children, but this will throw if any of the children are
* performing asynchronous work. To asynchronously collect a renderer, just `await` it.
*
* The `string` values within a renderer are always associated with the {@link type} of that renderer. To switch types,
* call {@link child} with a different `type` argument.
*/
export class Renderer {
/**
* The contents of the renderer.
* @type {RendererItem[]}
*/
#out = [];
/**
* Any `onDestroy` callbacks registered during execution of this renderer.
* @type {(() => void)[] | undefined}
*/
#on_destroy = undefined;
/**
* Whether this renderer is a component body.
* @type {boolean}
*/
#is_component_body = false;
/**
* The type of string content that this renderer is accumulating.
* @type {RendererType}
*/
type;
/** @type {Renderer | undefined} */
#parent;
/**
* Asynchronous work associated with this renderer
* @type {Promise<void> | undefined}
*/
promise = undefined;
/**
* State which is associated with the content tree as a whole.
* It will be re-exposed, uncopied, on all children.
* @type {SSRState}
* @readonly
*/
global;
/**
* State that is local to the branch it is declared in.
* It will be shallow-copied to all children.
*
* @type {{ select_value: string | undefined }}
*/
local;
/**
* @param {SSRState} global
* @param {Renderer | undefined} [parent]
*/
constructor(global, parent) {
this.#parent = parent;
this.global = global;
this.local = parent ? { ...parent.local } : { select_value: undefined };
this.type = parent ? parent.type : 'body';
}
/**
* @param {(renderer: Renderer) => void} fn
*/
head(fn) {
const head = new Renderer(this.global, this);
head.type = 'head';
this.#out.push(head);
head.child(fn);
}
/**
* @param {(renderer: Renderer) => void} fn
*/
async(fn) {
this.#out.push(BLOCK_OPEN);
this.child(fn);
this.#out.push(BLOCK_CLOSE);
}
/**
* Create a child renderer. The child renderer inherits the state from the parent,
* but has its own content.
* @param {(renderer: Renderer) => MaybePromise<void>} fn
*/
child(fn) {
const child = new Renderer(this.global, this);
this.#out.push(child);
const parent = ssr_context;
set_ssr_context({
...ssr_context,
p: parent,
c: null,
r: child
});
const result = fn(child);
set_ssr_context(parent);
if (result instanceof Promise) {
if (child.global.mode === 'sync') {
e.await_invalid();
}
// just to avoid unhandled promise rejections -- we'll end up throwing in `collect_async` if something fails
result.catch(() => {});
child.promise = result;
}
return child;
}
/**
* Create a component renderer. The component renderer inherits the state from the parent,
* but has its own content. It is treated as an ordering boundary for ondestroy callbacks.
* @param {(renderer: Renderer) => MaybePromise<void>} fn
* @param {Function} [component_fn]
* @returns {void}
*/
component(fn, component_fn) {
push(component_fn);
const child = this.child(fn);
child.#is_component_body = true;
pop();
}
/**
* @param {Record<string, any>} attrs
* @param {(renderer: Renderer) => void} fn
*/
select({ value, ...attrs }, fn) {
this.push(`<select${attributes(attrs)}>`);
this.child((renderer) => {
renderer.local.select_value = value;
fn(renderer);
});
this.push('</select>');
}
/**
* @param {Record<string, any>} attrs
* @param {string | number | boolean | ((renderer: Renderer) => void)} body
*/
option(attrs, body) {
this.#out.push(`<option${attributes(attrs)}`);
/**
* @param {Renderer} renderer
* @param {any} value
* @param {{ head?: string, body: any }} content
*/
const close = (renderer, value, { head, body }) => {
if ('value' in attrs) {
value = attrs.value;
}
if (value === this.local.select_value) {
renderer.#out.push(' selected');
}
renderer.#out.push(`>${body}</option>`);
// super edge case, but may as well handle it
if (head) {
renderer.head((child) => child.push(head));
}
};
if (typeof body === 'function') {
this.child((renderer) => {
const r = new Renderer(this.global, this);
body(r);
if (this.global.mode === 'async') {
return r.#collect_content_async().then((content) => {
close(renderer, content.body.replaceAll('<!---->', ''), content);
});
} else {
const content = r.#collect_content();
close(renderer, content.body.replaceAll('<!---->', ''), content);
}
});
} else {
close(this, body, { body });
}
}
/**
* @param {(renderer: Renderer) => void} fn
*/
title(fn) {
const path = this.get_path();
/** @param {string} head */
const close = (head) => {
this.global.set_title(head, path);
};
this.child((renderer) => {
const r = new Renderer(renderer.global, renderer);
fn(r);
if (renderer.global.mode === 'async') {
return r.#collect_content_async().then((content) => {
close(content.head);
});
} else {
const content = r.#collect_content();
close(content.head);
}
});
}
/**
* @param {string | (() => Promise<string>)} content
*/
push(content) {
if (typeof content === 'function') {
this.child(async (renderer) => renderer.push(await content()));
} else {
this.#out.push(content);
}
}
/**
* @param {() => void} fn
*/
on_destroy(fn) {
(this.#on_destroy ??= []).push(fn);
}
/**
* @returns {number[]}
*/
get_path() {
return this.#parent ? [...this.#parent.get_path(), this.#parent.#out.indexOf(this)] : [];
}
/**
* @deprecated this is needed for legacy component bindings
*/
copy() {
const copy = new Renderer(this.global, this.#parent);
copy.#out = this.#out.map((item) => (item instanceof Renderer ? item.copy() : item));
copy.promise = this.promise;
return copy;
}
/**
* @param {Renderer} other
* @deprecated this is needed for legacy component bindings
*/
subsume(other) {
if (this.global.mode !== other.global.mode) {
throw new Error(
"invariant: A renderer cannot switch modes. If you're seeing this, there's a compiler bug. File an issue!"
);
}
this.local = other.local;
this.#out = other.#out.map((item) => {
if (item instanceof Renderer) {
item.subsume(item);
}
return item;
});
this.promise = other.promise;
this.type = other.type;
}
get length() {
return this.#out.length;
}
/**
* Only available on the server and when compiling with the `server` option.
* Takes a component and returns an object with `body` and `head` properties on it, which you can use to populate the HTML when server-rendering your app.
* @template {Record<string, any>} Props
* @param {Component<Props>} component
* @param {{ props?: Omit<Props, '$$slots' | '$$events'>; context?: Map<any, any>; idPrefix?: string }} [options]
* @returns {RenderOutput}
*/
static render(component, options = {}) {
/** @type {AccumulatedContent | undefined} */
let sync;
/** @type {Promise<AccumulatedContent> | undefined} */
let async;
const result = /** @type {RenderOutput} */ ({});
// making these properties non-enumerable so that console.logging
// doesn't trigger a sync render
Object.defineProperties(result, {
html: {
get: () => {
return (sync ??= Renderer.#render(component, options)).body;
}
},
head: {
get: () => {
return (sync ??= Renderer.#render(component, options)).head;
}
},
body: {
get: () => {
return (sync ??= Renderer.#render(component, options)).body;
}
},
then: {
value:
/**
* this is not type-safe, but honestly it's the best I can do right now, and it's a straightforward function.
*
* @template TResult1
* @template [TResult2=never]
* @param { (value: SyncRenderOutput) => TResult1 } onfulfilled
* @param { (reason: unknown) => TResult2 } onrejected
*/
(onfulfilled, onrejected) => {
if (!async_mode_flag) {
w.experimental_async_ssr();
const result = (sync ??= Renderer.#render(component, options));
const user_result = onfulfilled({
head: result.head,
body: result.body,
html: result.body
});
return Promise.resolve(user_result);
}
async ??= Renderer.#render_async(component, options);
return async.then((result) => {
Object.defineProperty(result, 'html', {
// eslint-disable-next-line getter-return
get: () => {
e.html_deprecated();
}
});
return onfulfilled(/** @type {SyncRenderOutput} */ (result));
}, onrejected);
}
}
});
return result;
}
/**
* Collect all of the `onDestroy` callbacks regsitered during rendering. In an async context, this is only safe to call
* after awaiting `collect_async`.
*
* Child renderers are "porous" and don't affect execution order, but component body renderers
* create ordering boundaries. Within a renderer, callbacks run in order until hitting a component boundary.
* @returns {Iterable<() => void>}
*/
*#collect_on_destroy() {
for (const component of this.#traverse_components()) {
yield* component.#collect_ondestroy();
}
}
/**
* Performs a depth-first search of renderers, yielding the deepest components first, then additional components as we backtrack up the tree.
* @returns {Iterable<Renderer>}
*/
*#traverse_components() {
for (const child of this.#out) {
if (typeof child !== 'string') {
yield* child.#traverse_components();
}
}
if (this.#is_component_body) {
yield this;
}
}
/**
* @returns {Iterable<() => void>}
*/
*#collect_ondestroy() {
if (this.#on_destroy) {
for (const fn of this.#on_destroy) {
yield fn;
}
}
for (const child of this.#out) {
if (child instanceof Renderer && !child.#is_component_body) {
yield* child.#collect_ondestroy();
}
}
}
/**
* Render a component. Throws if any of the children are performing asynchronous work.
*
* @template {Record<string, any>} Props
* @param {Component<Props>} component
* @param {{ props?: Omit<Props, '$$slots' | '$$events'>; context?: Map<any, any>; idPrefix?: string }} options
* @returns {AccumulatedContent}
*/
static #render(component, options) {
var previous_context = ssr_context;
try {
const renderer = Renderer.#open_render('sync', component, options);
const content = renderer.#collect_content();
return Renderer.#close_render(content, renderer);
} finally {
abort();
set_ssr_context(previous_context);
}
}
/**
* Render a component.
*
* @template {Record<string, any>} Props
* @param {Component<Props>} component
* @param {{ props?: Omit<Props, '$$slots' | '$$events'>; context?: Map<any, any>; idPrefix?: string }} options
* @returns {Promise<AccumulatedContent>}
*/
static async #render_async(component, options) {
var previous_context = ssr_context;
try {
const renderer = Renderer.#open_render('async', component, options);
const content = await renderer.#collect_content_async();
return Renderer.#close_render(content, renderer);
} finally {
abort();
set_ssr_context(previous_context);
}
}
/**
* Collect all of the code from the `out` array and return it as a string, or a promise resolving to a string.
* @param {AccumulatedContent} content
* @returns {AccumulatedContent}
*/
#collect_content(content = { head: '', body: '' }) {
for (const item of this.#out) {
if (typeof item === 'string') {
content[this.type] += item;
} else if (item instanceof Renderer) {
item.#collect_content(content);
}
}
return content;
}
/**
* Collect all of the code from the `out` array and return it as a string.
* @param {AccumulatedContent} content
* @returns {Promise<AccumulatedContent>}
*/
async #collect_content_async(content = { head: '', body: '' }) {
await this.promise;
// no danger to sequentially awaiting stuff in here; all of the work is already kicked off
for (const item of this.#out) {
if (typeof item === 'string') {
content[this.type] += item;
} else if (item instanceof Renderer) {
await item.#collect_content_async(content);
}
}
return content;
}
/**
* @template {Record<string, any>} Props
* @param {'sync' | 'async'} mode
* @param {import('svelte').Component<Props>} component
* @param {{ props?: Omit<Props, '$$slots' | '$$events'>; context?: Map<any, any>; idPrefix?: string }} options
* @returns {Renderer}
*/
static #open_render(mode, component, options) {
const renderer = new Renderer(
new SSRState(mode, options.idPrefix ? options.idPrefix + '-' : '')
);
renderer.push(BLOCK_OPEN);
if (options.context) {
push();
/** @type {SSRContext} */ (ssr_context).c = options.context;
/** @type {SSRContext} */ (ssr_context).r = renderer;
}
// @ts-expect-error
component(renderer, options.props ?? {});
if (options.context) {
pop();
}
renderer.push(BLOCK_CLOSE);
return renderer;
}
/**
* @param {AccumulatedContent} content
* @param {Renderer} renderer
*/
static #close_render(content, renderer) {
for (const cleanup of renderer.#collect_on_destroy()) {
cleanup();
}
let head = content.head + renderer.global.get_title();
let body = content.body;
for (const { hash, code } of renderer.global.css) {
head += `<style id="${hash}">${code}</style>`;
}
return {
head,
body
};
}
}
export class SSRState {
/** @readonly @type {'sync' | 'async'} */
mode;
/** @readonly @type {() => string} */
uid;
/** @readonly @type {Set<{ hash: string; code: string }>} */
css = new Set();
/** @type {{ path: number[], value: string }} */
#title = { path: [], value: '' };
/**
* @param {'sync' | 'async'} mode
* @param {string} [id_prefix]
*/
constructor(mode, id_prefix = '') {
this.mode = mode;
let uid = 1;
this.uid = () => `${id_prefix}s${uid++}`;
}
get_title() {
return this.#title.value;
}
/**
* Performs a depth-first (lexicographic) comparison using the path. Rejects sets
* from earlier than or equal to the current value.
* @param {string} value
* @param {number[]} path
*/
set_title(value, path) {
const current = this.#title.path;
let i = 0;
let l = Math.min(path.length, current.length);
// skip identical prefixes - [1, 2, 3, ...] === [1, 2, 3, ...]
while (i < l && path[i] === current[i]) i += 1;
if (path[i] === undefined) return;
// replace title if
// - incoming path is longer - [7, 8, 9] > [7, 8]
// - incoming path is later - [7, 8, 9] > [7, 8, 8]
if (current[i] === undefined || path[i] > current[i]) {
this.#title.path = path;
this.#title.value = value;
}
}
}

@ -0,0 +1,339 @@
import { afterAll, beforeAll, describe, expect, test } from 'vitest';
import { Renderer, SSRState } from './renderer.js';
import type { Component } from 'svelte';
import { disable_async_mode_flag, enable_async_mode_flag } from '../flags/index.js';
test('collects synchronous body content by default', () => {
const component = (renderer: Renderer) => {
renderer.push('a');
renderer.child(($$renderer) => {
$$renderer.push('b');
});
renderer.push('c');
};
const { head, body } = Renderer.render(component as unknown as Component);
expect(head).toBe('');
expect(body).toBe('<!--[-->abc<!--]-->');
});
test('child type switches content area (head vs body)', () => {
const component = (renderer: Renderer) => {
renderer.push('a');
renderer.head(($$renderer) => {
$$renderer.push('<title>T</title>');
});
renderer.push('b');
};
const { head, body } = Renderer.render(component as unknown as Component);
expect(head).toBe('<title>T</title>');
expect(body).toBe('<!--[-->ab<!--]-->');
});
test('child inherits parent type when not specified', () => {
const component = (renderer: Renderer) => {
renderer.head((renderer) => {
renderer.push('<meta name="x"/>');
renderer.child((renderer) => {
renderer.push('<style>/* css */</style>');
});
});
};
const { head, body } = Renderer.render(component as unknown as Component);
expect(body).toBe('<!--[--><!--]-->');
expect(head).toBe('<meta name="x"/><style>/* css */</style>');
});
test('get_path returns the path indexes to a renderer', () => {
const root = new Renderer(new SSRState('sync'));
let child_a: InstanceType<typeof Renderer> | undefined;
let child_b: InstanceType<typeof Renderer> | undefined;
let child_b_0: InstanceType<typeof Renderer> | undefined;
root.child(($$renderer) => {
child_a = $$renderer;
$$renderer.push('A');
});
root.child(($$renderer) => {
child_b = $$renderer;
$$renderer.child(($$inner) => {
child_b_0 = $$inner;
$$inner.push('B0');
});
$$renderer.push('B1');
});
expect(child_a!.get_path()).toEqual([0]);
expect(child_b!.get_path()).toEqual([1]);
expect(child_b_0!.get_path()).toEqual([1, 0]);
});
test('creating an async child in a sync context throws', () => {
const component = (renderer: Renderer) => {
renderer.push('a');
renderer.child(async ($$renderer) => {
await Promise.resolve();
$$renderer.push('x');
});
};
expect(() => Renderer.render(component as unknown as Component).head).toThrow('await_invalid');
expect(() => Renderer.render(component as unknown as Component).html).toThrow('await_invalid');
expect(() => Renderer.render(component as unknown as Component).body).toThrow('await_invalid');
});
test('local state is shallow-copied to children', () => {
const root = new Renderer(new SSRState('sync'));
root.local.select_value = 'A';
let child: InstanceType<typeof Renderer> | undefined;
root.child(($$renderer) => {
child = $$renderer;
});
expect(child!.local.select_value).toBe('A');
child!.local.select_value = 'B';
expect(root.local.select_value).toBe('A');
});
test('subsume replaces tree content and state from other', () => {
const a = new Renderer(new SSRState('async'));
a.type = 'head';
a.push('<meta />');
a.local.select_value = 'A';
const b = new Renderer(new SSRState('async'));
b.child(async ($$renderer) => {
await Promise.resolve();
$$renderer.push('body');
});
b.global.css.add({ hash: 'h', code: 'c' });
b.global.set_title('Title', [1]);
b.local.select_value = 'B';
b.promise = Promise.resolve();
a.subsume(b);
expect(a.type).toBe('body');
expect(a.local.select_value).toBe('B');
expect(a.promise).toBe(b.promise);
});
test('subsume refuses to switch modes', () => {
const a = new Renderer(new SSRState('sync'));
a.type = 'head';
a.push('<meta />');
a.local.select_value = 'A';
const b = new Renderer(new SSRState('async'));
b.child(async ($$renderer) => {
await Promise.resolve();
$$renderer.push('body');
});
b.global.css.add({ hash: 'h', code: 'c' });
b.global.set_title('Title', [1]);
b.local.select_value = 'B';
b.promise = Promise.resolve();
expect(() => a.subsume(b)).toThrow(
"invariant: A renderer cannot switch modes. If you're seeing this, there's a compiler bug. File an issue!"
);
});
test('SSRState uid generator uses prefix', () => {
const state = new SSRState('sync', 'id-');
expect(state.uid()).toBe('id-s1');
});
test('SSRState title ordering favors later lexicographic paths', () => {
const state = new SSRState('sync');
state.set_title('A', [1]);
expect(state.get_title()).toBe('A');
// equal path -> unchanged
state.set_title('B', [1]);
expect(state.get_title()).toBe('A');
// earlier -> unchanged
state.set_title('C', [0, 9]);
expect(state.get_title()).toBe('A');
// later -> update
state.set_title('D', [2]);
expect(state.get_title()).toBe('D');
// longer but same prefix -> update
state.set_title('E', [2, 0]);
expect(state.get_title()).toBe('E');
// shorter (earlier) than current with same prefix -> unchanged
state.set_title('F', [2]);
expect(state.get_title()).toBe('E');
});
test('selects an option with an explicit value', () => {
const component = (renderer: Renderer) => {
renderer.select({ value: 2 }, (renderer) => {
renderer.option({ value: 1 }, (renderer) => renderer.push('one'));
renderer.option({ value: 2 }, (renderer) => renderer.push('two'));
renderer.option({ value: 3 }, (renderer) => renderer.push('three'));
});
};
const { head, body } = Renderer.render(component as unknown as Component);
expect(head).toBe('');
expect(body).toBe(
'<!--[--><select><option value="1">one</option><option value="2" selected>two</option><option value="3">three</option></select><!--]-->'
);
});
test('selects an option with an implicit value', () => {
const component = (renderer: Renderer) => {
renderer.select({ value: 'two' }, (renderer) => {
renderer.option({}, (renderer) => renderer.push('one'));
renderer.option({}, (renderer) => renderer.push('two'));
renderer.option({}, (renderer) => renderer.push('three'));
});
};
const { head, body } = Renderer.render(component as unknown as Component);
expect(head).toBe('');
expect(body).toBe(
'<!--[--><select><option>one</option><option selected>two</option><option>three</option></select><!--]-->'
);
});
describe('async', () => {
beforeAll(() => {
enable_async_mode_flag();
});
afterAll(() => {
disable_async_mode_flag();
});
test('awaiting renderer gets async content', async () => {
const component = (renderer: Renderer) => {
renderer.push('1');
renderer.child(async ($$renderer) => {
await Promise.resolve();
$$renderer.push('2');
});
renderer.push('3');
};
const result = await Renderer.render(component as unknown as Component);
expect(result.head).toBe('');
expect(result.body).toBe('<!--[-->123<!--]-->');
expect(() => result.html).toThrow('html_deprecated');
});
test('push accepts async functions in async context', async () => {
const component = (renderer: Renderer) => {
renderer.push('a');
renderer.push(async () => {
await Promise.resolve();
return 'b';
});
renderer.push('c');
};
const { head, body } = await Renderer.render(component as unknown as Component);
expect(head).toBe('');
expect(body).toBe('<!--[-->abc<!--]-->');
});
test('push handles async functions with different timing', async () => {
const component = (renderer: Renderer) => {
renderer.push(async () => {
await Promise.resolve();
return 'fast';
});
renderer.push(async () => {
await new Promise((resolve) => setTimeout(resolve, 10));
return 'slow';
});
renderer.push('sync');
};
const { head, body } = await Renderer.render(component as unknown as Component);
expect(head).toBe('');
expect(body).toBe('<!--[-->fastslowsync<!--]-->');
});
test('push async functions work with head content type', async () => {
const component = (renderer: Renderer) => {
renderer.head(($$renderer) => {
$$renderer.push(async () => {
await Promise.resolve();
return '<title>Async Title</title>';
});
});
};
const { head, body } = await Renderer.render(component as unknown as Component);
expect(body).toBe('<!--[--><!--]-->');
expect(head).toBe('<title>Async Title</title>');
});
test('push async functions can be mixed with child renderers', async () => {
const component = (renderer: Renderer) => {
renderer.push('start-');
renderer.push(async () => {
await Promise.resolve();
return 'async-';
});
renderer.child(($$renderer) => {
$$renderer.push('child-');
});
renderer.push('-end');
};
const { head, body } = await Renderer.render(component as unknown as Component);
expect(head).toBe('');
expect(body).toBe('<!--[-->start-async-child--end<!--]-->');
});
test('push async functions are not supported in sync context', () => {
const component = (renderer: Renderer) => {
renderer.push('a');
renderer.push(() => Promise.resolve('b'));
};
expect(() => Renderer.render(component as unknown as Component).body).toThrow('await_invalid');
expect(() => Renderer.render(component as unknown as Component).html).toThrow('await_invalid');
expect(() => Renderer.render(component as unknown as Component).head).toThrow('await_invalid');
});
test('on_destroy yields callbacks in the correct order', async () => {
const destroyed: string[] = [];
const component = (renderer: Renderer) => {
renderer.component((renderer) => {
renderer.on_destroy(() => destroyed.push('a'));
// children should not alter relative order
renderer.child(async (renderer) => {
await Promise.resolve();
renderer.on_destroy(() => destroyed.push('b'));
renderer.on_destroy(() => destroyed.push('b*'));
});
// but child components should
renderer.component((renderer) => {
renderer.on_destroy(() => destroyed.push('c'));
});
renderer.child((renderer) => {
renderer.on_destroy(() => destroyed.push('d'));
});
renderer.component((renderer) => {
renderer.on_destroy(() => destroyed.push('e'));
});
});
};
await Renderer.render(component as unknown as Component);
expect(destroyed).toEqual(['c', 'e', 'a', 'b', 'b*', 'd']);
});
});

@ -1,17 +1,20 @@
export interface Component {
import type { Element } from './dev';
import type { Renderer } from './renderer';
export interface SSRContext {
/** parent */
p: null | Component;
/** context */
p: null | SSRContext;
/** component context */
c: null | Map<unknown, unknown>;
/** ondestroy */
d: null | Array<() => void>;
/**
* dev mode only: the component function
*/
/** renderer */
r: null | Renderer;
/** dev mode only: the current component function */
function?: any;
/** dev mode only: the current element */
element?: Element;
}
export interface RenderOutput {
export interface SyncRenderOutput {
/** HTML that goes into the `<head>` */
head: string;
/** @deprecated use `body` instead */
@ -19,3 +22,5 @@ export interface RenderOutput {
/** HTML that goes somewhere into the `<body>` */
body: string;
}
export type RenderOutput = SyncRenderOutput & PromiseLike<SyncRenderOutput>;

@ -0,0 +1,17 @@
/* This file is generated by scripts/process-messages/index.js. Do not edit! */
import { DEV } from 'esm-env';
var bold = 'font-weight: bold';
var normal = 'font-weight: normal';
/**
* Attempted to use asynchronous rendering without `experimental.async` enabled
*/
export function experimental_async_ssr() {
if (DEV) {
console.warn(`%c[svelte] experimental_async_ssr\n%cAttempted to use asynchronous rendering without \`experimental.async\` enabled\nhttps://svelte.dev/e/experimental_async_ssr`, bold, normal);
} else {
console.warn(`https://svelte.dev/e/experimental_async_ssr`);
}
}

@ -22,6 +22,10 @@ const replacements = {
* @returns {string}
*/
export function attr(name, value, is_boolean = false) {
// attribute hidden for values other than "until-found" behaves like a boolean attribute
if (name === 'hidden' && value !== 'until-found') {
is_boolean = true;
}
if (value == null || (!value && is_boolean)) return '';
const normalized = (name in replacements && replacements[name].get(value)) || value;
const assignment = is_boolean ? '' : `="${escape_html(normalized, true)}"`;

@ -2,22 +2,6 @@
import { DEV } from 'esm-env';
/**
* Cannot await outside a `<svelte:boundary>` with a `pending` snippet
* @returns {never}
*/
export function await_outside_boundary() {
if (DEV) {
const error = new Error(`await_outside_boundary\nCannot await outside a \`<svelte:boundary>\` with a \`pending\` snippet\nhttps://svelte.dev/e/await_outside_boundary`);
error.name = 'Svelte error';
throw error;
} else {
throw new Error(`https://svelte.dev/e/await_outside_boundary`);
}
}
/**
* Cannot use `{@render children(...)}` if the parent component uses `let:` directives. Consider using a named snippet instead
* @returns {never}

@ -1,11 +1,15 @@
/** @import { SvelteComponent } from '../index.js' */
import { asClassComponent as as_class_component, createClassComponent } from './legacy-client.js';
import { render } from '../internal/server/index.js';
import { async_mode_flag } from '../internal/flags/index.js';
import * as w from '../internal/server/warnings.js';
// By having this as a separate entry point for server environments, we save the client bundle from having to include the server runtime
export { createClassComponent };
/** @typedef {{ head: string, html: string, css: { code: string, map: null }}} LegacyRenderResult */
/**
* Takes a Svelte 5 component and returns a Svelte 4 compatible component constructor.
*
@ -21,16 +25,58 @@ export { createClassComponent };
*/
export function asClassComponent(component) {
const component_constructor = as_class_component(component);
/** @type {(props?: {}, opts?: { $$slots?: {}; context?: Map<any, any>; }) => { html: any; css: { code: string; map: any; }; head: string; } } */
/** @type {(props?: {}, opts?: { $$slots?: {}; context?: Map<any, any>; }) => LegacyRenderResult & PromiseLike<LegacyRenderResult> } */
const _render = (props, { context } = {}) => {
// @ts-expect-error the typings are off, but this will work if the component is compiled in SSR mode
const result = render(component, { props, context });
return {
css: { code: '', map: null },
head: result.head,
html: result.body
};
const munged = Object.defineProperties(
/** @type {LegacyRenderResult & PromiseLike<LegacyRenderResult>} */ ({}),
{
css: {
value: { code: '', map: null }
},
head: {
get: () => result.head
},
html: {
get: () => result.body
},
then: {
/**
* this is not type-safe, but honestly it's the best I can do right now, and it's a straightforward function.
*
* @template TResult1
* @template [TResult2=never]
* @param { (value: LegacyRenderResult) => TResult1 } onfulfilled
* @param { (reason: unknown) => TResult2 } onrejected
*/
value: (onfulfilled, onrejected) => {
if (!async_mode_flag) {
w.experimental_async_ssr();
const user_result = onfulfilled({
css: munged.css,
head: munged.head,
html: munged.html
});
return Promise.resolve(user_result);
}
return result.then((result) => {
return onfulfilled({
css: munged.css,
head: result.head,
html: result.body
});
}, onrejected);
}
}
}
);
return munged;
};
// @ts-expect-error this is present for SSR
component_constructor.render = _render;

@ -154,7 +154,6 @@ const DOM_BOOLEAN_ATTRIBUTES = [
'default',
'disabled',
'formnovalidate',
'hidden',
'indeterminate',
'inert',
'ismap',

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

@ -1 +1 @@
<!--[--> <p>start</p><!--[--><p>cond</p><!--]--><!--]-->
<!--[--><!--[--> <p>start</p><!--[--><p>cond</p><!--]--><!--]--><!--]-->

@ -1 +1 @@
<!--[--><!--[--><!--]--> <div><!----><!----></div> hello<!--]-->
<!--[--><!--[--><!--]--> <div><!--[--><!--]--></div> hello<!--]-->

@ -1,2 +1,2 @@
<!--[-->
<main><p>nested</p><!----></main><!--]-->
<main><p>nested</p><!----></main><!--]-->

@ -259,7 +259,8 @@
"kind": "let"
},
"specifiers": [],
"source": null
"source": null,
"attributes": []
}
],
"sourceType": "module"

@ -169,7 +169,8 @@
"kind": "const"
},
"specifiers": [],
"source": null
"source": null,
"attributes": []
}
],
"sourceType": "module"

@ -6,5 +6,9 @@ import config from '__CONFIG__';
import { render } from 'svelte/server';
export default function () {
return render(SvelteComponent, { props: config.props || {}, idPrefix: config?.id_prefix });
const { head, body, html } = render(SvelteComponent, {
props: config.props || {},
idPrefix: config?.id_prefix
});
return { head, body, html };
}

@ -1,7 +1,7 @@
import { test } from '../../test';
export default test({
skip_mode: ['server'],
skip_mode: ['server', 'async-server'],
get props() {
return { value: 'hello!' };

@ -1,7 +1,7 @@
import { test } from '../../test';
export default test({
skip_mode: ['server'],
skip_mode: ['server', 'async-server'],
get props() {
return { value: 'hello!' };

@ -1,7 +1,7 @@
import { test } from '../../test';
export default test({
skip_mode: ['server'],
skip_mode: ['server', 'async-server'],
get props() {
return { value: 'hello!' };

@ -8,9 +8,9 @@ export default test({
ssrHtml: `
<select>
<option value="1">1</option>
<option selected value="2">2</option>
<option value="3">3</option>
<option>1</option>
<option selected>2</option>
<option>3</option>
</select>
<p>foo: 2</p>
@ -21,9 +21,9 @@ export default test({
target.innerHTML,
`
<select>
<option value="1">1</option>
<option ${variant === 'hydrate' ? 'selected ' : ''}value="2">2</option>
<option value="3">3</option>
<option>1</option>
<option ${variant === 'hydrate' ? 'selected ' : ''}>2</option>
<option>3</option>
</select>
<p>foo: 2</p>
@ -47,9 +47,9 @@ export default test({
target.innerHTML,
`
<select>
<option value="1">1</option>
<option ${variant === 'hydrate' ? 'selected ' : ''}value="2">2</option>
<option value="3">3</option>
<option>1</option>
<option ${variant === 'hydrate' ? 'selected ' : ''}>2</option>
<option>3</option>
</select>
<p>foo: 3</p>

@ -8,9 +8,9 @@ export default test({
<select>
<option disabled=''>x</option>
<option value="a">a</option>
<option value="b">b</option>
<option value="c">c</option>
<option>a</option>
<option>b</option>
<option>c</option>
</select>
`,

@ -28,9 +28,9 @@ export default test({
target.innerHTML,
`
<select>
<option value="one">one</option>
<option value="two">two</option>
<option value="three">three</option>
<option>one</option>
<option>two</option>
<option>three</option>
</select>
<p>selected: two</p>
`

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

Loading…
Cancel
Save