Merge branch 'main' into svelte-custom-renderer-unified-mount-api

svelte-custom-renderer-unified-mount-api
Rich Harris 4 days ago
commit fe5e82a2e1

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

@ -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,70 @@
---
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] 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

@ -159,7 +159,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",
@ -169,12 +169,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,18 @@
/** @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;
const regex_unsupported_declaration = /(?:var|function|class|type|interface|enum)\b/y;
const pointy_bois = { '<': '>' };
@ -31,6 +33,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 +63,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
},
@ -725,6 +728,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,
@ -792,6 +796,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, custom_renderer } 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,

@ -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));
}

@ -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,
@ -303,11 +303,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'
};
@ -321,8 +324,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);
@ -446,11 +460,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[]} */

@ -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
);
@ -209,7 +216,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;

@ -161,6 +161,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';
@ -623,6 +635,7 @@ export namespace AST {
export type Tag =
| AST.AttachTag
| AST.ConstTag
| AST.DeclarationTag
| AST.DebugTag
| AST.ExpressionTag
| AST.HtmlTag

@ -417,7 +417,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 {

@ -140,12 +140,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
*/
@ -634,7 +628,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();
@ -840,16 +833,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;
}
@ -1410,10 +1393,6 @@ export function fork(fn) {
source.wv = increment_write_version();
}
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

@ -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>

@ -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": 10
}
}
]

@ -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}

@ -1345,6 +1345,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';
@ -1655,6 +1661,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