mirror of https://github.com/sveltejs/svelte
Allows `{let/var/const/function ...}` declarations in all places where we already allow `{@const ...}` (which will eventually get deprecated in favor of this new feature).
pull/18312/head
parent
fe9ab936b6
commit
f094ac5346
@ -0,0 +1,5 @@
|
||||
---
|
||||
'svelte': minor
|
||||
---
|
||||
|
||||
feat: allow declarations in the template
|
||||
@ -0,0 +1,32 @@
|
||||
---
|
||||
title: {<declaration ...>}
|
||||
---
|
||||
|
||||
Declaration tags define local variables and functions inside markup.
|
||||
|
||||
You can use `let`, `const`, `var` and `function` declarations:
|
||||
|
||||
```svelte
|
||||
{#each boxes as box}
|
||||
{const area = box.width * box.height}
|
||||
{function label(value) {
|
||||
return `${value} square pixels`;
|
||||
}}
|
||||
|
||||
<p>{label(area)}</p>
|
||||
{/each}
|
||||
```
|
||||
|
||||
Unlike [`{@const ...}`](@const), declaration tags are plain JavaScript declarations. This means `{const ...}` is not reactive by itself; use runes such as `$state` or `$derived` when you need reactive values:
|
||||
|
||||
```svelte
|
||||
{#if user}
|
||||
{let name = $state(user.name)}
|
||||
{let greeting = $derived(`Hello ${name}`)}
|
||||
|
||||
<input bind:value={name} />
|
||||
<p>{greeting}</p>
|
||||
{/if}
|
||||
```
|
||||
|
||||
Declaration tags are only allowed as an immediate child of a block — `{#if ...}`, `{#each ...}`, `{#snippet ...}` and so on — a `<Component />` or a `<svelte:boundary>`.
|
||||
@ -0,0 +1,65 @@
|
||||
/** @import { AST } from '#compiler' */
|
||||
/** @import { Context } from '../types' */
|
||||
import * as e from '../../../errors.js';
|
||||
import * as b from '#compiler/builders';
|
||||
import { validate_opening_tag, validate_tag_placement } from './shared/utils.js';
|
||||
|
||||
/**
|
||||
* @param {AST.DeclarationTag} node
|
||||
* @param {Context} context
|
||||
*/
|
||||
export function DeclarationTag(node, context) {
|
||||
if (context.state.analysis.runes) {
|
||||
const expected =
|
||||
node.declaration.type === 'FunctionDeclaration' ? 'f' : node.declaration.kind[0];
|
||||
validate_opening_tag(node, context.state, expected);
|
||||
}
|
||||
|
||||
validate_tag_placement(node, context, e.declaration_tag_invalid_placement);
|
||||
|
||||
if (node.declaration.type !== 'VariableDeclaration') {
|
||||
context.visit(node.declaration);
|
||||
return;
|
||||
}
|
||||
|
||||
context.visit(node.declaration, {
|
||||
...context.state,
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,86 @@
|
||||
/** @import { Expression, Identifier, Pattern, Statement, 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 {Statement[]} 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));
|
||||
run.thunks.push(b.thunk(b.block(assignments), has_await));
|
||||
}
|
||||
@ -0,0 +1,84 @@
|
||||
/** @import { Expression, Identifier, Pattern, Statement, 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 {Statement[]} 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));
|
||||
run.thunks.push(b.thunk(b.block(assignments), has_await));
|
||||
}
|
||||
@ -0,0 +1,5 @@
|
||||
{#if true}
|
||||
{let }
|
||||
{const x = }
|
||||
{function f}
|
||||
{/if}
|
||||
@ -0,0 +1,146 @@
|
||||
{
|
||||
"css": null,
|
||||
"js": [],
|
||||
"start": 0,
|
||||
"end": 52,
|
||||
"type": "Root",
|
||||
"fragment": {
|
||||
"type": "Fragment",
|
||||
"nodes": [
|
||||
{
|
||||
"type": "IfBlock",
|
||||
"elseif": false,
|
||||
"start": 0,
|
||||
"end": 52,
|
||||
"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": 34,
|
||||
"raw": "\n\t",
|
||||
"data": "\n\t"
|
||||
},
|
||||
{
|
||||
"type": "DeclarationTag",
|
||||
"start": 34,
|
||||
"end": 46,
|
||||
"declaration": {
|
||||
"type": "FunctionDeclaration",
|
||||
"id": {
|
||||
"type": "Identifier",
|
||||
"name": "",
|
||||
"start": 45,
|
||||
"end": 45
|
||||
},
|
||||
"generator": false,
|
||||
"async": false,
|
||||
"params": [],
|
||||
"body": {
|
||||
"type": "BlockStatement",
|
||||
"body": [],
|
||||
"start": 45,
|
||||
"end": 45
|
||||
},
|
||||
"start": 35,
|
||||
"end": 45
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "Text",
|
||||
"start": 46,
|
||||
"end": 47,
|
||||
"raw": "\n",
|
||||
"data": "\n"
|
||||
}
|
||||
]
|
||||
},
|
||||
"alternate": null
|
||||
}
|
||||
]
|
||||
},
|
||||
"options": null,
|
||||
"comments": []
|
||||
}
|
||||
@ -0,0 +1,9 @@
|
||||
{#if visible}
|
||||
{let count = 1}
|
||||
{const doubled = count * 2;}
|
||||
{var label = 'count'}
|
||||
{function format(value) {
|
||||
return `${label}: ${value}`;
|
||||
}}
|
||||
<p>{format(doubled)}</p>
|
||||
{/if}
|
||||
@ -0,0 +1,11 @@
|
||||
{#if visible}
|
||||
{let count = 1;}
|
||||
{const doubled = count * 2;}
|
||||
{var label = 'count';}
|
||||
|
||||
{function format(value) {
|
||||
return `${label}: ${value}`;
|
||||
}}
|
||||
|
||||
<p>{format(doubled)}</p>
|
||||
{/if}
|
||||
@ -0,0 +1,27 @@
|
||||
import { tick } from 'svelte';
|
||||
import { test } from '../../test';
|
||||
|
||||
export default test({
|
||||
async test({ assert, target }) {
|
||||
await tick();
|
||||
const [change] = target.querySelectorAll('button');
|
||||
|
||||
assert.htmlEqual(
|
||||
target.innerHTML,
|
||||
`
|
||||
<button>change name</button>
|
||||
<p>Hello name</p>
|
||||
`
|
||||
);
|
||||
|
||||
change.click();
|
||||
await tick();
|
||||
assert.htmlEqual(
|
||||
target.innerHTML,
|
||||
`
|
||||
<button>change name</button>
|
||||
<p>Hello other</p>
|
||||
`
|
||||
);
|
||||
}
|
||||
});
|
||||
@ -0,0 +1,11 @@
|
||||
<script>
|
||||
const id = 'name';
|
||||
</script>
|
||||
|
||||
{#if id}
|
||||
{let name = $state(await id)}
|
||||
{let greeting = $derived(await `Hello ${name}`)}
|
||||
|
||||
<button onclick={() => name = 'other'}>change name</button>
|
||||
<p>{greeting}</p>
|
||||
{/if}
|
||||
@ -0,0 +1,18 @@
|
||||
<script>
|
||||
let visible = $state(true);
|
||||
let initial = $state(2);
|
||||
</script>
|
||||
|
||||
<button onclick={() => (visible = !visible)}>toggle</button>
|
||||
|
||||
{#if visible}
|
||||
{let counter = $state({ value: initial })}
|
||||
{let doubled = $derived(counter.value * 2)}
|
||||
{var suffix = ' total'}
|
||||
{function format(value) {
|
||||
return `${value}${suffix}`;
|
||||
}}
|
||||
|
||||
<button onclick={() => (counter.value += 1)}>{counter.value}</button>
|
||||
<p>{format(doubled)}</p>
|
||||
{/if}
|
||||
@ -0,0 +1,7 @@
|
||||
import { test } from '../../test';
|
||||
|
||||
export default test({
|
||||
props: {
|
||||
items: [1, 3]
|
||||
}
|
||||
});
|
||||
@ -0,0 +1 @@
|
||||
<p>value: 2</p><p>value: 6</p>
|
||||
@ -0,0 +1,11 @@
|
||||
<script>
|
||||
export let items;
|
||||
</script>
|
||||
|
||||
{#each items as item}
|
||||
{const doubled = item * 2}
|
||||
{function label(value) {
|
||||
return `value: ${value}`;
|
||||
}}
|
||||
<p>{label(doubled)}</p>
|
||||
{/each}
|
||||
@ -0,0 +1,14 @@
|
||||
[
|
||||
{
|
||||
"code": "declaration_tag_invalid_type",
|
||||
"message": "Declaration tags must be `let`, `const`, `var` or `function` declarations",
|
||||
"start": {
|
||||
"line": 2,
|
||||
"column": 2
|
||||
},
|
||||
"end": {
|
||||
"line": 2,
|
||||
"column": 7
|
||||
}
|
||||
}
|
||||
]
|
||||
@ -0,0 +1,3 @@
|
||||
{#if true}
|
||||
{class Foo {}}
|
||||
{/if}
|
||||
@ -0,0 +1,14 @@
|
||||
[
|
||||
{
|
||||
"code": "declaration_tag_invalid_placement",
|
||||
"message": "Declaration tags must be the immediate child of `{#snippet}`, `{#if}`, `{:else if}`, `{:else}`, `{#each}`, `{:then}`, `{:catch}`, `<svelte:fragment>`, `<svelte:boundary>` or `<Component>`",
|
||||
"start": {
|
||||
"line": 5,
|
||||
"column": 0
|
||||
},
|
||||
"end": {
|
||||
"line": 5,
|
||||
"column": 15
|
||||
}
|
||||
}
|
||||
]
|
||||
@ -0,0 +1,5 @@
|
||||
<script>
|
||||
export let a;
|
||||
</script>
|
||||
|
||||
{let b = a + 1}
|
||||
Loading…
Reference in new issue