Skip to content

Commit

Permalink
[feat] implement constants in markup (#6413)
Browse files Browse the repository at this point in the history
  • Loading branch information
tanhauhau authored Jan 11, 2022
1 parent d5fde79 commit b5aaa66
Show file tree
Hide file tree
Showing 66 changed files with 998 additions and 74 deletions.
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

0 comments on commit b5aaa66

Please sign in to comment.