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 contents: read # to clone the repo
steps: steps:
- name: monitor action permissions - name: monitor action permissions
uses: GitHubSecurityLab/actions-permissions/monitor@v1
- name: check user authorization # user needs triage permission - name: check user authorization # user needs triage permission
uses: actions/github-script@v7 uses: actions/github-script@v7
id: check-permissions id: check-permissions

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

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

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

@ -9,7 +9,7 @@ The [Open Source Guides](https://opensource.guide/) website has a collection of
## Get involved ## 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). - 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). - 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 #### 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 #### 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. 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 ### svelte_boundary_reset_noop
``` ```

@ -1,5 +1,19 @@
<!-- This file is generated by scripts/process-messages/index.js. Do not edit! --> <!-- 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 ### 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! --> <!-- 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 ### invalid_default_snippet
``` ```

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

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

@ -1,5 +1,51 @@
# svelte # 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 ## 5.38.9
### Patch Changes ### 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. 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 ## svelte_boundary_reset_noop
> A `<svelte:boundary>` `reset` function only resets the boundary the first time it is called > 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 ## invalid_default_snippet
> Cannot use `{@render children(...)}` if the parent component uses `let:` directives. Consider using a named snippet instead > Cannot use `{@render children(...)}` if the parent component uses `let:` directives. Consider using a named snippet instead

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

@ -401,6 +401,7 @@ function run() {
transform('client-warnings', 'src/internal/client/warnings.js'); transform('client-warnings', 'src/internal/client/warnings.js');
transform('client-errors', 'src/internal/client/errors.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('server-errors', 'src/internal/server/errors.js');
transform('shared-errors', 'src/internal/shared/errors.js'); transform('shared-errors', 'src/internal/shared/errors.js');
transform('shared-warnings', 'src/internal/shared/warnings.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, mathml: false,
scoped: false, scoped: false,
has_spread: false, has_spread: false,
path: [] path: [],
synthetic_value_node: null
} }
} }
: /** @type {AST.ElementLike} */ ({ : /** @type {AST.ElementLike} */ ({

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

@ -27,6 +27,11 @@ export interface AnalysisState {
// legacy stuff // legacy stuff
reactive_statement: null | ReactiveStatement; 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< 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 { Context } from '../types' */
/** @import { AST } from '#compiler' */
import * as e from '../../../errors.js'; import * as e from '../../../errors.js';
/** /**
@ -7,16 +8,25 @@ import * as e from '../../../errors.js';
* @param {Context} context * @param {Context} context
*/ */
export function AwaitExpression(node, 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) { if (context.state.expression) {
context.state.expression.has_await = true; context.state.expression.has_await = true;
if ( if (context.state.fragment && context.path.some((node) => node.type === 'ConstTag')) {
context.state.fragment &&
// TODO there's probably a better way to do this
context.path.some((node) => node.type === 'ConstTag')
) {
context.state.fragment.metadata.has_await = true; context.state.fragment.metadata.has_await = true;
} }
@ -37,3 +47,101 @@ export function AwaitExpression(node, context) {
context.next(); 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.next({
...context.state, ...context.state,
function_depth: context.state.function_depth + 1, function_depth: context.state.function_depth + 1,
in_derived: true,
expression expression
}); });

@ -35,5 +35,9 @@ export function ConstTag(node, context) {
const declaration = node.declaration.declarations[0]; const declaration = node.declaration.declarations[0];
context.visit(declaration.id); 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]; 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); 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 (rune === '$props') {
if (node.id.type !== 'ObjectPattern' && node.id.type !== 'Identifier') { if (node.id.type !== 'ObjectPattern' && node.id.type !== 'Identifier') {
e.props_invalid_identifier(node); e.props_invalid_identifier(node);

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

@ -6,8 +6,7 @@ import type {
Expression, Expression,
AssignmentExpression, AssignmentExpression,
UpdateExpression, UpdateExpression,
VariableDeclaration, VariableDeclaration
Declaration
} from 'estree'; } from 'estree';
import type { AST, Namespace, ValidatedCompileOptions } from '#compiler'; import type { AST, Namespace, ValidatedCompileOptions } from '#compiler';
import type { TransformState } from '../types.js'; import type { TransformState } from '../types.js';
@ -22,11 +21,6 @@ export interface ClientTransformState extends TransformState {
*/ */
readonly in_constructor: boolean; 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>` */ /** `true` if we're transforming the contents of `<script>` */
readonly is_instance: boolean; readonly is_instance: boolean;

@ -4,7 +4,7 @@
/** @import { Analysis } from '../../types.js' */ /** @import { Analysis } from '../../types.js' */
/** @import { Scope } from '../../scope.js' */ /** @import { Scope } from '../../scope.js' */
import * as b from '#compiler/builders'; import * as b from '#compiler/builders';
import { is_simple_expression } from '../../../utils/ast.js'; import { is_simple_expression, save } from '../../../utils/ast.js';
import { import {
PROPS_IS_LAZY_INITIAL, PROPS_IS_LAZY_INITIAL,
PROPS_IS_IMMUTABLE, PROPS_IS_IMMUTABLE,
@ -296,7 +296,7 @@ export function create_derived(state, expression, async = false) {
const thunk = b.thunk(expression, async); const thunk = b.thunk(expression, async);
if (async) { if (async) {
return b.call(b.await(b.call('$.save', b.call('$.async_derived', thunk)))); return save(b.call('$.async_derived', thunk));
} else { } else {
return b.call(state.analysis.runes ? '$.derived' : '$.derived_safe_equal', thunk); 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 { Context } from '../types' */
import { dev, is_ignored } from '../../../../state.js'; import { dev, is_ignored } from '../../../../state.js';
import { save } from '../../../../utils/ast.js';
import * as b from '../../../../utils/builders.js'; import * as b from '../../../../utils/builders.js';
/** /**
@ -10,13 +11,8 @@ import * as b from '../../../../utils/builders.js';
export function AwaitExpression(node, context) { export function AwaitExpression(node, context) {
const argument = /** @type {Expression} */ (context.visit(node.argument)); const argument = /** @type {Expression} */ (context.visit(node.argument));
const tla = context.state.is_instance && context.state.scope.function_depth === 1; if (context.state.analysis.pickled_awaits.has(node)) {
return save(argument);
// 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)));
} }
// in dev, note which values are read inside a reactive expression, // 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 }; 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':
case '$derived.by': { case '$derived.by': {
let fn = /** @type {Expression} */ ( let fn = /** @type {Expression} */ (context.visit(node.arguments[0]));
context.visit(node.arguments[0], { ...context.state, in_derived: rune === '$derived' })
);
return b.call('$.derived', rune === '$derived' ? b.thunk(fn) : fn); 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]; const declaration = node.declaration.declarations[0];
// TODO we can almost certainly share some code with $derived(...) // TODO we can almost certainly share some code with $derived(...)
if (declaration.id.type === 'Identifier') { if (declaration.id.type === 'Identifier') {
const init = build_expression( const init = build_expression(context, declaration.init, node.metadata.expression);
{ ...context, state: { ...context.state, in_derived: true } },
declaration.init,
node.metadata.expression
);
let expression = create_derived(context.state, init, node.metadata.expression.has_await); 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']} */ ({ const child_state = /** @type {ComponentContext['state']} */ ({
...context.state, ...context.state,
transform, transform
in_derived: true
}); });
// TODO optimise the simple `{ x } = y` case — we can just return `y` // 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_ignored } from '../../../../state.js';
import { is_event_attribute, is_text_attribute } from '../../../../utils/ast.js'; import { is_event_attribute, is_text_attribute } from '../../../../utils/ast.js';
import * as b from '#compiler/builders'; 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 { clean_nodes, determine_namespace_for_children } from '../../utils.js';
import { build_getter } from '../utils.js'; import { build_getter } from '../utils.js';
import { import {
@ -406,10 +406,24 @@ export function RegularElement(node, context) {
} }
if (!has_spread && needs_special_value_handling) { if (!has_spread && needs_special_value_handling) {
for (const attribute of /** @type {AST.Attribute[]} */ (attributes)) { if (node.metadata.synthetic_value_node) {
if (attribute.name === 'value') { const synthetic_node = node.metadata.synthetic_value_node;
build_element_special_value_attribute(node.name, node_id, attribute, context); const synthetic_attribute = create_attribute(
break; '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 {Identifier} node_id
* @param {AST.Attribute} attribute * @param {AST.Attribute} attribute
* @param {ComponentContext} context * @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 state = context.state;
const is_select_with_value = const is_select_with_value =
// attribute.metadata.dynamic would give false negatives because even if the value does not change, // 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 evaluated = context.state.scope.evaluate(value);
const assignment = b.assignment('=', b.member(node_id, '__value'), 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'), b.member(node_id, 'value'),
evaluated.is_defined ? assignment : b.logical('??', assignment, b.literal('')) 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( const update = b.stmt(
is_select_with_value is_select_with_value
? b.sequence([ ? b.sequence([
inner_assignment, set_value_assignment,
// This ensures a one-way street to the DOM in case it's <select {value}> // 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 // 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 // because the select value is not reflected as an attribute, so the
// mutation observer wouldn't notice. // mutation observer wouldn't notice.
b.call('$.select_option', node_id, value) b.call('$.select_option', node_id, value)
]) ])
: inner_assignment : synthetic
? assignment
: set_value_assignment
); );
if (has_state) { if (has_state) {

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

@ -130,7 +130,7 @@ export function build_component(node, component_name, context) {
} else if (attribute.type === 'SpreadAttribute') { } else if (attribute.type === 'SpreadAttribute') {
const expression = /** @type {Expression} */ (context.visit(attribute)); 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( props_and_spreads.push(
b.thunk( b.thunk(
attribute.metadata.expression.has_await || attribute.metadata.expression.has_call 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 { set_scope } from '../../scope.js';
import { extract_identifiers } from '../../../utils/ast.js'; import { extract_identifiers } from '../../../utils/ast.js';
import * as b from '#compiler/builders'; 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 { render_stylesheet } from '../css/index.js';
import { AssignmentExpression } from './visitors/AssignmentExpression.js'; import { AssignmentExpression } from './visitors/AssignmentExpression.js';
import { AwaitBlock } from './visitors/AwaitBlock.js'; import { AwaitBlock } from './visitors/AwaitBlock.js';
@ -40,6 +40,7 @@ import { TitleElement } from './visitors/TitleElement.js';
import { UpdateExpression } from './visitors/UpdateExpression.js'; import { UpdateExpression } from './visitors/UpdateExpression.js';
import { VariableDeclaration } from './visitors/VariableDeclaration.js'; import { VariableDeclaration } from './visitors/VariableDeclaration.js';
import { SvelteBoundary } from './visitors/SvelteBoundary.js'; import { SvelteBoundary } from './visitors/SvelteBoundary.js';
import { call_component_renderer, create_async_block } from './visitors/shared/utils.js';
/** @type {Visitors} */ /** @type {Visitors} */
const global_visitors = { const global_visitors = {
@ -187,23 +188,21 @@ export function server_component(analysis, options) {
template.body = [ template.body = [
...snippets, ...snippets,
b.let('$$settled', b.true), b.let('$$settled', b.true),
b.let('$$inner_payload'), b.let('$$inner_renderer'),
b.function_declaration( b.function_declaration(
b.id('$$render_inner'), b.id('$$render_inner'),
[b.id('$$payload')], [b.id('$$renderer')],
b.block(/** @type {Statement[]} */ (rest)) b.block(/** @type {Statement[]} */ (rest))
), ),
b.do_while( b.do_while(
b.unary('!', b.id('$$settled')), b.unary('!', b.id('$$settled')),
b.block([ b.block([
b.stmt(b.assignment('=', b.id('$$settled'), b.true)), b.stmt(b.assignment('=', b.id('$$settled'), b.true)),
b.stmt( b.stmt(b.assignment('=', b.id('$$inner_renderer'), b.call('$$renderer.copy'))),
b.assignment('=', b.id('$$inner_payload'), b.call('$.copy_payload', b.id('$$payload'))) b.stmt(b.call('$$render_inner', b.id('$$inner_renderer')))
),
b.stmt(b.call('$$render_inner', b.id('$$inner_payload')))
]) ])
), ),
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)))); 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[]} */ (instance.body),
.../** @type {Statement[]} */ (template.body) .../** @type {Statement[]} */ (template.body)
]); ]);
if (analysis.instance.has_await) {
component_block = b.block([create_async_block(component_block)]);
}
// trick esrap into including comments // trick esrap into including comments
component_block.loc = instance.loc; component_block.loc = instance.loc;
if (analysis.props_id) { if (analysis.props_id) {
// need to be placed on first line of the component for hydration // need to be placed on first line of the component for hydration
component_block.body.unshift( 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; let should_inject_context = dev || analysis.needs_context;
if (should_inject_context) { if (should_inject_context) {
component_block.body.unshift(b.stmt(b.call('$.push', dev && b.id(analysis.name)))); component_block = b.block([
component_block.body.push(b.stmt(b.call('$.pop'))); call_component_renderer(component_block, dev && b.id(component_name))
]);
} }
if (analysis.uses_rest_props) { 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); 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)]))); 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 = let should_inject_props =
@ -311,7 +315,7 @@ export function server_component(analysis, options) {
const component_function = b.function_declaration( const component_function = b.function_declaration(
b.id(analysis.name), 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 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 { return {
type: 'Program', type: 'Program',
sourceType: 'module', sourceType: 'module',

@ -1,29 +1,33 @@
/** @import { BlockStatement, Expression, Pattern } from 'estree' */ /** @import { BlockStatement, Expression, Pattern, Statement } from 'estree' */
/** @import { AST } from '#compiler' */ /** @import { AST } from '#compiler' */
/** @import { ComponentContext } from '../types.js' */ /** @import { ComponentContext } from '../types.js' */
import * as b from '#compiler/builders'; 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 {AST.AwaitBlock} node
* @param {ComponentContext} context * @param {ComponentContext} context
*/ */
export function AwaitBlock(node, context) { export function AwaitBlock(node, context) {
context.state.template.push( /** @type {Statement} */
b.stmt( let statement = b.stmt(
b.call( b.call(
'$.await', '$.await',
b.id('$$payload'), b.id('$$renderer'),
/** @type {Expression} */ (context.visit(node.expression)), /** @type {Expression} */ (context.visit(node.expression)),
b.thunk( b.thunk(
node.pending ? /** @type {BlockStatement} */ (context.visit(node.pending)) : b.block([]) node.pending ? /** @type {BlockStatement} */ (context.visit(node.pending)) : b.block([])
), ),
b.arrow( b.arrow(
node.value ? [/** @type {Pattern} */ (context.visit(node.value))] : [], node.value ? [/** @type {Pattern} */ (context.visit(node.value))] : [],
node.then ? /** @type {BlockStatement} */ (context.visit(node.then)) : b.block([]) 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 { AwaitExpression, Expression } from 'estree' */
/** @import { Context } from '../types.js' */ /** @import { Context } from '../types' */
import * as b from '../../../../utils/builders.js'; import { save } from '../../../../utils/ast.js';
/** /**
* @param {AwaitExpression} node * @param {AwaitExpression} node
* @param {Context} context * @param {Context} context
*/ */
export function AwaitExpression(node, context) { export function AwaitExpression(node, context) {
// if `await` is inside a function, or inside `<script module>`, const argument = /** @type {Expression} */ (context.visit(node.argument));
// allow it, otherwise error
if ( if (context.state.analysis.pickled_awaits.has(node)) {
context.state.scope.function_depth === 0 || return save(argument);
context.path.some( }
(node) =>
node.type === 'ArrowFunctionExpression' || // we also need to restore context after block expressions
node.type === 'FunctionDeclaration' || let i = context.path.length;
node.type === 'FunctionExpression' while (i--) {
) const parent = context.path[i];
) {
return context.next(); 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 { BlockStatement, Expression, Statement } from 'estree' */
/** @import { AST } from '#compiler' */ /** @import { AST } from '#compiler' */
/** @import { ComponentContext } from '../types.js' */ /** @import { ComponentContext } from '../types.js' */
import { BLOCK_OPEN_ELSE } from '../../../../../internal/server/hydration.js';
import * as b from '#compiler/builders'; 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 * @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); 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'); 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[]} */ /** @type {Statement[]} */
const each = []; const each = [];
@ -44,23 +45,27 @@ export function EachBlock(node, context) {
); );
if (node.fallback) { 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)); const fallback = /** @type {BlockStatement} */ (context.visit(node.fallback));
fallback.body.unshift( fallback.body.unshift(b.stmt(b.call(b.id('$$renderer.push'), block_open_else)));
b.stmt(b.call(b.member(b.id('$$payload.out'), b.id('push')), b.literal(BLOCK_OPEN_ELSE)))
);
state.template.push( block.body.push(
b.if( b.if(
b.binary('!==', b.member(array_id, 'length'), b.literal(0)), b.binary('!==', b.member(array_id, 'length'), b.literal(0)),
b.block([open, for_loop]), b.block([open, for_loop]),
fallback fallback
), )
block_close
); );
} else { } 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 { AST } from '#compiler' */
/** @import { ComponentContext } from '../types.js' */ /** @import { ComponentContext } from '../types.js' */
import { BLOCK_OPEN_ELSE } from '../../../../../internal/server/hydration.js';
import * as b from '#compiler/builders'; 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 * @param {AST.IfBlock} node
@ -17,13 +16,16 @@ export function IfBlock(node, context) {
? /** @type {BlockStatement} */ (context.visit(node.alternate)) ? /** @type {BlockStatement} */ (context.visit(node.alternate))
: b.block([]); : b.block([]);
consequent.body.unshift( consequent.body.unshift(b.stmt(b.call(b.id('$$renderer.push'), block_open)));
b.stmt(b.call(b.member(b.id('$$payload.out'), b.id('push')), block_open))
);
alternate.body.unshift( alternate.body.unshift(b.stmt(b.call(b.id('$$renderer.push'), block_open_else)));
b.stmt(b.call(b.member(b.id('$$payload.out'), b.id('push')), b.literal(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 * as b from '#compiler/builders';
import { clean_nodes, determine_namespace_for_children } from '../../utils.js'; import { clean_nodes, determine_namespace_for_children } from '../../utils.js';
import { build_element_attributes, build_spread_object } from './shared/element.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 * @param {AST.RegularElement} node
@ -22,21 +28,38 @@ export function RegularElement(node, context) {
...context.state, ...context.state,
namespace, namespace,
preserve_whitespace: 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); const node_is_void = is_void(node.name);
context.state.template.push(b.literal(`<${node.name}`)); const optimiser = new PromiseOptimiser();
const body = build_element_attributes(node, { ...context, state });
context.state.template.push(b.literal(node_is_void ? '/>' : '>')); // add `/>` for XHTML compliance 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) { 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(/** @type {AST.Text} */ (node.fragment.nodes[0]).data),
b.literal(`</${node.name}>`) 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; return;
} }
@ -63,7 +86,7 @@ export function RegularElement(node, context) {
b.stmt( b.stmt(
b.call( b.call(
'$.push_element', '$.push_element',
b.id('$$payload'), b.id('$$renderer'),
b.literal(node.name), b.literal(node.name),
b.literal(location.line), b.literal(location.line),
b.literal(location.column) 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 ( if (
node.name === 'option' && node.name === 'select' &&
!node.attributes.some( node.attributes.some(
(attribute) => (attribute) =>
attribute.type === 'SpreadAttribute' ||
((attribute.type === 'Attribute' || attribute.type === 'BindDirective') && ((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: [] }; const inner_state = { ...state, template: [], init: [] };
process_children(trimmed, { ...context, state: inner_state }); process_children(trimmed, { ...context, state: inner_state });
state.template.push( const fn = b.arrow(
b.stmt( [b.id('$$renderer')],
b.call( b.block([...state.init, ...build_template(inner_state.template)])
'$.valueless_option',
b.id('$$payload'),
b.thunk(b.block([...inner_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 // if this is a `<textarea>` value or a contenteditable binding, we only add
// the body if the attribute/binding is falsy // the body if the attribute/binding is falsy
const inner_state = { ...state, template: [], init: [] }; const inner_state = { ...state, template: [], init: [] };
@ -174,10 +205,6 @@ export function RegularElement(node, context) {
process_children(trimmed, { ...context, state }); 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) { if (!node_is_void) {
state.template.push(b.literal(`</${node.name}>`)); state.template.push(b.literal(`</${node.name}>`));
} }
@ -185,4 +212,16 @@ export function RegularElement(node, context) {
if (dev) { if (dev) {
state.template.push(b.stmt(b.call('$.pop_element'))); 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( b.stmt(
(node.expression.type === 'CallExpression' ? b.call : b.maybe_call)( (node.expression.type === 'CallExpression' ? b.call : b.maybe_call)(
snippet_function, snippet_function,
b.id('$$payload'), b.id('$$renderer'),
...snippet_args ...snippet_args
) )
) )

@ -2,7 +2,13 @@
/** @import { AST } from '#compiler' */ /** @import { AST } from '#compiler' */
/** @import { ComponentContext } from '../types.js' */ /** @import { ComponentContext } from '../types.js' */
import * as b from '#compiler/builders'; 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 * @param {AST.SlotElement} node
@ -15,13 +21,22 @@ export function SlotElement(node, context) {
/** @type {Expression[]} */ /** @type {Expression[]} */
const spreads = []; const spreads = [];
const optimiser = new PromiseOptimiser();
let name = b.literal('default'); let name = b.literal('default');
for (const attribute of node.attributes) { for (const attribute of node.attributes) {
if (attribute.type === 'SpreadAttribute') { 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') { } 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') { if (attribute.name === 'name') {
name = /** @type {Literal} */ (value); name = /** @type {Literal} */ (value);
@ -43,12 +58,17 @@ export function SlotElement(node, context) {
const slot = b.call( const slot = b.call(
'$.slot', '$.slot',
b.id('$$payload'), b.id('$$renderer'),
b.id('$$props'), b.id('$$props'),
name, name,
props_expression, props_expression,
fallback 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) { export function SnippetBlock(node, context) {
let fn = b.function_declaration( let fn = b.function_declaration(
node.expression, node.expression,
[b.id('$$payload'), ...node.parameters], [b.id('$$renderer'), ...node.parameters],
/** @type {BlockStatement} */ (context.visit(node.body)) /** @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; const statements = node.metadata.can_hoist ? context.state.hoisted : context.state.init;
if (dev) { 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))); statements.push(b.stmt(b.call('$.prevent_snippet_stringification', fn.id)));
} }

@ -1,17 +1,14 @@
/** @import { BlockStatement } from 'estree' */ /** @import { BlockStatement } from 'estree' */
/** @import { AST } from '#compiler' */ /** @import { AST } from '#compiler' */
/** @import { ComponentContext } from '../types' */ /** @import { ComponentContext } from '../types' */
import { BLOCK_CLOSE, BLOCK_OPEN } from '../../../../../internal/server/hydration.js';
import * as b from '#compiler/builders'; 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 {AST.SvelteBoundary} node
* @param {ComponentContext} context * @param {ComponentContext} context
*/ */
export function SvelteBoundary(node, context) { export function SvelteBoundary(node, context) {
context.state.template.push(b.literal(BLOCK_OPEN));
// if this has a `pending` snippet, render it // if this has a `pending` snippet, render it
const pending_attribute = /** @type {AST.Attribute} */ ( const pending_attribute = /** @type {AST.Attribute} */ (
node.attributes.find((node) => node.type === 'Attribute' && node.name === 'pending') node.attributes.find((node) => node.type === 'Attribute' && node.name === 'pending')
@ -23,16 +20,23 @@ export function SvelteBoundary(node, context) {
) )
); );
if (pending_attribute) { if (pending_attribute || pending_snippet) {
const value = build_attribute_value(pending_attribute.value, context, false, true); const pending = pending_attribute
context.state.template.push(b.call(value, b.id('$$payload'))); ? b.call(
} else if (pending_snippet) { build_attribute_value(
context.state.template.push( pending_attribute.value,
/** @type {BlockStatement} */ (context.visit(pending_snippet.body)) 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 { } 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 { Location } from 'locate-character' */
/** @import { BlockStatement, Expression } from 'estree' */ /** @import { BlockStatement, Expression, Statement } from 'estree' */
/** @import { AST } from '#compiler' */ /** @import { AST } from '#compiler' */
/** @import { ComponentContext } from '../types.js' */ /** @import { ComponentContext } from '../types.js' */
import { dev, locator } from '../../../../state.js'; import { dev, locator } from '../../../../state.js';
import * as b from '#compiler/builders'; import * as b from '#compiler/builders';
import { determine_namespace_for_children } from '../../utils.js'; import { determine_namespace_for_children } from '../../utils.js';
import { build_element_attributes } from './shared/element.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 * @param {AST.SvelteElement} node
@ -37,7 +37,9 @@ export function SvelteElement(node, context) {
init: [] init: []
}; };
build_element_attributes(node, { ...context, state }); const optimiser = new PromiseOptimiser();
build_element_attributes(node, { ...context, state }, optimiser.transform);
if (dev) { if (dev) {
const location = /** @type {Location} */ (locator(node.start)); const location = /** @type {Location} */ (locator(node.start));
@ -45,7 +47,7 @@ export function SvelteElement(node, context) {
b.stmt( b.stmt(
b.call( b.call(
'$.push_element', '$.push_element',
b.id('$$payload'), b.id('$$renderer'),
tag, tag,
b.literal(location.line), b.literal(location.line),
b.literal(location.column) b.literal(location.column)
@ -57,18 +59,23 @@ export function SvelteElement(node, context) {
const attributes = b.block([...state.init, ...build_template(state.template)]); const attributes = b.block([...state.init, ...build_template(state.template)]);
const children = /** @type {BlockStatement} */ (context.visit(node.fragment, state)); const children = /** @type {BlockStatement} */ (context.visit(node.fragment, state));
context.state.template.push( /** @type {Statement} */
b.stmt( let statement = b.stmt(
b.call( b.call(
'$.element', '$.element',
b.id('$$payload'), b.id('$$renderer'),
tag, tag,
attributes.body.length > 0 && b.thunk(attributes), attributes.body.length > 0 && b.thunk(attributes),
children.body.length > 0 && b.thunk(children) 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) { if (dev) {
context.state.template.push(b.stmt(b.call('$.pop_element'))); 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)); const block = /** @type {BlockStatement} */ (context.visit(node.fragment));
context.state.template.push( 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 } }); process_children(node.fragment.nodes, { ...context, state: { ...context.state, template } });
template.push(b.literal('</title>')); 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 { BlockStatement, Expression, Pattern, Property, SequenceExpression, Statement } from 'estree' */
/** @import { AST } from '#compiler' */ /** @import { AST } from '#compiler' */
/** @import { ComponentContext } from '../../types.js' */ /** @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 * as b from '#compiler/builders';
import { is_element_node } from '../../../../nodes.js'; import { is_element_node } from '../../../../nodes.js';
import { dev } from '../../../../../state.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) { for (const attribute of node.attributes) {
if (attribute.type === 'LetDirective') { if (attribute.type === 'LetDirective') {
if (!slot_scope_applies_to_itself) { if (!slot_scope_applies_to_itself) {
lets.default.push(attribute); lets.default.push(attribute);
} }
} else if (attribute.type === 'SpreadAttribute') { } 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') { } else if (attribute.type === 'Attribute') {
const value = build_attribute_value(
attribute.value,
context,
optimiser.transform,
false,
true
);
if (attribute.name.startsWith('--')) { if (attribute.name.startsWith('--')) {
const value = build_attribute_value(attribute.value, context, false, true);
custom_css_props.push(b.init(attribute.name, value)); custom_css_props.push(b.init(attribute.name, value));
continue; continue;
} }
@ -90,7 +106,6 @@ export function build_inline_component(node, expression, context) {
has_children_prop = true; has_children_prop = true;
} }
const value = build_attribute_value(attribute.value, context, false, true);
push_prop(b.prop('init', b.key(attribute.name), value)); push_prop(b.prop('init', b.key(attribute.name), value));
} else if (attribute.type === 'BindDirective' && attribute.name !== 'this') { } else if (attribute.type === 'BindDirective' && attribute.name !== 'this') {
if (attribute.expression.type === 'SequenceExpression') { if (attribute.expression.type === 'SequenceExpression') {
@ -201,7 +216,7 @@ export function build_inline_component(node, expression, context) {
if (block.body.length === 0) continue; if (block.body.length === 0) continue;
/** @type {Pattern[]} */ /** @type {Pattern[]} */
const params = [b.id('$$payload')]; const params = [b.id('$$renderer')];
if (lets[slot_name].length > 0) { if (lets[slot_name].length > 0) {
const pattern = b.object_pattern( const pattern = b.object_pattern(
@ -278,7 +293,7 @@ export function build_inline_component(node, expression, context) {
let statement = b.stmt( let statement = b.stmt(
(node.type === 'SvelteComponent' ? b.maybe_call : b.call)( (node.type === 'SvelteComponent' ? b.maybe_call : b.call)(
expression, expression,
b.id('$$payload'), b.id('$$renderer'),
props_expression props_expression
) )
); );
@ -291,27 +306,33 @@ export function build_inline_component(node, expression, context) {
node.type === 'SvelteComponent' || (node.type === 'Component' && node.metadata.dynamic); node.type === 'SvelteComponent' || (node.type === 'Component' && node.metadata.dynamic);
if (custom_css_props.length > 0) { if (custom_css_props.length > 0) {
context.state.template.push( statement = b.stmt(
b.stmt( b.call(
b.call( '$.css_props',
'$.css_props', b.id('$$renderer'),
b.id('$$payload'), b.literal(context.state.namespace === 'svg' ? false : true),
b.literal(context.state.namespace === 'svg' ? false : true), b.object(custom_css_props),
b.object(custom_css_props), b.thunk(b.block([statement])),
b.thunk(b.block([statement])), dynamic && b.true
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) { if (dynamic && custom_css_props.length === 0) {
context.state.template.push(empty_comment); 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 { ArrayExpression, Expression, Literal, ObjectExpression } from 'estree' */
/** @import { AST } from '#compiler' */ /** @import { AST, ExpressionMetadata } from '#compiler' */
/** @import { ComponentContext, ComponentServerTransformState } from '../../types.js' */ /** @import { ComponentContext, ComponentServerTransformState } from '../../types.js' */
import { is_event_attribute, is_text_attribute } from '../../../../../utils/ast.js'; import { is_event_attribute, is_text_attribute } from '../../../../../utils/ast.js';
import { binding_properties } from '../../../../bindings.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. * their output to be the child content instead. In this case, an object is returned.
* @param {AST.RegularElement | AST.SvelteElement} node * @param {AST.RegularElement | AST.SvelteElement} node
* @param {import('zimmerframe').Context<AST.SvelteNode, ComponentServerTransformState>} context * @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>} */ /** @type {Array<AST.Attribute | AST.SpreadAttribute>} */
const attributes = []; const attributes = [];
@ -62,7 +63,8 @@ export function build_element_attributes(node, context) {
// also see related code in analysis phase // also see related code in analysis phase
attribute.value[0].data = '\n' + attribute.value[0].data; 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') { } else if (node.name !== 'select') {
// omit value attribute for select elements, it's irrelevant for the initially selected value and has no // 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) // 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 expression: is_checkbox
? b.call( ? b.call(
b.member(attribute.expression, 'includes'), b.member(attribute.expression, 'includes'),
build_attribute_value(value_attribute.value, context) build_attribute_value(value_attribute.value, context, transform)
) )
: b.binary( : b.binary(
'===', '===',
attribute.expression, attribute.expression,
build_attribute_value(value_attribute.value, context) build_attribute_value(value_attribute.value, context, transform)
), ),
metadata: { metadata: {
expression: create_expression_metadata() expression: create_expression_metadata()
@ -202,30 +204,14 @@ export function build_element_attributes(node, context) {
} }
if (has_spread) { if (has_spread) {
build_element_spread_attributes(node, attributes, style_directives, class_directives, context); build_element_spread_attributes(
if (node.name === 'option') { node,
context.state.template.push( attributes,
b.call( style_directives,
'$.maybe_selected', class_directives,
b.id('$$payload'), context,
b.member( transform
build_spread_object( );
node,
node.attributes.filter(
(attribute) =>
attribute.type === 'Attribute' ||
attribute.type === 'BindDirective' ||
attribute.type === 'SpreadAttribute'
),
context
),
'value',
false,
true
)
)
);
}
} else { } else {
const css_hash = node.metadata.scoped ? context.state.analysis.css.hash : null; 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( build_attribute_value(
attribute.value, attribute.value,
context, context,
transform,
WHITESPACE_INSENSITIVE_ATTRIBUTES.includes(name) WHITESPACE_INSENSITIVE_ATTRIBUTES.includes(name)
) )
).value; ).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; continue;
} }
const value = build_attribute_value( const value = build_attribute_value(
attribute.value, attribute.value,
context, context,
transform,
WHITESPACE_INSENSITIVE_ATTRIBUTES.includes(name) 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)}"`)); context.state.template.push(b.literal(` ${name}="${escape_html(value.value, true)}"`));
} else if (name === 'class') { } 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') { } 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 { } else {
context.state.template.push( context.state.template.push(
b.call('$.attr', b.literal(name), value, is_boolean_attribute(name) && b.true) 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 {AST.RegularElement | AST.SvelteElement} element
* @param {Array<AST.Attribute | AST.SpreadAttribute | AST.BindDirective>} attributes * @param {Array<AST.Attribute | AST.SpreadAttribute | AST.BindDirective>} attributes
* @param {ComponentContext} context * @param {ComponentContext} context
* @param {(expression: Expression, metadata: ExpressionMetadata) => Expression} transform
*/ */
export function build_spread_object(element, attributes, context) { export function build_spread_object(element, attributes, context, transform) {
return b.object( const object = b.object(
attributes.map((attribute) => { attributes.map((attribute) => {
if (attribute.type === 'Attribute') { if (attribute.type === 'Attribute') {
const name = get_attribute_name(element, attribute); const name = get_attribute_name(element, attribute);
const value = build_attribute_value( const value = build_attribute_value(
attribute.value, attribute.value,
context, context,
transform,
WHITESPACE_INSENSITIVE_ATTRIBUTES.includes(name) WHITESPACE_INSENSITIVE_ATTRIBUTES.includes(name)
); );
return b.prop('init', b.key(name), value); return b.prop('init', b.key(name), value);
} else if (attribute.type === 'BindDirective') { } else if (attribute.type === 'BindDirective') {
const name = get_attribute_name(element, attribute); const name = get_attribute_name(element, attribute);
@ -346,12 +325,20 @@ export function build_spread_object(element, attributes, context) {
attribute.expression.type === 'SequenceExpression' attribute.expression.type === 'SequenceExpression'
? b.call(attribute.expression.expressions[0]) ? b.call(attribute.expression.expressions[0])
: /** @type {Expression} */ (context.visit(attribute.expression)); : /** @type {Expression} */ (context.visit(attribute.expression));
return b.prop('init', b.key(name), value); 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.StyleDirective[]} style_directives
* @param {AST.ClassDirective[]} class_directives * @param {AST.ClassDirective[]} class_directives
* @param {ComponentContext} context * @param {ComponentContext} context
* @param {(expression: Expression, metadata: ExpressionMetadata) => Expression} transform
*/ */
function build_element_spread_attributes( function build_element_spread_attributes(
element, element,
attributes, attributes,
style_directives, style_directives,
class_directives, class_directives,
context context,
transform
) { ) {
let classes; let classes;
let styles; let styles;
let flags = 0; let flags = 0;
let has_await = false;
if (class_directives.length) { if (class_directives.length) {
const properties = class_directives.map((directive) => const properties = class_directives.map((directive) => {
b.init( has_await ||= directive.metadata.expression.has_await;
return b.init(
directive.name, directive.name,
directive.expression.type === 'Identifier' && directive.expression.name === directive.name directive.expression.type === 'Identifier' && directive.expression.name === directive.name
? b.id(directive.name) ? b.id(directive.name)
: /** @type {Expression} */ (context.visit(directive.expression)) : /** @type {Expression} */ (context.visit(directive.expression))
) );
); });
classes = b.object(properties); classes = b.object(properties);
} }
if (style_directives.length > 0) { if (style_directives.length > 0) {
const properties = style_directives.map((directive) => const properties = style_directives.map((directive) => {
b.init( has_await ||= directive.metadata.expression.has_await;
return b.init(
directive.name, directive.name,
directive.value === true directive.value === true
? b.id(directive.name) ? b.id(directive.name)
: build_attribute_value(directive.value, context, true) : build_attribute_value(directive.value, context, transform, true)
) );
); });
styles = b.object(properties); styles = b.object(properties);
} }
@ -406,15 +402,18 @@ function build_element_spread_attributes(
flags |= ELEMENT_IS_INPUT; flags |= ELEMENT_IS_INPUT;
} }
const object = build_spread_object(element, attributes, context); const object = build_spread_object(element, attributes, context, transform);
const css_hash = const css_hash =
element.metadata.scoped && context.state.analysis.css.hash element.metadata.scoped && context.state.analysis.css.hash
? b.literal(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]; 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 {Expression} expression
* @param {ComponentContext} context * @param {ComponentContext} context
* @param {string | null} hash * @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} */ /** @type {ObjectExpression | undefined} */
let directives; let directives;
@ -434,7 +434,10 @@ function build_attr_class(class_directives, expression, context, hash) {
b.prop( b.prop(
'init', 'init',
b.literal(directive.name), 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 {AST.StyleDirective[]} style_directives
* @param {Expression} expression * @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} */ /** @type {ArrayExpression | ObjectExpression | undefined} */
let directives; let directives;
@ -471,7 +475,7 @@ function build_attr_style(style_directives, expression, context) {
const expression = const expression =
directive.value === true directive.value === true
? b.id(directive.name) ? b.id(directive.name)
: build_attribute_value(directive.value, context, true); : build_attribute_value(directive.value, context, transform, true);
let name = directive.name; let name = directive.name;
if (name[0] !== '-' || name[1] !== '-') { if (name[0] !== '-' || name[1] !== '-') {

@ -1,20 +1,25 @@
/** @import { AssignmentOperator, Expression, Identifier, Node, Statement } from 'estree' */ /** @import { AssignmentOperator, Expression, Identifier, Node, Statement, BlockStatement } from 'estree' */
/** @import { AST } from '#compiler' */ /** @import { AST, ExpressionMetadata } from '#compiler' */
/** @import { ComponentContext, ServerTransformState } from '../../types.js' */ /** @import { ComponentContext, ServerTransformState } from '../../types.js' */
import { escape_html } from '../../../../../../escaping.js'; import { escape_html } from '../../../../../../escaping.js';
import { import {
BLOCK_CLOSE, BLOCK_CLOSE,
BLOCK_OPEN, BLOCK_OPEN,
BLOCK_OPEN_ELSE,
EMPTY_COMMENT EMPTY_COMMENT
} from '../../../../../../internal/server/hydration.js'; } from '../../../../../../internal/server/hydration.js';
import * as b from '#compiler/builders'; import * as b from '#compiler/builders';
import { sanitize_template_string } from '../../../../../utils/sanitize_template_string.js'; import { sanitize_template_string } from '../../../../../utils/sanitize_template_string.js';
import { regex_whitespaces_strict } from '../../../../patterns.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 */ /** 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); 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 */ /** 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); export const block_close = b.literal(BLOCK_CLOSE);
@ -32,6 +37,10 @@ export function process_children(nodes, { visit, state }) {
let sequence = []; let sequence = [];
function flush() { function flush() {
if (sequence.length === 0) {
return;
}
let quasi = b.quasi('', false); let quasi = b.quasi('', false);
const quasis = [quasi]; const quasis = [quasi];
@ -63,26 +72,25 @@ export function process_children(nodes, { visit, state }) {
} }
state.template.push(b.template(quasis, expressions)); state.template.push(b.template(quasis, expressions));
sequence = [];
} }
for (let i = 0; i < nodes.length; i += 1) { for (const node of nodes) {
const node = nodes[i]; if (node.type === 'ExpressionTag' && node.metadata.expression.has_await) {
flush();
if (node.type === 'Text' || node.type === 'Comment' || node.type === 'ExpressionTag') { 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); sequence.push(node);
} else { } else {
if (sequence.length > 0) { flush();
flush();
sequence = [];
}
visit(node, { ...state }); visit(node, { ...state });
} }
} }
if (sequence.length > 0) { flush();
flush();
}
} }
/** /**
@ -95,11 +103,9 @@ function is_statement(node) {
/** /**
* @param {Array<Statement | Expression>} template * @param {Array<Statement | Expression>} template
* @param {Identifier} out
* @param {AssignmentOperator | 'push'} operator
* @returns {Statement[]} * @returns {Statement[]}
*/ */
export function build_template(template, out = b.id('$$payload.out'), operator = 'push') { export function build_template(template) {
/** @type {string[]} */ /** @type {string[]} */
let strings = []; let strings = [];
@ -110,32 +116,18 @@ export function build_template(template, out = b.id('$$payload.out'), operator =
const statements = []; const statements = [];
const flush = () => { const flush = () => {
if (operator === 'push') { statements.push(
statements.push( b.stmt(
b.stmt( b.call(
b.call( b.id('$$renderer.push'),
b.member(out, b.id('push')), b.template(
b.template( strings.map((cooked, i) => b.quasi(cooked, i === strings.length - 1)),
strings.map((cooked, i) => b.quasi(cooked, i === strings.length - 1)), expressions
expressions
)
)
)
);
} else {
statements.push(
b.stmt(
b.assignment(
operator,
out,
b.template(
strings.map((cooked, i) => b.quasi(cooked, i === strings.length - 1)),
expressions
)
) )
) )
); )
} );
strings = []; strings = [];
expressions = []; expressions = [];
}; };
@ -178,6 +170,7 @@ export function build_template(template, out = b.id('$$payload.out'), operator =
* *
* @param {AST.Attribute['value']} value * @param {AST.Attribute['value']} value
* @param {ComponentContext} context * @param {ComponentContext} context
* @param {(expression: Expression, metadata: ExpressionMetadata) => Expression} transform
* @param {boolean} trim_whitespace * @param {boolean} trim_whitespace
* @param {boolean} is_component * @param {boolean} is_component
* @returns {Expression} * @returns {Expression}
@ -185,6 +178,7 @@ export function build_template(template, out = b.id('$$payload.out'), operator =
export function build_attribute_value( export function build_attribute_value(
value, value,
context, context,
transform,
trim_whitespace = false, trim_whitespace = false,
is_component = false is_component = false
) { ) {
@ -203,7 +197,10 @@ export function build_attribute_value(
return b.literal(is_component ? data : escape_html(data, true)); 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); let quasi = b.quasi('', false);
@ -221,7 +218,13 @@ export function build_attribute_value(
: node.data; : node.data;
} else { } else {
expressions.push( 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); quasi = b.quasi('', i + 1 === value.length);
@ -257,3 +260,70 @@ export function build_getter(node, state) {
return node; 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 { AST, Binding, StateField } from '#compiler';
import type { import type {
AwaitExpression,
CallExpression, CallExpression,
ClassBody, ClassBody,
Identifier, Identifier,
LabeledStatement, LabeledStatement,
Node,
Program Program
} from 'estree'; } from 'estree';
import type { Scope, ScopeRoot } from './scope.js'; import type { Scope, ScopeRoot } from './scope.js';
@ -47,6 +47,8 @@ export interface Analysis {
/** A set of deriveds that contain `await` expressions */ /** A set of deriveds that contain `await` expressions */
async_deriveds: Set<CallExpression>; async_deriveds: Set<CallExpression>;
/** Awaits needing context preservation */
pickled_awaits: Set<AwaitExpression>;
} }
export interface ComponentAnalysis extends Analysis { export interface ComponentAnalysis extends Analysis {

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

@ -344,6 +344,8 @@ export namespace AST {
has_spread: boolean; has_spread: boolean;
scoped: boolean; scoped: boolean;
path: SvelteNode[]; 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.logical(/** @type {ESTree.LogicalOperator} */ (operator.slice(0, -1)), left, right)
: b.binary(/** @type {ESTree.BinaryOperator} */ (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 { walk } from 'zimmerframe';
import { regex_is_valid_identifier } from '../phases/patterns.js'; import { regex_is_valid_identifier } from '../phases/patterns.js';
import { sanitize_template_string } from './sanitize_template_string.js'; import { sanitize_template_string } from './sanitize_template_string.js';
import { has_await } from './ast.js';
/** /**
* @param {Array<ESTree.Expression | ESTree.SpreadElement | null>} elements * @param {Array<ESTree.Expression | ESTree.SpreadElement | null>} elements
@ -363,7 +364,14 @@ export function prop(kind, key, value, computed = false) {
* @returns {ESTree.PropertyDefinition} * @returns {ESTree.PropertyDefinition}
*/ */
export function prop_def(key, value, computed = false, is_static = false) { 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) { export function unthunk(expression) {
// optimize `async () => await x()`, but not `async () => await x(await y)` // optimize `async () => await x()`, but not `async () => await x(await y)`
if (expression.async && expression.body.type === 'AwaitExpression') { if (expression.async && expression.body.type === 'AwaitExpression') {
let has_await = false; if (!has_await(expression.body.argument)) {
walk(expression.body.argument, null, {
AwaitExpression(_node, context) {
has_await = true;
context.stop();
}
});
if (!has_await) {
return unthunk(arrow(expression.params, 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) { export function method(kind, key, params, body, computed = false, is_static = false) {
return { return {
type: 'MethodDefinition', type: 'MethodDefinition',
decorators: [],
key, key,
kind, kind,
value: function_builder(null, params, block(body)), value: function_builder(null, params, block(body)),
@ -618,6 +618,7 @@ function if_builder(test, consequent, alternate) {
export function import_all(as, source) { export function import_all(as, source) {
return { return {
type: 'ImportDeclaration', type: 'ImportDeclaration',
attributes: [],
source: literal(source), source: literal(source),
specifiers: [import_namespace(as)] specifiers: [import_namespace(as)]
}; };
@ -631,6 +632,7 @@ export function import_all(as, source) {
export function imports(parts, source) { export function imports(parts, source) {
return { return {
type: 'ImportDeclaration', type: 'ImportDeclaration',
attributes: [],
source: literal(source), source: literal(source),
specifiers: parts.map((p) => ({ specifiers: parts.map((p) => ({
type: 'ImportSpecifier', type: 'ImportSpecifier',

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

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

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

@ -1,5 +1,11 @@
/** @import { Effect, Source, TemplateNode, } from '#client' */ /** @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 { component_context, set_component_context } from '../../context.js';
import { handle_error, invoke_error_boundary } from '../../error-handling.js'; import { handle_error, invoke_error_boundary } from '../../error-handling.js';
import { block, branch, destroy_effect, pause_effect } from '../../reactivity/effects.js'; import { block, branch, destroy_effect, pause_effect } from '../../reactivity/effects.js';
@ -15,7 +21,7 @@ import {
hydrate_node, hydrate_node,
hydrating, hydrating,
next, next,
remove_nodes, skip_nodes,
set_hydrate_node set_hydrate_node
} from '../hydration.js'; } from '../hydration.js';
import { get_next_sibling } from '../operations.js'; import { get_next_sibling } from '../operations.js';
@ -57,8 +63,8 @@ export class Boundary {
/** @type {TemplateNode} */ /** @type {TemplateNode} */
#anchor; #anchor;
/** @type {TemplateNode} */ /** @type {TemplateNode | null} */
#hydrate_open; #hydrate_open = hydrating ? hydrate_node : null;
/** @type {BoundaryProps} */ /** @type {BoundaryProps} */
#props; #props;
@ -83,6 +89,7 @@ export class Boundary {
#local_pending_count = 0; #local_pending_count = 0;
#pending_count = 0; #pending_count = 0;
#is_creating_fallback = false; #is_creating_fallback = false;
/** /**
@ -122,8 +129,6 @@ export class Boundary {
this.#props = props; this.#props = props;
this.#children = children; this.#children = children;
this.#hydrate_open = hydrate_node;
this.parent = /** @type {Effect} */ (active_effect).b; this.parent = /** @type {Effect} */ (active_effect).b;
this.#pending = !!this.#props.pending; this.#pending = !!this.#props.pending;
@ -132,34 +137,18 @@ export class Boundary {
/** @type {Effect} */ (active_effect).b = this; /** @type {Effect} */ (active_effect).b = this;
if (hydrating) { if (hydrating) {
const comment = this.#hydrate_open;
hydrate_next(); hydrate_next();
}
const pending = this.#props.pending; const server_rendered_pending =
/** @type {Comment} */ (comment).nodeType === COMMENT_NODE &&
if (hydrating && pending) { /** @type {Comment} */ (comment).data === HYDRATION_START_ELSE;
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;
});
this.#pending = false; if (server_rendered_pending) {
} this.#hydrate_pending_content();
}); } else {
this.#hydrate_resolved_content();
}
} else { } else {
try { try {
this.#main_effect = branch(() => children(this.#anchor)); 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 `true` if the effect exists inside a boundary whose pending snippet is shown
* @returns {boolean} * @returns {boolean}
@ -238,10 +264,10 @@ export class Boundary {
if (!this.has_pending_snippet()) { if (!this.has_pending_snippet()) {
if (this.parent) { if (this.parent) {
this.parent.#update_pending_count(d); 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; this.#pending_count += d;
@ -307,9 +333,9 @@ export class Boundary {
} }
if (hydrating) { if (hydrating) {
set_hydrate_node(this.#hydrate_open); set_hydrate_node(/** @type {TemplateNode} */ (this.#hydrate_open));
next(); next();
set_hydrate_node(remove_nodes()); set_hydrate_node(skip_nodes());
} }
var did_reset = false; var did_reset = false;
@ -330,7 +356,7 @@ export class Boundary {
// If the failure happened while flushing effects, current_batch can be null // If the failure happened while flushing effects, current_batch can be null
Batch.ensure(); Batch.ensure();
this.#pending_count = 0; this.#local_pending_count = 0;
if (this.#failed_effect !== null) { if (this.#failed_effect !== null) {
pause_effect(this.#failed_effect, () => { 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.#main_effect = this.#run(() => {
this.#is_creating_fallback = false; this.#is_creating_fallback = false;
@ -409,13 +437,7 @@ function move_effect(effect, fragment) {
} }
export function get_boundary() { export function get_boundary() {
const boundary = /** @type {Effect} */ (active_effect).b; return /** @type {Boundary} */ (/** @type {Effect} */ (active_effect).b);
if (boundary === null) {
e.await_outside_boundary();
}
return boundary;
} }
export function pending() { export function pending() {

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

@ -6,7 +6,7 @@ import {
hydrate_node, hydrate_node,
hydrating, hydrating,
read_hydration_instruction, read_hydration_instruction,
remove_nodes, skip_nodes,
set_hydrate_node, set_hydrate_node,
set_hydrating set_hydrating
} from '../hydration.js'; } from '../hydration.js';
@ -93,7 +93,7 @@ export function if_block(node, fn, elseif = false) {
if (!!condition === is_else) { if (!!condition === is_else) {
// Hydration mismatch: remove everything inside the anchor and start fresh. // Hydration mismatch: remove everything inside the anchor and start fresh.
// This could happen with `{#if browser}...{/if}`, for example // This could happen with `{#if browser}...{/if}`, for example
anchor = remove_nodes(); anchor = skip_nodes();
set_hydrate_node(anchor); set_hydrate_node(anchor);
set_hydrating(false); 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 depth = 0;
var node = hydrate_node; var node = hydrate_node;
@ -100,7 +101,7 @@ export function remove_nodes() {
} }
var next = /** @type {TemplateNode} */ (get_next_sibling(node)); var next = /** @type {TemplateNode} */ (get_next_sibling(node));
node.remove(); if (remove) node.remove();
node = next; 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 * Don't mark this as side-effect-free, hydration needs to walk all nodes
* @param {DocumentFragment | TemplateNode[]} fragment * @param {DocumentFragment | TemplateNode | TemplateNode[]} fragment
* @param {boolean} is_text * @param {boolean} [is_text]
* @returns {Node | null} * @returns {Node | null}
*/ */
export function first_child(fragment, is_text) { export function first_child(fragment, is_text = false) {
if (!hydrating) { if (!hydrating) {
// when not hydrating, `fragment` is a `DocumentFragment` (the result of calling `open_frag`) // when not hydrating, `fragment` is a `DocumentFragment` (the result of calling `open_frag`)
var first = /** @type {DocumentFragment} */ (get_first_child(/** @type {Node} */ (fragment))); var first = /** @type {DocumentFragment} */ (get_first_child(/** @type {Node} */ (fragment)));

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

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

@ -11,7 +11,7 @@ import {
set_active_effect, set_active_effect,
set_active_reaction set_active_reaction
} from '../runtime.js'; } from '../runtime.js';
import { current_batch, suspend } from './batch.js'; import { Batch, current_batch } from './batch.js';
import { import {
async_derived, async_derived,
current_async_effect, current_async_effect,
@ -20,6 +20,14 @@ import {
set_from_async_derived set_from_async_derived
} from './deriveds.js'; } from './deriveds.js';
import { aborted } from './effects.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 parent = /** @type {Effect} */ (active_effect);
var restore = capture(); var restore = capture();
var boundary = get_boundary();
var was_hydrating = hydrating;
Promise.all(async.map((expression) => async_derived(expression))) Promise.all(async.map((expression) => async_derived(expression)))
.then((result) => { .then((result) => {
@ -56,11 +65,15 @@ export function flatten(sync, async, fn) {
} }
} }
if (was_hydrating) {
set_hydrating(false);
}
batch?.deactivate(); batch?.deactivate();
unset_context(); unset_context();
}) })
.catch((error) => { .catch((error) => {
boundary.error(error); invoke_error_boundary(error, parent);
}); });
} }
@ -75,12 +88,23 @@ function capture() {
var previous_component_context = component_context; var previous_component_context = component_context;
var previous_batch = current_batch; var previous_batch = current_batch;
var was_hydrating = hydrating;
if (was_hydrating) {
var previous_hydrate_node = hydrate_node;
}
return function restore() { return function restore() {
set_active_effect(previous_effect); set_active_effect(previous_effect);
set_active_reaction(previous_reaction); set_active_reaction(previous_reaction);
set_component_context(previous_component_context); set_component_context(previous_component_context);
previous_batch?.activate(); previous_batch?.activate();
if (was_hydrating) {
set_hydrating(true);
set_hydrate_node(previous_hydrate_node);
}
if (DEV) { if (DEV) {
set_from_async_derived(null); set_from_async_derived(null);
} }
@ -178,16 +202,52 @@ export function unset_context() {
* @param {() => Promise<void>} fn * @param {() => Promise<void>} fn
*/ */
export async function async_body(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 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 { try {
await fn(); var promise = fn();
} finally {
if (next_hydrate_node) {
set_hydrate_node(next_hydrate_node);
hydrate_next();
}
}
try {
await promise;
} catch (error) { } catch (error) {
if (!aborted(active)) { if (!aborted(active)) {
invoke_error_boundary(error, active); invoke_error_boundary(error, active);
} }
} finally { } 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, INERT,
RENDER_EFFECT, RENDER_EFFECT,
ROOT_EFFECT, ROOT_EFFECT,
USER_EFFECT,
MAYBE_DIRTY MAYBE_DIRTY
} from '#client/constants'; } from '#client/constants';
import { async_mode_flag } from '../../flags/index.js'; import { async_mode_flag } from '../../flags/index.js';
import { deferred, define_property } from '../../shared/utils.js'; import { deferred, define_property } from '../../shared/utils.js';
import { get_boundary } from '../dom/blocks/boundary.js';
import { import {
active_effect, active_effect,
is_dirty, is_dirty,
@ -30,7 +28,6 @@ import { DEV } from 'esm-env';
import { invoke_error_boundary } from '../error-handling.js'; import { invoke_error_boundary } from '../error-handling.js';
import { old_values } from './sources.js'; import { old_values } from './sources.js';
import { unlink_effect } from './effects.js'; import { unlink_effect } from './effects.js';
import { unset_context } from './async.js';
/** @type {Set<Batch>} */ /** @type {Set<Batch>} */
const batches = new Set(); const batches = new Set();
@ -653,28 +650,6 @@ export function schedule_effect(signal) {
queued_root_effects.push(effect); 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 * 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 { HYDRATION_END, HYDRATION_ERROR, HYDRATION_START } from '../../constants.js';
import { active_effect } from './runtime.js'; import { active_effect } from './runtime.js';
import { push, pop, component_context } from './context.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 { import {
hydrate_next, hydrate_next,
hydrate_node, hydrate_node,
@ -30,7 +30,8 @@ import * as w from './warnings.js';
import * as e from './errors.js'; import * as e from './errors.js';
import { assign_nodes } from './dom/template.js'; import { assign_nodes } from './dom/template.js';
import { is_passive_event } from '../../utils.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 * This is normally true block effects should run their intro transitions
@ -119,19 +120,9 @@ export function hydrate(component, options) {
set_hydrating(true); set_hydrating(true);
set_hydrate_node(/** @type {Comment} */ (anchor)); set_hydrate_node(/** @type {Comment} */ (anchor));
hydrate_next();
const instance = _mount(component, { ...options, anchor }); 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); set_hydrating(false);
return /** @type {Exports} */ (instance); return /** @type {Exports} */ (instance);
@ -218,35 +209,50 @@ function _mount(Component, { target, anchor, props = {}, events, context, intro
var unmount = component_root(() => { var unmount = component_root(() => {
var anchor_node = anchor ?? target.appendChild(create_text()); var anchor_node = anchor ?? target.appendChild(create_text());
branch(() => { boundary(
if (context) { /** @type {TemplateNode} */ (anchor_node),
push({}); {
var ctx = /** @type {ComponentContext} */ (component_context); pending: () => {}
ctx.c = context; },
} (anchor_node) => {
if (context) {
if (events) { push({});
// We can't spread the object or else we'd lose the state proxy stuff, if it is one var ctx = /** @type {ComponentContext} */ (component_context);
/** @type {any} */ (props).$$events = events; ctx.c = context;
} }
if (hydrating) { if (events) {
assign_nodes(/** @type {TemplateNode} */ (anchor_node), null); // 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; if (hydrating) {
// @ts-expect-error the public typings are not what the actual function looks like assign_nodes(/** @type {TemplateNode} */ (anchor_node), null);
component = Component(anchor_node, props) || {}; }
should_intro = true;
if (hydrating) { should_intro = intro;
/** @type {Effect} */ (active_effect).nodes_end = hydrate_node; // @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) { if (context) {
pop(); pop();
}
} }
}); );
return () => { return () => {
for (var event_name of registered_events) { for (var event_name of registered_events) {
@ -309,7 +315,11 @@ export function unmount(component, options) {
} }
if (DEV) { if (DEV) {
w.lifecycle_double_unmount(); if (STATE_SYMBOL in component) {
w.state_proxy_unmount();
} else {
w.lifecycle_double_unmount();
}
} }
return Promise.resolve(); 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 * A `<svelte:boundary>` `reset` function only resets the boundary the first time it is called
*/ */

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

@ -1,10 +1,14 @@
/** @import { Component } from '#server' */ /** @import { SSRContext } from '#server' */
import { DEV } from 'esm-env'; import { DEV } from 'esm-env';
import { on_destroy } from './index.js';
import * as e from './errors.js'; import * as e from './errors.js';
/** @type {Component | null} */ /** @type {SSRContext | null} */
export var current_component = null; export var ssr_context = null;
/** @param {SSRContext | null} v */
export function set_ssr_context(v) {
ssr_context = v;
}
/** /**
* @template T * @template T
@ -47,42 +51,35 @@ export function getAllContexts() {
* @returns {Map<unknown, unknown>} * @returns {Map<unknown, unknown>}
*/ */
function get_or_init_context_map(name) { function get_or_init_context_map(name) {
if (current_component === null) { if (ssr_context === null) {
e.lifecycle_outside_component(name); 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] * @param {Function} [fn]
*/ */
export function push(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) { if (DEV) {
// component function ssr_context.function = fn;
current_component.function = fn; ssr_context.element = ssr_context.p?.element;
} }
} }
export function pop() { export function pop() {
var component = /** @type {Component} */ (current_component); ssr_context = /** @type {SSRContext} */ (ssr_context).p;
var ondestroy = component.d;
if (ondestroy) {
on_destroy.push(...ondestroy);
}
current_component = component.p;
} }
/** /**
* @param {Component} component_context * @param {SSRContext} ssr_context
* @returns {Map<unknown, unknown> | null} * @returns {Map<unknown, unknown> | null}
*/ */
function get_parent_context(component_context) { function get_parent_context(ssr_context) {
let parent = component_context.p; let parent = ssr_context.p;
while (parent !== null) { while (parent !== null) {
const context_map = parent.c; const context_map = parent.c;
@ -94,3 +91,22 @@ function get_parent_context(component_context) {
return null; 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 { FILENAME } from '../../constants.js';
import { import {
is_tag_valid_with_ancestor, is_tag_valid_with_ancestor,
is_tag_valid_with_parent is_tag_valid_with_parent
} from '../../html-tree-validation.js'; } 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 * as e from './errors.js';
import { HeadPayload, Payload } from './payload.js'; import { Renderer } from './renderer.js';
// TODO move this
/** /**
* @typedef {{ * @typedef {{
* tag: string; * tag: string;
* parent: null | Element; * parent: undefined | Element;
* filename: null | string; * filename: undefined | string;
* line: number; * line: number;
* column: number; * column: number;
* }} Element * }} Element
*/ */
/** /**
* @type {Element | null} * This is exported so that it can be cleared between tests
* @type {Set<string>}
*/ */
let parent = null; export let seen;
/** @type {Set<string>} */
let seen;
/** /**
* @param {Payload} payload * @param {Renderer} renderer
* @param {string} message * @param {string} message
*/ */
function print_error(payload, message) { function print_error(renderer, message) {
message = message =
`node_invalid_placement_ssr: ${message}\n\n` + `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.'; '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 // eslint-disable-next-line no-console
console.error(message); console.error(message);
payload.head.out.push(`<script>console.error(${JSON.stringify(message)})</script>`); renderer.head((r) => r.push(`<script>console.error(${JSON.stringify(message)})</script>`));
}
export function reset_elements() {
let old_parent = parent;
parent = null;
return () => {
parent = old_parent;
};
} }
/** /**
* @param {Payload} payload * @param {Renderer} renderer
* @param {string} tag * @param {string} tag
* @param {number} line * @param {number} line
* @param {number} column * @param {number} column
*/ */
export function push_element(payload, tag, line, column) { export function push_element(renderer, tag, line, column) {
var filename = /** @type {Component} */ (current_component).function[FILENAME]; var context = /** @type {SSRContext} */ (ssr_context);
var child = { tag, parent, filename, line, column }; 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 ancestor = parent.parent;
var ancestors = [parent.tag]; var ancestors = [parent.tag];
@ -71,7 +64,7 @@ export function push_element(payload, tag, line, column) {
: undefined; : undefined;
const message = is_tag_valid_with_parent(tag, parent.tag, child_loc, parent_loc); 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) { while (ancestor != null) {
ancestors.push(ancestor.tag); ancestors.push(ancestor.tag);
@ -80,27 +73,27 @@ export function push_element(payload, tag, line, column) {
: undefined; : undefined;
const message = is_tag_valid_with_ancestor(tag, ancestors, child_loc, ancestor_loc); 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; ancestor = ancestor.parent;
} }
} }
parent = child; set_ssr_context({ ...context, p: context, element });
} }
export function pop_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 ( if (
typeof payload !== 'object' || typeof renderer !== 'object' ||
// for some reason typescript consider the type of payload as never after the first instanceof // for some reason typescript consider the type of renderer as never after the first instanceof
!(payload instanceof Payload || /** @type {any} */ (payload) instanceof HeadPayload) !(renderer instanceof Renderer)
) { ) {
e.invalid_snippet_arguments(); e.invalid_snippet_arguments();
} }

@ -2,6 +2,30 @@
export * from '../shared/errors.js'; 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 * `%name%(...)` is not available on the server
* @param {string} name * @param {string} name

@ -1,6 +1,7 @@
/** @import { ComponentType, SvelteComponent } from 'svelte' */ /** @import { ComponentType, SvelteComponent, Component } from 'svelte' */
/** @import { Component, RenderOutput } from '#server' */ /** @import { RenderOutput } from '#server' */
/** @import { Store } from '#shared' */ /** @import { Store } from '#shared' */
/** @import { AccumulatedContent } from './renderer.js' */
export { FILENAME, HMR } from '../../constants.js'; export { FILENAME, HMR } from '../../constants.js';
import { attr, clsx, to_class, to_style } from '../shared/attributes.js'; import { attr, clsx, to_class, to_style } from '../shared/attributes.js';
import { is_promise, noop } from '../shared/utils.js'; import { is_promise, noop } from '../shared/utils.js';
@ -13,13 +14,10 @@ import {
} from '../../constants.js'; } from '../../constants.js';
import { escape_html } from '../../escaping.js'; import { escape_html } from '../../escaping.js';
import { DEV } from 'esm-env'; 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 { EMPTY_COMMENT, BLOCK_CLOSE, BLOCK_OPEN, BLOCK_OPEN_ELSE } from './hydration.js';
import { validate_store } from '../shared/validate.js'; import { validate_store } from '../shared/validate.js';
import { is_boolean_attribute, is_raw_text_element, is_void } from '../../utils.js'; import { is_boolean_attribute, is_raw_text_element, is_void } from '../../utils.js';
import { reset_elements } from './dev.js'; import { Renderer } from './renderer.js';
import { Payload } from './payload.js';
import { abort } from './abort-signal.js';
// https://html.spec.whatwg.org/multipage/syntax.html#attributes-2 // https://html.spec.whatwg.org/multipage/syntax.html#attributes-2
// https://infra.spec.whatwg.org/#noncharacter // 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; /[\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 {string} tag
* @param {() => void} attributes_fn * @param {() => void} attributes_fn
* @param {() => void} children_fn * @param {() => void} children_fn
* @returns {void} * @returns {void}
*/ */
export function element(payload, tag, attributes_fn = noop, children_fn = noop) { export function element(renderer, tag, attributes_fn = noop, children_fn = noop) {
payload.out.push('<!---->'); renderer.push('<!---->');
if (tag) { if (tag) {
payload.out.push(`<${tag}`); renderer.push(`<${tag}`);
attributes_fn(); attributes_fn();
payload.out.push(`>`); renderer.push(`>`);
if (!is_void(tag)) { if (!is_void(tag)) {
children_fn(); children_fn();
if (!is_raw_text_element(tag)) { 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. * 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. * 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 * @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] * @param {{ props?: Omit<Props, '$$slots' | '$$events'>; context?: Map<any, any>; idPrefix?: string }} [options]
* @returns {RenderOutput} * @returns {RenderOutput}
*/ */
export function render(component, options = {}) { export function render(component, options = {}) {
try { return Renderer.render(/** @type {Component<Props>} */ (component), options);
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();
}
} }
/** /**
* @param {Payload} payload * @param {Renderer} renderer
* @param {(head_payload: Payload['head']) => void} fn * @param {(renderer: Renderer) => Promise<void> | void} fn
* @returns {void} * @returns {void}
*/ */
export function head(payload, fn) { export function head(renderer, fn) {
const head_payload = payload.head; renderer.head((renderer) => {
head_payload.out.push(BLOCK_OPEN); renderer.push(BLOCK_OPEN);
fn(head_payload); renderer.child(fn);
head_payload.out.push(BLOCK_CLOSE); renderer.push(BLOCK_CLOSE);
});
} }
/** /**
* @param {Payload} payload * @param {Renderer} renderer
* @param {boolean} is_html * @param {boolean} is_html
* @param {Record<string, string>} props * @param {Record<string, string>} props
* @param {() => void} component * @param {() => void} component
* @param {boolean} dynamic * @param {boolean} dynamic
* @returns {void} * @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); const styles = style_object_to_string(props);
if (is_html) { if (is_html) {
payload.out.push(`<svelte-css-wrapper style="display: contents; ${styles}">`); renderer.push(`<svelte-css-wrapper style="display: contents; ${styles}">`);
} else { } else {
payload.out.push(`<g style="${styles}">`); renderer.push(`<g style="${styles}">`);
} }
if (dynamic) { if (dynamic) {
payload.out.push('<!---->'); renderer.push('<!---->');
} }
component(); component();
if (is_html) { if (is_html) {
payload.out.push(`<!----></svelte-css-wrapper>`); renderer.push(`<!----></svelte-css-wrapper>`);
} else { } else {
payload.out.push(`<!----></g>`); renderer.push(`<!----></g>`);
} }
} }
/** /**
* @param {Record<string, unknown>} attrs * @param {Record<string, unknown>} attrs
* @param {string | null} css_hash * @param {string} [css_hash]
* @param {Record<string, boolean>} [classes] * @param {Record<string, boolean>} [classes]
* @param {Record<string, string>} [styles] * @param {Record<string, string>} [styles]
* @param {number} [flags] * @param {number} [flags]
* @returns {string} * @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) { if (styles) {
attrs.style = to_style(attrs.style, 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 {Record<string, any>} $$props
* @param {string} name * @param {string} name
* @param {Record<string, unknown>} slot_props * @param {Record<string, unknown>} slot_props
* @param {null | (() => void)} fallback_fn * @param {null | (() => void)} fallback_fn
* @returns {void} * @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]; var slot_fn = $$props.$$slots?.[name];
// Interop: Can use snippets to fill slots // Interop: Can use snippets to fill slots
if (slot_fn === true) { if (slot_fn === true) {
@ -375,7 +319,7 @@ export function slot(payload, $$props, name, slot_props, fallback_fn) {
} }
if (slot_fn !== undefined) { if (slot_fn !== undefined) {
slot_fn(payload, slot_props); slot_fn(renderer, slot_props);
} else { } else {
fallback_fn?.(); fallback_fn?.();
} }
@ -443,21 +387,21 @@ export function bind_props(props_parent, props_now) {
/** /**
* @template V * @template V
* @param {Payload} payload * @param {Renderer} renderer
* @param {Promise<V>} promise * @param {Promise<V>} promise
* @param {null | (() => void)} pending_fn * @param {null | (() => void)} pending_fn
* @param {(value: V) => void} then_fn * @param {(value: V) => void} then_fn
* @returns {void} * @returns {void}
*/ */
function await_block(payload, promise, pending_fn, then_fn) { function await_block(renderer, promise, pending_fn, then_fn) {
if (is_promise(promise)) { if (is_promise(promise)) {
payload.out.push(BLOCK_OPEN); renderer.push(BLOCK_OPEN);
promise.then(null, noop); promise.then(null, noop);
if (pending_fn !== null) { if (pending_fn !== null) {
pending_fn(); pending_fn();
} }
} else if (then_fn !== null) { } else if (then_fn !== null) {
payload.out.push(BLOCK_OPEN_ELSE); renderer.push(BLOCK_OPEN_ELSE);
then_fn(promise); then_fn(promise);
} }
} }
@ -499,12 +443,12 @@ export function once(get_value) {
/** /**
* Create an unique ID * Create an unique ID
* @param {Payload} payload * @param {Renderer} renderer
* @returns {string} * @returns {string}
*/ */
export function props_id(payload) { export function props_id(renderer) {
const uid = payload.uid(); const uid = renderer.global.uid();
payload.out.push('<!--#' + uid + '-->'); renderer.push('<!--#' + uid + '-->');
return uid; return uid;
} }
@ -512,12 +456,10 @@ export { attr, clsx };
export { html } from './blocks/html.js'; 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 { 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 { snapshot } from '../shared/clone.js';
export { fallback, to_array } from '../shared/utils.js'; export { fallback, to_array } from '../shared/utils.js';
@ -531,8 +473,6 @@ export {
export { escape_html as escape }; export { escape_html as escape };
export { await_outside_boundary } from '../shared/errors.js';
/** /**
* @template T * @template T
* @param {()=>T} fn * @param {()=>T} fn
@ -553,33 +493,3 @@ export function derived(fn) {
return updated_value; 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 */ /** parent */
p: null | Component; p: null | SSRContext;
/** context */ /** component context */
c: null | Map<unknown, unknown>; c: null | Map<unknown, unknown>;
/** ondestroy */ /** renderer */
d: null | Array<() => void>; r: null | Renderer;
/** /** dev mode only: the current component function */
* dev mode only: the component function
*/
function?: any; function?: any;
/** dev mode only: the current element */
element?: Element;
} }
export interface RenderOutput { export interface SyncRenderOutput {
/** HTML that goes into the `<head>` */ /** HTML that goes into the `<head>` */
head: string; head: string;
/** @deprecated use `body` instead */ /** @deprecated use `body` instead */
@ -19,3 +22,5 @@ export interface RenderOutput {
/** HTML that goes somewhere into the `<body>` */ /** HTML that goes somewhere into the `<body>` */
body: string; 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} * @returns {string}
*/ */
export function attr(name, value, is_boolean = false) { 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 ''; if (value == null || (!value && is_boolean)) return '';
const normalized = (name in replacements && replacements[name].get(value)) || value; const normalized = (name in replacements && replacements[name].get(value)) || value;
const assignment = is_boolean ? '' : `="${escape_html(normalized, true)}"`; const assignment = is_boolean ? '' : `="${escape_html(normalized, true)}"`;

@ -2,22 +2,6 @@
import { DEV } from 'esm-env'; 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 * Cannot use `{@render children(...)}` if the parent component uses `let:` directives. Consider using a named snippet instead
* @returns {never} * @returns {never}

@ -1,11 +1,15 @@
/** @import { SvelteComponent } from '../index.js' */ /** @import { SvelteComponent } from '../index.js' */
import { asClassComponent as as_class_component, createClassComponent } from './legacy-client.js'; import { asClassComponent as as_class_component, createClassComponent } from './legacy-client.js';
import { render } from '../internal/server/index.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 // 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 }; 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. * Takes a Svelte 5 component and returns a Svelte 4 compatible component constructor.
* *
@ -21,16 +25,58 @@ export { createClassComponent };
*/ */
export function asClassComponent(component) { export function asClassComponent(component) {
const component_constructor = as_class_component(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 } = {}) => { const _render = (props, { context } = {}) => {
// @ts-expect-error the typings are off, but this will work if the component is compiled in SSR mode // @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 }); const result = render(component, { props, context });
return {
css: { code: '', map: null }, const munged = Object.defineProperties(
head: result.head, /** @type {LegacyRenderResult & PromiseLike<LegacyRenderResult>} */ ({}),
html: result.body {
}; 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 // @ts-expect-error this is present for SSR
component_constructor.render = _render; component_constructor.render = _render;

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

@ -4,5 +4,5 @@
* The current version, as set in package.json. * The current version, as set in package.json.
* @type {string} * @type {string}
*/ */
export const VERSION = '5.38.9'; export const VERSION = '5.39.4';
export const PUBLIC_VERSION = '5'; 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" "kind": "let"
}, },
"specifiers": [], "specifiers": [],
"source": null "source": null,
"attributes": []
} }
], ],
"sourceType": "module" "sourceType": "module"

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

@ -6,5 +6,9 @@ import config from '__CONFIG__';
import { render } from 'svelte/server'; import { render } from 'svelte/server';
export default function () { 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'; import { test } from '../../test';
export default test({ export default test({
skip_mode: ['server'], skip_mode: ['server', 'async-server'],
get props() { get props() {
return { value: 'hello!' }; return { value: 'hello!' };

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

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

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

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

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

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

Loading…
Cancel
Save