Skip to content

[feat] Implement RFC 33 - Constants in markup #6413

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 23 additions & 0 deletions site/content/docs/02-template-syntax.md
Original file line number Diff line number Diff line change
Expand Up @@ -453,6 +453,29 @@ The `{@debug ...}` tag offers an alternative to `console.log(...)`. It logs the
The `{@debug}` tag without any arguments will insert a `debugger` statement that gets triggered when *any* state changes, as opposed to the specified variables.


### {@const ...}

```sv
{@const assignment}
```

---

The `{@const ...}` tag defines a local constant.

```sv
<script>
export let boxes;
</script>

{#each boxes as box}
{@const area = box.width * box.height}
{box.width} * {box.height} = {area}
{/each}
```

`{@const}` is only allowed as direct child of `{#each}`, `{:then}`, `{:catch}`, `<Component />` or `<svelte:fragment />`.


### Element directives

Expand Down
24 changes: 22 additions & 2 deletions src/compiler/compile/compiler_errors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,10 @@ export default {
code: 'invalid-binding',
message: 'Cannot bind to a variable declared with {#await ... then} or {:catch} blocks'
},
invalid_binding_const: {
code: 'invalid-binding',
message: 'Cannot bind to a variable declared with {@const ...}'
},
invalid_binding_writibale: {
code: 'invalid-binding',
message: 'Cannot bind to a variable which is not writable'
Expand Down Expand Up @@ -208,7 +212,7 @@ export default {
},
invalid_attribute_value: (name: string) => ({
code: `invalid-${name}-value`,
message: `${name} attribute must be true or false`
message: `${name} attribute must be true or false`
}),
invalid_options_attribute_unknown: {
code: 'invalid-options-attribute',
Expand Down Expand Up @@ -241,5 +245,21 @@ export default {
invalid_directive_value: {
code: 'invalid-directive-value',
message: 'Can only bind to an identifier (e.g. `foo`) or a member expression (e.g. `foo.bar` or `foo[baz]`)'
}
},
invalid_const_placement: {
code: 'invalid-const-placement',
message: '{@const} must be the immediate child of {#each}, {:then}, {:catch}, <svelte:fragment> or <Component>'
},
invalid_const_declaration: (name: string) => ({
code: 'invalid-const-declaration',
message: `'${name}' has already been declared`
}),
invalid_const_update: (name: string) => ({
code: 'invalid-const-update',
message: `'${name}' is declared using {@const ...} and is read-only`
}),
cyclical_const_tags: (cycle: string[]) => ({
code: 'cyclical-const-tags',
message: `Cyclical dependency detected: ${cycle.join(' → ')}`
})
};
3 changes: 3 additions & 0 deletions src/compiler/compile/nodes/Binding.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,9 @@ export default class Binding extends Node {
component.error(this, compiler_errors.invalid_binding_await);
return;
}
if (scope.is_const(name)) {
component.error(this, compiler_errors.invalid_binding_const);
}

scope.dependencies_for_name.get(name).forEach(name => {
const variable = component.var_lookup.get(name);
Expand Down
7 changes: 5 additions & 2 deletions src/compiler/compile/nodes/CatchBlock.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,15 @@
import map_children from './shared/map_children';
import TemplateScope from './shared/TemplateScope';
import AbstractBlock from './shared/AbstractBlock';
import AwaitBlock from './AwaitBlock';
import Component from '../Component';
import { TemplateNode } from '../../interfaces';
import get_const_tags from './shared/get_const_tags';
import ConstTag from './ConstTag';

export default class CatchBlock extends AbstractBlock {
type: 'CatchBlock';
scope: TemplateScope;
const_tags: ConstTag[];

constructor(component: Component, parent: AwaitBlock, scope: TemplateScope, info: TemplateNode) {
super(component, parent, scope, info);
Expand All @@ -18,7 +20,8 @@ export default class CatchBlock extends AbstractBlock {
this.scope.add(context.key.name, parent.expression.dependencies, this);
});
}
this.children = map_children(component, parent, this.scope, info.children);

([this.const_tags, this.children] = get_const_tags(info.children, component, this, parent));

if (!info.skip) {
this.warn_if_empty_block();
Expand Down
72 changes: 72 additions & 0 deletions src/compiler/compile/nodes/ConstTag.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
import Node from './shared/Node';
import Expression from './shared/Expression';
import Component from '../Component';
import TemplateScope from './shared/TemplateScope';
import { Context, unpack_destructuring } from './shared/Context';
import { ConstTag as ConstTagType } from '../../interfaces';
import { INodeAllowConstTag } from './interfaces';
import { walk } from 'estree-walker';
import { extract_identifiers } from 'periscopic';
import is_reference, { NodeWithPropertyDefinition } from 'is-reference';
import get_object from '../utils/get_object';
import compiler_errors from '../compiler_errors';

const allowed_parents = new Set(['EachBlock', 'CatchBlock', 'ThenBlock', 'InlineComponent', 'SlotTemplate']);

export default class ConstTag extends Node {
type: 'ConstTag';
expression: Expression;
contexts: Context[] = [];
node: ConstTagType;
scope: TemplateScope;

assignees: Set<string> = new Set();
dependencies: Set<string> = new Set();

constructor(component: Component, parent: INodeAllowConstTag, scope: TemplateScope, info: ConstTagType) {
super(component, parent, scope, info);

if (!allowed_parents.has(parent.type)) {
component.error(info, compiler_errors.invalid_const_placement);
}
this.node = info;
this.scope = scope;

const { assignees, dependencies } = this;

extract_identifiers(info.expression.left).forEach(({ name }) => {
assignees.add(name);
const owner = this.scope.get_owner(name);
if (owner === parent) {
component.error(info, compiler_errors.invalid_const_declaration(name));
}
});

walk(info.expression.right, {
enter(node, parent) {
if (is_reference(node as NodeWithPropertyDefinition, parent as NodeWithPropertyDefinition)) {
const identifier = get_object(node as any);
const { name } = identifier;
dependencies.add(name);
}
}
});
}

parse_expression() {
unpack_destructuring({
contexts: this.contexts,
node: this.node.expression.left,
scope: this.scope,
component: this.component
});
this.expression = new Expression(this.component, this, this.scope, this.node.expression.right);
this.contexts.forEach(context => {
const owner = this.scope.get_owner(context.key.name);
if (owner && owner.type === 'ConstTag' && owner.parent === this.parent) {
this.component.error(this.node, compiler_errors.invalid_const_declaration(context.key.name));
}
this.scope.add(context.key.name, this.expression.dependencies, this);
});
}
}
28 changes: 0 additions & 28 deletions src/compiler/compile/nodes/DefaultSlotTemplate.ts

This file was deleted.

6 changes: 4 additions & 2 deletions src/compiler/compile/nodes/EachBlock.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,15 @@
import ElseBlock from './ElseBlock';
import Expression from './shared/Expression';
import map_children from './shared/map_children';
import TemplateScope from './shared/TemplateScope';
import AbstractBlock from './shared/AbstractBlock';
import Element from './Element';
import ConstTag from './ConstTag';
import { Context, unpack_destructuring } from './shared/Context';
import { Node } from 'estree';
import Component from '../Component';
import { TemplateNode } from '../../interfaces';
import compiler_errors from '../compiler_errors';
import get_const_tags from './shared/get_const_tags';

export default class EachBlock extends AbstractBlock {
type: 'EachBlock';
Expand All @@ -22,6 +23,7 @@ export default class EachBlock extends AbstractBlock {
key: Expression;
scope: TemplateScope;
contexts: Context[];
const_tags: ConstTag[];
has_animation: boolean;
has_binding = false;
has_index_binding = false;
Expand Down Expand Up @@ -57,7 +59,7 @@ export default class EachBlock extends AbstractBlock {

this.has_animation = false;

this.children = map_children(component, this, this.scope, info.children);
([this.const_tags, this.children] = get_const_tags(info.children, component, this, this));

if (this.has_animation) {
if (this.children.length !== 1) {
Expand Down
10 changes: 9 additions & 1 deletion src/compiler/compile/nodes/InlineComponent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -126,7 +126,15 @@ export default class InlineComponent extends Node {
slot_template.attributes.push(attribute);
}
}

// transfer const
for (let i = child.children.length - 1; i >= 0; i--) {
const child_child = child.children[i];
if (child_child.type === 'ConstTag') {
slot_template.children.push(child_child);
child.children.splice(i, 1);
}
}

children.push(slot_template);
info.children.splice(i, 1);
}
Expand Down
6 changes: 4 additions & 2 deletions src/compiler/compile/nodes/SlotTemplate.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,19 @@
import map_children from './shared/map_children';
import Component from '../Component';
import TemplateScope from './shared/TemplateScope';
import Node from './shared/Node';
import Let from './Let';
import Attribute from './Attribute';
import { INode } from './interfaces';
import compiler_errors from '../compiler_errors';
import get_const_tags from './shared/get_const_tags';
import ConstTag from './ConstTag';

export default class SlotTemplate extends Node {
type: 'SlotTemplate';
scope: TemplateScope;
children: INode[];
lets: Let[] = [];
const_tags: ConstTag[];
slot_attribute: Attribute;
slot_template_name: string = 'default';

Expand Down Expand Up @@ -63,7 +65,7 @@ export default class SlotTemplate extends Node {
});

this.scope = scope;
this.children = map_children(component, this, this.scope, info.children);
([this.const_tags, this.children] = get_const_tags(info.children, component, this, this));
}

validate_slot_template_placement() {
Expand Down
7 changes: 5 additions & 2 deletions src/compiler/compile/nodes/ThenBlock.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,15 @@
import map_children from './shared/map_children';
import TemplateScope from './shared/TemplateScope';
import AbstractBlock from './shared/AbstractBlock';
import AwaitBlock from './AwaitBlock';
import Component from '../Component';
import { TemplateNode } from '../../interfaces';
import get_const_tags from './shared/get_const_tags';
import ConstTag from './ConstTag';

export default class ThenBlock extends AbstractBlock {
type: 'ThenBlock';
scope: TemplateScope;
const_tags: ConstTag[];

constructor(component: Component, parent: AwaitBlock, scope: TemplateScope, info: TemplateNode) {
super(component, parent, scope, info);
Expand All @@ -18,7 +20,8 @@ export default class ThenBlock extends AbstractBlock {
this.scope.add(context.key.name, parent.expression.dependencies, this);
});
}
this.children = map_children(component, parent, this.scope, info.children);

([this.const_tags, this.children] = get_const_tags(info.children, component, this, parent));

if (!info.skip) {
this.warn_if_empty_block();
Expand Down
11 changes: 9 additions & 2 deletions src/compiler/compile/nodes/interfaces.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import CatchBlock from './CatchBlock';
import Class from './Class';
import Style from './Style';
import Comment from './Comment';
import ConstTag from './ConstTag';
import DebugTag from './DebugTag';
import EachBlock from './EachBlock';
import Element from './Element';
Expand All @@ -27,7 +28,6 @@ import PendingBlock from './PendingBlock';
import RawMustacheTag from './RawMustacheTag';
import Slot from './Slot';
import SlotTemplate from './SlotTemplate';
import DefaultSlotTemplate from './DefaultSlotTemplate';
import Text from './Text';
import ThenBlock from './ThenBlock';
import Title from './Title';
Expand All @@ -45,6 +45,7 @@ export type INode = Action
| CatchBlock
| Class
| Comment
| ConstTag
| DebugTag
| EachBlock
| Element
Expand All @@ -62,11 +63,17 @@ export type INode = Action
| RawMustacheTag
| Slot
| SlotTemplate
| DefaultSlotTemplate
| Style
| Tag
| Text
| ThenBlock
| Title
| Transition
| Window;

export type INodeAllowConstTag =
| EachBlock
| CatchBlock
| ThenBlock
| InlineComponent
| SlotTemplate;
8 changes: 6 additions & 2 deletions src/compiler/compile/nodes/shared/Expression.ts
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,10 @@ export default class Expression {
if (names) {
names.forEach(name => {
if (template_scope.names.has(name)) {
if (template_scope.is_const(name)) {
component.error(node, compiler_errors.invalid_const_update(name));
}

template_scope.dependencies_for_name.get(name).forEach(name => {
const variable = component.var_lookup.get(name);
if (variable) variable[deep ? 'mutated' : 'reassigned'] = true;
Expand Down Expand Up @@ -172,7 +176,7 @@ export default class Expression {
}

// TODO move this into a render-dom wrapper?
manipulate(block?: Block) {
manipulate(block?: Block, ctx?: string | void) {
// TODO ideally we wouldn't end up calling this method
// multiple times
if (this.manipulated) return this.manipulated;
Expand Down Expand Up @@ -219,7 +223,7 @@ export default class Expression {
component.add_reference(name); // TODO is this redundant/misplaced?
}
} else if (is_contextual(component, template_scope, name)) {
const reference = block.renderer.reference(node);
const reference = block.renderer.reference(node, ctx);
this.replace(reference);
}

Expand Down
Loading