Merge branch 'main' into developer-guide

developer-guide
paoloricciuti 3 weeks ago
commit ee30cc9214

@ -1,5 +0,0 @@
---
'svelte': patch
---
fix: `append_styles` in an effect to make them available on mount

@ -1,5 +0,0 @@
---
'svelte': patch
---
chore: remove `parser.template_untrimmed`

@ -1,5 +0,0 @@
---
'svelte': patch
---
fix: always inject styles when compiling as a custom element

@ -60,6 +60,23 @@ jobs:
env:
CI: true
SVELTE_NO_ASYNC: true
TSGo:
permissions: {}
runs-on: ubuntu-latest
timeout-minutes: 5
steps:
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v4
- uses: actions/setup-node@v4
with:
node-version: 24
cache: pnpm
- name: install
run: pnpm install --frozen-lockfile
- name: install tsgo
run: cd packages/svelte && pnpm i -D @typescript/native-preview
- name: type check
run: cd packages/svelte && pnpm check:tsgo
Lint:
permissions: {}
runs-on: ubuntu-latest

@ -135,7 +135,7 @@ An effect only reruns when the object it reads changes, not when a property insi
An effect only depends on the values that it read the last time it ran. This has interesting implications for effects that have conditional code.
For instance, if `condition` is `true` in the code snippet below, the code inside the `if` block will run and `color` will be evaluated. As such, changes to either `condition` or `color` [will cause the effect to re-run](/playground/untitled#H4sIAAAAAAAAE21RQW6DMBD8ytaNBJHaJFLViwNIVZ8RcnBgXVk1xsILTYT4e20TQg89IOPZ2fHM7siMaJBx9tmaWpFqjQNlAKXEihx7YVJpdIyfRkY3G4gB8Pi97cPanRtQU8AuwuF_eNUaQuPlOMtc1SlLRWlKUo1tOwJflUikQHZtA0klzCDc64Imx0ANn8bInV1CDhtHgjClrsftcSXotluLybOUb3g4JJHhOZs5WZpuIS9gjNqkJKQP5e2ClrR4SMdZ13E4xZ8zTPOTJU2A2uE_PQ9COCI926_hTVarIU4hu_REPlBrKq2q73ycrf1N-vS4TMUsulaVg3EtR8H9rFgsg8uUsT1B2F9eshigZHBRpuaD0D3mY8Qm2BfB5N2YyRzdNEYVDy0Ja-WsFjcOUuP1HvFLWA6H3XuHTUSmmDV2--0TXonxsKbp7G9C6R__NONS-MFNvxj_d6mBAgAA).
For instance, if `condition` is `true` in the code snippet below, the code inside the `if` block will run and `color` will be evaluated. This means that changes to either `condition` or `color` [will cause the effect to re-run](/playground/untitled#H4sIAAAAAAAAE21RQW6DMBD8ytaNBJHaJFLViwNIVZ8RcnBgXVk1xsILTYT4e20TQg89IOPZ2fHM7siMaJBx9tmaWpFqjQNlAKXEihx7YVJpdIyfRkY3G4gB8Pi97cPanRtQU8AuwuF_eNUaQuPlOMtc1SlLRWlKUo1tOwJflUikQHZtA0klzCDc64Imx0ANn8bInV1CDhtHgjClrsftcSXotluLybOUb3g4JJHhOZs5WZpuIS9gjNqkJKQP5e2ClrR4SMdZ13E4xZ8zTPOTJU2A2uE_PQ9COCI926_hTVarIU4hu_REPlBrKq2q73ycrf1N-vS4TMUsulaVg3EtR8H9rFgsg8uUsT1B2F9eshigZHBRpuaD0D3mY8Qm2BfB5N2YyRzdNEYVDy0Ja-WsFjcOUuP1HvFLWA6H3XuHTUSmmDV2--0TXonxsKbp7G9C6R__NONS-MFNvxj_d6mBAgAA).
Conversely, if `condition` is `false`, `color` will not be evaluated, and the effect will _only_ re-run again when `condition` changes.

@ -277,4 +277,4 @@ Snippets can be created programmatically with the [`createRawSnippet`](svelte#cr
## Snippets and slots
In Svelte 4, content can be passed to components using [slots](legacy-slots). Snippets are more powerful and flexible, and as such slots are deprecated in Svelte 5.
In Svelte 4, content can be passed to components using [slots](legacy-slots). Snippets are more powerful and flexible, and so slots have been deprecated in Svelte 5.

@ -22,7 +22,7 @@ It also will not compile Svelte code.
## Styling
Content rendered this way is 'invisible' to Svelte and as such will not receive [scoped styles](scoped-styles) — in other words, this will not work, and the `a` and `img` styles will be regarded as unused:
Content rendered this way is 'invisible' to Svelte and as such will not receive [scoped styles](scoped-styles). In other words, this will not work, and the `a` and `img` styles will be regarded as unused:
<!-- prettier-ignore -->
```svelte

@ -71,7 +71,7 @@ The user of this component has the same flexibility to use a mixture of objects,
</Button>
```
Svelte also exposes the `ClassValue` type, which is the type of value that the `class` attribute on elements accept. This is useful if you want to use a type-safe class name in component props:
Since Svelte 5.19, Svelte also exposes the `ClassValue` type, which is the type of value that the `class` attribute on elements accept. This is useful if you want to use a type-safe class name in component props:
```svelte
<script lang="ts">

@ -12,7 +12,7 @@ The `<svelte:options>` element provides a place to specify per-component compile
- `runes={false}` — forces a component into _legacy mode_
- `namespace="..."` — the namespace where this component will be used, can be "html" (the default), "svg" or "mathml"
- `customElement={...}` — the [options](custom-elements#Component-options) to use when compiling this component as a custom element. If a string is passed, it is used as the `tag` option
- `css="injected"` — the component will inject its styles inline: During server side rendering, it's injected as a `<style>` tag in the `head`, during client side rendering, it's loaded via JavaScript
- `css="injected"` — the component will inject its styles inline: During server-side rendering, it's injected as a `<style>` tag in the `head`, during client side rendering, it's loaded via JavaScript
> [!LEGACY] Deprecated options
> Svelte 4 also included the following options. They are deprecated in Svelte 5 and non-functional in runes mode.

@ -41,7 +41,7 @@ If a function is returned from `onMount`, it will be called when the component i
</script>
```
> [!NOTE] This behaviour will only work when the function passed to `onMount` _synchronously_ returns a value. `async` functions always return a `Promise`, and as such cannot _synchronously_ return a function.
> [!NOTE] This behaviour will only work when the function passed to `onMount` is _synchronous_. `async` functions always return a `Promise`.
## `onDestroy`

@ -245,7 +245,7 @@ In Svelte 4, you can add event modifiers to handlers:
<button on:click|once|preventDefault={handler}>...</button>
```
Modifiers are specific to `on:` and as such do not work with modern event handlers. Adding things like `event.preventDefault()` inside the handler itself is preferable, since all the logic lives in one place rather than being split between handler and modifiers.
Modifiers are specific to `on:` and so do not work with modern event handlers. Adding things like `event.preventDefault()` inside the handler itself is preferable, since all the logic lives in one place rather than being split between handler and modifiers.
Since event handlers are just functions, you can create your own wrappers as necessary:
@ -340,7 +340,7 @@ When spreading props, local event handlers must go _after_ the spread, or they r
## Snippets instead of slots
In Svelte 4, content can be passed to components using slots. Svelte 5 replaces them with snippets which are more powerful and flexible, and as such slots are deprecated in Svelte 5.
In Svelte 4, content can be passed to components using slots. Svelte 5 replaces them with snippets, which are more powerful and flexible, and so slots are deprecated in Svelte 5.
They continue to work, however, and you can pass snippets to a component that uses slots:
@ -599,7 +599,7 @@ Note that `mount` and `hydrate` are _not_ synchronous, so things like `onMount`
### Server API changes
Similarly, components no longer have a `render` method when compiled for server side rendering. Instead, pass the function to `render` from `svelte/server`:
Similarly, components no longer have a `render` method when compiled for server-side rendering. Instead, pass the function to `render` from `svelte/server`:
```js
+++import { render } from 'svelte/server';+++
@ -803,7 +803,7 @@ Note that Svelte 5 will also warn if you have a single expression wrapped in quo
### HTML structure is stricter
In Svelte 4, you were allowed to write HTML code that would be repaired by the browser when server side rendering it. For example you could write this...
In Svelte 4, you were allowed to write HTML code that would be repaired by the browser when server-side rendering it. For example you could write this...
```svelte
<table>
@ -835,7 +835,7 @@ Assignments to destructured parts of a `@const` declaration are no longer allowe
### :is(...), :has(...), and :where(...) are scoped
Previously, Svelte did not analyse selectors inside `:is(...)`, `:has(...)`, and `:where(...)`, effectively treating them as global. Svelte 5 analyses them in the context of the current component. As such, some selectors may now be treated as unused if they were relying on this treatment. To fix this, use `:global(...)` inside the `:is(...)/:has(...)/:where(...)` selectors.
Previously, Svelte did not analyse selectors inside `:is(...)`, `:has(...)`, and `:where(...)`, effectively treating them as global. Svelte 5 analyses them in the context of the current component. Some selectors may now therefore be treated as unused if they were relying on this treatment. To fix this, use `:global(...)` inside the `:is(...)/:has(...)/:where(...)` selectors.
When using Tailwind's `@apply` directive, add a `:global` selector to preserve rules that use Tailwind-generated `:is(...)` selectors:
@ -964,7 +964,7 @@ Since these mismatches are extremely rare, Svelte 5 assumes that the values are
### Hydration works differently
Svelte 5 makes use of comments during server side rendering which are used for more robust and efficient hydration on the client. As such, you shouldn't remove comments from your HTML output if you intend to hydrate it, and if you manually authored HTML to be hydrated by a Svelte component, you need to adjust that HTML to include said comments at the correct positions.
Svelte 5 makes use of comments during server-side rendering which are used for more robust and efficient hydration on the client. You therefore should not remove comments from your HTML output if you intend to hydrate it, and if you manually authored HTML to be hydrated by a Svelte component, you need to adjust that HTML to include said comments at the correct positions.
### `onevent` attributes are delegated

@ -679,7 +679,7 @@ In HTML, there's [no such thing as a self-closing tag](https://jakearchibald.com
</div>
```
Some templating languages (including Svelte) will 'fix' HTML by turning `<span />` into `<span></span>`. Others adhere to the spec. Both result in ambiguity and confusion when copy-pasting code between different contexts, and as such Svelte prompts you to resolve the ambiguity directly by having an explicit closing tag.
Some templating languages (including Svelte) will 'fix' HTML by turning `<span />` into `<span></span>`. Others adhere to the spec. Both result in ambiguity and confusion when copy-pasting code between different contexts, so Svelte prompts you to resolve the ambiguity directly by having an explicit closing tag.
To automate this, run the dedicated migration:

@ -2,4 +2,6 @@
title: svelte/action
---
This module provides types for [actions](use), which have been superseded by [attachments](@attach).
> MODULE: svelte/action

@ -1,5 +1,61 @@
# svelte
## 5.38.1
### Patch Changes
- fix: wrap `abort` in `without_reactive_context` ([#16570](https://github.com/sveltejs/svelte/pull/16570))
- fix: add `hint` as a possible value for `popover` attribute ([#16581](https://github.com/sveltejs/svelte/pull/16581))
- fix: skip effects inside dynamic component that is about to be destroyed ([#16601](https://github.com/sveltejs/svelte/pull/16601))
## 5.38.0
### Minor Changes
- feat: allow `await` inside `@const` declarations ([#16542](https://github.com/sveltejs/svelte/pull/16542))
### Patch Changes
- fix: remount at any hydration error ([#16248](https://github.com/sveltejs/svelte/pull/16248))
- chore: emit `await_reactivity_loss` in `for await` loops ([#16521](https://github.com/sveltejs/svelte/pull/16521))
- fix: emit `snippet_invalid_export` instead of `undefined_export` for exported snippets ([#16539](https://github.com/sveltejs/svelte/pull/16539))
## 5.37.3
### Patch Changes
- fix: reset attribute cache after setting corresponding property ([#16543](https://github.com/sveltejs/svelte/pull/16543))
## 5.37.2
### Patch Changes
- fix: double event processing in firefox due to event object being garbage collected ([#16527](https://github.com/sveltejs/svelte/pull/16527))
- fix: add bindable dimension attributes types to SVG and MathML elements ([#16525](https://github.com/sveltejs/svelte/pull/16525))
- fix: correctly differentiate static fields before emitting `duplicate_class_field` ([#16526](https://github.com/sveltejs/svelte/pull/16526))
- fix: prevent last_propagated_event from being DCE'd ([#16538](https://github.com/sveltejs/svelte/pull/16538))
## 5.37.1
### Patch Changes
- chore: remove some todos ([#16515](https://github.com/sveltejs/svelte/pull/16515))
- fix: allow await expressions inside `{#await ...}` argument ([#16514](https://github.com/sveltejs/svelte/pull/16514))
- fix: `append_styles` in an effect to make them available on mount ([#16509](https://github.com/sveltejs/svelte/pull/16509))
- chore: remove `parser.template_untrimmed` ([#16511](https://github.com/sveltejs/svelte/pull/16511))
- fix: always inject styles when compiling as a custom element ([#16509](https://github.com/sveltejs/svelte/pull/16509))
## 5.37.0
### Minor Changes

@ -30,7 +30,7 @@ See [the SvelteKit documentation](https://svelte.dev/docs/kit) to learn more.
## Changelog
[The Changelog for this package is available on GitHub](https://github.com/sveltejs/svelte/blob/master/packages/svelte/CHANGELOG.md).
[The Changelog for this package is available on GitHub](https://github.com/sveltejs/svelte/blob/main/packages/svelte/CHANGELOG.md).
## Supporting Svelte

@ -464,6 +464,14 @@ export interface DOMAttributes<T extends EventTarget> {
onfullscreenerror?: EventHandler<Event, T> | undefined | null;
onfullscreenerrorcapture?: EventHandler<Event, T> | undefined | null;
// Dimensions
readonly 'bind:contentRect'?: DOMRectReadOnly | undefined | null;
readonly 'bind:contentBoxSize'?: Array<ResizeObserverSize> | undefined | null;
readonly 'bind:borderBoxSize'?: Array<ResizeObserverSize> | undefined | null;
readonly 'bind:devicePixelContentBoxSize'?: Array<ResizeObserverSize> | undefined | null;
readonly 'bind:clientWidth'?: number | undefined | null;
readonly 'bind:clientHeight'?: number | undefined | null;
xmlns?: string | undefined | null;
}
@ -773,7 +781,7 @@ export interface HTMLAttributes<T extends EventTarget> extends AriaAttributes, D
title?: string | undefined | null;
translate?: 'yes' | 'no' | '' | undefined | null;
inert?: boolean | undefined | null;
popover?: 'auto' | 'manual' | '' | undefined | null;
popover?: 'auto' | 'manual' | 'hint' | '' | undefined | null;
writingsuggestions?: Booleanish | undefined | null;
// Unknown
@ -839,13 +847,7 @@ export interface HTMLAttributes<T extends EventTarget> extends AriaAttributes, D
*/
'bind:innerText'?: string | undefined | null;
readonly 'bind:contentRect'?: DOMRectReadOnly | undefined | null;
readonly 'bind:contentBoxSize'?: Array<ResizeObserverSize> | undefined | null;
readonly 'bind:borderBoxSize'?: Array<ResizeObserverSize> | undefined | null;
readonly 'bind:devicePixelContentBoxSize'?: Array<ResizeObserverSize> | undefined | null;
readonly 'bind:focused'?: boolean | undefined | null;
readonly 'bind:clientWidth'?: number | undefined | null;
readonly 'bind:clientHeight'?: number | undefined | null;
readonly 'bind:offsetWidth'?: number | undefined | null;
readonly 'bind:offsetHeight'?: number | undefined | null;

@ -67,7 +67,7 @@ In HTML, there's [no such thing as a self-closing tag](https://jakearchibald.com
</div>
```
Some templating languages (including Svelte) will 'fix' HTML by turning `<span />` into `<span></span>`. Others adhere to the spec. Both result in ambiguity and confusion when copy-pasting code between different contexts, and as such Svelte prompts you to resolve the ambiguity directly by having an explicit closing tag.
Some templating languages (including Svelte) will 'fix' HTML by turning `<span />` into `<span></span>`. Others adhere to the spec. Both result in ambiguity and confusion when copy-pasting code between different contexts, so Svelte prompts you to resolve the ambiguity directly by having an explicit closing tag.
To automate this, run the dedicated migration:

@ -2,7 +2,7 @@
"name": "svelte",
"description": "Cybernetically enhanced web apps",
"license": "MIT",
"version": "5.37.0",
"version": "5.38.1",
"type": "module",
"types": "./types/index.d.ts",
"engines": {
@ -141,6 +141,7 @@
"build": "node scripts/process-messages && rollup -c && pnpm generate:types && node scripts/check-treeshakeability.js",
"dev": "node scripts/process-messages -w & rollup -cw",
"check": "tsc --project tsconfig.runtime.json && tsc && cd ./tests/types && tsc",
"check:tsgo": "tsgo --project tsconfig.runtime.json --skipLibCheck && tsgo --skipLibCheck",
"check:watch": "tsc --watch",
"generate:version": "node ./scripts/generate-version.js",
"generate:types": "node ./scripts/generate-types.js && tsc -p tsconfig.generated.json",
@ -165,7 +166,7 @@
"vitest": "^2.1.9"
},
"dependencies": {
"@ampproject/remapping": "^2.3.0",
"@jridgewell/remapping": "^2.3.4",
"@jridgewell/sourcemap-codec": "^1.5.0",
"@sveltejs/acorn-typescript": "^1.0.5",
"@types/estree": "^1.0.5",

@ -26,9 +26,11 @@ await createBundle({
// so that types/properties with `@internal` (and its dependencies) are removed from the output
stripInternal: true,
paths: Object.fromEntries(
Object.entries(pkg.imports).map(([key, value]) => {
return [key, [value.types ?? value.default ?? value]];
})
Object.entries(pkg.imports).map(
/** @param {[string,any]} import */ ([key, value]) => {
return [key, [value.types ?? value.default ?? value]];
}
)
)
},
modules: {

@ -229,7 +229,7 @@ declare namespace $derived {
*
* If you return a function from the effect, it will be called right before the effect is run again, or when the component is unmounted.
*
* Does not run during server side rendering.
* Does not run during server-side rendering.
*
* https://svelte.dev/docs/svelte/$effect
* @param fn The function to execute
@ -248,7 +248,7 @@ declare namespace $effect {
*
* If you return a function from the effect, it will be called right before the effect is run again, or when the component is unmounted.
*
* Does not run during server side rendering.
* Does not run during server-side rendering.
*
* https://svelte.dev/docs/svelte/$effect#$effect.pre
* @param fn The function to execute

@ -55,7 +55,9 @@ export function convert(source, ast) {
// Insert svelte:options back into the root nodes
if (/** @type {any} */ (options)?.__raw__) {
let idx = node.fragment.nodes.findIndex((node) => options.end <= node.start);
let idx = node.fragment.nodes.findIndex(
(node) => /** @type {any} */ (options).end <= node.start
);
if (idx === -1) {
idx = node.fragment.nodes.length;
}

@ -1707,14 +1707,14 @@ function extract_type_and_comment(declarator, state, path) {
}
// Ensure modifiers are applied in the same order as Svelte 4
const modifier_order = [
const modifier_order = /** @type {const} */ ([
'preventDefault',
'stopPropagation',
'stopImmediatePropagation',
'self',
'trusted',
'once'
];
]);
/**
* @param {AST.RegularElement | AST.SvelteElement | AST.SvelteWindow | AST.SvelteDocument | AST.SvelteBody} element

@ -1,5 +1,4 @@
/** @import { AST } from '#compiler' */
/** @import { Comment } from 'estree' */
// @ts-expect-error acorn type definitions are borked in the release we use
import { isIdentifierStart, isIdentifierChar } from 'acorn';
import fragment from './state/fragment.js';

@ -10,7 +10,8 @@ export function create_fragment(transparent = false) {
nodes: [],
metadata: {
transparent,
dynamic: false
dynamic: false,
has_await: false
}
};
}

@ -9,8 +9,8 @@ import {
import { regex_ends_with_whitespace, regex_starts_with_whitespace } from '../../patterns.js';
import { get_attribute_chunks, is_text_attribute } from '../../../utils/ast.js';
/** @typedef {NODE_PROBABLY_EXISTS | NODE_DEFINITELY_EXISTS} NodeExistsValue */
/** @typedef {FORWARD | BACKWARD} Direction */
/** @typedef {typeof NODE_PROBABLY_EXISTS | typeof NODE_DEFINITELY_EXISTS} NodeExistsValue */
/** @typedef {typeof FORWARD | typeof BACKWARD} Direction */
const NODE_PROBABLY_EXISTS = 0;
const NODE_DEFINITELY_EXISTS = 1;

@ -37,6 +37,7 @@ import { ExportNamedDeclaration } from './visitors/ExportNamedDeclaration.js';
import { ExportSpecifier } from './visitors/ExportSpecifier.js';
import { ExpressionStatement } from './visitors/ExpressionStatement.js';
import { ExpressionTag } from './visitors/ExpressionTag.js';
import { Fragment } from './visitors/Fragment.js';
import { FunctionDeclaration } from './visitors/FunctionDeclaration.js';
import { FunctionExpression } from './visitors/FunctionExpression.js';
import { HtmlTag } from './visitors/HtmlTag.js';
@ -156,6 +157,7 @@ const visitors = {
ExportSpecifier,
ExpressionStatement,
ExpressionTag,
Fragment,
FunctionDeclaration,
FunctionExpression,
HtmlTag,
@ -295,11 +297,12 @@ export function analyze_module(source, options) {
// TODO the following are not needed for modules, but we have to pass them in order to avoid type error,
// and reducing the type would result in a lot of tedious type casts elsewhere - find a good solution one day
ast_type: /** @type {any} */ (null),
component_slots: new Set(),
component_slots: /** @type {Set<string>} */ (new Set()),
expression: null,
function_depth: 0,
has_props_rune: false,
options: /** @type {ValidatedCompileOptions} */ (options),
fragment: null,
parent_element: null,
reactive_statement: null
},
@ -526,7 +529,6 @@ export function analyze_component(root, source, options) {
has_global: false
},
source,
undefined_exports: new Map(),
snippet_renderers: new Map(),
snippets: new Set(),
async_deriveds: new Set()
@ -688,6 +690,7 @@ export function analyze_component(root, source, options) {
analysis,
options,
ast_type: ast === instance.ast ? 'instance' : ast === template.ast ? 'template' : 'module',
fragment: ast === template.ast ? ast : null,
parent_element: null,
has_props_rune: false,
component_slots: new Set(),
@ -753,6 +756,7 @@ export function analyze_component(root, source, options) {
scopes,
analysis,
options,
fragment: ast === template.ast ? ast : null,
parent_element: null,
has_props_rune: false,
ast_type: ast === instance.ast ? 'instance' : ast === template.ast ? 'template' : 'module',
@ -787,9 +791,15 @@ export function analyze_component(root, source, options) {
if (node.type === 'ExportNamedDeclaration' && node.specifiers !== null && node.source == null) {
for (const specifier of node.specifiers) {
if (specifier.local.type !== 'Identifier') continue;
const binding = analysis.module.scope.get(specifier.local.name);
if (!binding) e.export_undefined(specifier, specifier.local.name);
const name = specifier.local.name;
const binding = analysis.module.scope.get(name);
if (!binding) {
if ([...analysis.snippets].find((snippet) => snippet.expression.name === name)) {
e.snippet_invalid_export(specifier);
} else {
e.export_undefined(specifier, name);
}
}
}
}
}

@ -8,6 +8,7 @@ export interface AnalysisState {
analysis: ComponentAnalysis;
options: ValidatedCompileOptions;
ast_type: 'instance' | 'template' | 'module';
fragment: AST.Fragment | null;
/**
* Tag name of the parent element. `null` if the parent is `svelte:element`, `#snippet`, a component or the root.
* Parent doesn't necessarily mean direct path predecessor because there could be `#each`, `#if` etc in-between.

@ -14,6 +14,7 @@ export default function check_graph_for_cycles(edges) {
}, new Map());
const visited = new Set();
/** @type {Set<T>} */
const on_stack = new Set();
/** @type {Array<Array<T>>} */
const cycles = [];

@ -11,6 +11,15 @@ export function AwaitExpression(node, context) {
if (context.state.expression) {
context.state.expression.has_await = true;
if (
context.state.fragment &&
// TODO there's probably a better way to do this
context.path.some((node) => node.type === 'ConstTag')
) {
context.state.fragment.metadata.has_await = true;
}
suspend = true;
}

@ -57,7 +57,7 @@ export function ClassBody(node, context) {
e.state_field_duplicate(node, name);
}
const _key = (key.type === 'PrivateIdentifier' ? '#' : '') + name;
const _key = (node.type === 'AssignmentExpression' || !node.static ? '' : '@') + name;
const field = fields.get(_key);
// if there's already a method or assigned field, error
@ -78,7 +78,7 @@ export function ClassBody(node, context) {
for (const child of node.body) {
if (child.type === 'PropertyDefinition' && !child.computed && !child.static) {
handle(child, child.key, child.value);
const key = (child.key.type === 'PrivateIdentifier' ? '#' : '') + get_name(child.key);
const key = /** @type {string} */ (get_name(child.key));
const field = fields.get(key);
if (!field) {
fields.set(key, [child.value ? 'assigned_prop' : 'prop']);
@ -91,7 +91,7 @@ export function ClassBody(node, context) {
if (child.kind === 'constructor') {
constructor = child;
} else if (!child.computed) {
const key = (child.key.type === 'PrivateIdentifier' ? '#' : '') + get_name(child.key);
const key = (child.static ? '@' : '') + get_name(child.key);
const field = fields.get(key);
if (!field) {
fields.set(key, [child.kind]);

@ -0,0 +1,10 @@
/** @import { AST } from '#compiler' */
/** @import { Context } from '../types.js' */
/**
* @param {AST.Fragment} node
* @param {Context} context
*/
export function Fragment(node, context) {
context.next({ ...context.state, fragment: node });
}

@ -35,11 +35,6 @@ export function SnippetBlock(node, context) {
if (can_hoist) {
const binding = /** @type {Binding} */ (context.state.scope.get(name));
context.state.analysis.module.scope.declarations.set(name, binding);
} else {
const undefined_export = context.state.analysis.undefined_exports.get(name);
if (undefined_export) {
e.snippet_invalid_export(undefined_export);
}
}
node.metadata.can_hoist = can_hoist;

@ -599,7 +599,7 @@ function has_disabled_attribute(attribute_map) {
/**
* @param {string} tag_name
* @param {Map<string, AST.Attribute>} attribute_map
* @returns {ElementInteractivity[keyof ElementInteractivity]}
* @returns {typeof ElementInteractivity[keyof typeof ElementInteractivity]}
*/
function element_interactivity(tag_name, attribute_map) {
if (

@ -145,6 +145,7 @@ export function visit_component(node, context) {
if (slot_name !== 'default') comments = [];
}
/** @type {Set<string>} */
const component_slots = new Set();
for (const slot_name in nodes) {

@ -26,6 +26,7 @@ import { DebugTag } from './visitors/DebugTag.js';
import { EachBlock } from './visitors/EachBlock.js';
import { ExportNamedDeclaration } from './visitors/ExportNamedDeclaration.js';
import { ExpressionStatement } from './visitors/ExpressionStatement.js';
import { ForOfStatement } from './visitors/ForOfStatement.js';
import { Fragment } from './visitors/Fragment.js';
import { FunctionDeclaration } from './visitors/FunctionDeclaration.js';
import { FunctionExpression } from './visitors/FunctionExpression.js';
@ -103,6 +104,7 @@ const visitors = {
EachBlock,
ExportNamedDeclaration,
ExpressionStatement,
ForOfStatement,
Fragment,
FunctionDeclaration,
FunctionExpression,
@ -170,6 +172,7 @@ export function client_component(analysis, options) {
// these are set inside the `Fragment` visitor, and cannot be used until then
init: /** @type {any} */ (null),
consts: /** @type {any} */ (null),
update: /** @type {any} */ (null),
after_update: /** @type {any} */ (null),
template: /** @type {any} */ (null),

@ -6,7 +6,8 @@ import type {
Expression,
AssignmentExpression,
UpdateExpression,
VariableDeclaration
VariableDeclaration,
Declaration
} from 'estree';
import type { AST, Namespace, ValidatedCompileOptions } from '#compiler';
import type { TransformState } from '../types.js';
@ -57,6 +58,8 @@ export interface ComponentClientTransformState extends ClientTransformState {
readonly update: Statement[];
/** Stuff that happens after the render effect (control blocks, dynamic elements, bindings, actions, etc) */
readonly after_update: Statement[];
/** Transformed `{@const }` declarations */
readonly consts: Statement[];
/** Memoized expressions */
readonly memoizer: Memoizer;
/** The HTML template string */

@ -1,4 +1,4 @@
/** @import { ArrowFunctionExpression, AssignmentExpression, Expression, FunctionDeclaration, FunctionExpression, Identifier, Node, Pattern, UpdateExpression } from 'estree' */
/** @import { ArrowFunctionExpression, AssignmentExpression, BlockStatement, Expression, FunctionDeclaration, FunctionExpression, Identifier, Node, Pattern, UpdateExpression } from 'estree' */
/** @import { Binding } from '#compiler' */
/** @import { ClientTransformState, ComponentClientTransformState, ComponentContext } from './types.js' */
/** @import { Analysis } from '../../types.js' */
@ -289,8 +289,15 @@ export function should_proxy(node, scope) {
/**
* Svelte legacy mode should use safe equals in most places, runes mode shouldn't
* @param {ComponentClientTransformState} state
* @param {Expression} arg
* @param {Expression | BlockStatement} expression
* @param {boolean} [async]
*/
export function create_derived(state, arg) {
return b.call(state.analysis.runes ? '$.derived' : '$.derived_safe_equal', arg);
export function create_derived(state, expression, async = false) {
const thunk = b.thunk(expression, async);
if (async) {
return b.call(b.await(b.call('$.save', b.call('$.async_derived', thunk))));
} else {
return b.call(state.analysis.runes ? '$.derived' : '$.derived_safe_equal', thunk);
}
}

@ -1,7 +1,7 @@
/** @import { BlockStatement, Pattern, Statement } from 'estree' */
/** @import { AST } from '#compiler' */
/** @import { ComponentClientTransformState, ComponentContext } from '../types' */
import { extract_identifiers } from '../../../../utils/ast.js';
import { extract_identifiers, is_expression_async } from '../../../../utils/ast.js';
import * as b from '#compiler/builders';
import { create_derived } from '../utils.js';
import { get_value } from './shared/declarations.js';
@ -15,7 +15,10 @@ export function AwaitBlock(node, context) {
context.state.template.push_comment();
// Visit {#await <expression>} first to ensure that scopes are in the correct order
const expression = b.thunk(build_expression(context, node.expression, node.metadata.expression));
const expression = b.thunk(
build_expression(context, node.expression, node.metadata.expression),
node.metadata.expression.has_await
);
let then_block;
let catch_block;
@ -93,13 +96,13 @@ function create_derived_block_argument(node, context) {
b.return(b.object(identifiers.map((identifier) => b.prop('init', identifier, identifier))))
]);
const declarations = [b.var(value, create_derived(context.state, b.thunk(block)))];
const declarations = [b.var(value, create_derived(context.state, block))];
for (const id of identifiers) {
context.state.transform[id.name] = { read: get_value };
declarations.push(
b.var(id, create_derived(context.state, b.thunk(b.member(b.call('$.get', value), id))))
b.var(id, create_derived(context.state, b.member(b.call('$.get', value), id)))
);
}

@ -82,7 +82,9 @@ export function CallExpression(node, context) {
['debug', 'dir', 'error', 'group', 'groupCollapsed', 'info', 'log', 'trace', 'warn'].includes(
node.callee.property.name
) &&
node.arguments.some((arg) => arg.type !== 'Literal') // TODO more cases?
node.arguments.some(
(arg) => arg.type === 'SpreadElement' || context.state.scope.evaluate(arg).has_unknown
)
) {
return b.call(
node.callee,

@ -16,21 +16,26 @@ export function ConstTag(node, context) {
const declaration = node.declaration.declarations[0];
// TODO we can almost certainly share some code with $derived(...)
if (declaration.id.type === 'Identifier') {
const init = build_expression(context, declaration.init, node.metadata.expression);
let expression = create_derived(context.state, b.thunk(init));
const init = build_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);
if (dev) {
expression = b.call('$.tag', expression, b.literal(declaration.id.name));
}
context.state.init.push(b.const(declaration.id, expression));
context.state.consts.push(b.const(declaration.id, expression));
context.state.transform[declaration.id.name] = { read: get_value };
// we need to eagerly evaluate the expression in order to hit any
// 'Cannot access x before initialization' errors
if (dev) {
context.state.init.push(b.stmt(b.call('$.get', declaration.id)));
context.state.consts.push(b.stmt(b.call('$.get', declaration.id)));
}
} else {
const identifiers = extract_identifiers(declaration.id);
@ -44,7 +49,11 @@ export function ConstTag(node, context) {
delete transform[node.name];
}
const child_state = { ...context.state, transform };
const child_state = /** @type {ComponentContext['state']} */ ({
...context.state,
transform,
in_derived: true
});
// TODO optimise the simple `{ x } = y` case — we can just return `y`
// instead of destructuring it only to return a new object
@ -53,26 +62,24 @@ export function ConstTag(node, context) {
declaration.init,
node.metadata.expression
);
const fn = b.arrow(
[],
b.block([
b.const(/** @type {Pattern} */ (context.visit(declaration.id, child_state)), init),
b.return(b.object(identifiers.map((node) => b.prop('init', node, node))))
])
);
let expression = create_derived(context.state, fn);
const block = b.block([
b.const(/** @type {Pattern} */ (context.visit(declaration.id, child_state)), init),
b.return(b.object(identifiers.map((node) => b.prop('init', node, node))))
]);
let expression = create_derived(context.state, block, node.metadata.expression.has_await);
if (dev) {
expression = b.call('$.tag', expression, b.literal('[@const]'));
}
context.state.init.push(b.const(tmp, expression));
context.state.consts.push(b.const(tmp, expression));
// we need to eagerly evaluate the expression in order to hit any
// 'Cannot access x before initialization' errors
if (dev) {
context.state.init.push(b.stmt(b.call('$.get', tmp)));
context.state.consts.push(b.stmt(b.call('$.get', tmp)));
}
for (const node of identifiers) {

@ -0,0 +1,20 @@
/** @import { Expression, ForOfStatement, Pattern, Statement, VariableDeclaration } from 'estree' */
/** @import { ComponentContext } from '../types' */
import * as b from '#compiler/builders';
import { dev, is_ignored } from '../../../../state.js';
/**
* @param {ForOfStatement} node
* @param {ComponentContext} context
*/
export function ForOfStatement(node, context) {
if (node.await && dev && !is_ignored(node, 'await_reactivity_loss')) {
const left = /** @type {VariableDeclaration | Pattern} */ (context.visit(node.left));
const argument = /** @type {Expression} */ (context.visit(node.right));
const body = /** @type {Statement} */ (context.visit(node.body));
const right = b.call('$.for_await_track_reactivity_loss', argument);
return b.for_of(left, right, body, true);
}
context.next();
}

@ -48,8 +48,10 @@ export function Fragment(node, context) {
const is_single_child_not_needing_template =
trimmed.length === 1 &&
(trimmed[0].type === 'SvelteFragment' || trimmed[0].type === 'TitleElement');
const has_await = context.state.init !== null && (node.metadata.has_await || false);
const template_name = context.state.scope.root.unique('root'); // TODO infer name from parent
const unsuspend = b.id('$$unsuspend');
/** @type {Statement[]} */
const body = [];
@ -61,6 +63,7 @@ export function Fragment(node, context) {
const state = {
...context.state,
init: [],
consts: [],
update: [],
after_update: [],
memoizer: new Memoizer(),
@ -76,11 +79,6 @@ export function Fragment(node, context) {
context.visit(node, state);
}
if (is_text_first) {
// skip over inserted comment
body.push(b.stmt(b.call('$.next')));
}
if (is_single_element) {
const element = /** @type {AST.RegularElement} */ (trimmed[0]);
@ -96,13 +94,13 @@ export function Fragment(node, context) {
const template = transform_template(state, namespace, flags);
state.hoisted.push(b.var(template_name, template));
body.push(b.var(id, b.call(template_name)));
state.init.unshift(b.var(id, b.call(template_name)));
close = b.stmt(b.call('$.append', b.id('$$anchor'), id));
} else if (is_single_child_not_needing_template) {
context.visit(trimmed[0], state);
} else if (trimmed.length === 1 && trimmed[0].type === 'Text') {
const id = b.id(context.state.scope.generate('text'));
body.push(b.var(id, b.call('$.text', b.literal(trimmed[0].data))));
state.init.unshift(b.var(id, b.call('$.text', b.literal(trimmed[0].data))));
close = b.stmt(b.call('$.append', b.id('$$anchor'), id));
} else if (trimmed.length > 0) {
const id = b.id(context.state.scope.generate('fragment'));
@ -120,7 +118,7 @@ export function Fragment(node, context) {
state
});
body.push(b.var(id, b.call('$.text')));
state.init.unshift(b.var(id, b.call('$.text')));
close = b.stmt(b.call('$.append', b.id('$$anchor'), id));
} else {
if (is_standalone) {
@ -140,12 +138,12 @@ export function Fragment(node, context) {
if (state.template.nodes.length === 1 && state.template.nodes[0].type === 'comment') {
// special case — we can use `$.comment` instead of creating a unique template
body.push(b.var(id, b.call('$.comment')));
state.init.unshift(b.var(id, b.call('$.comment')));
} else {
const template = transform_template(state, namespace, flags);
state.hoisted.push(b.var(template_name, template));
body.push(b.var(id, b.call(template_name)));
state.init.unshift(b.var(id, b.call(template_name)));
}
close = b.stmt(b.call('$.append', b.id('$$anchor'), id));
@ -153,6 +151,21 @@ export function Fragment(node, context) {
}
}
if (has_await) {
body.push(b.var(unsuspend, b.call('$.suspend')));
}
body.push(...state.consts);
if (has_await) {
body.push(b.if(b.call('$.aborted'), b.return()));
}
if (is_text_first) {
// skip over inserted comment
body.push(b.stmt(b.call('$.next')));
}
body.push(...state.init);
if (state.update.length > 0) {
@ -168,5 +181,9 @@ export function Fragment(node, context) {
body.push(close);
}
if (has_await) {
body.push(b.stmt(b.call(unsuspend)));
}
return b.block(body);
}

@ -46,9 +46,6 @@ export function LetDirective(node, context) {
read: (node) => b.call('$.get', node)
};
return b.const(
name,
create_derived(context.state, b.thunk(b.member(b.id('$$slotProps'), node.name)))
);
return b.const(name, create_derived(context.state, b.member(b.id('$$slotProps'), node.name)));
}
}

@ -3,14 +3,14 @@
import * as b from '#compiler/builders';
import { build_event, build_event_handler } from './shared/events.js';
const modifiers = [
const modifiers = /** @type {const} */ ([
'stopPropagation',
'stopImmediatePropagation',
'preventDefault',
'self',
'trusted',
'once'
];
]);
/**
* @param {AST.OnDirective} node

@ -14,6 +14,7 @@ export function SnippetBlock(node, context) {
// TODO hoist where possible
/** @type {(Identifier | AssignmentPattern)[]} */
const args = [b.id('$$anchor')];
const has_await = node.body.metadata.has_await || false;
/** @type {BlockStatement} */
let body;
@ -21,10 +22,6 @@ export function SnippetBlock(node, context) {
/** @type {Statement[]} */
const declarations = [];
if (dev) {
declarations.push(b.stmt(b.call('$.validate_snippet_args', b.spread(b.id('arguments')))));
}
const transform = { ...context.state.transform };
const child_state = { ...context.state, transform };
@ -72,16 +69,21 @@ export function SnippetBlock(node, context) {
}
}
}
const block = /** @type {BlockStatement} */ (context.visit(node.body, child_state)).body;
body = b.block([
dev ? b.stmt(b.call('$.validate_snippet_args', b.spread(b.id('arguments')))) : b.empty,
...declarations,
.../** @type {BlockStatement} */ (context.visit(node.body, child_state)).body
...block
]);
// in dev we use a FunctionExpression (not arrow function) so we can use `arguments`
let snippet = dev
? b.call('$.wrap_snippet', b.id(context.state.analysis.name), b.function(null, args, body))
: b.arrow(args, body);
? b.call(
'$.wrap_snippet',
b.id(context.state.analysis.name),
b.function(null, args, body, has_await)
)
: b.arrow(args, body, has_await);
const declaration = b.const(node.expression, snippet);

@ -43,7 +43,7 @@ export function SvelteBoundary(node, context) {
// to resolve this we cheat: we duplicate const tags inside snippets
for (const child of node.fragment.nodes) {
if (child.type === 'ConstTag') {
context.visit(child, { ...context.state, init: const_tags });
context.visit(child, { ...context.state, consts: const_tags });
}
}

@ -65,13 +65,14 @@ export function visit_assignment_expression(node, context, build_assignment) {
statements.push(b.return(rhs));
}
const iife = b.arrow([rhs], b.block(statements));
const iife_is_async =
const async =
is_expression_async(value) ||
assignments.some((assignment) => is_expression_async(assignment));
return iife_is_async ? b.await(b.call(b.async(iife), value)) : b.call(iife, value);
const iife = b.arrow([rhs], b.block(statements), async);
const call = b.call(iife, value);
return async ? b.await(call) : call;
}
const sequence = b.sequence(assignments);

@ -22,7 +22,7 @@ const NUMBER = Symbol('number');
const STRING = Symbol('string');
const FUNCTION = Symbol('string');
/** @type {Record<string, [type: NUMBER | STRING | UNKNOWN, fn?: Function]>} */
/** @type {Record<string, [type: typeof NUMBER | typeof STRING | typeof UNKNOWN, fn?: Function]>} */
const globals = {
BigInt: [NUMBER],
'Math.min': [NUMBER, Math.min],
@ -180,6 +180,13 @@ class Evaluation {
*/
is_known = true;
/**
* True if the possible values contains `UNKNOWN`
* @readonly
* @type {boolean}
*/
has_unknown = false;
/**
* True if the value is known to not be null/undefined
* @readonly
@ -540,6 +547,10 @@ class Evaluation {
if (value == null || value === UNKNOWN) {
this.is_defined = false;
}
if (value === UNKNOWN) {
this.has_unknown = true;
}
}
if (this.values.size > 1 || typeof this.value === 'symbol') {

@ -95,7 +95,6 @@ export interface ComponentAnalysis extends Analysis {
};
/** @deprecated use `source` from `state.js` instead */
source: string;
undefined_exports: Map<string, Node>;
/**
* Every render tag/component, and whether it could be definitively resolved or not
*/

@ -1,6 +1,6 @@
/** @import { Processed, Preprocessor, MarkupPreprocessor, PreprocessorGroup } from './public.js' */
/** @import { SourceUpdate, Source } from './private.js' */
/** @import { DecodedSourceMap, RawSourceMap } from '@ampproject/remapping' */
/** @import { DecodedSourceMap, RawSourceMap } from '@jridgewell/remapping' */
import { getLocator } from 'locate-character';
import {
MappedCode,
@ -25,7 +25,7 @@ class PreprocessResult {
// sourcemap_list is sorted in reverse order from last map (index 0) to first map (index -1)
// so we use sourcemap_list.unshift() to add new maps
// https://github.com/ampproject/remapping#multiple-transformations-of-a-file
// https://github.com/jridgewell/sourcemaps/tree/main/packages/remapping#multiple-transformations-of-a-file
/**
* @default []

@ -1,4 +1,4 @@
import { DecodedSourceMap } from '@ampproject/remapping';
import { DecodedSourceMap } from '@jridgewell/remapping';
import { Location } from 'locate-character';
import { MappedCode } from '../utils/mapped_code.js';

@ -87,7 +87,7 @@ export function pop_ignore() {
/**
* @param {AST.SvelteNode | NodeLike} node
* @param {import('../constants.js').IGNORABLE_RUNTIME_WARNINGS[number]} code
* @param {typeof import('../constants.js').IGNORABLE_RUNTIME_WARNINGS[number]} code
* @returns
*/
export function is_ignored(node, code) {

@ -56,6 +56,7 @@ export namespace AST {
* Whether or not we need to traverse into the fragment during mount/hydrate
*/
dynamic: boolean;
has_await: boolean;
};
}
@ -247,7 +248,17 @@ export namespace AST {
name: string;
/** The 'y' in `on:x={y}` */
expression: null | Expression;
modifiers: string[]; // TODO specify
modifiers: Array<
| 'capture'
| 'nonpassive'
| 'once'
| 'passive'
| 'preventDefault'
| 'self'
| 'stopImmediatePropagation'
| 'stopPropagation'
| 'trusted'
>;
/** @internal */
metadata: {
expression: ExpressionMetadata;

@ -56,15 +56,6 @@ export function assignment(operator, left, right) {
return { type: 'AssignmentExpression', operator, left, right };
}
/**
* @template T
* @param {T & ESTree.BaseFunction} func
* @returns {T & ESTree.BaseFunction}
*/
export function async(func) {
return { ...func, async: true };
}
/**
* @param {ESTree.Expression} argument
* @returns {ESTree.AwaitExpression}
@ -214,6 +205,23 @@ export function export_default(declaration) {
return { type: 'ExportDefaultDeclaration', declaration };
}
/**
* @param {ESTree.VariableDeclaration | ESTree.Pattern} left
* @param {ESTree.Expression} right
* @param {ESTree.Statement} body
* @param {boolean} [_await]
* @returns {ESTree.ForOfStatement}
*/
export function for_of(left, right, body, _await = false) {
return {
type: 'ForOfStatement',
left,
right,
body,
await: _await
};
}
/**
* @param {ESTree.Identifier} id
* @param {ESTree.Pattern[]} params
@ -580,14 +588,14 @@ export function method(kind, key, params, body, computed = false, is_static = fa
* @param {ESTree.BlockStatement} body
* @returns {ESTree.FunctionExpression}
*/
function function_builder(id, params, body) {
function function_builder(id, params, body, async = false) {
return {
type: 'FunctionExpression',
id,
params,
body,
generator: false,
async: false,
async,
metadata: /** @type {any} */ (null) // should not be used by codegen
};
}

@ -2,8 +2,8 @@
/** @import { Processed } from '../preprocess/public.js' */
/** @import { SourceMap } from 'magic-string' */
/** @import { Source } from '../preprocess/private.js' */
/** @import { DecodedSourceMap, SourceMapSegment, RawSourceMap } from '@ampproject/remapping' */
import remapping from '@ampproject/remapping';
/** @import { DecodedSourceMap, SourceMapSegment, RawSourceMap } from '@jridgewell/remapping' */
import remapping from '@jridgewell/remapping';
import { push_array } from './push_array.js';
/**

@ -160,10 +160,14 @@ export function createEventDispatcher() {
e.lifecycle_outside_component('createEventDispatcher');
}
/**
* @param [detail]
* @param [options]
*/
return (type, detail, options) => {
const events = /** @type {Record<string, Function | Function[]>} */ (
active_component_context.s.$$events
)?.[/** @type {any} */ (type)];
)?.[/** @type {string} */ (type)];
if (events) {
const callbacks = is_array(events) ? events.slice() : [events];

@ -64,7 +64,7 @@ export function hmr(original, get_source) {
// @ts-expect-error
wrapper[FILENAME] = original[FILENAME];
// @ts-expect-error
// @ts-ignore
wrapper[HMR] = {
// When we accept an update, we set the original source to the new component
original,

@ -28,6 +28,8 @@ const PENDING = 0;
const THEN = 1;
const CATCH = 2;
/** @typedef {typeof PENDING | typeof THEN | typeof CATCH} AwaitState */
/**
* @template V
* @param {TemplateNode} node
@ -67,9 +69,8 @@ export function await_block(node, get_input, pending_fn, then_fn, catch_fn) {
: mutable_source(/** @type {V} */ (undefined), false, false);
var error_source = runes ? source(undefined) : mutable_source(undefined, false, false);
var resolved = false;
/**
* @param {PENDING | THEN | CATCH} state
* @param {AwaitState} state
* @param {boolean} restore
*/
function update(state, restore) {

@ -191,7 +191,7 @@ export function each(node, flags, get_collection, get_key, render_fn, fallback_f
// store a reference to the effect so that we can update the start/end nodes in reconciliation
each_effect ??= /** @type {Effect} */ (active_effect);
array = get(each_array);
array = /** @type {V[]} */ (get(each_array));
var length = array.length;
if (was_empty && length === 0) {

@ -36,7 +36,7 @@ export function if_block(node, fn, elseif = false) {
/** @type {Effect | null} */
var alternate_effect = null;
/** @type {UNINITIALIZED | boolean | null} */
/** @type {typeof UNINITIALIZED | boolean | null} */
var condition = UNINITIALIZED;
var flags = elseif ? EFFECT_TRANSPARENT : 0;

@ -62,8 +62,10 @@ export function component(node, get_component, render_fn) {
if (defer) {
offscreen_fragment = document.createDocumentFragment();
offscreen_fragment.append((target = create_text()));
if (effect) {
/** @type {Batch} */ (current_batch).skipped_effects.add(effect);
}
}
pending_effect = branch(() => render_fn(target, component));
}

@ -19,7 +19,7 @@ import { attach } from './attachments.js';
import { clsx } from '../../../shared/attributes.js';
import { set_class } from './class.js';
import { set_style } from './style.js';
import { ATTACHMENT_KEY, NAMESPACE_HTML } from '../../../../constants.js';
import { ATTACHMENT_KEY, NAMESPACE_HTML, UNINITIALIZED } from '../../../../constants.js';
import { block, branch, destroy_effect, effect } from '../../reactivity/effects.js';
import { init_select, select_option } from './bindings/select.js';
import { flatten } from '../../reactivity/async.js';
@ -446,6 +446,8 @@ export function set_attributes(element, prev, next, css_hash, skip_warning = fal
) {
// @ts-ignore
element[name] = value;
// remove it from attributes's cache
if (name in attributes) attributes[name] = UNINITIALIZED;
} else if (typeof value !== 'function') {
set_attribute(element, name, value, skip_warning);
}

@ -245,6 +245,7 @@ export function bind_checked(input, get, set = get) {
* @returns {V[]}
*/
function get_binding_group_value(group, __value, checked) {
/** @type {Set<V>} */
var value = new Set();
for (var i = 0; i < group.length; i += 1) {

@ -141,6 +141,13 @@ export function delegate(events) {
}
}
// used to store the reference to the currently propagated event
// to prevent garbage collection between microtasks in Firefox
// If the event object is GCed too early, the expando __root property
// set on the event object is lost, causing the event delegation
// to process the event twice
let last_propagated_event = null;
/**
* @this {EventTarget}
* @param {Event} event
@ -153,14 +160,19 @@ export function handle_event_propagation(event) {
var path = event.composedPath?.() || [];
var current_target = /** @type {null | Element} */ (path[0] || event.target);
last_propagated_event = event;
// composedPath contains list of nodes the event has propagated through.
// We check __root to skip all nodes below it in case this is a
// parent of the __root node, which indicates that there's nested
// mounted apps. In this case we don't want to trigger events multiple times.
var path_idx = 0;
// the `last_propagated_event === event` check is redundant, but
// without it the variable will be DCE'd and things will
// fail mysteriously in Firefox
// @ts-expect-error is added below
var handled_at = event.__root;
var handled_at = last_propagated_event === event && event.__root;
if (handled_at) {
var at_idx = path.indexOf(handled_at);

@ -156,7 +156,7 @@ export function from_mathml(content, flags) {
/**
* @param {TemplateStructure[]} structure
* @param {NAMESPACE_SVG | NAMESPACE_MATHML | undefined} [ns]
* @param {typeof NAMESPACE_SVG | typeof NAMESPACE_MATHML | undefined} [ns]
*/
function fragment_from_tree(structure, ns) {
var fragment = create_fragment();

@ -98,7 +98,11 @@ export {
props_id,
with_script
} from './dom/template.js';
export { save, track_reactivity_loss } from './reactivity/async.js';
export {
for_await_track_reactivity_loss,
save,
track_reactivity_loss
} from './reactivity/async.js';
export { flushSync as flush, suspend } from './reactivity/batch.js';
export {
async_derived,

@ -119,6 +119,51 @@ export async function track_reactivity_loss(promise) {
};
}
/**
* Used in `for await` loops in DEV, so
* that we can emit `await_reactivity_loss` warnings
* after each `async_iterator` result resolves and
* after the `async_iterator` return resolves (if it runs)
* @template T
* @template TReturn
* @param {Iterable<T> | AsyncIterable<T>} iterable
* @returns {AsyncGenerator<T, TReturn | undefined>}
*/
export async function* for_await_track_reactivity_loss(iterable) {
// This is based on the algorithms described in ECMA-262:
// ForIn/OfBodyEvaluation
// https://tc39.es/ecma262/multipage/ecmascript-language-statements-and-declarations.html#sec-runtime-semantics-forin-div-ofbodyevaluation-lhs-stmt-iterator-lhskind-labelset
// AsyncIteratorClose
// https://tc39.es/ecma262/multipage/abstract-operations.html#sec-asynciteratorclose
/** @type {AsyncIterator<T, TReturn>} */
// @ts-ignore
const iterator = iterable[Symbol.asyncIterator]?.() ?? iterable[Symbol.iterator]?.();
if (iterator === undefined) {
throw new TypeError('value is not async iterable');
}
/** Whether the completion of the iterator was "normal", meaning it wasn't ended via `break` or a similar method */
let normal_completion = false;
try {
while (true) {
const { done, value } = (await track_reactivity_loss(iterator.next()))();
if (done) {
normal_completion = true;
break;
}
yield value;
}
} finally {
// If the iterator had a normal completion and `return` is defined on the iterator, call it and return the value
if (normal_completion && iterator.return !== undefined) {
// eslint-disable-next-line no-unsafe-finally
return /** @type {TReturn} */ ((await track_reactivity_loss(iterator.return()))().value);
}
}
}
export function unset_context() {
set_active_effect(null);
set_active_reaction(null);

@ -42,6 +42,7 @@ import { get_next_sibling } from '../dom/operations.js';
import { component_context, dev_current_component_function, dev_stack } from '../context.js';
import { Batch, schedule_effect } from './batch.js';
import { flatten } from './async.js';
import { without_reactive_context } from '../dom/elements/bindings/shared.js';
/**
* @param {'$effect' | '$effect.pre' | '$inspect'} rune
@ -406,7 +407,13 @@ export function destroy_effect_children(signal, remove_dom = false) {
signal.first = signal.last = null;
while (effect !== null) {
effect.ac?.abort(STALE_REACTION);
const controller = effect.ac;
if (controller !== null) {
without_reactive_context(() => {
controller.abort(STALE_REACTION);
});
}
var next = effect.next;

@ -184,8 +184,7 @@ export function legacy_rest_props(props, exclude) {
* The proxy handler for spread props. Handles the incoming array of props
* that looks like `() => { dynamic: props }, { static: prop }, ..` and wraps
* them so that the whole thing is passed to the component as the `$$props` argument.
* @template {Record<string | symbol, unknown>} T
* @type {ProxyHandler<{ props: Array<T | (() => T)> }>}}
* @type {ProxyHandler<{ props: Array<Record<string | symbol, unknown> | (() => Record<string | symbol, unknown>)> }>}}
*/
const spread_props_handler = {
get(target, key) {
@ -362,22 +361,23 @@ export function prop(props, key, flags, fallback) {
// means we can just call `$$props.foo = value` directly
if (setter) {
var legacy_parent = props.$$legacy;
return function (/** @type {any} */ value, /** @type {boolean} */ mutation) {
if (arguments.length > 0) {
// We don't want to notify if the value was mutated and the parent is in runes mode.
// In that case the state proxy (if it exists) should take care of the notification.
// If the parent is not in runes mode, we need to notify on mutation, too, that the prop
// has changed because the parent will not be able to detect the change otherwise.
if (!runes || !mutation || legacy_parent || is_store_sub) {
/** @type {Function} */ (setter)(mutation ? getter() : value);
return /** @type {() => V} */ (
function (/** @type {V} */ value, /** @type {boolean} */ mutation) {
if (arguments.length > 0) {
// We don't want to notify if the value was mutated and the parent is in runes mode.
// In that case the state proxy (if it exists) should take care of the notification.
// If the parent is not in runes mode, we need to notify on mutation, too, that the prop
// has changed because the parent will not be able to detect the change otherwise.
if (!runes || !mutation || legacy_parent || is_store_sub) {
/** @type {Function} */ (setter)(mutation ? getter() : value);
}
return value;
}
return value;
return getter();
}
return getter();
};
);
}
// Either prop is written to, but there's no binding, which means we
@ -400,29 +400,31 @@ export function prop(props, key, flags, fallback) {
var parent_effect = /** @type {Effect} */ (active_effect);
return function (/** @type {any} */ value, /** @type {boolean} */ mutation) {
if (arguments.length > 0) {
const new_value = mutation ? get(d) : runes && bindable ? proxy(value) : value;
return /** @type {() => V} */ (
function (/** @type {any} */ value, /** @type {boolean} */ mutation) {
if (arguments.length > 0) {
const new_value = mutation ? get(d) : runes && bindable ? proxy(value) : value;
set(d, new_value);
overridden = true;
set(d, new_value);
overridden = true;
if (fallback_value !== undefined) {
fallback_value = new_value;
}
if (fallback_value !== undefined) {
fallback_value = new_value;
return value;
}
return value;
}
// special case — avoid recalculating the derived if we're in a
// teardown function and the prop was overridden locally, or the
// component was already destroyed (this latter part is necessary
// because `bind:this` can read props after the component has
// been destroyed. TODO simplify `bind:this`
if ((is_destroying_effect && overridden) || (parent_effect.f & DESTROYED) !== 0) {
return d.v;
}
// special case — avoid recalculating the derived if we're in a
// teardown function and the prop was overridden locally, or the
// component was already destroyed (this latter part is necessary
// because `bind:this` can read props after the component has
// been destroyed. TODO simplify `bind:this`
if ((is_destroying_effect && overridden) || (parent_effect.f & DESTROYED) !== 0) {
return d.v;
return get(d);
}
return get(d);
};
);
}

@ -37,6 +37,7 @@ import { Batch, schedule_effect } from './batch.js';
import { proxy } from '../proxy.js';
import { execute_derived } from './deriveds.js';
/** @type {Set<any>} */
export let inspect_effects = new Set();
/** @type {Map<Source, any>} */

@ -136,20 +136,28 @@ export function hydrate(component, options) {
return /** @type {Exports} */ (instance);
} catch (error) {
if (error === HYDRATION_ERROR) {
if (options.recover === false) {
e.hydration_failed();
}
// If an error occured above, the operations might not yet have been initialised.
init_operations();
clear_text_content(target);
// re-throw Svelte errors - they are certainly not related to hydration
if (
error instanceof Error &&
error.message.split('\n').some((line) => line.startsWith('https://svelte.dev/e/'))
) {
throw error;
}
if (error !== HYDRATION_ERROR) {
// eslint-disable-next-line no-console
console.warn('Failed to hydrate: ', error);
}
set_hydrating(false);
return mount(component, options);
if (options.recover === false) {
e.hydration_failed();
}
throw error;
// If an error occured above, the operations might not yet have been initialised.
init_operations();
clear_text_content(target);
set_hydrating(false);
return mount(component, options);
} finally {
set_hydrating(was_hydrating);
set_hydrate_node(previous_hydrate_node);
@ -169,6 +177,7 @@ const document_listeners = new Map();
function _mount(Component, { target, anchor, props = {}, events, context, intro = true }) {
init_operations();
/** @type {Set<string>} */
var registered_events = new Set();
/** @param {Array<string>} events */

@ -46,6 +46,7 @@ import { Batch, batch_deriveds, flushSync, schedule_effect } from './reactivity/
import { handle_error } from './error-handling.js';
import { UNINITIALIZED } from '../../constants.js';
import { captured_signals } from './legacy.js';
import { without_reactive_context } from './dom/elements/bindings/shared.js';
export let is_updating_effect = false;
@ -278,13 +279,17 @@ export function update_reaction(reaction) {
update_version = ++read_version;
if (reaction.ac !== null) {
reaction.ac.abort(STALE_REACTION);
without_reactive_context(() => {
/** @type {AbortController} */ (reaction.ac).abort(STALE_REACTION);
});
reaction.ac = null;
}
try {
reaction.f |= REACTION_IS_UPDATING;
var result = /** @type {Function} */ (0, reaction.fn)();
var fn = /** @type {Function} */ (reaction.fn);
var result = fn();
var deps = reaction.deps;
if (new_deps !== null) {

@ -6,7 +6,12 @@ export class HeadPayload {
uid = () => '';
title = '';
constructor(css = new Set(), /** @type {string[]} */ out = [], title = '', uid = () => '') {
constructor(
/** @type {Set<{ hash: string; code: string }>} */ css = new Set(),
/** @type {string[]} */ out = [],
title = '',
uid = () => ''
) {
this.css = css;
this.out = out;
this.title = title;

@ -35,7 +35,7 @@ export function validate_store(store, name) {
}
/**
* @template {() => unknown} T
* @template {(...args: any[]) => unknown} T
* @param {T} fn
*/
export function prevent_snippet_stringification(fn) {

@ -452,7 +452,7 @@ const RUNES = /** @type {const} */ ([
'$host'
]);
/** @typedef {RUNES[number]} RuneName */
/** @typedef {typeof RUNES[number]} RuneName */
/**
* @param {string} name
@ -462,7 +462,7 @@ export function is_rune(name) {
return RUNES.includes(/** @type {RuneName} */ (name));
}
/** @typedef {STATE_CREATION_RUNES[number]} StateCreationRuneName */
/** @typedef {typeof STATE_CREATION_RUNES[number]} StateCreationRuneName */
/**
* @param {string} name
@ -477,7 +477,7 @@ const RAW_TEXT_ELEMENTS = /** @type {const} */ (['textarea', 'script', 'style',
/** @param {string} name */
export function is_raw_text_element(name) {
return RAW_TEXT_ELEMENTS.includes(/** @type {RAW_TEXT_ELEMENTS[number]} */ (name));
return RAW_TEXT_ELEMENTS.includes(/** @type {typeof RAW_TEXT_ELEMENTS[number]} */ (name));
}
/**

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

@ -3,7 +3,9 @@ import { raf as svelte_raf } from 'svelte/internal/client';
import { queue_micro_task } from '../src/internal/client/dom/task.js';
export const raf = {
/** @type {Set<Animation>} */
animations: new Set(),
/** @type {Set<(n: number) => void>} */
ticks: new Set(),
tick,
time: 0,
@ -54,14 +56,24 @@ class Animation {
/**
* @param {HTMLElement} target
* @param {Keyframe[]} keyframes
* @param {{ duration: number, delay: number }} options
* @param {Keyframe[] | PropertyIndexedKeyframes | null} keyframes
* @param {number | KeyframeAnimationOptions | undefined} options
*/
constructor(target, keyframes, { duration, delay }) {
constructor(target, keyframes, options) {
this.target = target;
this.#keyframes = keyframes;
this.#duration = Math.round(duration);
this.#delay = delay ?? 0;
this.#keyframes = Array.isArray(keyframes) ? keyframes : [];
if (typeof options === 'number') {
this.#duration = options;
this.#delay = 0;
} else {
const { duration = 0, delay = 0 } = options ?? {};
if (typeof duration === 'object') {
this.#duration = 0;
} else {
this.#duration = Math.round(+duration);
}
this.#delay = delay;
}
this._update();
}
@ -189,6 +201,7 @@ function interpolate(a, b, p) {
* @param {{duration: number, delay: number}} options
* @returns {globalThis.Animation}
*/
// @ts-ignore
HTMLElement.prototype.animate = function (keyframes, options) {
const animation = new Animation(this, keyframes, options);
raf.animations.add(animation);
@ -196,6 +209,7 @@ HTMLElement.prototype.animate = function (keyframes, options) {
return animation;
};
// @ts-ignore
HTMLElement.prototype.getAnimations = function () {
return Array.from(raf.animations).filter((animation) => animation.target === this);
};

@ -0,0 +1,10 @@
import { test } from '../../test';
export default test({
error: {
code: 'snippet_invalid_export',
message:
'An exported snippet can only reference things declared in a `<script module>`, or other exportable snippets',
position: [26, 29]
}
});

@ -0,0 +1,11 @@
<script module>
export { foo }
</script>
<script>
let x = 42;
</script>
{#snippet foo()}
{x}
{/snippet}

@ -43,6 +43,7 @@ export function create_deferred() {
/** @param {any} [reason] */
let reject = (reason) => {};
/** @type {Promise<any>} */
const promise = new Promise((f, r) => {
resolve = f;
reject = r;

@ -0,0 +1,8 @@
import { test } from '../../test';
export default test({
errors: [
'Failed to hydrate: ',
new DOMException("Node can't be inserted in a #text parent.", 'HierarchyRequestError')
]
});

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

@ -0,0 +1,7 @@
<script>
import Nested from './Nested.svelte';
</script>
<main>
<Nested />
</main>

@ -87,15 +87,17 @@ function normalize_html(window, html) {
/** @param {any} node */
function normalize_children(node) {
// sort attributes
const attributes = Array.from(node.attributes).sort((a, b) => {
return a.name < b.name ? -1 : 1;
});
const attributes = Array.from(node.attributes).sort(
(/** @type {any} */ a, /** @type {any} */ b) => {
return a.name < b.name ? -1 : 1;
}
);
attributes.forEach((attr) => {
attributes.forEach((/** @type{any} */ attr) => {
node.removeAttribute(attr.name);
});
attributes.forEach((attr) => {
attributes.forEach((/** @type{any} */ attr) => {
node.setAttribute(attr.name, attr.value);
});

@ -0,0 +1,19 @@
import { flushSync } from 'svelte';
import { test } from '../../test';
export default test({
test({ target, assert }) {
const input = target.querySelector('input');
const button = target.querySelector('button');
assert.equal(input?.step, 'any');
button?.click();
flushSync();
assert.equal(input?.step, '10');
button?.click();
flushSync();
assert.equal(input?.step, 'any');
}
});

@ -0,0 +1,6 @@
<script>
let step = "any";
</script>
<input type="range" {...{step}} />
<button onclick={() => step = step === "any" ? 10 : "any"}>change step</button>

@ -0,0 +1,12 @@
import { ok, test } from '../../test';
import { flushSync } from 'svelte';
export default test({
async test({ assert, target, errors }) {
const btn = target.querySelector('button');
flushSync(() => {
btn?.click();
});
assert.deepEqual(errors, []);
}
});

@ -0,0 +1,24 @@
<script lang="ts">
import { getAbortSignal } from "svelte";
let aborted = $state(0);
let count = $state(0);
let der = $derived.by(()=>{
const signal = getAbortSignal();
signal.addEventListener("abort", () => {
try{
aborted++;
}catch(e){
console.error(e);
}
});
return count;
})
</script>
{der}
<button onclick={() => count++}></button>

@ -0,0 +1,43 @@
import { tick } from 'svelte';
import { test } from '../../test';
export default test({
async test({ assert, target }) {
const [reset, one, two, reject] = target.querySelectorAll('button');
await tick();
assert.htmlEqual(
target.innerHTML,
'<button>reset</button><button>one</button><button>two</button><button>reject</button> waiting'
);
one.click();
await tick();
assert.htmlEqual(
target.innerHTML,
'<button>reset</button><button>one</button><button>two</button><button>reject</button> one_res'
);
reset.click();
await tick();
assert.htmlEqual(
target.innerHTML,
'<button>reset</button><button>one</button><button>two</button><button>reject</button> waiting'
);
two.click();
await tick();
assert.htmlEqual(
target.innerHTML,
'<button>reset</button><button>one</button><button>two</button><button>reject</button> two_res'
);
reset.click();
reject.click();
await tick();
assert.htmlEqual(
target.innerHTML,
'<button>reset</button><button>one</button><button>two</button><button>reject</button> reject_catch'
);
}
});

@ -0,0 +1,22 @@
<script>
let deferred = $state(Promise.withResolvers());
</script>
<button onclick={() => deferred = Promise.withResolvers()}>reset</button>
<button onclick={() => deferred.resolve("one")}>one</button>
<button onclick={() => deferred.resolve("two")}>two</button>
<button onclick={() => deferred.reject("reject")}>reject</button>
<svelte:boundary>
{#await await deferred.promise + "_res"}
waiting
{:then res}
{res}
{:catch err}
{err}_catch
{/await}
{#snippet pending()}
<p>pending</p>
{/snippet}
</svelte:boundary>

@ -0,0 +1,12 @@
import { tick } from 'svelte';
import { test } from '../../test';
export default test({
html: `<h1>Loading...</h1>`,
async test({ assert, target }) {
await tick();
assert.htmlEqual(target.innerHTML, `<h1>Hello, world!</h1>`);
}
});

@ -0,0 +1,16 @@
<script>
let name = $state('world');
</script>
<svelte:boundary>
{#snippet pending()}
<h1>Loading...</h1>
{/snippet}
{#snippet greet()}
{@const greeting = await `Hello, ${name}!`}
<h1>{greeting}</h1>
{/snippet}
{@render greet()}
</svelte:boundary>

@ -0,0 +1,24 @@
import { tick } from 'svelte';
import { test } from '../../test';
export default test({
compileOptions: {
dev: true
},
html: `<button>a</button><button>b</button><p>pending</p>`,
async test({ assert, target, warnings }) {
await tick();
assert.htmlEqual(target.innerHTML, '<button>a</button><button>b</button><h1>3</h1>');
assert.equal(
warnings[0],
'Detected reactivity loss when reading `values[1]`. This happens when state is read in an async function after an earlier `await`'
);
assert.equal(warnings[1].name, 'TracedAtError');
assert.equal(warnings.length, 2);
}
});

@ -0,0 +1,24 @@
<script>
let values = $state([1, 2]);
async function get_total() {
let total = 0;
for await (const n of values) {
total += n;
}
return total;
}
</script>
<button onclick={() => values[0]++}>a</button>
<button onclick={() => values[1]++}>b</button>
<svelte:boundary>
<h1>{await get_total()}</h1>
{#snippet pending()}
<p>pending</p>
{/snippet}
</svelte:boundary>

@ -0,0 +1,7 @@
<script>
let { data } = $props();
</script>
{#each data.obj.arr as i}
<p>{i}</p>
{/each}

@ -0,0 +1,11 @@
import { flushSync } from 'svelte';
import { test } from '../../test';
export default test({
async test({ assert, target }) {
const [btn] = target.querySelectorAll('button');
btn.click();
flushSync();
assert.htmlEqual(target.innerHTML, `<button>Change</button> <p>Comp 2</p>`);
}
});

@ -0,0 +1,16 @@
<script>
import Comp_1 from './Comp-1.svelte';
import Comp_2 from './Comp-2.svelte';
let Comp = $state.raw(Comp_1);
let data = $state.raw({ obj: { arr: [1, 2, 3] } });
function change() {
Comp = Comp_2;
data = {};
}
</script>
<button onclick={change}>Change</button>
<Comp {data} />

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

Loading…
Cancel
Save