[feat] implement constants in markup (#6413)

pull/7127/head
Tan Li Hau 3 years ago committed by GitHub
parent d5fde793c1
commit b5aaa6641b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -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

@ -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'
@ -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',
@ -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(' → ')}`
})
};

@ -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);

@ -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);
@ -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();

@ -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);
});
}
}

@ -1,28 +0,0 @@
import Component from '../Component';
import TemplateScope from './shared/TemplateScope';
import Node from './shared/Node';
import Let from './Let';
import { INode } from './interfaces';
export default class DefaultSlotTemplate extends Node {
type: 'SlotTemplate';
scope: TemplateScope;
children: INode[];
lets: Let[] = [];
slot_template_name = 'default';
constructor(
component: Component,
parent: INode,
scope: TemplateScope,
info: any,
lets: Let[],
children: INode[]
) {
super(component, parent, scope, info);
this.type = 'SlotTemplate';
this.children = children;
this.scope = scope;
this.lets = lets;
}
}

@ -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';
@ -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;
@ -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) {

@ -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);
}

@ -1,4 +1,3 @@
import map_children from './shared/map_children';
import Component from '../Component';
import TemplateScope from './shared/TemplateScope';
import Node from './shared/Node';
@ -6,12 +5,15 @@ 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';
@ -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() {

@ -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);
@ -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();

@ -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';
@ -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';
@ -45,6 +45,7 @@ export type INode = Action
| CatchBlock
| Class
| Comment
| ConstTag
| DebugTag
| EachBlock
| Element
@ -62,7 +63,6 @@ export type INode = Action
| RawMustacheTag
| Slot
| SlotTemplate
| DefaultSlotTemplate
| Style
| Tag
| Text
@ -70,3 +70,10 @@ export type INode = Action
| Title
| Transition
| Window;
export type INodeAllowConstTag =
| EachBlock
| CatchBlock
| ThenBlock
| InlineComponent
| SlotTemplate;

@ -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;
@ -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;
@ -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);
}

@ -4,8 +4,9 @@ import CatchBlock from '../CatchBlock';
import InlineComponent from '../InlineComponent';
import Element from '../Element';
import SlotTemplate from '../SlotTemplate';
import ConstTag from '../ConstTag';
type NodeWithScope = EachBlock | ThenBlock | CatchBlock | InlineComponent | Element | SlotTemplate;
type NodeWithScope = EachBlock | ThenBlock | CatchBlock | InlineComponent | Element | SlotTemplate | ConstTag;
export default class TemplateScope {
names: Set<string>;
@ -48,4 +49,9 @@ export default class TemplateScope {
const owner = this.get_owner(name);
return owner && (owner.type === 'ThenBlock' || owner.type === 'CatchBlock');
}
is_const(name: string) {
const owner = this.get_owner(name);
return owner && owner.type === 'ConstTag';
}
}

@ -0,0 +1,91 @@
import { TemplateNode, ConstTag as ConstTagType } from '../../../interfaces';
import Component from '../../Component';
import ConstTag from '../ConstTag';
import map_children from './map_children';
import { INodeAllowConstTag, INode } from '../interfaces';
import check_graph_for_cycles from '../../utils/check_graph_for_cycles';
import compiler_errors from '../../compiler_errors';
export default function get_const_tags(children: TemplateNode[], component: Component, node: INodeAllowConstTag, parent: INode): [ConstTag[], Array<Exclude<INode, ConstTag>>] {
const const_tags: ConstTagType[] = [];
const others: Array<Exclude<TemplateNode, ConstTagType>> = [];
for (const child of children) {
if (child.type === 'ConstTag') {
const_tags.push(child as ConstTagType);
} else {
others.push(child);
}
}
const consts_nodes = const_tags.map(tag => new ConstTag(component, node, node.scope, tag));
const sorted_consts_nodes = sort_consts_nodes(consts_nodes, component);
sorted_consts_nodes.forEach(node => node.parse_expression());
const children_nodes = map_children(component, parent, node.scope, others);
return [sorted_consts_nodes, children_nodes as Array<Exclude<INode, ConstTag>>];
}
function sort_consts_nodes(consts_nodes: ConstTag[], component: Component) {
type ConstNode = {
assignees: Set<string>;
dependencies: Set<string>;
node: ConstTag;
};
const sorted_consts_nodes: ConstNode[] = [];
const unsorted_consts_nodes: ConstNode[] = consts_nodes.map(node => {
return {
assignees: node.assignees,
dependencies: node.dependencies,
node
};
});
const lookup = new Map();
unsorted_consts_nodes.forEach(node => {
node.assignees.forEach(name => {
if (!lookup.has(name)) {
lookup.set(name, []);
}
lookup.get(name).push(node);
});
});
const cycle = check_graph_for_cycles(unsorted_consts_nodes.reduce((acc, node) => {
node.assignees.forEach(v => {
node.dependencies.forEach(w => {
if (!node.assignees.has(w)) {
acc.push([v, w]);
}
});
});
return acc;
}, []));
if (cycle && cycle.length) {
const nodeList = lookup.get(cycle[0]);
const node = nodeList[0];
component.error(node.node, compiler_errors.cyclical_const_tags(cycle));
}
const add_node = (node: ConstNode) => {
if (sorted_consts_nodes.includes(node)) return;
node.dependencies.forEach(name => {
if (node.assignees.has(name)) return;
const earlier_nodes = lookup.get(name);
if (earlier_nodes) {
earlier_nodes.forEach(add_node);
}
});
sorted_consts_nodes.push(node);
};
unsorted_consts_nodes.forEach(add_node);
return sorted_consts_nodes.map(node => node.node);
}

@ -1,5 +1,6 @@
import AwaitBlock from '../AwaitBlock';
import Body from '../Body';
import ConstTag from '../ConstTag';
import Comment from '../Comment';
import EachBlock from '../EachBlock';
import Element from '../Element';
@ -25,6 +26,7 @@ function get_constructor(type) {
case 'AwaitBlock': return AwaitBlock;
case 'Body': return Body;
case 'Comment': return Comment;
case 'ConstTag': return ConstTag;
case 'EachBlock': return EachBlock;
case 'Element': return Element;
case 'Head': return Head;

@ -254,7 +254,7 @@ export default class Renderer {
};
}
reference(node: string | Identifier | MemberExpression) {
reference(node: string | Identifier | MemberExpression, ctx: string | void = '#ctx') {
if (typeof node === 'string') {
node = { type: 'Identifier', name: node };
}
@ -268,7 +268,7 @@ export default class Renderer {
}
if (member !== undefined) {
const replacement = x`/*${member.name}*/ #ctx[${member.index}]` as MemberExpression;
const replacement = x`/*${member.name}*/ ${ctx}[${member.index}]` as MemberExpression;
if (nodes[0].loc) replacement.object.loc = nodes[0].loc;
nodes[0] = replacement;

