Merge branch 'main' into incremental-batches

incremental-batches-perf-attempt
Rich Harris 2 months ago committed by GitHub
commit 1065572c41
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -0,0 +1,5 @@
---
'svelte': patch
---
perf: deduplicate identical hoisted templates within a component

@ -0,0 +1,5 @@
---
'svelte': minor
---
feat: allow declarations in the template

@ -0,0 +1,5 @@
---
'svelte': patch
---
perf: hoist `rest_props` exclude list as a module-scope `Set`

@ -80,7 +80,7 @@ jobs:
Lint:
permissions: {}
runs-on: ubuntu-latest
timeout-minutes: 5
timeout-minutes: 10
steps:
- uses: actions/checkout@v6
- uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v4
@ -99,7 +99,7 @@ jobs:
if: (${{ success() }} || ${{ failure() }}) # ensures this step runs even if previous steps fail
run: pnpm build && { [ "`git status --porcelain=v1`" == "" ] || (echo "Generated types have changed — please regenerate types locally with `cd packages/svelte && pnpm generate:types` and commit the changes after you have reviewed them"; git diff; exit 1); }
- name: check browser-support docs page is up to date
run: '{ [ "`git status --porcelain=v1 documentation/docs/07-misc/05-browser-support.md`" == "" ] || (echo "The browser-support docs page is out of date — please regenerate it locally with \`cd packages/svelte && pnpm generate:browser-support\` and commit the changes"; git diff documentation/docs/07-misc/05-browser-support.md; exit 1); }'
run: '{ [ "`git status --porcelain=v1 documentation/docs/07-misc/.generated/`" == "" ] || (echo "The browser-support docs page is out of date — please regenerate it locally with \`cd packages/svelte && pnpm generate:browser-support\` and commit the changes"; git diff documentation/docs/07-misc/.generated/; exit 1); }'
Benchmarks:
permissions: {}
runs-on: ubuntu-latest

@ -4,6 +4,8 @@ This guide is for AI coding agents working in the Svelte monorepo.
**Important:** Read and follow [`CONTRIBUTING.md`](./CONTRIBUTING.md) as well - it contains essential information about testing, code structure, and contribution guidelines that applies here.
When submitting a PR, you **MUST** read [`PULL_REQUEST_TEMPLATE.md`](./.github/PULL_REQUEST_TEMPLATE.md) and fill it out correctly. **DO NOT** submit a PR without running the full test suite.
## Quick Reference
If asked to do a performance investigation, use the `performance-investigation` skill.

