mirror of https://github.com/sveltejs/svelte
feat: allow declarations in the template (#18282)
Allows `{let/const ...}` declarations in all places (and more) where we
already allow `{@const ...}` (which will eventually get deprecated in
favor of this new feature).
Closes: #16490
Companion PRs:
- https://github.com/sveltejs/language-tools/pull/3033
- https://github.com/sveltejs/prettier-plugin-svelte/pull/533
- https://github.com/sveltejs/eslint-plugin-svelte/pull/1533
- https://github.com/sveltejs/svelte-eslint-parser/pull/891
---------
Co-authored-by: Rich Harris <rich.harris@vercel.com>
pull/18319/head
parent
656dae6780
commit
59d3a36f82
@ -0,0 +1,5 @@
|
|||||||
|
---
|
||||||
|
'svelte': minor
|
||||||
|
---
|
||||||
|
|
||||||
|
feat: allow declarations in the template
|
||||||
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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));
|
||||||
|
}
|
||||||
@ -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));
|
||||||
|
}
|
||||||
@ -0,0 +1,4 @@
|
|||||||
|
{#if true}
|
||||||
|
{let }
|
||||||
|
{const x = }
|
||||||
|
{/if}
|
||||||
@ -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,3 @@
|
|||||||
|
{#if true}
|
||||||
|
{function foo() {}}
|
||||||
|
{/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,3 @@
|
|||||||
|
{#if true}
|
||||||
|
{var foo = 1}
|
||||||
|
{/if}
|
||||||
@ -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 @@
|
|||||||
|
[]
|
||||||
@ -0,0 +1,6 @@
|
|||||||
|
<script>
|
||||||
|
let name = 'world';
|
||||||
|
</script>
|
||||||
|
|
||||||
|
Usage when no explicit runes/legacy mode should be ok
|
||||||
|
{const foo = world}
|
||||||
Loading…
Reference in new issue