@ -10,6 +10,7 @@ import ThenBlock from '../../nodes/ThenBlock';
import CatchBlock from '../../nodes/CatchBlock';
import { Context } from '../../nodes/shared/Context';
import { Identifier, Literal, Node } from 'estree';
import { add_const_tags, add_const_tags_context } from './shared/add_const_tags';
type Status = 'pending' | 'then' | 'catch';
@ -76,22 +77,34 @@ class AwaitBlockBranch extends Wrapper {
this.is_destructured = true;
}
this.value_index = this.renderer.context_lookup.get(this.value).index;
if (this.has_consts(this.node)) {
add_const_tags_context(this.renderer, this.node.const_tags);
}
}
has_consts(node: PendingBlock | ThenBlock | CatchBlock): node is ThenBlock | CatchBlock {
return node instanceof ThenBlock || node instanceof CatchBlock;
}
render(block: Block, parent_node: Identifier, parent_nodes: Identifier) {
this.fragment.render(block, parent_node, parent_nodes);
if (this.is_destructured) {
this.render_destructure();
if (this.is_destructured || (this.has_consts(this.node) && this.node.const_tags.length > 0)) {
this.render_get_context();
}
}
render_destructure() {
const props = this.value_contexts.map(prop => b`#ctx[${this.block.renderer.context_lookup.get(prop.key.name).index}] = ${prop.default_modifier(prop.modifier(x`#ctx[${this.value_index}]`), name => this.renderer.reference(name))};`);
render_get_context() {
const props = this.is_destructured ? this.value_contexts.map(prop => b`#ctx[${this.block.renderer.context_lookup.get(prop.key.name).index}] = ${prop.default_modifier(prop.modifier(x`#ctx[${this.value_index}]`), name => this.renderer.reference(name))};`) : null;
const const_tags_props = this.has_consts(this.node) ? add_const_tags(this.block, this.node.const_tags, '#ctx') : null;
const get_context = this.block.renderer.component.get_unique_name(`get_${this.status}_context`);
this.block.renderer.blocks.push(b`
function ${get_context}(#ctx) {
${props}
${const_tags_props}
}
`);
this.block.chunks.declarations.push(b`${get_context}(#ctx)`);

@ -8,6 +8,7 @@ import { b, x } from 'code-red';
import ElseBlock from '../../nodes/ElseBlock';
import { Identifier, Node } from 'estree';
import get_object from '../../utils/get_object';
import { add_const_tags, add_const_tags_context } from './shared/add_const_tags';
export class ElseBlockWrapper extends Wrapper {
node: ElseBlock;
@ -86,6 +87,7 @@ export default class EachBlockWrapper extends Wrapper {
this.node.contexts.forEach(context => {
renderer.add_to_context(context.key.name, true);
});
add_const_tags_context(renderer, this.node.const_tags);
this.block = block.child({
comment: create_debugging_comment(this.node, this.renderer.component),
@ -350,11 +352,13 @@ export default class EachBlockWrapper extends Wrapper {
if (this.node.has_binding) this.context_props.push(b`child_ctx[${renderer.context_lookup.get(this.vars.each_block_value.name).index}] = list;`);
if (this.node.has_binding || this.node.has_index_binding || this.node.index) this.context_props.push(b`child_ctx[${renderer.context_lookup.get(this.index_name.name).index}] = i;`);
// TODO which is better — Object.create(array) or array.slice()?
renderer.blocks.push(b`
function ${this.vars.get_each_context}(#ctx, list, i) {
const child_ctx = #ctx.slice();
${this.context_props}
${add_const_tags(this.block, this.node.const_tags, 'child_ctx')}
return child_ctx;
}
`);

@ -4,23 +4,16 @@ import Block from '../Block';
import FragmentWrapper from './Fragment';
import create_debugging_comment from './shared/create_debugging_comment';
import { get_slot_definition } from './shared/get_slot_definition';
import { x } from 'code-red';
import { b, x } from 'code-red';
import { sanitize } from '../../../utils/names';
import { Identifier } from 'estree';
import InlineComponentWrapper from './InlineComponent';
import { extract_names } from 'periscopic';
import { INode } from '../../nodes/interfaces';
import Let from '../../nodes/Let';
import TemplateScope from '../../nodes/shared/TemplateScope';
type NodeWithLets = INode & {
scope: TemplateScope;
lets: Let[];
slot_template_name: string;
};
import SlotTemplate from '../../nodes/SlotTemplate';
import { add_const_tags, add_const_tags_context } from './shared/add_const_tags';
export default class SlotTemplateWrapper extends Wrapper {
node: NodeWithLets;
node: SlotTemplate;
fragment: FragmentWrapper;
block: Block;
parent: InlineComponentWrapper;
@ -29,13 +22,13 @@ export default class SlotTemplateWrapper extends Wrapper {
renderer: Renderer,
block: Block,
parent: Wrapper,
node: NodeWithLets,
node: SlotTemplate,
strip_whitespace: boolean,
next_sibling: Wrapper
) {
super(renderer, block, parent, node);
const { scope, lets, slot_template_name } = this.node;
const { scope, lets, const_tags, slot_template_name } = this.node;
lets.forEach(l => {
extract_names(l.value || l.name).forEach(name => {
@ -43,6 +36,8 @@ export default class SlotTemplateWrapper extends Wrapper {
});
});
add_const_tags_context(renderer, const_tags);
this.block = block.child({
comment: create_debugging_comment(this.node, this.renderer.component),
name: this.renderer.component.get_unique_name(
@ -76,5 +71,21 @@ export default class SlotTemplateWrapper extends Wrapper {
render() {
this.fragment.render(this.block, null, x`#nodes` as Identifier);
if (this.node.const_tags.length > 0) {
this.render_get_context();
}
}
render_get_context() {
const get_context = this.block.renderer.component.get_unique_name('get_context');
this.block.renderer.blocks.push(b`
function ${get_context}(#ctx) {
${add_const_tags(this.block, this.node.const_tags, '#ctx')}
}
`);
this.block.chunks.declarations.push(b`${get_context}(#ctx)`);
if (this.block.has_update_method) {
this.block.chunks.update.unshift(b`${get_context}(#ctx)`);
}
}
}

@ -0,0 +1,24 @@
import ConstTag from '../../../nodes/ConstTag';
import Block from '../../Block';
import { b, x } from 'code-red';
import Renderer from '../../Renderer';
export function add_const_tags(block: Block, const_tags: ConstTag[], ctx: string) {
const const_tags_props = [];
const_tags.forEach((const_tag, i) => {
const name = `#constants_${i}`;
const_tags_props.push(b`const ${name} = ${const_tag.expression.manipulate(block, ctx)}`);
const_tag.contexts.forEach(context => {
const_tags_props.push(b`${ctx}[${block.renderer.context_lookup.get(context.key.name).index}] = ${context.default_modifier(context.modifier({ type: 'Identifier', name }), name => block.renderer.context_lookup.has(name) ? x`${ctx}[${block.renderer.context_lookup.get(name).index}]` : { type: 'Identifier', name })};`);
});
});
return const_tags_props;
}
export function add_const_tags_context(renderer: Renderer, const_tags: ConstTag[]) {
const_tags.forEach(const_tag => {
const_tag.contexts.forEach(context => {
renderer.add_to_context(context.key.name, true);
});
});
}

@ -1,6 +1,7 @@
import Renderer, { RenderOptions } from '../Renderer';
import AwaitBlock from '../../nodes/AwaitBlock';
import { x } from 'code-red';
import { get_const_tags } from './shared/get_const_tags';
export default function(node: AwaitBlock, renderer: Renderer, options: RenderOptions) {
renderer.push();
@ -17,7 +18,7 @@ export default function(node: AwaitBlock, renderer: Renderer, options: RenderOpt
__value.then(null, @noop);
return ${pending};
}
return (function(${node.then_node ? node.then_node : ''}) { return ${then}; }(__value));
return (function(${node.then_node ? node.then_node : ''}) { ${get_const_tags(node.then.const_tags)}; return ${then}; }(__value));
}(${node.expression.node})
`);
}

@ -1,6 +1,7 @@
import Renderer, { RenderOptions } from '../Renderer';
import EachBlock from '../../nodes/EachBlock';
import { x } from 'code-red';
import { get_const_tags } from './shared/get_const_tags';
export default function(node: EachBlock, renderer: Renderer, options: RenderOptions) {
const args = [node.context_node];
@ -10,7 +11,7 @@ export default function(node: EachBlock, renderer: Renderer, options: RenderOpti
renderer.render(node.children, options);
const result = renderer.pop();
const consequent = x`@each(${node.expression.node}, (${args}) => ${result})`;
const consequent = x`@each(${node.expression.node}, (${args}) => { ${get_const_tags(node.const_tags)}; return ${result} })`;
if (node.else) {
renderer.push();

@ -76,9 +76,9 @@ export default function(node: InlineComponent, renderer: Renderer, options: Rend
slot_scopes
}));
slot_scopes.forEach(({ input, output }, name) => {
slot_scopes.forEach(({ input, output, statements }, name) => {
slot_fns.push(
p`${name}: (${input}) => ${output}`
p`${name}: (${input}) => { ${statements}; return ${output}; }`
);
});
}

@ -3,9 +3,9 @@ import SlotTemplate from '../../nodes/SlotTemplate';
import remove_whitespace_children from './utils/remove_whitespace_children';
import { get_slot_scope } from './shared/get_slot_scope';
import InlineComponent from '../../nodes/InlineComponent';
import Element from '../../nodes/Element';
import { get_const_tags } from './shared/get_const_tags';
export default function(node: SlotTemplate | Element | InlineComponent, renderer: Renderer, options: RenderOptions & {
export default function(node: SlotTemplate, renderer: Renderer, options: RenderOptions & {
slot_scopes: Map<any, any>;
}) {
const parent_inline_component = node.parent as InlineComponent;
@ -31,7 +31,8 @@ export default function(node: SlotTemplate | Element | InlineComponent, renderer
options.slot_scopes.set(node.slot_template_name, {
input: get_slot_scope(node.lets),
output: slot_fragment_content
output: slot_fragment_content,
statements: get_const_tags(node.const_tags)
});
}
}

@ -0,0 +1,17 @@
import ConstTag from '../../../nodes/ConstTag';
export function get_const_tags(const_tags: ConstTag[]) {
if (const_tags.length === 0) return null;
return {
type: 'VariableDeclaration',
kind: 'let',
declarations: const_tags.map(const_tag => {
const assignment = const_tag.node.expression;
return {
type: 'VariableDeclarator',
id: assignment.left,
init: assignment.right
};
})
};
}

@ -1,4 +1,4 @@
import { Node, Program } from 'estree';
import { AssignmentExpression, Node, Program } from 'estree';
import { SourceMap } from 'magic-string';
interface BaseNode {
@ -20,7 +20,7 @@ export interface Text extends BaseNode {
}
export interface MustacheTag extends BaseNode {
type: 'MustacheTag';
type: 'MustacheTag' | 'RawMustacheTag';
expression: Node;
}
@ -30,6 +30,16 @@ export interface Comment extends BaseNode {
ignores: string[];
}
export interface ConstTag extends BaseNode {
type: 'ConstTag';
expression: AssignmentExpression;
}
interface DebugTag extends BaseNode {
type: 'DebugTag';
identifiers: Node[]
}
export type DirectiveType = 'Action'
| 'Animation'
| 'Binding'
@ -73,6 +83,8 @@ export interface Transition extends BaseDirective {
export type Directive = BaseDirective | Transition;
export type TemplateNode = Text
| ConstTag
| DebugTag
| MustacheTag
| BaseNode
| Element

@ -375,6 +375,28 @@ export default function mustache(parser: Parser) {
type: 'DebugTag',
identifiers
});
} else if (parser.eat('@const')) {
// {@const a = b}
parser.require_whitespace();
const expression = read_expression(parser);
if (!(expression.type === 'AssignmentExpression' && expression.operator === '=')) {
parser.error({
code: 'invalid-const-args',
message: '{@const ...} must be an assignment.'
}, start);
}
parser.allow_whitespace();
parser.eat('}', true);
parser.current().children.push({
start,
end: parser.index,
type: 'ConstTag',
expression
});
} else {
const expression = read_expression(parser);

@ -19,6 +19,8 @@ export function to_string(node: TemplateNode) {
return '{@html} block';
case 'DebugTag':
return '{@debug} block';
case 'ConstTag':
return '{@const} tag';
case 'Element':
case 'InlineComponent':
case 'Slot':

@ -7,8 +7,10 @@ const Component = create_ssr_component(($$result, $$props, $$bindings, slots) =>
if ($$props.things === void 0 && $$bindings.things && things !== void 0) $$bindings.things(things);
if ($$props.foo === void 0 && $$bindings.foo && foo !== void 0) $$bindings.foo(foo);
return `${each(things, thing => `<span>${escape(thing.name)}</span>
${debug(null, 7, 2, { foo })}`)}
return `${each(things, thing => {
return `<span>${escape(thing.name)}</span>
${debug(null, 7, 2, { foo })}`;
})}
<p>foo: ${escape(foo)}</p>`;
});

@ -0,0 +1,19 @@
export default {
html: '<div>12 120 70, 30+4=34</div>',
async test({ component, target, assert }) {
component.promise1 = Promise.resolve({width: 5, height: 6});
component.promise2 = Promise.reject({width: 6, height: 7});
await Promise.resolve();
assert.htmlEqual(target.innerHTML, `
<div>30 300 110, 50+6=56</div>
<div>42 420 130, 60+7=67</div>
`);
component.constant = 20;
assert.htmlEqual(target.innerHTML, `
<div>30 600 220, 100+6=106</div>
<div>42 840 260, 120+7=127</div>
`);
}
};

@ -0,0 +1,23 @@
<script>
export let promise1 = {width: 3, height: 4};
export let promise2 = {width: 5, height: 7};
export let constant = 10;
function calculate(width, height, constant) {
return { area: width * height, volume: width * height * constant };
}
</script>
{#await promise1 then { width, height }}
{@const {area, volume} = calculate(width, height, constant)}
{@const perimeter = (width + height) * constant}
{@const [_width, _height, sum] = [width * constant, height, width * constant + height]}
<div>{area} {volume} {perimeter}, {_width}+{_height}={sum}</div>
{/await}
{#await promise2 catch { width, height }}
{@const {area, volume} = calculate(width, height, constant)}
{@const perimeter = (width + height) * constant}
{@const [_width, _height, sum] = [width * constant, height, width * constant + height]}
<div>{area} {volume} {perimeter}, {_width}+{_height}={sum}</div>
{/await}

@ -0,0 +1,19 @@
export default {
html: '<div>12 120 70, 30+4=34</div>',
async test({ component, target, assert }) {
component.promise1 = Promise.resolve({width: 5, height: 6});
component.promise2 = Promise.reject({width: 6, height: 7});
await Promise.resolve();
assert.htmlEqual(target.innerHTML, `
<div>30 300 110, 50+6=56</div>
<div>42 420 130, 60+7=67</div>
`);
component.constant = 20;
assert.htmlEqual(target.innerHTML, `
<div>30 600 220, 100+6=106</div>
<div>42 840 260, 120+7=127</div>
`);
}
};

@ -0,0 +1,23 @@
<script>
export let promise1 = {width: 3, height: 4};
export let promise2 = {width: 5, height: 7};
export let constant = 10;
function calculate(width, height, constant) {
return { area: width * height, volume: width * height * constant };
}
</script>
{#await promise1 then box}
{@const {area, volume} = calculate(box.width, box.height, constant)}
{@const perimeter = (box.width + box.height) * constant}
{@const [width, height, sum] = [box.width * constant, box.height, box.width * constant + box.height]}
<div>{area} {volume} {perimeter}, {width}+{height}={sum}</div>
{/await}
{#await promise2 catch box}
{@const {area, volume} = calculate(box.width, box.height, constant)}
{@const perimeter = (box.width + box.height) * constant}
{@const [width, height, sum] = [box.width * constant, box.height, box.width * constant + box.height]}
<div>{area} {volume} {perimeter}, {width}+{height}={sum}</div>
{/await}

@ -0,0 +1,7 @@
<script>
export let box;
</script>
<slot name="box1" {box} />
<slot name="box2" width={box.width} height={box.height} />
<slot {box} />

@ -0,0 +1,46 @@
export default {
html: `
<div>12 120 70, 30+4=34</div>
<div>12 120 70, 30+4=34</div>
<div>12 120 70, 30+4=34</div>
<div slot="box1">
<div>12 120 70, 30+4=34</div>
</div>
<div slot="box2">
<div>12 120 70, 30+4=34</div>
</div>
<div>12 120 70, 30+4=34</div>
<div>12 120 70, 30+4=34</div>
`,
async test({ component, target, assert }) {
component.constant = 20;
assert.htmlEqual(target.innerHTML, `
<div>12 240 140, 60+4=64</div>
<div>12 240 140, 60+4=64</div>
<div>12 240 140, 60+4=64</div>
<div slot="box1">
<div>12 240 140, 60+4=64</div>
</div>
<div slot="box2">
<div>12 240 140, 60+4=64</div>
</div>
<div>12 240 140, 60+4=64</div>
<div>12 240 140, 60+4=64</div>
`);
component.box = {width: 5, height: 6};
assert.htmlEqual(target.innerHTML, `
<div>30 600 220, 100+6=106</div>
<div>30 600 220, 100+6=106</div>
<div>30 600 220, 100+6=106</div>
<div slot="box1">
<div>30 600 220, 100+6=106</div>
</div>
<div slot="box2">
<div>30 600 220, 100+6=106</div>
</div>
<div>30 600 220, 100+6=106</div>
<div>30 600 220, 100+6=106</div>
`);
}
};

@ -0,0 +1,60 @@
<script>
import Component from './Component.svelte';
export let box = {width: 3, height: 4};
export let constant = 10;
function calculate(width, height, constant) {
return { area: width * height, volume: width * height * constant };
}
</script>
<Component {box}>
<svelte:fragment slot="box1" let:box>
{@const {area, volume} = calculate(box.width, box.height, constant)}
{@const perimeter = (box.width + box.height) * constant}
{@const [width, height, sum] = [box.width * constant, box.height, box.width * constant + box.height]}
<div>{area} {volume} {perimeter}, {width}+{height}={sum}</div>
</svelte:fragment>
<svelte:fragment slot="box2" let:width let:height>
{@const {area, volume} = calculate(width, height, constant)}
{@const perimeter = (width + height) * constant}
{@const [_width, _height, sum] = [width * constant, height, width * constant + height]}
<div>{area} {volume} {perimeter}, {_width}+{_height}={sum}</div>
</svelte:fragment>
<svelte:fragment let:box={{width, height}}>
{@const {area, volume} = calculate(width, height, constant)}
{@const perimeter = (width + height) * constant}
{@const [_width, _height, sum] = [width * constant, height, width * constant + height]}
<div>{area} {volume} {perimeter}, {_width}+{_height}={sum}</div>
</svelte:fragment>
</Component>
<Component {box} let:box>
<div slot="box1" let:box>
{@const {area, volume} = calculate(box.width, box.height, constant)}
{@const perimeter = (box.width + box.height) * constant}
{@const [width, height, sum] = [box.width * constant, box.height, box.width * constant + box.height]}
<div>{area} {volume} {perimeter}, {width}+{height}={sum}</div>
</div>
<div slot="box2" let:width let:height>
{@const {area, volume} = calculate(width, height, constant)}
{@const perimeter = (width + height) * constant}
{@const [_width, _height, sum] = [width * constant, height, width * constant + height]}
<div>{area} {volume} {perimeter}, {_width}+{_height}={sum}</div>
</div>
{@const {area, volume} = calculate(box.width, box.height, constant)}
{@const perimeter = (box.width + box.height) * constant}
{@const [width, height, sum] = [box.width * constant, box.height, box.width * constant + box.height]}
<div>{area} {volume} {perimeter}, {width}+{height}={sum}</div>
</Component>
<Component {box} let:box={{width, height}}>
{@const {area, volume} = calculate(width, height, constant)}
{@const perimeter = (width + height) * constant}
{@const [_width, _height, sum] = [width * constant, height, width * constant + height]}
<div>{area} {volume} {perimeter}, {_width}+{_height}={sum}</div>
</Component>

@ -0,0 +1,12 @@
export default {
html: `
<div>7</div>
`,
async test({ component, target, assert }) {
component.a = 5;
assert.htmlEqual(target.innerHTML, `
<div>9</div>
`);
}
};

@ -0,0 +1,10 @@
<script>
export let value = 4;
export let a = 3;
export let b = 4;
</script>
{#each [value] as n}
{@const ab = a + b}
<div>{ab}</div>
{/each}

@ -0,0 +1,30 @@
export default {
html: `
<div>12 120 70, 30+4=34</div>
<div>35 350 120, 50+7=57</div>
<div>48 480 140, 60+8=68</div>
`,
async test({ component, target, assert }) {
component.constant = 20;
assert.htmlEqual(target.innerHTML, `
<div>12 240 140, 60+4=64</div>
<div>35 700 240, 100+7=107</div>
<div>48 960 280, 120+8=128</div>
`);
component.boxes = [
{width: 3, height: 4},
{width: 4, height: 5},
{width: 5, height: 6},
{width: 6, height: 7}
];
assert.htmlEqual(target.innerHTML, `
<div>12 240 140, 60+4=64</div>
<div>20 400 180, 80+5=85</div>
<div>30 600 220, 100+6=106</div>
<div>42 840 260, 120+7=127</div>
`);
}
};

@ -0,0 +1,19 @@
<script>
export let boxes = [
{width: 3, height: 4},
{width: 5, height: 7},
{width: 6, height: 8},
];
export let constant = 10;
function calculate(width, height, constant) {
return { area: width * height, volume: width * height * constant };
}
</script>
{#each boxes as { width, height }}
{@const {area, volume} = calculate(width, height, constant)}
{@const perimeter = (width + height) * constant}
{@const [_width, _height, sum] = [width * constant, height, width * constant + height]}
<div>{area} {volume} {perimeter}, {_width}+{_height}={sum}</div>
{/each}

@ -0,0 +1,30 @@
export default {
html: `
<div>12 120 70, 30+4=34</div>
<div>35 350 120, 50+7=57</div>
<div>48 480 140, 60+8=68</div>
`,
async test({ component, target, assert }) {
component.constant = 20;
assert.htmlEqual(target.innerHTML, `
<div>12 240 140, 60+4=64</div>
<div>35 700 240, 100+7=107</div>
<div>48 960 280, 120+8=128</div>
`);
component.boxes = [
{width: 3, height: 4},
{width: 4, height: 5},
{width: 5, height: 6},
{width: 6, height: 7}
];
assert.htmlEqual(target.innerHTML, `
<div>12 240 140, 60+4=64</div>
<div>20 400 180, 80+5=85</div>
<div>30 600 220, 100+6=106</div>
<div>42 840 260, 120+7=127</div>
`);
}
};

@ -0,0 +1,19 @@
<script>
export let boxes = [
{width: 3, height: 4},
{width: 5, height: 7},
{width: 6, height: 8},
];
export let constant = 10;
function calculate(width, height, constant) {
return { area: width * height, volume: width * height * constant };
}
</script>
{#each boxes as box}
{@const {area, volume} = calculate(box.width, box.height, constant)}
{@const perimeter = (box.width + box.height) * constant}
{@const [width, height, sum] = [box.width * constant, box.height, box.width * constant + box.height]}
<div>{area} {volume} {perimeter}, {width}+{height}={sum}</div>
{/each}

@ -0,0 +1,12 @@
export default {
html: `
<div>4 ^ 4 = 256</div>
`,
async test({ component, target, assert }) {
component.value = 3;
assert.htmlEqual(target.innerHTML, `
<div>3 ^ 4 = 81</div>
`);
}
};

@ -0,0 +1,11 @@
<script>
export let value = 4;
</script>
{#each [value] as n}
<div>{n} ^ 4 = {hypercubed}</div>
{@const squared = n * n}
{@const cubed = squared * n}
{@const hypercubed = cubed * n}
{/each}

@ -0,0 +1,12 @@
export default {
html: `
<div>4 ^ 4 = 256</div>
`,
async test({ component, target, assert }) {
component.value = 3;
assert.htmlEqual(target.innerHTML, `
<div>3 ^ 4 = 81</div>
`);
}
};

@ -0,0 +1,11 @@
<script>
export let value = 4;
</script>
{#each [value] as n}
<div>{n} ^ 4 = {hypercubed}</div>
{@const squared = n * n}
{@const cubed = squared * n}
{@const hypercubed = cubed * n}
{/each}

@ -0,0 +1,43 @@
export default {
html: `
<b>7</b>
<u>11</u>
<u>15</u>
<i>7</i>
<b>19</b>
<u>23</u>
<u>27</u>
<i>19</i>
`,
async test({ component, target, assert }) {
component.numbers = [
{
a: 4,
b: 5,
children: [
{ a: 6, b: 7 },
{ a: 8, b: 9 }
]
},
{
a: 10,
b: 11,
children: [
{ a: 12, b: 13 },
{ a: 14, b: 15 }
]
}
];
assert.htmlEqual(target.innerHTML, `
<b>9</b>
<u>13</u>
<u>17</u>
<i>9</i>
<b>21</b>
<u>25</u>
<u>29</u>
<i>21</i>
`);
}
};

@ -0,0 +1,31 @@
<script>
export let numbers = [
{
a: 3,
b: 4,
children: [
{ a: 5, b: 6 },
{ a: 7, b: 8 }
]
},
{
a: 9,
b: 10,
children: [
{ a: 11, b: 12 },
{ a: 13, b: 14 }
]
}
];
</script>
{#each numbers as {a, b, children}}
{@const ab = a + b}
<b>{ab}</b>
{#each children as {a, b}}
{@const ab = a + b}
<u>{ab}</u>
{/each}
<i>{ab}</i>
{/each}

@ -0,0 +1,9 @@
[
{
"code": "invalid-const-declaration",
"message": "'a' has already been declared",
"start": { "line": 7, "column": 2, "character": 84 },
"end": { "line": 7, "column": 19, "character": 101 },
"pos": 84
}
]

@ -0,0 +1,8 @@
<script>
export let array;
</script>
{#each array as item}
{@const a = item}
{@const a = item}
{/each}

@ -0,0 +1,9 @@
[
{
"code": "invalid-const-declaration",
"message": "'item' has already been declared",
"start": { "line": 6, "column": 2, "character": 64 },
"end": { "line": 6, "column": 21, "character": 83 },
"pos": 64
}
]

@ -0,0 +1,7 @@
<script>
export let array;
</script>
{#each array as item}
{@const item = 123}
{/each}

@ -0,0 +1,9 @@
[
{
"code": "cyclical-const-tags",
"message": "Cyclical dependency detected: b → c → b",
"start": { "line": 6, "column": 2, "character": 61 },
"end": { "line": 6, "column": 20, "character": 79 },
"pos": 61
}
]

@ -0,0 +1,8 @@
<script>
export let array;
</script>
{#each array as a}
{@const b = a + c}
{@const c = b + a}
{/each}

@ -0,0 +1,10 @@
<script>
export let array;
</script>
{#each array as a}
{@const b = a + 1}
<div />
{/each}
{b}

@ -0,0 +1,17 @@
[
{
"code": "missing-declaration",
"message": "'b' is not defined",
"pos": 100,
"start": {
"character": 100,
"column": 1,
"line": 10
},
"end": {
"character": 101,
"column": 2,
"line": 10
}
}
]

@ -0,0 +1,9 @@
[
{
"code": "invalid-const-placement",
"message": "{@const} must be the immediate child of {#each}, {:then}, {:catch}, <svelte:fragment> or <Component>",
"start": { "line": 5, "column": 0, "character": 36 },
"end": { "line": 5, "column": 18, "character": 54 },
"pos": 36
}
]

@ -0,0 +1,5 @@
<script>
export let a;
</script>
{@const b = a + 1}

@ -0,0 +1,9 @@
[
{
"code": "invalid-const-placement",
"message": "{@const} must be the immediate child of {#each}, {:then}, {:catch}, <svelte:fragment> or <Component>",
"start": { "line": 6, "column": 2, "character": 46 },
"end": { "line": 6, "column": 20, "character": 64 },
"pos": 46
}
]

@ -0,0 +1,7 @@
<script>
export let a;
</script>
{#if a}
{@const b = a + 1}
{/if}

@ -0,0 +1,9 @@
[
{
"code": "invalid-const-placement",
"message": "{@const} must be the immediate child of {#each}, {:then}, {:catch}, <svelte:fragment> or <Component>",
"start": { "line": 7, "column": 4, "character": 63 },
"end": { "line": 7, "column": 18, "character": 77 },
"pos": 63
}
]

@ -0,0 +1,9 @@
<script>
export let a;
</script>
{#each a as i}
<div>
{@const b = i}
</div>
{/each}

@ -0,0 +1,9 @@
[
{
"code": "invalid-const-update",
"message": "'b' is declared using {@const ...} and is read-only",
"start": { "line": 7, "column": 26, "character": 106 },
"end": { "line": 7, "column": 30, "character": 110 },
"pos": 106
}
]

@ -0,0 +1,8 @@
<script>
export let array;
</script>
{#each array as a}
{@const b = a + 1}
<button on:click={() => b ++} />
{/each}

@ -0,0 +1,9 @@
[
{
"code": "invalid-binding",
"message": "Cannot bind to a variable declared with {@const ...}",
"start": { "line": 7, "column": 9, "character": 89 },
"end": { "line": 7, "column": 23, "character": 103 },
"pos": 89
}
]

@ -0,0 +1,8 @@
<script>
export let array;
</script>
{#each array as a}
{@const b = a + 1}
<input bind:value={b} />
{/each}
Loading…
Cancel
Save