@ -2,6 +2,8 @@
title: {@const ...}
---
> [!NOTE] `{@const x = y}` is legacy syntax — use [`{const x = $derived(y)}`](declaration-tags) instead
The `{@const ...}` tag defines a local constant.
```svelte

@ -0,0 +1,72 @@
---
title: {let/const ...}
---
Declaration tags define local variables inside markup with `const` or `let`:
<!-- codeblock:start {"title":"Declaration tags"} -->
```svelte
<!--- file: App.svelte --->
<script>
let boxes = [{ width: 10, height: 10 }, { width: 15, height: 15 }];
</script>
{#each boxes as box}
{const area = box.width * box.height}
{const label = `${box.width} ⨉ ${box.height} = ${area}`}
<p>{label}</p>
{/each}
```
<!-- codeblock:end -->
> [!NOTE] Declaration tags are available since Svelte 5.56.
> [!NOTE] The [`{@const ...}`](@const) syntax is considered legacy — use declaration tags instead.
When values should be reactive, you can use `$state` and `$derived`:
<!-- codeblock:start {"title":"Reactive declaration tags"} -->
```svelte
<!--- file: App.svelte --->
<script>
let user = $state({ name: 'Svelte' });
let editing = $state(false);
</script>
<p>Hello {user.name}</p>
<button onclick={() => editing = true}>edit name</button>
{#if editing}
{let name = $state(user.name)}
{const greeting = $derived(`Hello ${name}`)}
<hr>
<input bind:value={name} />
<p>{greeting}</p>
<button onclick={() => {
user.name = name;
editing = false;
}}>save</button>
{/if}
```
<!-- codeblock:end -->
Declaration tags can be used anywhere inside the component. They can reference values declared outside themselves (for example in the `<script>` tag or in `{#each ...}` blocks) and are 'visible' to everything in the same lexical scope (i.e. siblings, and children of those siblings):
<!-- codeblock:start {"title":"Declaration tag scope"} -->
```svelte
<!--- file: App.svelte --->
{const hello = 'hello'}
{hello} <!-- 'hello' -->
<div>
{const hello = 'hi'}
{hello} <!-- 'hi' -->
<div>
{hello} <!-- 'hi' -->
</div>
</div>
{hello} <!-- 'hello' -->
```
<!-- codeblock:end -->

@ -399,6 +399,18 @@ Invalid selector
Cannot declare a variable with the same name as an import from `<script module>`
```
### declaration_tag_invalid_type
```
Declaration tags must be `let` or `const` declarations
```
### declaration_tag_no_legacy_mode
```
Declaration tags cannot be used in legacy mode
```
### derived_invalid_export
```

@ -37,7 +37,7 @@
"eslint-plugin-lube": "^0.5.1",
"eslint-plugin-svelte": "^3.15.0",
"jsdom": "25.0.1",
"playwright": "^1.58.0",
"playwright": "^1.60.0",
"prettier": "^3.2.4",
"prettier-plugin-svelte": "^3.4.0",
"svelte": "workspace:^",

@ -191,6 +191,14 @@ The same applies to components:
> {@debug ...} arguments must be identifiers, not arbitrary expressions
## declaration_tag_invalid_type
> Declaration tags must be `let` or `const` declarations
## declaration_tag_no_legacy_mode
> Declaration tags cannot be used in legacy mode
## directive_invalid_value
> Directive value must be a JavaScript expression enclosed in curly braces

@ -152,7 +152,7 @@
},
"devDependencies": {
"@jridgewell/trace-mapping": "^0.3.25",
"@playwright/test": "^1.58.0",
"@playwright/test": "^1.60.0",
"@rollup/plugin-commonjs": "^28.0.1",
"@rollup/plugin-node-resolve": "^15.3.0",
"@rollup/plugin-terser": "^0.4.4",
@ -162,12 +162,12 @@
"baseline-browser-mapping": "^2.10.32",
"dts-buddy": "^0.5.5",
"esbuild": "^0.25.10",
"web-features": "^3.29.0",
"rollup": "^4.59.0",
"source-map": "^0.7.4",
"tinyglobby": "^0.2.12",
"typescript": "^5.5.4",
"vitest": "^2.1.9"
"vitest": "^2.1.9",
"web-features": "^3.29.0"
},
"dependencies": {
"@jridgewell/remapping": "^2.3.4",

@ -1004,6 +1004,24 @@ export function debug_tag_invalid_arguments(node) {
e(node, 'debug_tag_invalid_arguments', `{@debug ...} arguments must be identifiers, not arbitrary expressions\nhttps://svelte.dev/e/debug_tag_invalid_arguments`);
}
/**
* Declaration tags must be `let` or `const` declarations
* @param {null | number | NodeLike} node
* @returns {never}
*/
export function declaration_tag_invalid_type(node) {
e(node, 'declaration_tag_invalid_type', `Declaration tags must be \`let\` or \`const\` declarations\nhttps://svelte.dev/e/declaration_tag_invalid_type`);
}
/**
* Declaration tags cannot be used in legacy mode
* @param {null | number | NodeLike} node
* @returns {never}
*/
export function declaration_tag_no_legacy_mode(node) {
e(node, 'declaration_tag_no_legacy_mode', `Declaration tags cannot be used in legacy mode\nhttps://svelte.dev/e/declaration_tag_no_legacy_mode`);
}
/**
* Directive value must be a JavaScript expression enclosed in curly braces
* @param {null | number | NodeLike} node

@ -262,6 +262,10 @@ export function convert(source, ast) {
};
},
// @ts-ignore
DeclarationTag(node) {
return node;
},
// @ts-ignore
KeyBlock(node, { visit }) {
remove_surrounding_whitespace_nodes(node.fragment.nodes);
return {

@ -1,10 +1,11 @@
/** @import { Comment, Program } from 'estree' */
/** @import { Comment, Program, Statement } from 'estree' */
/** @import { AST } from '#compiler' */
/** @import { Parser } from './index.js' */
import * as acorn from 'acorn';
import { walk } from 'zimmerframe';
import { tsPlugin } from '@sveltejs/acorn-typescript';
import * as e from '../../errors.js';
import { find_matching_bracket } from './utils/bracket.js';
const JSParser = acorn.Parser;
const TSParser = JSParser.extend(tsPlugin());
@ -98,6 +99,48 @@ export function parse_expression_at(parser, source, index) {
}
}
/**
* @param {Parser} parser
* @param {string} source
* @param {number} index
* @returns {Statement}
*/
export function parse_statement_at(parser, source, index) {
const acorn = parser.ts ? TSParser : JSParser;
let end = find_matching_bracket(source, index, '{');
if (end === undefined) e.unexpected_eof(source.length);
while (source[end - 1] === ';') {
end -= 1;
}
const padded_source = `${' '.repeat(index)}${source.slice(index, end)}`;
const { onComment, add_comments } = get_comment_handlers(
padded_source,
parser.root.comments,
index
);
try {
const ast = acorn.parse(padded_source, {
onComment,
sourceType: 'module',
ecmaVersion: 16,
locations: true
});
add_comments(ast);
const statement = /** @type {Statement} */ (
/** @type {unknown} */ (/** @type {Program} */ (ast).body[0])
);
statement.end = Math.min(/** @type {number} */ (statement.end), end);
return statement;
} catch (e) {
handle_parse_error(e);
}
}
const regex_position_indicator = / \(\d+:\d+\)$/;
/**

@ -302,7 +302,10 @@ export class Parser {
}
pop() {
this.fragments.pop();
const fragment = this.fragments.pop();
if (fragment?.metadata.transparent && fragment.nodes.some((n) => n.type === 'DeclarationTag')) {
fragment.metadata.transparent = false;
}
return this.stack.pop();
}

@ -1,16 +1,20 @@
/** @import { ArrowFunctionExpression, Expression, Identifier, Pattern } from 'estree' */
/** @import { ArrowFunctionExpression, Expression, Identifier, Pattern, VariableDeclaration } from 'estree' */
/** @import { AST } from '#compiler' */
/** @import { Parser } from '../index.js' */
import { walk } from 'zimmerframe';
import * as e from '../../../errors.js';
import { ExpressionMetadata } from '../../nodes.js';
import { parse_expression_at } from '../acorn.js';
import { parse_expression_at, parse_statement_at } from '../acorn.js';
import read_pattern from '../read/context.js';
import read_expression, { get_loose_identifier } from '../read/expression.js';
import { create_fragment } from '../utils/create.js';
import { match_bracket } from '../utils/bracket.js';
import { find_matching_bracket, match_bracket } from '../utils/bracket.js';
const regex_whitespace_with_closing_curly_brace = /\s*}/y;
const regex_supported_declaration = /(?:let|const)\b/y;
// All except `type` are reserved keywords and cannot be used as variable names.
// For type we check if it's not something like `type .x` / `type ()` / `type % 2` / ...
const regex_unsupported_declaration = /(?:(?:var|interface|enum)\b)|(?:type\s+[^?.(`<[&|%^}])/y;
const pointy_bois = { '<': '>' };
@ -31,6 +35,20 @@ export default function tag(parser) {
}
}
const declaration = read_declaration(parser);
if (declaration) {
parser.append({
type: 'DeclarationTag',
start,
end: parser.index,
declaration: /** @type {VariableDeclaration} */ (declaration),
metadata: {
expression: new ExpressionMetadata()
}
});
return;
}
const expression = read_expression(parser);
parser.allow_whitespace();
@ -47,6 +65,76 @@ export default function tag(parser) {
});
}
/**
* @param {Parser} parser
* @returns {null | import('estree').VariableDeclaration}
*/
function read_declaration(parser) {
const start = parser.index;
const unsupported = parser.match_regex(regex_unsupported_declaration);
if (unsupported) {
e.declaration_tag_invalid_type({ start, end: start + unsupported.length });
}
if (!parser.match_regex(regex_supported_declaration)) {
return null;
}
/** @type {import('estree').Statement | import('estree').VariableDeclaration} */
let declaration;
try {
declaration = parse_statement_at(parser, parser.template, start);
} catch (error) {
if (!parser.loose) throw error;
const end = find_matching_bracket(parser.template, start, '{');
if (end === undefined) throw error;
parser.index = end;
const kind = parser.template.startsWith('const', start) ? 'const' : 'let';
declaration = {
type: 'VariableDeclaration',
kind,
declarations: [
{
type: 'VariableDeclarator',
id: {
type: 'Identifier',
name: '',
start: parser.index,
end: parser.index
},
init: null,
start: parser.index,
end: parser.index
}
],
start,
end
};
}
if (declaration.type !== 'VariableDeclaration') {
e.declaration_tag_invalid_type({
start: declaration.start ?? start,
end: declaration.end ?? parser.index
});
}
// TODO support using
if (declaration.kind !== 'let' && declaration.kind !== 'const') {
e.declaration_tag_invalid_type(declaration);
}
parser.index = /** @type {number} */ (declaration.end);
parser.allow_whitespace();
parser.eat('}', true);
return declaration;
}
/** @param {Parser} parser */
function open(parser) {
let start = parser.index - 2;

@ -36,6 +36,7 @@ import { ClassDeclaration } from './visitors/ClassDeclaration.js';
import { ClassDirective } from './visitors/ClassDirective.js';
import { Component } from './visitors/Component.js';
import { ConstTag } from './visitors/ConstTag.js';
import { DeclarationTag } from './visitors/DeclarationTag.js';
import { DebugTag } from './visitors/DebugTag.js';
import { EachBlock } from './visitors/EachBlock.js';
import { ExportDefaultDeclaration } from './visitors/ExportDefaultDeclaration.js';
@ -157,6 +158,7 @@ const visitors = {
ClassDirective,
Component,
ConstTag,
DeclarationTag,
DebugTag,
EachBlock,
ExportDefaultDeclaration,
@ -312,6 +314,7 @@ export function analyze_module(source, options) {
options: /** @type {ValidatedCompileOptions} */ (options),
fragment: null,
parent_element: null,
in_declaration_tag: false,
reactive_statement: null,
derived_function_depth: -1
},
@ -718,6 +721,7 @@ export function analyze_component(root, source, options) {
ast_type: ast === instance.ast ? 'instance' : ast === template.ast ? 'template' : 'module',
fragment: ast === template.ast ? ast : null,
parent_element: null,
in_declaration_tag: false,
has_props_rune: false,
component_slots: new Set(),
expression: null,
@ -785,6 +789,7 @@ export function analyze_component(root, source, options) {
options,
fragment: ast === template.ast ? ast : null,
parent_element: null,
in_declaration_tag: false,
has_props_rune: false,
ast_type: ast === instance.ast ? 'instance' : ast === template.ast ? 'template' : 'module',
reactive_statement: null,

@ -16,6 +16,8 @@ export interface AnalysisState {
* Parent doesn't necessarily mean direct path predecessor because there could be `#each`, `#if` etc in-between.
*/
parent_element: string | null;
/** True if inside DeclarationTag */
in_declaration_tag: boolean;
has_props_rune: boolean;
/** Which slots the current parent component has */
component_slots: Set<string>;
@ -35,7 +37,7 @@ export interface AnalysisState {
*/
derived_function_depth: number;
/** Collected info about async `{@const }` declarations */
/** Collected info about async `{@const }`/`{let/const ...}` declarations */
async_consts?: {
id: Identifier;
/** How many `$.run(...)` entries are already allocated in this scope */

@ -255,6 +255,9 @@ export function CallExpression(node, context) {
if (expression.has_await) {
context.state.analysis.async_deriveds.add(node);
}
// Tell surrounding declaration tag about metadata for correct calculation of blockers etc
if (context.state.in_declaration_tag) context.state.expression?.merge(expression);
} else if (rune === '$inspect') {
context.next({ ...context.state, function_depth: context.state.function_depth + 1 });
} else {

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

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

@ -162,7 +162,7 @@ export function Identifier(node, context) {
if (binding.metadata?.is_template_declaration && context.state.options.experimental.async) {
let snippet_name;
// Find out if this references a {@const ...} declaration of an implicit children snippet
// Find out if this references a {@const ...}/{let/const ...} declaration of an implicit children snippet
// when it is itself inside a snippet block at the same level. If so, error.
for (let i = context.path.length - 1; i >= 0; i--) {
const parent = context.path[i];

@ -4,7 +4,7 @@
/** @import { Visitors, ComponentClientTransformState, ClientTransformState } from './types' */
import { walk } from 'zimmerframe';
import * as b from '#compiler/builders';
import { build_getter, is_state_source } from './utils.js';
import { build_getter, get_transform } from './utils.js';
import { render_stylesheet } from '../css/index.js';
import { dev, filename } from '../../../state.js';
import { AnimateDirective } from './visitors/AnimateDirective.js';
@ -22,6 +22,7 @@ import { ClassBody } from './visitors/ClassBody.js';
import { Comment } from './visitors/Comment.js';
import { Component } from './visitors/Component.js';
import { ConstTag } from './visitors/ConstTag.js';
import { DeclarationTag } from './visitors/DeclarationTag.js';
import { DebugTag } from './visitors/DebugTag.js';
import { EachBlock } from './visitors/EachBlock.js';
import { ExportNamedDeclaration } from './visitors/ExportNamedDeclaration.js';
@ -66,20 +67,7 @@ const visitors = {
const scope = state.scopes.get(node);
if (scope && scope !== state.scope) {
const transform = { ...state.transform };
for (const [name, binding] of scope.declarations) {
if (
binding.kind === 'normal' ||
// Reads of `$state(...)` declarations are not
// transformed if they are never reassigned
(binding.kind === 'state' && !is_state_source(binding, state.analysis))
) {
delete transform[name];
}
}
next({ ...state, transform, scope });
next({ ...state, transform: get_transform(scope, state), scope });
} else {
next();
}
@ -99,6 +87,7 @@ const visitors = {
Comment,
Component,
ConstTag,
DeclarationTag,
DebugTag,
EachBlock,
ExportNamedDeclaration,
@ -152,6 +141,7 @@ export function client_component(analysis, options) {
scopes: analysis.module.scopes,
is_instance: false,
hoisted: [b.import_all('$', 'svelte/internal/client'), ...analysis.instance_body.hoisted],
templates: new Map(),
node: /** @type {any} */ (null), // populated by the root node
legacy_reactive_imports: [],
legacy_reactive_statements: new Map(),

@ -1,3 +1,4 @@
/** @import { TemplateLiteral } from 'estree' */
/** @import { Namespace } from '#compiler' */
/** @import { ComponentClientTransformState } from '../types.js' */
/** @import { Node } from './types.js' */
@ -31,14 +32,29 @@ function build_locations(nodes) {
/**
* @param {ComponentClientTransformState} state
* @param {Namespace} namespace
* @param {string} name
* @param {number} [flags]
*/
export function transform_template(state, namespace, flags = 0) {
export function transform_template(state, name, flags = 0) {
const namespace = state.metadata.namespace;
const tree = state.options.fragments === 'tree';
const expression = tree ? state.template.as_tree() : state.template.as_html();
const key =
tree || dev
? null
: get_template_key(
/** @type {TemplateLiteral} */ (expression),
state.metadata.namespace,
flags
);
if (key !== null) {
const existing = state.templates.get(key);
if (existing !== undefined) return existing;
}
if (tree) {
if (namespace === 'svg') flags |= TEMPLATE_USE_SVG;
if (namespace === 'mathml') flags |= TEMPLATE_USE_MATHML;
@ -63,5 +79,26 @@ export function transform_template(state, namespace, flags = 0) {
);
}
return call;
const id = state.scope.root.unique(name);
state.hoisted.push(b.var(id, call));
if (key !== null) {
state.templates.set(key, id);
}
return id;
}
/**
* Returns a stable key for templates that are safe to deduplicate - plain
* `$.from_html`/`from_svg`/`from_mathml` factories with literal arguments - or `null`
* for anything else. Dev-mode templates are wrapped in `$.add_locations(...)`, which
* embeds per-call-site locations, so they never produce a key and are never shared.
* @param {TemplateLiteral} template
* @param {Namespace} namespace
* @param {number} flags
* @returns {string | null}
*/
function get_template_key(template, namespace, flags) {
return `${namespace} ${flags} ${template.quasis[0].value.raw}`;
}

@ -40,6 +40,8 @@ export interface ComponentClientTransformState extends ClientTransformState {
readonly analysis: ComponentAnalysis;
readonly options: ValidatedCompileOptions;
readonly hoisted: Array<Statement | ModuleDeclaration>;
/** Deduplicates hoisted templates by content, mapping a template key to its hoisted identifier */
readonly templates: Map<string, Identifier>;
readonly events: Set<string>;
readonly store_to_invalidate?: string;

@ -179,3 +179,24 @@ export function create_derived(state, expression, async = false) {
return b.call(state.analysis.runes ? '$.derived' : '$.derived_safe_equal', thunk);
}
}
/**
* @param {Scope} scope
* @param {ClientTransformState} state
*/
export function get_transform(scope, state) {
const transform = { ...state.transform };
for (const [name, binding] of scope.declarations) {
if (
binding.kind === 'normal' ||
// Reads of `$state(...)` declarations are not
// transformed if they are never reassigned
(binding.kind === 'state' && !is_state_source(binding, state.analysis))
) {
delete transform[name];
}
}
return transform;
}

@ -7,6 +7,7 @@ import * as b from '#compiler/builders';
import { create_derived } from '../utils.js';
import { get_value } from './shared/declarations.js';
import { build_expression } from './shared/utils.js';
import { add_async_declaration } from './DeclarationTag.js';
/**
* @param {AST.ConstTag} node
@ -26,7 +27,7 @@ export function ConstTag(node, context) {
context.state.transform[declaration.id.name] = { read: get_value };
add_const_declaration(context.state, declaration.id, expression, node.metadata);
add_const_declaration(context, declaration.id, expression, node.metadata);
} else {
const identifiers = extract_identifiers(declaration.id);
const tmp = b.id(context.state.scope.generate('computed_const'));
@ -63,7 +64,7 @@ export function ConstTag(node, context) {
expression = b.call('$.tag', expression, b.literal('[@const]'));
}
add_const_declaration(context.state, tmp, expression, node.metadata);
add_const_declaration(context, tmp, expression, node.metadata);
for (const node of identifiers) {
context.state.transform[node.name] = {
@ -74,38 +75,26 @@ export function ConstTag(node, context) {
}
/**
* @param {ComponentContext['state']} state
* @param {ComponentContext} context
* @param {Identifier} id
* @param {Expression} expression
* @param {AST.ConstTag['metadata']} metadata
*/
function add_const_declaration(state, id, expression, metadata) {
function add_const_declaration(context, id, expression, metadata) {
// we need to eagerly evaluate the expression in order to hit any
// 'Cannot access x before initialization' errors
const after = dev ? [b.stmt(b.call('$.get', id))] : [];
const blockers = [...metadata.expression.dependencies]
.map((dep) => dep.blocker)
.filter((b) => b !== null && b.object !== state.async_consts?.id);
if (metadata.promises_id) {
const run = (state.async_consts ??= {
id: metadata.promises_id,
thunks: []
});
state.consts.push(b.let(id));
if (blockers.length === 1) {
run.thunks.push(b.thunk(b.member(/** @type {Expression} */ (blockers[0]), 'promise')));
} else if (blockers.length > 0) {
run.thunks.push(b.thunk(b.call('$.wait', b.array(blockers))));
}
// keep the number of thunks pushed in sync with ConstTag in analysis phase
const assignment = b.assignment('=', id, expression);
run.thunks.push(b.thunk(assignment, metadata.expression.has_await));
add_async_declaration(
context,
metadata,
[id],
[b.stmt(b.assignment('=', id, expression))],
'let'
);
} else {
const { state } = context;
state.consts.push(b.const(id, expression));
state.consts.push(...after);
}

@ -0,0 +1,87 @@
/** @import { Expression, Identifier, Pattern, Statement, ExpressionStatement, VariableDeclaration } from 'estree' */
/** @import { AST } from '#compiler' */
/** @import { ComponentContext } from '../types' */
import { extract_identifiers, has_await_expression } from '../../../../utils/ast.js';
import * as b from '#compiler/builders';
import { add_state_transformers } from './shared/declarations.js';
/**
* @param {AST.DeclarationTag} node
* @param {ComponentContext} context
*/
export function DeclarationTag(node, context) {
const declaration = /** @type {Statement | undefined} */ (context.visit(node.declaration));
add_state_transformers(context);
if (
node.metadata.promises_id &&
node.declaration.type === 'VariableDeclaration' &&
declaration?.type === 'VariableDeclaration'
) {
const { ids, assignments } = build_async_declaration_parts(declaration);
add_async_declaration(context, node.metadata, ids, assignments, declaration.kind);
} else {
context.state.consts.push(declaration ?? node.declaration);
}
}
/**
* @param {VariableDeclaration} declaration
*/
export function build_async_declaration_parts(declaration) {
const ids = new Map();
for (const declarator of declaration.declarations) {
for (const id of extract_identifiers(declarator.id)) {
ids.set(id.name, id);
}
}
const assignments = declaration.declarations
.filter((declarator) => declarator.init !== null)
.map((declarator) =>
b.stmt(
b.assignment(
'=',
/** @type {Pattern} */ (declarator.id),
/** @type {Expression} */ (declarator.init)
)
)
);
return { ids: [...ids.values()], assignments };
}
/**
* @param {ComponentContext} context
* @param {AST.ConstTag['metadata'] | AST.DeclarationTag['metadata']} metadata
* @param {Identifier[]} ids
* @param {ExpressionStatement[]} assignments
* @param {VariableDeclaration['kind']} [kind]
*/
export function add_async_declaration(context, metadata, ids, assignments, kind = 'let') {
const run = (context.state.async_consts ??= {
id: /** @type {Identifier} */ (metadata.promises_id),
thunks: []
});
for (const id of ids) {
context.state.consts.push(kind === 'var' ? b.var(id.name) : b.let(id.name));
}
const blockers = [...metadata.expression.dependencies]
.map((dep) => dep.blocker)
.filter((b) => b !== null && b.object !== context.state.async_consts?.id);
if (blockers.length === 1) {
run.thunks.push(b.thunk(b.member(/** @type {Expression} */ (blockers[0]), 'promise')));
} else if (blockers.length > 0) {
run.thunks.push(b.thunk(b.call('$.wait', b.array(blockers))));
}
// keep the number of thunks pushed in sync with analysis phase
const has_await =
metadata.expression.has_await ||
assignments.some((assignment) => has_await_expression(assignment));
const body = assignments.length === 1 ? assignments[0].expression : b.block(assignments);
run.thunks.push(b.thunk(body, has_await));
}

@ -52,7 +52,6 @@ export function Fragment(node, context) {
(trimmed[0].type === 'IfBlock' &&
trimmed[0].elseif &&
/** @type {AST.IfBlock} */ (parent).metadata.flattened?.includes(trimmed[0])));
const template_name = context.state.scope.root.unique('root'); // TODO infer name from parent
/** @type {Statement[]} */
const body = [];
@ -96,8 +95,7 @@ export function Fragment(node, context) {
let flags = state.template.needs_import_node ? TEMPLATE_USE_IMPORT_NODE : undefined;
const template = transform_template(state, namespace, flags);
state.hoisted.push(b.var(template_name, template));
const template_name = transform_template(state, 'root', flags);
state.init.unshift(b.var(id, b.call(template_name)));
close = b.stmt(b.call('$.append', b.id('$$anchor'), id));
@ -147,8 +145,7 @@ export function Fragment(node, context) {
// special case — we can use `$.comment` instead of creating a unique template
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));
const template_name = transform_template(state, 'root', flags);
state.init.unshift(b.var(id, b.call(template_name)));
}

@ -18,7 +18,7 @@ import {
is_customizable_select_element
} from '../../../nodes.js';
import { clean_nodes, determine_namespace_for_children } from '../../utils.js';
import { build_getter } from '../utils.js';
import { build_getter, get_transform } from '../utils.js';
import {
get_attribute_name,
build_attribute_value,
@ -300,11 +300,14 @@ export function RegularElement(node, context) {
}
}
const scope = /** @type {Scope} */ (context.state.scopes.get(node.fragment));
/** @type {ComponentClientTransformState} */
const state = {
...context.state,
metadata,
scope: /** @type {Scope} */ (context.state.scopes.get(node.fragment)),
scope,
transform: get_transform(scope, context.state),
preserve_whitespace: context.state.preserve_whitespace || name === 'pre' || name === 'textarea'
};
@ -318,8 +321,19 @@ export function RegularElement(node, context) {
state.options.preserveComments
);
const has_declarations = !node.fragment.metadata.transparent;
/** @type {typeof state} */
const child_state = { ...state, init: [], update: [], after_update: [], snippets: [] };
const child_state = {
...state,
init: [],
update: [],
after_update: [],
snippets: [],
consts: has_declarations ? [] : state.consts,
async_consts: has_declarations ? undefined : state.async_consts,
memoizer: has_declarations ? new Memoizer() : state.memoizer
};
for (const node of hoisted) {
context.visit(node, child_state);
@ -360,7 +374,6 @@ export function RegularElement(node, context) {
context.state.template.push_comment();
// Create a separate template for the rich content
const template_name = context.state.scope.root.unique(`${name}_content`);
const fragment_id = b.id(context.state.scope.generate('fragment'));
const anchor_id = b.id(context.state.scope.generate('anchor'));
@ -384,9 +397,8 @@ export function RegularElement(node, context) {
}
);
// Transform the template to $.from_html(...) and hoist it
const template = transform_template(select_state, metadata.namespace, TEMPLATE_FRAGMENT);
context.state.hoisted.push(b.var(template_name, template));
// Transform the template to $.from_html(...) and hoist it (deduplicating identical templates)
const template_name = transform_template(select_state, `${name}_content`, TEMPLATE_FRAGMENT);
// Build the rich content function body
// The anchor is the child of the element (a hydration marker during hydration)
@ -427,11 +439,21 @@ export function RegularElement(node, context) {
}
}
if (node.fragment.nodes.some((node) => node.type === 'SnippetBlock')) {
if (node.fragment.nodes.some((node) => node.type === 'SnippetBlock') || has_declarations) {
if (child_state.async_consts && child_state.async_consts.thunks.length > 0) {
child_state.consts.push(
b.var(
child_state.async_consts.id,
b.call('$.run', b.array(child_state.async_consts.thunks))
)
);
}
// Wrap children in `{...}` to avoid declaration conflicts
context.state.init.push(
b.block([
...child_state.snippets,
...child_state.consts,
...child_state.init,
...element_state.init,
child_state.update.length > 0 ? build_render_statement(child_state) : b.empty,

@ -40,6 +40,7 @@ export function SvelteBoundary(node, context) {
const hoisted = [];
let has_const = false;
let has_declaration = false;
// const tags need to live inside the boundary, but might also be referenced in hoisted snippets.
// to resolve this we cheat: we duplicate const tags inside snippets
@ -55,6 +56,10 @@ export function SvelteBoundary(node, context) {
});
}
}
if (child.type === 'DeclarationTag') {
has_declaration = true;
}
}
for (const child of node.fragment.nodes) {
@ -68,10 +73,10 @@ export function SvelteBoundary(node, context) {
if (child.type === 'SnippetBlock') {
if (
context.state.options.experimental.async &&
has_const &&
(has_const || has_declaration) &&
!['failed', 'pending'].includes(child.expression.name)
) {
// we can't hoist snippets as they may reference const tags, so we just keep them in the fragment
// we can't hoist snippets as they may reference const/declaration tags, so we just keep them in the fragment
nodes.push(child);
} else {
/** @type {Statement[]} */

@ -49,8 +49,13 @@ export function VariableDeclaration(node, context) {
}
if (declarator.id.type === 'Identifier') {
const exclude_id = context.state.scope.root.unique('rest_excludes');
context.state.hoisted.push(
b.var(exclude_id, b.new('Set', b.array(seen.map((name) => b.literal(name)))))
);
/** @type {Expression[]} */
const args = [b.id('$$props'), b.array(seen.map((name) => b.literal(name)))];
const args = [b.id('$$props'), exclude_id];
if (dev) {
// include rest name, so we can provide informative error messages
@ -95,8 +100,13 @@ export function VariableDeclaration(node, context) {
}
} else {
// RestElement
const exclude_id = context.state.scope.root.unique('rest_excludes');
context.state.hoisted.push(
b.var(exclude_id, b.new('Set', b.array(seen.map((name) => b.literal(name)))))
);
/** @type {Expression[]} */
const args = [b.id('$$props'), b.array(seen.map((name) => b.literal(name)))];
const args = [b.id('$$props'), exclude_id];
if (dev) {
// include rest name, so we can provide informative error messages

@ -15,6 +15,7 @@ import { CallExpression } from './visitors/CallExpression.js';
import { ClassBody } from './visitors/ClassBody.js';
import { Component } from './visitors/Component.js';
import { ConstTag } from './visitors/ConstTag.js';
import { DeclarationTag } from './visitors/DeclarationTag.js';
import { DebugTag } from './visitors/DebugTag.js';
import { EachBlock } from './visitors/EachBlock.js';
import { ExpressionStatement } from './visitors/ExpressionStatement.js';
@ -64,6 +65,7 @@ const template_visitors = {
AwaitBlock,
Component,
ConstTag,
DeclarationTag,
DebugTag,
EachBlock,
Fragment,

@ -28,7 +28,7 @@ export interface ComponentServerTransformState extends ServerTransformState {
readonly preserve_whitespace: boolean;
/** True if the current node is a) a component or render tag and b) the sole child of a block */
readonly is_standalone: boolean;
/** Transformed async `{@const }` declarations (if any) and those coming after them */
/** Transformed async `{@const }`/`{let/const ...}` declarations (if any) and those coming after them */
async_consts?: {
id: Identifier;
thunks: Expression[];

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

@ -0,0 +1,85 @@
/** @import { Expression, Identifier, Pattern, Statement, ExpressionStatement, VariableDeclaration } from 'estree' */
/** @import { AST } from '#compiler' */
/** @import { ComponentContext } from '../types.js' */
import { extract_identifiers, has_await_expression } from '../../../../utils/ast.js';
import * as b from '#compiler/builders';
/**
* @param {AST.DeclarationTag} node
* @param {ComponentContext} context
*/
export function DeclarationTag(node, context) {
const declaration = /** @type {Statement} */ (context.visit(node.declaration));
if (
node.metadata.promises_id &&
node.declaration.type === 'VariableDeclaration' &&
declaration.type === 'VariableDeclaration'
) {
const { ids, assignments } = build_async_declaration_parts(declaration);
add_async_declaration(context, node.metadata, ids, assignments, declaration.kind);
} else {
context.state.init.push(declaration);
}
}
/**
* @param {VariableDeclaration} declaration
*/
export function build_async_declaration_parts(declaration) {
const ids = new Map();
for (const declarator of declaration.declarations) {
for (const id of extract_identifiers(declarator.id)) {
ids.set(id.name, id);
}
}
const assignments = declaration.declarations
.filter((declarator) => declarator.init !== null)
.map((declarator) =>
b.stmt(
b.assignment(
'=',
/** @type {Pattern} */ (declarator.id),
/** @type {Expression} */ (declarator.init)
)
)
);
return { ids: [...ids.values()], assignments };
}
/**
* @param {ComponentContext} context
* @param {AST.ConstTag['metadata'] | AST.DeclarationTag['metadata']} metadata
* @param {Identifier[]} ids
* @param {ExpressionStatement[]} assignments
* @param {VariableDeclaration['kind']} [kind]
*/
export function add_async_declaration(context, metadata, ids, assignments, kind = 'let') {
const run = (context.state.async_consts ??= {
id: /** @type {Identifier} */ (metadata.promises_id),
thunks: []
});
for (const id of ids) {
context.state.init.push(kind === 'var' ? b.var(id.name) : b.let(id.name));
}
const blockers = [...metadata.expression.dependencies]
.map((dep) => dep.blocker)
.filter((b) => b !== null && b.object !== context.state.async_consts?.id);
if (blockers.length === 1) {
run.thunks.push(b.thunk(/** @type {Expression} */ (blockers[0])));
} else if (blockers.length > 0) {
run.thunks.push(b.thunk(b.call('Promise.all', b.array(blockers))));
}
// keep the number of thunks pushed in sync with analysis phase
const has_await =
metadata.expression.has_await ||
assignments.some((assignment) => has_await_expression(assignment));
const body = assignments.length === 1 ? assignments[0].expression : b.block(assignments);
run.thunks.push(b.thunk(body, has_await));
}

@ -17,17 +17,23 @@ import { is_customizable_select_element } from '../../../nodes.js';
export function RegularElement(node, context) {
const name = context.state.namespace === 'html' ? node.name.toLowerCase() : node.name;
const namespace = determine_namespace_for_children(node, context.state.namespace);
const has_child_declarations = !node.fragment.metadata.transparent;
/** @type {ComponentServerTransformState} */
const state = {
...context.state,
namespace,
scope: /** @type {Scope} */ (context.state.scopes.get(node.fragment)),
preserve_whitespace:
context.state.preserve_whitespace || node.name === 'pre' || node.name === 'textarea',
init: [],
template: []
template: [],
async_consts: undefined
};
/** @type {ComponentServerTransformState} */
const attribute_state = { ...state, scope: context.state.scope };
const node_is_void = is_void(name);
const optimiser = new PromiseOptimiser();
@ -50,7 +56,11 @@ export function RegularElement(node, context) {
if (!is_special) {
// only open the tag in the non-special path
state.template.push(b.literal(`<${name}`));
body = build_element_attributes(node, { ...context, state }, optimiser.transform);
body = build_element_attributes(
node,
{ ...context, state: attribute_state },
optimiser.transform
);
state.template.push(b.literal(node_is_void ? '/>' : '>')); // add `/>` for XHTML compliance
}
@ -72,10 +82,7 @@ export function RegularElement(node, context) {
node.fragment.nodes,
context.path,
namespace,
{
...state,
scope: /** @type {Scope} */ (state.scopes.get(node.fragment))
},
state,
state.preserve_whitespace,
state.options.preserveComments
);
@ -205,7 +212,17 @@ export function RegularElement(node, context) {
state.template.push(b.stmt(b.call('$.pop_element')));
}
if (optimiser.is_async()) {
if (has_child_declarations && state.async_consts && state.async_consts.thunks.length > 0) {
state.init.push(
b.var(state.async_consts.id, b.call('$$renderer.run', b.array(state.async_consts.thunks)))
);
}
if (has_child_declarations) {
context.state.template.push(
...optimiser.render([b.block([...state.init, ...build_template(state.template)])])
);
} else if (optimiser.is_async()) {
context.state.template.push(
...optimiser.render([...state.init, ...build_template(state.template)])
);

@ -152,6 +152,7 @@ export function clean_nodes(
if (
node.type === 'ConstTag' ||
node.type === 'DeclarationTag' ||
node.type === 'DebugTag' ||
node.type === 'SvelteBody' ||
node.type === 'SvelteWindow' ||

@ -217,6 +217,7 @@ function* find_descendants(fragment) {
case 'SnippetBlock':
case 'DebugTag':
case 'ConstTag':
case 'DeclarationTag':
case 'Comment':
case 'ExpressionTag':
break;

@ -603,6 +603,48 @@ const svelte_visitors = (comments) => ({
context.write('}');
},
DeclarationTag(node, context) {
context.write('{');
// This is duplicated from esrap's handling of VariableDeclaration,
// which we need to do in order to omit the trailing semicolon that esrap would add.
const open = context.new();
const join = context.new();
const child_context = context.new();
context.append(child_context);
child_context.write(`${node.declaration.kind} `);
child_context.append(open);
const declarations = node.declaration.declarations;
let first = true;
for (const d of declarations) {
if (!first) child_context.append(join);
first = false;
child_context.visit(d);
}
const length = child_context.measure() + 2 * (declarations.length - 1);
const multiline = child_context.multiline || (declarations.length > 1 && length > 50);
if (multiline) {
context.multiline = true;
if (declarations.length > 1) open.indent();
join.write(',');
join.newline();
if (declarations.length > 1) context.dedent();
} else {
join.write(', ');
}
context.write('}');
},
DebugTag(node, context) {
context.write('{@debug ');
let started = false;

@ -160,6 +160,18 @@ export namespace AST {
};
}
/** A `{let ...}` or `{const ...}` tag */
export interface DeclarationTag extends BaseNode {
type: 'DeclarationTag';
declaration: VariableDeclaration;
/** @internal */
metadata: {
expression: ExpressionMetadata;
/** If this declaration tag contains an await expression, or needs to wait on other async, this is set */
promises_id?: Identifier;
};
}
/** A `{@debug ...}` tag */
export interface DebugTag extends BaseNode {
type: 'DebugTag';
@ -622,6 +634,7 @@ export namespace AST {
export type Tag =
| AST.AttachTag
| AST.ConstTag
| AST.DeclarationTag
| AST.DebugTag
| AST.ExpressionTag
| AST.HtmlTag

@ -686,7 +686,8 @@ export {
if_builder as if,
this_instance as this,
null_instance as null,
debugger_builder as debugger
debugger_builder as debugger,
new_builder as new
};
/**

@ -387,7 +387,7 @@ export class Boundary {
if (this.#pending_effect) current_batch.skip_effect(this.#pending_effect);
if (this.#failed_effect) current_batch.skip_effect(this.#failed_effect);
current_batch.on_fork_commit(() => {
current_batch.oncommit(() => {
this.#handle_error(error);
});
} else {

@ -155,12 +155,6 @@ export class Batch {
*/
#discard_callbacks = new Set();
/**
* Callbacks that should run only when a fork is committed.
* @type {Set<(batch: Batch) => void>}
*/
#fork_commit_callbacks = new Set();
/**
* The number of async effects that are currently in flight
*/
@ -665,7 +659,6 @@ export class Batch {
discard() {
for (const fn of this.#discard_callbacks) fn(this);
this.#discard_callbacks.clear();
this.#fork_commit_callbacks.clear();
this.#unlink();
this.#deferred?.resolve();
@ -864,16 +857,6 @@ export class Batch {
this.#discard_callbacks.add(fn);
}
/** @param {(batch: Batch) => void} fn */
on_fork_commit(fn) {
this.#fork_commit_callbacks.add(fn);
}
run_fork_commit_callbacks() {
for (const fn of this.#fork_commit_callbacks) fn(this);
this.#fork_commit_callbacks.clear();
}
settled() {
return (this.#deferred ??= deferred()).promise;
}
@ -1436,10 +1419,6 @@ export function fork(fn) {
value.wv = snapshot.wv;
}
batch.activate();
batch.run_fork_commit_callbacks();
batch.deactivate();
// trigger any `$state.eager(...)` expressions with the new state.
// eager effects don't get scheduled like other effects, so we
// can't just encounter them during traversal, we need to

@ -49,11 +49,11 @@ export function update_pre_prop(fn, d = 1) {
/**
* The proxy handler for rest props (i.e. `const { x, ...rest } = $props()`).
* Is passed the full `$$props` object and excludes the named props.
* @type {ProxyHandler<{ props: Record<string | symbol, unknown>, exclude: Array<string | symbol>, name?: string }>}}
* @type {ProxyHandler<{ props: Record<string | symbol, unknown>, exclude: Set<string | symbol>, name?: string }>}}
*/
const rest_props_handler = {
get(target, key) {
if (target.exclude.includes(key)) return;
if (target.exclude.has(key)) return;
return target.props[key];
},
set(target, key) {
@ -65,7 +65,7 @@ const rest_props_handler = {
return false;
},
getOwnPropertyDescriptor(target, key) {
if (target.exclude.includes(key)) return;
if (target.exclude.has(key)) return;
if (key in target.props) {
return {
enumerable: true,
@ -75,17 +75,17 @@ const rest_props_handler = {
}
},
has(target, key) {
if (target.exclude.includes(key)) return false;
if (target.exclude.has(key)) return false;
return key in target.props;
},
ownKeys(target) {
return Reflect.ownKeys(target.props).filter((key) => !target.exclude.includes(key));
return Reflect.ownKeys(target.props).filter((key) => !target.exclude.has(key));
}
};
/**
* @param {Record<string, unknown>} props
* @param {string[]} exclude
* @param {Set<string>} exclude
* @param {string} [name]
* @returns {Record<string, unknown>}
*/

@ -0,0 +1,113 @@
{
"css": null,
"js": [],
"start": 0,
"end": 38,
"type": "Root",
"fragment": {
"type": "Fragment",
"nodes": [
{
"type": "IfBlock",
"elseif": false,
"start": 0,
"end": 38,
"test": {
"type": "Literal",
"start": 5,
"end": 9,
"loc": {
"start": {
"line": 1,
"column": 5
},
"end": {
"line": 1,
"column": 9
}
},
"value": true,
"raw": "true"
},
"consequent": {
"type": "Fragment",
"nodes": [
{
"type": "Text",
"start": 10,
"end": 12,
"raw": "\n\t",
"data": "\n\t"
},
{
"type": "DeclarationTag",
"start": 12,
"end": 18,
"declaration": {
"type": "VariableDeclaration",
"kind": "let",
"declarations": [
{
"type": "VariableDeclarator",
"id": {
"type": "Identifier",
"name": "",
"start": 17,
"end": 17
},
"init": null,
"start": 17,
"end": 17
}
],
"start": 13,
"end": 17
}
},
{
"type": "Text",
"start": 18,
"end": 20,
"raw": "\n\t",
"data": "\n\t"
},
{
"type": "DeclarationTag",
"start": 20,
"end": 32,
"declaration": {
"type": "VariableDeclaration",
"kind": "const",
"declarations": [
{
"type": "VariableDeclarator",
"id": {
"type": "Identifier",
"name": "",
"start": 31,
"end": 31
},
"init": null,
"start": 31,
"end": 31
}
],
"start": 21,
"end": 31
}
},
{
"type": "Text",
"start": 32,
"end": 33,
"raw": "\n",
"data": "\n"
}
]
},
"alternate": null
}
]
},
"options": null
}

@ -0,0 +1,7 @@
{#if visible}
{let count = 1}
{const doubled = count * 2}
{const label = 'count'}
{const format = (value) => `${label}: ${value}`}
<p>{format(doubled)}</p>
{/if}

@ -0,0 +1,7 @@
{#if visible}
{let count = 1}
{const doubled = count * 2}
{const label = 'count'}
{const format = (value) => `${label}: ${value}`}
<p>{format(doubled)}</p>
{/if}

@ -0,0 +1,13 @@
import { tick } from 'svelte';
import { test } from '../../test';
export default test({
mode: ['async-server', 'client', 'hydrate'],
ssrHtml: `<h1>Hello, world!</h1> 5 01234 5 sync 6 5 0 10`,
async test({ assert, target }) {
await tick();
assert.htmlEqual(target.innerHTML, `<h1>Hello, world!</h1> 5 01234 5 sync 6 5 0 10`);
}
});

@ -0,0 +1,31 @@
<script>
let name = $state('world');
</script>
<svelte:boundary>
{const sync = 'sync'}
{const number = await Promise.resolve(5)}
{const after_async =number + 1}
{const { length, 0: first } = await '01234'}
{#snippet greet()}
{const greeting = $derived(await `Hello, ${name}!`)}
<h1>{greeting}</h1>
{number}
{#if number > 4 && after_async && greeting}
{const length = $derived(await number)}
{#each { length }, index}
{const i = $derived(await index)}
{i}
{/each}
{/if}
{/snippet}
{@render greet()}
{number} {sync} {after_async} {length} {first}
{#if sync}
{const double = $derived(number * 2)}
{double}
{/if}
</svelte:boundary>

@ -0,0 +1,43 @@
import { tick } from 'svelte';
import { test } from '../../test';
export default test({
async test({ assert, target }) {
await tick();
const [top, change] = target.querySelectorAll('button');
assert.htmlEqual(
target.innerHTML,
`
<button>name</button>
<button>change name</button>
<p>Hello name</p>
<div><span>nested Hi name</span></div>
`
);
top.click();
await tick();
assert.htmlEqual(
target.innerHTML,
`
<button>other</button>
<button>change name</button>
<p>Hello name</p>
<div><span>nested Hi name</span></div>
`
);
change.click();
await tick();
assert.htmlEqual(
target.innerHTML,
`
<button>other</button>
<button>change name</button>
<p>Hello other</p>
<div><span>nested Hi other</span></div>
`
);
}
});

@ -0,0 +1,20 @@
<script>
const id = 'name';
const top_id = await 'name';
</script>
{let name = $state(top_id)}
<button onclick={() => name = 'other'}>{name}</button>
{#if id}
{let name = $state(await id)}
{let greeting = $derived(await `Hello ${name}`)}
<button onclick={() => name = 'other'}>change name</button>
<p>{greeting}</p>
<div>
{const nested = 'nested'}
{const greeting2 = $derived(await `Hi ${name}`)}
<span>{nested} {greeting2}</span>
</div>
{/if}

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

@ -0,0 +1,3 @@
{let count = $state(0)}
{let doubled = $derived(count * 2)}
<button onclick={() => (count += 1)}>{count} | {doubled}</button>

@ -0,0 +1,37 @@
import { tick } from 'svelte';
import { test } from '../../test';
export default test({
html: `<button>top 2</button><button>toggle</button><button>2</button><p>4 total</p><div><span>nested</span></div><div><span>nested</span></div>`,
async test({ assert, target }) {
const [top, toggle, increment] = target.querySelectorAll('button');
top.click();
await tick();
assert.htmlEqual(
target.innerHTML,
`<button>top 4</button><button>toggle</button><button>2</button><p>4 total</p><div><span>nested</span></div><div><span>nested</span></div>`
);
increment.click();
await tick();
assert.htmlEqual(
target.innerHTML,
`<button>top 4</button><button>toggle</button><button>3</button><p>6 total</p><div><span>nested</span></div><div><span>nested</span></div>`
);
toggle.click();
await tick();
assert.htmlEqual(
target.innerHTML,
`<button>top 4</button><button>toggle</button><div><span>nested</span></div>`
);
toggle.click();
await tick();
assert.htmlEqual(
target.innerHTML,
`<button>top 4</button><button>toggle</button><button>2</button><p>4 total</p><div><span>nested</span></div><div><span>nested</span></div>`
);
}
});

@ -0,0 +1,29 @@
<script>
let visible = $state(true);
let initial = $state(2);
</script>
{let top = $state(1)}
{let top_doubled = $derived(top * 2)}
<button onclick={() => (top += 1)}>top {top_doubled}</button>
<button onclick={() => (visible = !visible)}>toggle</button>
{#if visible}
{let counter = $state({ value: initial })}
{let doubled = $derived(counter.value * 2)}
{const suffix = ' total'}
{const format = (value) => `${value}${suffix}`}
<button onclick={() => (counter.value += 1)}>{counter.value}</button>
<p>{format(doubled)}</p>
<div>
{const doubled = 'nested'}
<span>{doubled}</span>
</div>
{/if}
<div>
{const nested = 'nested'}
<span>{nested}</span>
</div>

@ -2,7 +2,7 @@ import 'svelte/internal/disclose-version';
import 'svelte/internal/flags/async';
import * as $ from 'svelte/internal/client';
var root_1 = $.from_html(`<p> </p>`);
var root = $.from_html(`<p> </p>`);
export default function Async_const($$anchor) {
var fragment = $.comment();
@ -18,7 +18,7 @@ export default function Async_const($$anchor) {
() => b = $.derived(() => $.get(a) + 1)
]);
var p = root_1();
var p = root();
var text = $.child(p, true);
$.reset(p);

@ -0,0 +1,44 @@
import 'svelte/internal/disclose-version';
import * as $ from 'svelte/internal/client';
var root = $.from_html(`<p class="x">hello</p>`);
var root_1 = $.from_html(`<!> <!>`, 1);
export default function Dedupe_templates($$anchor, $$props) {
var fragment = root_1();
var node = $.first_child(fragment);
{
var consequent = ($$anchor) => {
var p = root();
$.append($$anchor, p);
};
var alternate = ($$anchor) => {
var p_1 = root();
$.append($$anchor, p_1);
};
$.if(node, ($$render) => {
if ($$props.a) $$render(consequent); else $$render(alternate, -1);
});
}
var node_1 = $.sibling(node, 2);
{
var consequent_1 = ($$anchor) => {
var p_2 = root();
$.append($$anchor, p_2);
};
$.if(node_1, ($$render) => {
if ($$props.b) $$render(consequent_1);
});
}
$.append($$anchor, fragment);
}

@ -0,0 +1,24 @@
import * as $ from 'svelte/internal/server';
export default function Dedupe_templates($$renderer, $$props) {
let { a, b } = $$props;
if (a) {
$$renderer.push('<!--[0-->');
$$renderer.push(`<p class="x">hello</p>`);
} else {
$$renderer.push('<!--[-1-->');
$$renderer.push(`<p class="x">hello</p>`);
}
$$renderer.push(`<!--]--> `);
if (b) {
$$renderer.push('<!--[0-->');
$$renderer.push(`<p class="x">hello</p>`);
} else {
$$renderer.push('<!--[-1-->');
}
$$renderer.push(`<!--]-->`);
}

@ -0,0 +1,14 @@
<script>
let { a, b } = $props();
</script>
<!-- identical static markup repeated across branches should share one hoisted template -->
{#if a}
<p class="x">hello</p>
{:else}
<p class="x">hello</p>
{/if}
{#if b}
<p class="x">hello</p>
{/if}

@ -2,14 +2,14 @@ import 'svelte/internal/disclose-version';
import 'svelte/internal/flags/legacy';
import * as $ from 'svelte/internal/client';
var root_1 = $.from_html(`<button type="button">B</button>`);
var root = $.from_html(`<button type="button">B</button>`);
export default function Delegated_locally_declared_shadowed($$anchor) {
var fragment = $.comment();
var node = $.first_child(fragment);
$.each(node, 0, () => ({ length: 1 }), $.index, ($$anchor, $$item, index) => {
var button = root_1();
var button = root();
$.set_attribute(button, 'data-index', index);

@ -2,14 +2,14 @@ import 'svelte/internal/disclose-version';
import 'svelte/internal/flags/legacy';
import * as $ from 'svelte/internal/client';
var root_1 = $.from_html(`<p></p>`);
var root = $.from_html(`<p></p>`);
export default function Each_index_non_null($$anchor) {
var fragment = $.comment();
var node = $.first_child(fragment);
$.each(node, 0, () => Array(10), $.index, ($$anchor, $$item, i) => {
var p = root_1();
var p = root();
p.textContent = `index: ${i}`;
$.append($$anchor, p);

@ -1,10 +1,12 @@
import 'svelte/internal/disclose-version';
import * as $ from 'svelte/internal/client';
var rest_excludes = new Set(['$$slots', '$$events', '$$legacy']);
export default function Props_identifier($$anchor, $$props) {
$.push($$props, true);
let props = $.rest_props($$props, ['$$slots', '$$events', '$$legacy']);
let props = $.rest_props($$props, rest_excludes);
$$props.a;
props[a];

@ -4,63 +4,51 @@ import * as $ from 'svelte/internal/client';
import Option from './Option.svelte';
const opt = ($$anchor) => {
var option = root_1();
var option = root();
$.append($$anchor, option);
};
const option_snippet = ($$anchor) => {
var option_1 = root_2();
var option_1 = root_1();
$.append($$anchor, option_1);
};
const option_snippet2 = ($$anchor) => {
var option_2 = root_3();
var option_2 = root_2();
$.append($$anchor, option_2);
};
const conditional_option = ($$anchor) => {
var option_3 = root_4();
var option_3 = root_3();
$.append($$anchor, option_3);
};
var root_1 = $.from_html(`<option>Snippet</option>`);
var root_2 = $.from_html(`<option>Rendered</option>`);
var root_3 = $.from_html(`<option>Rendered in group</option>`);
var root_4 = $.from_html(`<option>Conditional</option>`);
var root = $.from_html(`<option>Snippet</option>`);
var root_1 = $.from_html(`<option>Rendered</option>`);
var root_2 = $.from_html(`<option>Rendered in group</option>`);
var root_3 = $.from_html(`<option>Conditional</option>`);
var option_content = $.from_html(`<span>Rich</span>`, 1);
var root_5 = $.from_html(`<option> </option>`);
var root_6 = $.from_html(`<option>Visible</option>`);
var root_7 = $.from_html(`<option>Keyed</option>`);
var root_4 = $.from_html(`<option> </option>`);
var root_5 = $.from_html(`<option>Visible</option>`);
var root_6 = $.from_html(`<option>Keyed</option>`);
var select_content = $.from_html(`<!>`, 1);
var root_8 = $.from_html(`<option> </option>`);
var option_content_1 = $.from_html(`<strong>Bold</strong>`, 1);
var root_9 = $.from_html(`<option> </option>`);
var option_content_2 = $.from_html(`<em>Italic</em> text`, 1);
var option_content_3 = $.from_html(`<span> </span>`, 1);
var root_10 = $.from_html(`<option><!></option>`);
var root_12 = $.from_html(`<option> </option>`);
var root_13 = $.from_html(`<option>Boundary</option>`);
var root_7 = $.from_html(`<option><!></option>`);
var root_8 = $.from_html(`<option>Boundary</option>`);
var option_content_4 = $.from_html(`<span>Rich in boundary</span>`, 1);
var root_14 = $.from_html(`<option><!></option>`);
var select_content_1 = $.from_html(`<!>`, 1);
var select_content_2 = $.from_html(`<!>`, 1);
var select_content_3 = $.from_html(`<!>`, 1);
var optgroup_content = $.from_html(`<!>`, 1);
var optgroup_content_1 = $.from_html(`<!>`, 1);
var option_content_5 = $.from_html(`<!>`, 1);
var select_content_4 = $.from_html(`<!>`, 1);
var select_content_5 = $.from_html(`<!>`, 1);
var root = $.from_html(`<select><option><!></option></select> <select></select> <select><!></select> <select><!></select> <select><!></select> <select></select> <select><optgroup label="Group"><option><!></option></optgroup></select> <select><optgroup label="Group"></optgroup></select> <select><option><!></option></select> <select></select> <select><!></select> <select><!></select> <select><!></select> <select><!></select> <select><!></select> <select><!></select> <select><optgroup label="Group"><!></optgroup></select> <select><optgroup label="Group"><!></optgroup></select> <select><option><!></option></select> <select><!></select> <select><!></select>`, 1);
var root_9 = $.from_html(`<select><option><!></option></select> <select></select> <select><!></select> <select><!></select> <select><!></select> <select></select> <select><optgroup label="Group"><option><!></option></optgroup></select> <select><optgroup label="Group"></optgroup></select> <select><option><!></option></select> <select></select> <select><!></select> <select><!></select> <select><!></select> <select><!></select> <select><!></select> <select><!></select> <select><optgroup label="Group"><!></optgroup></select> <select><optgroup label="Group"><!></optgroup></select> <select><option><!></option></select> <select><!></select> <select><!></select>`, 1);
export default function Select_with_rich_content($$anchor) {
let items = [1, 2, 3];
let show = true;
let html = '<option>From HTML</option>';
var fragment = root();
var fragment = root_9();
var select = $.first_child(fragment);
var option_4 = $.child(select);
@ -76,7 +64,7 @@ export default function Select_with_rich_content($$anchor) {
var select_1 = $.sibling(select, 2);
$.each(select_1, 5, () => items, $.index, ($$anchor, item) => {
var option_5 = root_5();
var option_5 = root_4();
var text = $.child(option_5, true);
$.reset(option_5);
@ -101,7 +89,7 @@ export default function Select_with_rich_content($$anchor) {
{
var consequent = ($$anchor) => {
var option_6 = root_6();
var option_6 = root_5();
$.append($$anchor, option_6);
};
@ -117,7 +105,7 @@ export default function Select_with_rich_content($$anchor) {
var node_1 = $.child(select_3);
$.key(node_1, () => items, ($$anchor) => {
var option_7 = root_7();
var option_7 = root_6();
$.append($$anchor, option_7);
});
@ -139,7 +127,7 @@ export default function Select_with_rich_content($$anchor) {
$.each(select_5, 5, () => items, $.index, ($$anchor, item) => {
const x = $.derived_safe_equal(() => $.get(item) * 2);
var option_8 = root_8();
var option_8 = root_4();
var text_1 = $.child(option_8, true);
$.reset(option_8);
@ -177,7 +165,7 @@ export default function Select_with_rich_content($$anchor) {
var optgroup_1 = $.child(select_7);
$.each(optgroup_1, 5, () => items, $.index, ($$anchor, item) => {
var option_10 = root_9();
var option_10 = root_4();
var text_2 = $.child(option_10, true);
$.reset(option_10);
@ -215,7 +203,7 @@ export default function Select_with_rich_content($$anchor) {
var select_9 = $.sibling(select_8, 2);
$.each(select_9, 5, () => items, $.index, ($$anchor, item) => {
var option_12 = root_10();
var option_12 = root_7();
$.customizable_select(option_12, () => {
var anchor_4 = $.child(option_12);
@ -242,7 +230,7 @@ export default function Select_with_rich_content($$anchor) {
var node_4 = $.first_child(fragment_6);
$.each(node_4, 1, () => items, $.index, ($$anchor, item) => {
var option_13 = root_12();
var option_13 = root_4();
var text_4 = $.child(option_13, true);
$.reset(option_13);
@ -274,7 +262,7 @@ export default function Select_with_rich_content($$anchor) {
var node_5 = $.child(select_11);
$.boundary(node_5, {}, ($$anchor) => {
var option_14 = root_13();
var option_14 = root_8();
$.append($$anchor, option_14);
});
@ -285,7 +273,7 @@ export default function Select_with_rich_content($$anchor) {
var node_6 = $.child(select_12);
$.boundary(node_6, {}, ($$anchor) => {
var option_15 = root_14();
var option_15 = root_7();
$.customizable_select(option_15, () => {
var anchor_5 = $.child(option_15);
@ -303,7 +291,7 @@ export default function Select_with_rich_content($$anchor) {
$.customizable_select(select_13, () => {
var anchor_6 = $.child(select_13);
var fragment_8 = select_content_1();
var fragment_8 = select_content();
var node_7 = $.first_child(fragment_8);
Option(node_7, {});
@ -314,7 +302,7 @@ export default function Select_with_rich_content($$anchor) {
$.customizable_select(select_14, () => {
var anchor_7 = $.child(select_14);
var fragment_9 = select_content_2();
var fragment_9 = select_content();
var node_8 = $.first_child(fragment_9);
option_snippet(node_8);
@ -325,7 +313,7 @@ export default function Select_with_rich_content($$anchor) {
$.customizable_select(select_15, () => {
var anchor_8 = $.child(select_15);
var fragment_10 = select_content_3();
var fragment_10 = select_content();
var node_9 = $.first_child(fragment_10);
$.html(node_9, () => html);
@ -337,7 +325,7 @@ export default function Select_with_rich_content($$anchor) {
$.customizable_select(optgroup_2, () => {
var anchor_9 = $.child(optgroup_2);
var fragment_11 = optgroup_content();
var fragment_11 = select_content();
var node_10 = $.first_child(fragment_11);
Option(node_10, {});
@ -351,7 +339,7 @@ export default function Select_with_rich_content($$anchor) {
$.customizable_select(optgroup_3, () => {
var anchor_10 = $.child(optgroup_3);
var fragment_12 = optgroup_content_1();
var fragment_12 = select_content();
var node_11 = $.first_child(fragment_12);
option_snippet2(node_11);
@ -365,7 +353,7 @@ export default function Select_with_rich_content($$anchor) {
$.customizable_select(option_16, () => {
var anchor_11 = $.child(option_16);
var fragment_13 = option_content_5();
var fragment_13 = select_content();
var node_12 = $.first_child(fragment_13);
$.html(node_12, () => '<strong>Bold HTML</strong>');
@ -378,7 +366,7 @@ export default function Select_with_rich_content($$anchor) {
$.customizable_select(select_19, () => {
var anchor_12 = $.child(select_19);
var fragment_14 = select_content_4();
var fragment_14 = select_content();
var node_13 = $.first_child(fragment_14);
$.each(node_13, 1, () => items, $.index, ($$anchor, item) => {
@ -392,7 +380,7 @@ export default function Select_with_rich_content($$anchor) {
$.customizable_select(select_20, () => {
var anchor_13 = $.child(select_20);
var fragment_16 = select_content_5();
var fragment_16 = select_content();
var node_14 = $.first_child(fragment_16);
{

@ -0,0 +1,14 @@
[
{
"code": "declaration_tag_invalid_type",
"message": "Declaration tags must be `let` or `const` declarations",
"start": {
"line": 14,
"column": 2
},
"end": {
"line": 14,
"column": 8
}
}
]

@ -0,0 +1,15 @@
{#if true}
<!-- check that these doesn't trigger already -->
{type}
{type }
{type && foo}
{type || bar}
{type % 2}
{type .x}
{type ?.x}
{type ()}
{type [1]}
{type `tag`}
<!-- ... this one should trigger though -->
{type foo = boolean}
{/if}

@ -0,0 +1,14 @@
[
{
"code": "declaration_tag_invalid_type",
"message": "Declaration tags must be `let` or `const` declarations",
"start": {
"line": 2,
"column": 2
},
"end": {
"line": 2,
"column": 5
}
}
]

@ -0,0 +1,14 @@
[
{
"code": "declaration_tag_no_legacy_mode",
"message": "Declaration tags cannot be used in legacy mode",
"start": {
"line": 5,
"column": 0
},
"end": {
"line": 5,
"column": 19
}
}
]

@ -0,0 +1,5 @@
<script>
export let name = 'world';
</script>
{const foo = 'foo'}

@ -0,0 +1,6 @@
<script>
let name = 'world';
</script>
Usage when no explicit runes/legacy mode should be ok
{const foo = world}

@ -1303,6 +1303,12 @@ declare module 'svelte/compiler' {
};
}
/** A `{let ...}` or `{const ...}` tag */
export interface DeclarationTag extends BaseNode {
type: 'DeclarationTag';
declaration: VariableDeclaration;
}
/** A `{@debug ...}` tag */
export interface DebugTag extends BaseNode {
type: 'DebugTag';
@ -1613,6 +1619,7 @@ declare module 'svelte/compiler' {
export type Tag =
| AST.AttachTag
| AST.ConstTag
| AST.DeclarationTag
| AST.DebugTag
| AST.ExpressionTag
| AST.HtmlTag

@ -42,8 +42,8 @@ importers:
specifier: 25.0.1
version: 25.0.1
playwright:
specifier: ^1.58.0
version: 1.58.0
specifier: ^1.60.0
version: 1.60.0
prettier:
specifier: ^3.2.4
version: 3.2.4
@ -121,8 +121,8 @@ importers:
specifier: ^0.3.25
version: 0.3.31
'@playwright/test':
specifier: ^1.58.0
version: 1.58.0
specifier: ^1.60.0
version: 1.60.0
'@rollup/plugin-commonjs':
specifier: ^28.0.1
version: 28.0.1(rollup@4.60.1)
@ -864,8 +864,8 @@ packages:
resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==}
engines: {node: '>=14'}
'@playwright/test@1.58.0':
resolution: {integrity: sha512-fWza+Lpbj6SkQKCrU6si4iu+fD2dD3gxNHFhUPxsfXBPhnv3rRSQVd0NtBUT9Z/RhF/boCBcuUaMUSTRTopjZg==}
'@playwright/test@1.60.0':
resolution: {integrity: sha512-O71yZIbAh/PxDMNGns37GHBIfrVkEVyn+AXyIa5dOTfb4/xNvRWV+Vv/NMbNCtODB/pO7vLlF2OTmMVLhmr7Ag==}
engines: {node: '>=18'}
hasBin: true
@ -1096,6 +1096,9 @@ packages:
'@types/estree@1.0.8':
resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==}
'@types/estree@1.0.9':
resolution: {integrity: sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==}
'@types/json-schema@7.0.15':
resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==}
@ -1459,8 +1462,8 @@ packages:
emoji-regex@9.2.2:
resolution: {integrity: sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==}
enhanced-resolve@5.20.1:
resolution: {integrity: sha512-Qohcme7V1inbAfvjItgw0EaxVX5q2rdVEZHRBrEQdRZTssLDGsL8Lwrznl8oQ/6kuTJONLaDcGjkNP247XEhcA==}
enhanced-resolve@5.22.1:
resolution: {integrity: sha512-6QEuw3zoX1SJQc7b87aBXke/no+mG2bTBgw29gWMQonLmpEkWoCAVkl+M49e48AZlWzxiDzDZzYdp6kobcyLww==}
engines: {node: '>=10.13.0'}
enquirer@2.4.1:
@ -1699,8 +1702,8 @@ packages:
function-bind@1.1.2:
resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==}
get-tsconfig@4.13.7:
resolution: {integrity: sha512-7tN6rFgBlMgpBML5j8typ92BKFi2sFQvIdpAqLA2beia5avZDrMs0FLZiM5etShWq5irVyGcGMEA1jcDaK7A/Q==}
get-tsconfig@4.14.0:
resolution: {integrity: sha512-yTb+8DXzDREzgvYmh6s9vHsSVCHeC0G3PI5bEXNBHtmshPnO+S5O7qgLEOn0I5QvMy6kpZN8K1NKGyilLb93wA==}
glob-parent@5.1.2:
resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==}
@ -2162,13 +2165,13 @@ packages:
resolution: {integrity: sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==}
engines: {node: '>=6'}
playwright-core@1.58.0:
resolution: {integrity: sha512-aaoB1RWrdNi3//rOeKuMiS65UCcgOVljU46At6eFcOFPFHWtd2weHRRow6z/n+Lec0Lvu0k9ZPKJSjPugikirw==}
playwright-core@1.60.0:
resolution: {integrity: sha512-9bW6zvX/m0lEbgTKJ6YppOKx8H3VOPBMOCFh2irXFOT4BbHgrx5hPjwJYLT40Lu+4qtD36qKc/Hn56StUW57IA==}
engines: {node: '>=18'}
hasBin: true
playwright@1.58.0:
resolution: {integrity: sha512-2SVA0sbPktiIY/MCOPX8e86ehA/e+tDNq+e5Y8qjKYti2Z/JG7xnronT/TXTIkKbYGWlCbuucZ6dziEgkoEjQQ==}
playwright@1.60.0:
resolution: {integrity: sha512-hheHdokM8cdqCb0lcE3s+zT4t4W+vvjpGxsZlDnikarzx8tSzMebh3UiFtgqwFwnTnjYQcsyMF8ei2mCO/tpeA==}
engines: {node: '>=18'}
hasBin: true
@ -2307,6 +2310,11 @@ packages:
engines: {node: '>=10'}
hasBin: true
semver@7.8.1:
resolution: {integrity: sha512-rkVq3IXh+4FDGch+KwzX3aV9W3kO54GyEgpvBzSyctDA6Xtd7RJQV1xmXbeQp5v7+VzLOfVqiutSE6GICgPFvg==}
engines: {node: '>=10'}
hasBin: true
serialize-javascript@6.0.2:
resolution: {integrity: sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g==}
@ -2403,8 +2411,8 @@ packages:
symbol-tree@3.2.4:
resolution: {integrity: sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==}
tapable@2.3.2:
resolution: {integrity: sha512-1MOpMXuhGzGL5TTCZFItxCc0AARf1EZFQkGqMm7ERKj8+Hgr5oLvJOVFcC+lRmR8hCe2S3jC4T5D7Vg/d7/fhA==}
tapable@2.3.3:
resolution: {integrity: sha512-uxc/zpqFg6x7C8vOE7lh6Lbda8eEL9zmVm/PLeTPBRhh1xCgdWaQ+J1CUieGpIfm2HdtsUpRv+HshiasBMcc6A==}
engines: {node: '>=6'}
term-size@2.2.1:
@ -3283,9 +3291,9 @@ snapshots:
'@pkgjs/parseargs@0.11.0':
optional: true
'@playwright/test@1.58.0':
'@playwright/test@1.60.0':
dependencies:
playwright: 1.58.0
playwright: 1.60.0
'@polka/url@1.0.0-next.25': {}
@ -3463,13 +3471,15 @@ snapshots:
'@types/eslint@8.56.12':
dependencies:
'@types/estree': 1.0.8
'@types/estree': 1.0.9
'@types/json-schema': 7.0.15
'@types/esrecurse@4.3.1': {}
'@types/estree@1.0.8': {}
'@types/estree@1.0.9': {}
'@types/json-schema@7.0.15': {}
'@types/node@12.20.55': {}
@ -3840,10 +3850,10 @@ snapshots:
emoji-regex@9.2.2: {}
enhanced-resolve@5.20.1:
enhanced-resolve@5.22.1:
dependencies:
graceful-fs: 4.2.11
tapable: 2.3.2
tapable: 2.3.3
enquirer@2.4.1:
dependencies:
@ -3945,7 +3955,7 @@ snapshots:
eslint-compat-utils@0.5.1(eslint@10.0.0):
dependencies:
eslint: 10.0.0
semver: 7.7.4
semver: 7.8.1
eslint-config-prettier@9.1.0(eslint@10.0.0):
dependencies:
@ -3965,14 +3975,14 @@ snapshots:
eslint-plugin-n@17.24.0(eslint@10.0.0)(typescript@5.5.4):
dependencies:
'@eslint-community/eslint-utils': 4.9.1(eslint@10.0.0)
enhanced-resolve: 5.20.1
enhanced-resolve: 5.22.1
eslint: 10.0.0
eslint-plugin-es-x: 7.8.0(eslint@10.0.0)
get-tsconfig: 4.13.7
get-tsconfig: 4.14.0
globals: 15.15.0
globrex: 0.1.2
ignore: 5.3.2
semver: 7.7.4
semver: 7.8.1
ts-declaration-location: 1.0.7(typescript@5.5.4)
transitivePeerDependencies:
- typescript
@ -4176,7 +4186,7 @@ snapshots:
function-bind@1.1.2: {}
get-tsconfig@4.13.7:
get-tsconfig@4.14.0:
dependencies:
resolve-pkg-maps: 1.0.0
@ -4596,11 +4606,11 @@ snapshots:
pify@4.0.1: {}
playwright-core@1.58.0: {}
playwright-core@1.60.0: {}
playwright@1.58.0:
playwright@1.60.0:
dependencies:
playwright-core: 1.58.0
playwright-core: 1.60.0
optionalDependencies:
fsevents: 2.3.2
@ -4742,6 +4752,8 @@ snapshots:
semver@7.7.4: {}
semver@7.8.1: {}
serialize-javascript@6.0.2:
dependencies:
randombytes: 2.1.0
@ -4829,7 +4841,7 @@ snapshots:
symbol-tree@3.2.4: {}
tapable@2.3.2: {}
tapable@2.3.3: {}
term-size@2.2.1: {}

Loading…
Cancel
Save