Skip to content

Commit b5aaa66

Browse files
authored
[feat] implement constants in markup (#6413)
1 parent d5fde79 commit b5aaa66

File tree

66 files changed

+998
-74
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

66 files changed

+998
-74
lines changed

site/content/docs/02-template-syntax.md

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -453,6 +453,29 @@ The `{@debug ...}` tag offers an alternative to `console.log(...)`. It logs the
453453
The `{@debug}` tag without any arguments will insert a `debugger` statement that gets triggered when *any* state changes, as opposed to the specified variables.
454454

455455

456+
### {@const ...}
457+
458+
```sv
459+
{@const assignment}
460+
```
461+
462+
---
463+
464+
The `{@const ...}` tag defines a local constant.
465+
466+
```sv
467+
<script>
468+
export let boxes;
469+
</script>
470+
471+
{#each boxes as box}
472+
{@const area = box.width * box.height}
473+
{box.width} * {box.height} = {area}
474+
{/each}
475+
```
476+
477+
`{@const}` is only allowed as direct child of `{#each}`, `{:then}`, `{:catch}`, `<Component />` or `<svelte:fragment />`.
478+
456479

457480
### Element directives
458481

src/compiler/compile/compiler_errors.ts

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,10 @@ export default {
4040
code: 'invalid-binding',
4141
message: 'Cannot bind to a variable declared with {#await ... then} or {:catch} blocks'
4242
},
43+
invalid_binding_const: {
44+
code: 'invalid-binding',
45+
message: 'Cannot bind to a variable declared with {@const ...}'
46+
},
4347
invalid_binding_writibale: {
4448
code: 'invalid-binding',
4549
message: 'Cannot bind to a variable which is not writable'
@@ -208,7 +212,7 @@ export default {
208212
},
209213
invalid_attribute_value: (name: string) => ({
210214
code: `invalid-${name}-value`,
211-
message: `${name} attribute must be true or false`
215+
message: `${name} attribute must be true or false`
212216
}),
213217
invalid_options_attribute_unknown: {
214218
code: 'invalid-options-attribute',
@@ -241,5 +245,21 @@ export default {
241245
invalid_directive_value: {
242246
code: 'invalid-directive-value',
243247
message: 'Can only bind to an identifier (e.g. `foo`) or a member expression (e.g. `foo.bar` or `foo[baz]`)'
244-
}
248+
},
249+
invalid_const_placement: {
250+
code: 'invalid-const-placement',
251+
message: '{@const} must be the immediate child of {#each}, {:then}, {:catch}, <svelte:fragment> or <Component>'
252+
},
253+
invalid_const_declaration: (name: string) => ({
254+
code: 'invalid-const-declaration',
255+
message: `'${name}' has already been declared`
256+
}),
257+
invalid_const_update: (name: string) => ({
258+
code: 'invalid-const-update',
259+
message: `'${name}' is declared using {@const ...} and is read-only`
260+
}),
261+
cyclical_const_tags: (cycle: string[]) => ({
262+
code: 'cyclical-const-tags',
263+
message: `Cyclical dependency detected: ${cycle.join(' → ')}`
264+
})
245265
};

src/compiler/compile/nodes/Binding.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,9 @@ export default class Binding extends Node {
5757
component.error(this, compiler_errors.invalid_binding_await);
5858
return;
5959
}
60+
if (scope.is_const(name)) {
61+
component.error(this, compiler_errors.invalid_binding_const);
62+
}
6063

6164
scope.dependencies_for_name.get(name).forEach(name => {
6265
const variable = component.var_lookup.get(name);

src/compiler/compile/nodes/CatchBlock.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,15 @@
1-
import map_children from './shared/map_children';
21
import TemplateScope from './shared/TemplateScope';
32
import AbstractBlock from './shared/AbstractBlock';
43
import AwaitBlock from './AwaitBlock';
54
import Component from '../Component';
65
import { TemplateNode } from '../../interfaces';
6+
import get_const_tags from './shared/get_const_tags';
7+
import ConstTag from './ConstTag';
78

89
export default class CatchBlock extends AbstractBlock {
910
type: 'CatchBlock';
1011
scope: TemplateScope;
12+
const_tags: ConstTag[];
1113

1214
constructor(component: Component, parent: AwaitBlock, scope: TemplateScope, info: TemplateNode) {
1315
super(component, parent, scope, info);
@@ -18,7 +20,8 @@ export default class CatchBlock extends AbstractBlock {
1820
this.scope.add(context.key.name, parent.expression.dependencies, this);
1921
});
2022
}
21-
this.children = map_children(component, parent, this.scope, info.children);
23+
24+
([this.const_tags, this.children] = get_const_tags(info.children, component, this, parent));
2225

2326
if (!info.skip) {
2427
this.warn_if_empty_block();
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
import Node from './shared/Node';
2+
import Expression from './shared/Expression';
3+
import Component from '../Component';
4+
import TemplateScope from './shared/TemplateScope';
5+
import { Context, unpack_destructuring } from './shared/Context';
6+
import { ConstTag as ConstTagType } from '../../interfaces';
7+
import { INodeAllowConstTag } from './interfaces';
8+
import { walk } from 'estree-walker';
9+
import { extract_identifiers } from 'periscopic';
10+
import is_reference, { NodeWithPropertyDefinition } from 'is-reference';
11+
import get_object from '../utils/get_object';
12+
import compiler_errors from '../compiler_errors';
13+
14+
const allowed_parents = new Set(['EachBlock', 'CatchBlock', 'ThenBlock', 'InlineComponent', 'SlotTemplate']);
15+
16+
export default class ConstTag extends Node {
17+
type: 'ConstTag';
18+
expression: Expression;
19+
contexts: Context[] = [];
20+
node: ConstTagType;
21+
scope: TemplateScope;
22+
23+
assignees: Set<string> = new Set();
24+
dependencies: Set<string> = new Set();
25+
26+
constructor(component: Component, parent: INodeAllowConstTag, scope: TemplateScope, info: ConstTagType) {
27+
super(component, parent, scope, info);
28+
29+
if (!allowed_parents.has(parent.type)) {
30+
component.error(info, compiler_errors.invalid_const_placement);
31+
}
32+
this.node = info;
33+
this.scope = scope;
34+
35+
const { assignees, dependencies } = this;
36+
37+
extract_identifiers(info.expression.left).forEach(({ name }) => {
38+
assignees.add(name);
39+
const owner = this.scope.get_owner(name);
40+
if (owner === parent) {
41+
component.error(info, compiler_errors.invalid_const_declaration(name));
42+
}
43+
});
44+
45+
walk(info.expression.right, {
46+
enter(node, parent) {
47+
if (is_reference(node as NodeWithPropertyDefinition, parent as NodeWithPropertyDefinition)) {
48+
const identifier = get_object(node as any);
49+
const { name } = identifier;
50+
dependencies.add(name);
51+
}
52+
}
53+
});
54+
}
55+
56+
parse_expression() {
57+
unpack_destructuring({
58+
contexts: this.contexts,
59+
node: this.node.expression.left,
60+
scope: this.scope,
61+
component: this.component
62+
});
63+
this.expression = new Expression(this.component, this, this.scope, this.node.expression.right);
64+
this.contexts.forEach(context => {
65+
const owner = this.scope.get_owner(context.key.name);
66+
if (owner && owner.type === 'ConstTag' && owner.parent === this.parent) {
67+
this.component.error(this.node, compiler_errors.invalid_const_declaration(context.key.name));
68+
}
69+
this.scope.add(context.key.name, this.expression.dependencies, this);
70+
});
71+
}
72+
}

src/compiler/compile/nodes/DefaultSlotTemplate.ts

Lines changed: 0 additions & 28 deletions
This file was deleted.

src/compiler/compile/nodes/EachBlock.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,15 @@
11
import ElseBlock from './ElseBlock';
22
import Expression from './shared/Expression';
3-
import map_children from './shared/map_children';
43
import TemplateScope from './shared/TemplateScope';
54
import AbstractBlock from './shared/AbstractBlock';
65
import Element from './Element';
6+
import ConstTag from './ConstTag';
77
import { Context, unpack_destructuring } from './shared/Context';
88
import { Node } from 'estree';
99
import Component from '../Component';
1010
import { TemplateNode } from '../../interfaces';
1111
import compiler_errors from '../compiler_errors';
12+
import get_const_tags from './shared/get_const_tags';
1213

1314
export default class EachBlock extends AbstractBlock {
1415
type: 'EachBlock';
@@ -22,6 +23,7 @@ export default class EachBlock extends AbstractBlock {
2223
key: Expression;
2324
scope: TemplateScope;
2425
contexts: Context[];
26+
const_tags: ConstTag[];
2527
has_animation: boolean;
2628
has_binding = false;
2729
has_index_binding = false;
@@ -57,7 +59,7 @@ export default class EachBlock extends AbstractBlock {
5759

5860
this.has_animation = false;
5961

60-
this.children = map_children(component, this, this.scope, info.children);
62+
([this.const_tags, this.children] = get_const_tags(info.children, component, this, this));
6163

6264
if (this.has_animation) {
6365
if (this.children.length !== 1) {

src/compiler/compile/nodes/InlineComponent.ts

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -126,7 +126,15 @@ export default class InlineComponent extends Node {
126126
slot_template.attributes.push(attribute);
127127
}
128128
}
129-
129+
// transfer const
130+
for (let i = child.children.length - 1; i >= 0; i--) {
131+
const child_child = child.children[i];
132+
if (child_child.type === 'ConstTag') {
133+
slot_template.children.push(child_child);
134+
child.children.splice(i, 1);
135+
}
136+
}
137+
130138
children.push(slot_template);
131139
info.children.splice(i, 1);
132140
}

src/compiler/compile/nodes/SlotTemplate.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,19 @@
1-
import map_children from './shared/map_children';
21
import Component from '../Component';
32
import TemplateScope from './shared/TemplateScope';
43
import Node from './shared/Node';
54
import Let from './Let';
65
import Attribute from './Attribute';
76
import { INode } from './interfaces';
87
import compiler_errors from '../compiler_errors';
8+
import get_const_tags from './shared/get_const_tags';
9+
import ConstTag from './ConstTag';
910

1011
export default class SlotTemplate extends Node {
1112
type: 'SlotTemplate';
1213
scope: TemplateScope;
1314
children: INode[];
1415
lets: Let[] = [];
16+
const_tags: ConstTag[];
1517
slot_attribute: Attribute;
1618
slot_template_name: string = 'default';
1719

@@ -63,7 +65,7 @@ export default class SlotTemplate extends Node {
6365
});
6466

6567
this.scope = scope;
66-
this.children = map_children(component, this, this.scope, info.children);
68+
([this.const_tags, this.children] = get_const_tags(info.children, component, this, this));
6769
}
6870

6971
validate_slot_template_placement() {

src/compiler/compile/nodes/ThenBlock.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,15 @@
1-
import map_children from './shared/map_children';
21
import TemplateScope from './shared/TemplateScope';
32
import AbstractBlock from './shared/AbstractBlock';
43
import AwaitBlock from './AwaitBlock';
54
import Component from '../Component';
65
import { TemplateNode } from '../../interfaces';
6+
import get_const_tags from './shared/get_const_tags';
7+
import ConstTag from './ConstTag';
78

89
export default class ThenBlock extends AbstractBlock {
910
type: 'ThenBlock';
1011
scope: TemplateScope;
12+
const_tags: ConstTag[];
1113

1214
constructor(component: Component, parent: AwaitBlock, scope: TemplateScope, info: TemplateNode) {
1315
super(component, parent, scope, info);
@@ -18,7 +20,8 @@ export default class ThenBlock extends AbstractBlock {
1820
this.scope.add(context.key.name, parent.expression.dependencies, this);
1921
});
2022
}
21-
this.children = map_children(component, parent, this.scope, info.children);
23+
24+
([this.const_tags, this.children] = get_const_tags(info.children, component, this, parent));
2225

2326
if (!info.skip) {
2427
this.warn_if_empty_block();

src/compiler/compile/nodes/interfaces.ts

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import CatchBlock from './CatchBlock';
1010
import Class from './Class';
1111
import Style from './Style';
1212
import Comment from './Comment';
13+
import ConstTag from './ConstTag';
1314
import DebugTag from './DebugTag';
1415
import EachBlock from './EachBlock';
1516
import Element from './Element';
@@ -27,7 +28,6 @@ import PendingBlock from './PendingBlock';
2728
import RawMustacheTag from './RawMustacheTag';
2829
import Slot from './Slot';
2930
import SlotTemplate from './SlotTemplate';
30-
import DefaultSlotTemplate from './DefaultSlotTemplate';
3131
import Text from './Text';
3232
import ThenBlock from './ThenBlock';
3333
import Title from './Title';
@@ -45,6 +45,7 @@ export type INode = Action
4545
| CatchBlock
4646
| Class
4747
| Comment
48+
| ConstTag
4849
| DebugTag
4950
| EachBlock
5051
| Element
@@ -62,11 +63,17 @@ export type INode = Action
6263
| RawMustacheTag
6364
| Slot
6465
| SlotTemplate
65-
| DefaultSlotTemplate
6666
| Style
6767
| Tag
6868
| Text
6969
| ThenBlock
7070
| Title
7171
| Transition
7272
| Window;
73+
74+
export type INodeAllowConstTag =
75+
| EachBlock
76+
| CatchBlock
77+
| ThenBlock
78+
| InlineComponent
79+
| SlotTemplate;

src/compiler/compile/nodes/shared/Expression.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -133,6 +133,10 @@ export default class Expression {
133133
if (names) {
134134
names.forEach(name => {
135135
if (template_scope.names.has(name)) {
136+
if (template_scope.is_const(name)) {
137+
component.error(node, compiler_errors.invalid_const_update(name));
138+
}
139+
136140
template_scope.dependencies_for_name.get(name).forEach(name => {
137141
const variable = component.var_lookup.get(name);
138142
if (variable) variable[deep ? 'mutated' : 'reassigned'] = true;
@@ -172,7 +176,7 @@ export default class Expression {
172176
}
173177

174178
// TODO move this into a render-dom wrapper?
175-
manipulate(block?: Block) {
179+
manipulate(block?: Block, ctx?: string | void) {
176180
// TODO ideally we wouldn't end up calling this method
177181
// multiple times
178182
if (this.manipulated) return this.manipulated;
@@ -219,7 +223,7 @@ export default class Expression {
219223
component.add_reference(name); // TODO is this redundant/misplaced?
220224
}
221225
} else if (is_contextual(component, template_scope, name)) {
222-
const reference = block.renderer.reference(node);
226+
const reference = block.renderer.reference(node, ctx);
223227
this.replace(reference);
224228
}
225229

0 commit comments

Comments
 (0)