From b5aaa6641b7ce1a6c64f9593b5afe0c4ba457546 Mon Sep 17 00:00:00 2001 From: Tan Li Hau Date: Wed, 12 Jan 2022 06:10:09 +0800 Subject: [PATCH] [feat] implement constants in markup (#6413) --- site/content/docs/02-template-syntax.md | 23 +++++ src/compiler/compile/compiler_errors.ts | 24 ++++- src/compiler/compile/nodes/Binding.ts | 3 + src/compiler/compile/nodes/CatchBlock.ts | 7 +- src/compiler/compile/nodes/ConstTag.ts | 72 +++++++++++++++ .../compile/nodes/DefaultSlotTemplate.ts | 28 ------ src/compiler/compile/nodes/EachBlock.ts | 6 +- src/compiler/compile/nodes/InlineComponent.ts | 10 +- src/compiler/compile/nodes/SlotTemplate.ts | 6 +- src/compiler/compile/nodes/ThenBlock.ts | 7 +- src/compiler/compile/nodes/interfaces.ts | 11 ++- .../compile/nodes/shared/Expression.ts | 8 +- .../compile/nodes/shared/TemplateScope.ts | 8 +- .../compile/nodes/shared/get_const_tags.ts | 91 +++++++++++++++++++ .../compile/nodes/shared/map_children.ts | 2 + src/compiler/compile/render_dom/Renderer.ts | 4 +- .../compile/render_dom/wrappers/AwaitBlock.ts | 21 ++++- .../compile/render_dom/wrappers/EachBlock.ts | 4 + .../render_dom/wrappers/SlotTemplate.ts | 37 +++++--- .../wrappers/shared/add_const_tags.ts | 24 +++++ .../compile/render_ssr/handlers/AwaitBlock.ts | 3 +- .../compile/render_ssr/handlers/EachBlock.ts | 3 +- .../render_ssr/handlers/InlineComponent.ts | 4 +- .../render_ssr/handlers/SlotTemplate.ts | 7 +- .../handlers/shared/get_const_tags.ts | 17 ++++ src/compiler/interfaces.ts | 16 +++- src/compiler/parse/state/mustache.ts | 22 +++++ src/compiler/parse/utils/node.ts | 2 + test/js/samples/debug-ssr-foo/expected.js | 6 +- .../_config.js | 19 ++++ .../main.svelte | 23 +++++ .../samples/const-tag-await-then/_config.js | 19 ++++ .../samples/const-tag-await-then/main.svelte | 23 +++++ .../const-tag-component/Component.svelte | 7 ++ .../samples/const-tag-component/_config.js | 46 ++++++++++ .../samples/const-tag-component/main.svelte | 60 ++++++++++++ .../samples/const-tag-dependencies/_config.js | 12 +++ .../const-tag-dependencies/main.svelte | 10 ++ .../const-tag-each-destructure/_config.js | 30 ++++++ .../const-tag-each-destructure/main.svelte | 19 ++++ .../runtime/samples/const-tag-each/_config.js | 30 ++++++ .../samples/const-tag-each/main.svelte | 19 ++++ .../samples/const-tag-hoisting/_config.js | 12 +++ .../samples/const-tag-hoisting/main.svelte | 11 +++ .../samples/const-tag-ordering/_config.js | 12 +++ .../samples/const-tag-ordering/main.svelte | 11 +++ .../samples/const-tag-shadow/_config.js | 43 +++++++++ .../samples/const-tag-shadow/main.svelte | 31 +++++++ .../samples/const-tag-conflict-1/errors.json | 9 ++ .../samples/const-tag-conflict-1/input.svelte | 8 ++ .../samples/const-tag-conflict-2/errors.json | 9 ++ .../samples/const-tag-conflict-2/input.svelte | 7 ++ .../samples/const-tag-cyclical/errors.json | 9 ++ .../samples/const-tag-cyclical/input.svelte | 8 ++ .../const-tag-out-of-scope/input.svelte | 10 ++ .../const-tag-out-of-scope/warnings.json | 17 ++++ .../samples/const-tag-placement-1/errors.json | 9 ++ .../const-tag-placement-1/input.svelte | 5 + .../samples/const-tag-placement-2/errors.json | 9 ++ .../const-tag-placement-2/input.svelte | 7 ++ .../samples/const-tag-placement-3/errors.json | 9 ++ .../const-tag-placement-3/input.svelte | 9 ++ .../samples/const-tag-readonly-1/errors.json | 9 ++ .../samples/const-tag-readonly-1/input.svelte | 8 ++ .../samples/const-tag-readonly-2/errors.json | 9 ++ .../samples/const-tag-readonly-2/input.svelte | 8 ++ 66 files changed, 998 insertions(+), 74 deletions(-) create mode 100644 src/compiler/compile/nodes/ConstTag.ts delete mode 100644 src/compiler/compile/nodes/DefaultSlotTemplate.ts create mode 100644 src/compiler/compile/nodes/shared/get_const_tags.ts create mode 100644 src/compiler/compile/render_dom/wrappers/shared/add_const_tags.ts create mode 100644 src/compiler/compile/render_ssr/handlers/shared/get_const_tags.ts create mode 100644 test/runtime/samples/const-tag-await-then-destructuring/_config.js create mode 100644 test/runtime/samples/const-tag-await-then-destructuring/main.svelte create mode 100644 test/runtime/samples/const-tag-await-then/_config.js create mode 100644 test/runtime/samples/const-tag-await-then/main.svelte create mode 100644 test/runtime/samples/const-tag-component/Component.svelte create mode 100644 test/runtime/samples/const-tag-component/_config.js create mode 100644 test/runtime/samples/const-tag-component/main.svelte create mode 100644 test/runtime/samples/const-tag-dependencies/_config.js create mode 100644 test/runtime/samples/const-tag-dependencies/main.svelte create mode 100644 test/runtime/samples/const-tag-each-destructure/_config.js create mode 100644 test/runtime/samples/const-tag-each-destructure/main.svelte create mode 100644 test/runtime/samples/const-tag-each/_config.js create mode 100644 test/runtime/samples/const-tag-each/main.svelte create mode 100644 test/runtime/samples/const-tag-hoisting/_config.js create mode 100644 test/runtime/samples/const-tag-hoisting/main.svelte create mode 100644 test/runtime/samples/const-tag-ordering/_config.js create mode 100644 test/runtime/samples/const-tag-ordering/main.svelte create mode 100644 test/runtime/samples/const-tag-shadow/_config.js create mode 100644 test/runtime/samples/const-tag-shadow/main.svelte create mode 100644 test/validator/samples/const-tag-conflict-1/errors.json create mode 100644 test/validator/samples/const-tag-conflict-1/input.svelte create mode 100644 test/validator/samples/const-tag-conflict-2/errors.json create mode 100644 test/validator/samples/const-tag-conflict-2/input.svelte create mode 100644 test/validator/samples/const-tag-cyclical/errors.json create mode 100644 test/validator/samples/const-tag-cyclical/input.svelte create mode 100644 test/validator/samples/const-tag-out-of-scope/input.svelte create mode 100644 test/validator/samples/const-tag-out-of-scope/warnings.json create mode 100644 test/validator/samples/const-tag-placement-1/errors.json create mode 100644 test/validator/samples/const-tag-placement-1/input.svelte create mode 100644 test/validator/samples/const-tag-placement-2/errors.json create mode 100644 test/validator/samples/const-tag-placement-2/input.svelte create mode 100644 test/validator/samples/const-tag-placement-3/errors.json create mode 100644 test/validator/samples/const-tag-placement-3/input.svelte create mode 100644 test/validator/samples/const-tag-readonly-1/errors.json create mode 100644 test/validator/samples/const-tag-readonly-1/input.svelte create mode 100644 test/validator/samples/const-tag-readonly-2/errors.json create mode 100644 test/validator/samples/const-tag-readonly-2/input.svelte diff --git a/site/content/docs/02-template-syntax.md b/site/content/docs/02-template-syntax.md index e6db1854d..ae39c18cd 100644 --- a/site/content/docs/02-template-syntax.md +++ b/site/content/docs/02-template-syntax.md @@ -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 + + +{#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}`, `` or ``. + ### Element directives diff --git a/src/compiler/compile/compiler_errors.ts b/src/compiler/compile/compiler_errors.ts index 54263c3eb..f6e9d17ea 100644 --- a/src/compiler/compile/compiler_errors.ts +++ b/src/compiler/compile/compiler_errors.ts @@ -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}, or ' + }, + 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(' → ')}` + }) }; diff --git a/src/compiler/compile/nodes/Binding.ts b/src/compiler/compile/nodes/Binding.ts index 1efc1a303..39f6fa374 100644 --- a/src/compiler/compile/nodes/Binding.ts +++ b/src/compiler/compile/nodes/Binding.ts @@ -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); diff --git a/src/compiler/compile/nodes/CatchBlock.ts b/src/compiler/compile/nodes/CatchBlock.ts index 1a92f617b..ba6a4b77a 100644 --- a/src/compiler/compile/nodes/CatchBlock.ts +++ b/src/compiler/compile/nodes/CatchBlock.ts @@ -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(); diff --git a/src/compiler/compile/nodes/ConstTag.ts b/src/compiler/compile/nodes/ConstTag.ts new file mode 100644 index 000000000..157361fb6 --- /dev/null +++ b/src/compiler/compile/nodes/ConstTag.ts @@ -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 = new Set(); + dependencies: Set = 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); + }); + } +} diff --git a/src/compiler/compile/nodes/DefaultSlotTemplate.ts b/src/compiler/compile/nodes/DefaultSlotTemplate.ts deleted file mode 100644 index 7f55ee3a6..000000000 --- a/src/compiler/compile/nodes/DefaultSlotTemplate.ts +++ /dev/null @@ -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; - } -} diff --git a/src/compiler/compile/nodes/EachBlock.ts b/src/compiler/compile/nodes/EachBlock.ts index a8312b246..e1e3107d9 100644 --- a/src/compiler/compile/nodes/EachBlock.ts +++ b/src/compiler/compile/nodes/EachBlock.ts @@ -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) { diff --git a/src/compiler/compile/nodes/InlineComponent.ts b/src/compiler/compile/nodes/InlineComponent.ts index a7bc986e9..9d88b136c 100644 --- a/src/compiler/compile/nodes/InlineComponent.ts +++ b/src/compiler/compile/nodes/InlineComponent.ts @@ -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); } diff --git a/src/compiler/compile/nodes/SlotTemplate.ts b/src/compiler/compile/nodes/SlotTemplate.ts index fe94a24e4..cd1885960 100644 --- a/src/compiler/compile/nodes/SlotTemplate.ts +++ b/src/compiler/compile/nodes/SlotTemplate.ts @@ -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() { diff --git a/src/compiler/compile/nodes/ThenBlock.ts b/src/compiler/compile/nodes/ThenBlock.ts index 720f88ad7..6aee3f916 100644 --- a/src/compiler/compile/nodes/ThenBlock.ts +++ b/src/compiler/compile/nodes/ThenBlock.ts @@ -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(); diff --git a/src/compiler/compile/nodes/interfaces.ts b/src/compiler/compile/nodes/interfaces.ts index 2e2fa8b5e..b038cf62a 100644 --- a/src/compiler/compile/nodes/interfaces.ts +++ b/src/compiler/compile/nodes/interfaces.ts @@ -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; diff --git a/src/compiler/compile/nodes/shared/Expression.ts b/src/compiler/compile/nodes/shared/Expression.ts index 7c6fc3918..c54540f24 100644 --- a/src/compiler/compile/nodes/shared/Expression.ts +++ b/src/compiler/compile/nodes/shared/Expression.ts @@ -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); } diff --git a/src/compiler/compile/nodes/shared/TemplateScope.ts b/src/compiler/compile/nodes/shared/TemplateScope.ts index df694ed45..eae8640ca 100644 --- a/src/compiler/compile/nodes/shared/TemplateScope.ts +++ b/src/compiler/compile/nodes/shared/TemplateScope.ts @@ -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; @@ -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'; + } } diff --git a/src/compiler/compile/nodes/shared/get_const_tags.ts b/src/compiler/compile/nodes/shared/get_const_tags.ts new file mode 100644 index 000000000..77bc26fe9 --- /dev/null +++ b/src/compiler/compile/nodes/shared/get_const_tags.ts @@ -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>] { + const const_tags: ConstTagType[] = []; + const others: Array> = []; + + 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>]; +} + +function sort_consts_nodes(consts_nodes: ConstTag[], component: Component) { + type ConstNode = { + assignees: Set; + dependencies: Set; + 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); +} diff --git a/src/compiler/compile/nodes/shared/map_children.ts b/src/compiler/compile/nodes/shared/map_children.ts index b1d0816aa..8fe53088b 100644 --- a/src/compiler/compile/nodes/shared/map_children.ts +++ b/src/compiler/compile/nodes/shared/map_children.ts @@ -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; diff --git a/src/compiler/compile/render_dom/Renderer.ts b/src/compiler/compile/render_dom/Renderer.ts index 94cd53908..c585cb3f7 100644 --- a/src/compiler/compile/render_dom/Renderer.ts +++ b/src/compiler/compile/render_dom/Renderer.ts @@ -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; diff --git a/src/compiler/compile/render_dom/wrappers/AwaitBlock.ts b/src/compiler/compile/render_dom/wrappers/AwaitBlock.ts index 637d32676..9c0726722 100644 --- a/src/compiler/compile/render_dom/wrappers/AwaitBlock.ts +++ b/src/compiler/compile/render_dom/wrappers/AwaitBlock.ts @@ -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)`); diff --git a/src/compiler/compile/render_dom/wrappers/EachBlock.ts b/src/compiler/compile/render_dom/wrappers/EachBlock.ts index 5a5a217bc..b77b2472d 100644 --- a/src/compiler/compile/render_dom/wrappers/EachBlock.ts +++ b/src/compiler/compile/render_dom/wrappers/EachBlock.ts @@ -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; } `); diff --git a/src/compiler/compile/render_dom/wrappers/SlotTemplate.ts b/src/compiler/compile/render_dom/wrappers/SlotTemplate.ts index daf41e10c..a50f74fc0 100644 --- a/src/compiler/compile/render_dom/wrappers/SlotTemplate.ts +++ b/src/compiler/compile/render_dom/wrappers/SlotTemplate.ts @@ -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)`); + } } } diff --git a/src/compiler/compile/render_dom/wrappers/shared/add_const_tags.ts b/src/compiler/compile/render_dom/wrappers/shared/add_const_tags.ts new file mode 100644 index 000000000..cd4dc7f03 --- /dev/null +++ b/src/compiler/compile/render_dom/wrappers/shared/add_const_tags.ts @@ -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); + }); + }); +} diff --git a/src/compiler/compile/render_ssr/handlers/AwaitBlock.ts b/src/compiler/compile/render_ssr/handlers/AwaitBlock.ts index e09abc817..eecc8ddf5 100644 --- a/src/compiler/compile/render_ssr/handlers/AwaitBlock.ts +++ b/src/compiler/compile/render_ssr/handlers/AwaitBlock.ts @@ -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}) `); } diff --git a/src/compiler/compile/render_ssr/handlers/EachBlock.ts b/src/compiler/compile/render_ssr/handlers/EachBlock.ts index 0d05836e8..008c023e1 100644 --- a/src/compiler/compile/render_ssr/handlers/EachBlock.ts +++ b/src/compiler/compile/render_ssr/handlers/EachBlock.ts @@ -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(); diff --git a/src/compiler/compile/render_ssr/handlers/InlineComponent.ts b/src/compiler/compile/render_ssr/handlers/InlineComponent.ts index 2b3ca5fbc..626365e3e 100644 --- a/src/compiler/compile/render_ssr/handlers/InlineComponent.ts +++ b/src/compiler/compile/render_ssr/handlers/InlineComponent.ts @@ -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}; }` ); }); } diff --git a/src/compiler/compile/render_ssr/handlers/SlotTemplate.ts b/src/compiler/compile/render_ssr/handlers/SlotTemplate.ts index 568942c3f..09f329330 100644 --- a/src/compiler/compile/render_ssr/handlers/SlotTemplate.ts +++ b/src/compiler/compile/render_ssr/handlers/SlotTemplate.ts @@ -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; }) { 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) }); } } diff --git a/src/compiler/compile/render_ssr/handlers/shared/get_const_tags.ts b/src/compiler/compile/render_ssr/handlers/shared/get_const_tags.ts new file mode 100644 index 000000000..2c156609c --- /dev/null +++ b/src/compiler/compile/render_ssr/handlers/shared/get_const_tags.ts @@ -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 + }; + }) + }; +} diff --git a/src/compiler/interfaces.ts b/src/compiler/interfaces.ts index 3e54ea55a..63dc29eec 100644 --- a/src/compiler/interfaces.ts +++ b/src/compiler/interfaces.ts @@ -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 diff --git a/src/compiler/parse/state/mustache.ts b/src/compiler/parse/state/mustache.ts index 584f9d6e9..da9016a9d 100644 --- a/src/compiler/parse/state/mustache.ts +++ b/src/compiler/parse/state/mustache.ts @@ -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); diff --git a/src/compiler/parse/utils/node.ts b/src/compiler/parse/utils/node.ts index 0d39529b5..944bdb4c5 100644 --- a/src/compiler/parse/utils/node.ts +++ b/src/compiler/parse/utils/node.ts @@ -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': diff --git a/test/js/samples/debug-ssr-foo/expected.js b/test/js/samples/debug-ssr-foo/expected.js index 69da37b2d..e0a29f18d 100644 --- a/test/js/samples/debug-ssr-foo/expected.js +++ b/test/js/samples/debug-ssr-foo/expected.js @@ -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 => `${escape(thing.name)} - ${debug(null, 7, 2, { foo })}`)} + return `${each(things, thing => { + return `${escape(thing.name)} + ${debug(null, 7, 2, { foo })}`; + })}

foo: ${escape(foo)}

`; }); diff --git a/test/runtime/samples/const-tag-await-then-destructuring/_config.js b/test/runtime/samples/const-tag-await-then-destructuring/_config.js new file mode 100644 index 000000000..cb40e7c45 --- /dev/null +++ b/test/runtime/samples/const-tag-await-then-destructuring/_config.js @@ -0,0 +1,19 @@ +export default { + html: '
12 120 70, 30+4=34
', + 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, ` +
30 300 110, 50+6=56
+
42 420 130, 60+7=67
+ `); + + component.constant = 20; + assert.htmlEqual(target.innerHTML, ` +
30 600 220, 100+6=106
+
42 840 260, 120+7=127
+ `); + } +}; diff --git a/test/runtime/samples/const-tag-await-then-destructuring/main.svelte b/test/runtime/samples/const-tag-await-then-destructuring/main.svelte new file mode 100644 index 000000000..ab450822c --- /dev/null +++ b/test/runtime/samples/const-tag-await-then-destructuring/main.svelte @@ -0,0 +1,23 @@ + + +{#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]} +
{area} {volume} {perimeter}, {_width}+{_height}={sum}
+{/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]} +
{area} {volume} {perimeter}, {_width}+{_height}={sum}
+{/await} \ No newline at end of file diff --git a/test/runtime/samples/const-tag-await-then/_config.js b/test/runtime/samples/const-tag-await-then/_config.js new file mode 100644 index 000000000..cb40e7c45 --- /dev/null +++ b/test/runtime/samples/const-tag-await-then/_config.js @@ -0,0 +1,19 @@ +export default { + html: '
12 120 70, 30+4=34
', + 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, ` +
30 300 110, 50+6=56
+
42 420 130, 60+7=67
+ `); + + component.constant = 20; + assert.htmlEqual(target.innerHTML, ` +
30 600 220, 100+6=106
+
42 840 260, 120+7=127
+ `); + } +}; diff --git a/test/runtime/samples/const-tag-await-then/main.svelte b/test/runtime/samples/const-tag-await-then/main.svelte new file mode 100644 index 000000000..f8eed8711 --- /dev/null +++ b/test/runtime/samples/const-tag-await-then/main.svelte @@ -0,0 +1,23 @@ + + +{#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]} +
{area} {volume} {perimeter}, {width}+{height}={sum}
+{/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]} +
{area} {volume} {perimeter}, {width}+{height}={sum}
+{/await} \ No newline at end of file diff --git a/test/runtime/samples/const-tag-component/Component.svelte b/test/runtime/samples/const-tag-component/Component.svelte new file mode 100644 index 000000000..68ba82f24 --- /dev/null +++ b/test/runtime/samples/const-tag-component/Component.svelte @@ -0,0 +1,7 @@ + + + + + \ No newline at end of file diff --git a/test/runtime/samples/const-tag-component/_config.js b/test/runtime/samples/const-tag-component/_config.js new file mode 100644 index 000000000..66399a06f --- /dev/null +++ b/test/runtime/samples/const-tag-component/_config.js @@ -0,0 +1,46 @@ +export default { + html: ` +
12 120 70, 30+4=34
+
12 120 70, 30+4=34
+
12 120 70, 30+4=34
+
+
12 120 70, 30+4=34
+
+
+
12 120 70, 30+4=34
+
+
12 120 70, 30+4=34
+
12 120 70, 30+4=34
+ `, + async test({ component, target, assert }) { + component.constant = 20; + assert.htmlEqual(target.innerHTML, ` +
12 240 140, 60+4=64
+
12 240 140, 60+4=64
+
12 240 140, 60+4=64
+
+
12 240 140, 60+4=64
+
+
+
12 240 140, 60+4=64
+
+
12 240 140, 60+4=64
+
12 240 140, 60+4=64
+ `); + + component.box = {width: 5, height: 6}; + assert.htmlEqual(target.innerHTML, ` +
30 600 220, 100+6=106
+
30 600 220, 100+6=106
+
30 600 220, 100+6=106
+
+
30 600 220, 100+6=106
+
+
+
30 600 220, 100+6=106
+
+
30 600 220, 100+6=106
+
30 600 220, 100+6=106
+ `); + } +}; diff --git a/test/runtime/samples/const-tag-component/main.svelte b/test/runtime/samples/const-tag-component/main.svelte new file mode 100644 index 000000000..1854c3fba --- /dev/null +++ b/test/runtime/samples/const-tag-component/main.svelte @@ -0,0 +1,60 @@ + + + + + {@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]} +
{area} {volume} {perimeter}, {width}+{height}={sum}
+
+ + + {@const {area, volume} = calculate(width, height, constant)} + {@const perimeter = (width + height) * constant} + {@const [_width, _height, sum] = [width * constant, height, width * constant + height]} +
{area} {volume} {perimeter}, {_width}+{_height}={sum}
+
+ + + {@const {area, volume} = calculate(width, height, constant)} + {@const perimeter = (width + height) * constant} + {@const [_width, _height, sum] = [width * constant, height, width * constant + height]} +
{area} {volume} {perimeter}, {_width}+{_height}={sum}
+
+
+ + +
+ {@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]} +
{area} {volume} {perimeter}, {width}+{height}={sum}
+
+ +
+ {@const {area, volume} = calculate(width, height, constant)} + {@const perimeter = (width + height) * constant} + {@const [_width, _height, sum] = [width * constant, height, width * constant + height]} +
{area} {volume} {perimeter}, {_width}+{_height}={sum}
+
+ + {@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]} +
{area} {volume} {perimeter}, {width}+{height}={sum}
+
+ + + {@const {area, volume} = calculate(width, height, constant)} + {@const perimeter = (width + height) * constant} + {@const [_width, _height, sum] = [width * constant, height, width * constant + height]} +
{area} {volume} {perimeter}, {_width}+{_height}={sum}
+
diff --git a/test/runtime/samples/const-tag-dependencies/_config.js b/test/runtime/samples/const-tag-dependencies/_config.js new file mode 100644 index 000000000..5cbb2da58 --- /dev/null +++ b/test/runtime/samples/const-tag-dependencies/_config.js @@ -0,0 +1,12 @@ +export default { + html: ` +
7
+ `, + async test({ component, target, assert }) { + component.a = 5; + + assert.htmlEqual(target.innerHTML, ` +
9
+ `); + } +}; diff --git a/test/runtime/samples/const-tag-dependencies/main.svelte b/test/runtime/samples/const-tag-dependencies/main.svelte new file mode 100644 index 000000000..cce3b0c1f --- /dev/null +++ b/test/runtime/samples/const-tag-dependencies/main.svelte @@ -0,0 +1,10 @@ + + +{#each [value] as n} + {@const ab = a + b} +
{ab}
+{/each} \ No newline at end of file diff --git a/test/runtime/samples/const-tag-each-destructure/_config.js b/test/runtime/samples/const-tag-each-destructure/_config.js new file mode 100644 index 000000000..00f8b3154 --- /dev/null +++ b/test/runtime/samples/const-tag-each-destructure/_config.js @@ -0,0 +1,30 @@ +export default { + html: ` +
12 120 70, 30+4=34
+
35 350 120, 50+7=57
+
48 480 140, 60+8=68
+ `, + async test({ component, target, assert }) { + component.constant = 20; + + assert.htmlEqual(target.innerHTML, ` +
12 240 140, 60+4=64
+
35 700 240, 100+7=107
+
48 960 280, 120+8=128
+ `); + + component.boxes = [ + {width: 3, height: 4}, + {width: 4, height: 5}, + {width: 5, height: 6}, + {width: 6, height: 7} + ]; + + assert.htmlEqual(target.innerHTML, ` +
12 240 140, 60+4=64
+
20 400 180, 80+5=85
+
30 600 220, 100+6=106
+
42 840 260, 120+7=127
+ `); + } +}; diff --git a/test/runtime/samples/const-tag-each-destructure/main.svelte b/test/runtime/samples/const-tag-each-destructure/main.svelte new file mode 100644 index 000000000..7f0391d5e --- /dev/null +++ b/test/runtime/samples/const-tag-each-destructure/main.svelte @@ -0,0 +1,19 @@ + + +{#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]} +
{area} {volume} {perimeter}, {_width}+{_height}={sum}
+{/each} \ No newline at end of file diff --git a/test/runtime/samples/const-tag-each/_config.js b/test/runtime/samples/const-tag-each/_config.js new file mode 100644 index 000000000..00f8b3154 --- /dev/null +++ b/test/runtime/samples/const-tag-each/_config.js @@ -0,0 +1,30 @@ +export default { + html: ` +
12 120 70, 30+4=34
+
35 350 120, 50+7=57
+
48 480 140, 60+8=68
+ `, + async test({ component, target, assert }) { + component.constant = 20; + + assert.htmlEqual(target.innerHTML, ` +
12 240 140, 60+4=64
+
35 700 240, 100+7=107
+
48 960 280, 120+8=128
+ `); + + component.boxes = [ + {width: 3, height: 4}, + {width: 4, height: 5}, + {width: 5, height: 6}, + {width: 6, height: 7} + ]; + + assert.htmlEqual(target.innerHTML, ` +
12 240 140, 60+4=64
+
20 400 180, 80+5=85
+
30 600 220, 100+6=106
+
42 840 260, 120+7=127
+ `); + } +}; diff --git a/test/runtime/samples/const-tag-each/main.svelte b/test/runtime/samples/const-tag-each/main.svelte new file mode 100644 index 000000000..004f81cd8 --- /dev/null +++ b/test/runtime/samples/const-tag-each/main.svelte @@ -0,0 +1,19 @@ + + +{#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]} +
{area} {volume} {perimeter}, {width}+{height}={sum}
+{/each} \ No newline at end of file diff --git a/test/runtime/samples/const-tag-hoisting/_config.js b/test/runtime/samples/const-tag-hoisting/_config.js new file mode 100644 index 000000000..f8f4a5c85 --- /dev/null +++ b/test/runtime/samples/const-tag-hoisting/_config.js @@ -0,0 +1,12 @@ +export default { + html: ` +
4 ^ 4 = 256
+ `, + async test({ component, target, assert }) { + component.value = 3; + + assert.htmlEqual(target.innerHTML, ` +
3 ^ 4 = 81
+ `); + } +}; diff --git a/test/runtime/samples/const-tag-hoisting/main.svelte b/test/runtime/samples/const-tag-hoisting/main.svelte new file mode 100644 index 000000000..49fce9327 --- /dev/null +++ b/test/runtime/samples/const-tag-hoisting/main.svelte @@ -0,0 +1,11 @@ + + +{#each [value] as n} +
{n} ^ 4 = {hypercubed}
+ + {@const squared = n * n} + {@const cubed = squared * n} + {@const hypercubed = cubed * n} +{/each} \ No newline at end of file diff --git a/test/runtime/samples/const-tag-ordering/_config.js b/test/runtime/samples/const-tag-ordering/_config.js new file mode 100644 index 000000000..f8f4a5c85 --- /dev/null +++ b/test/runtime/samples/const-tag-ordering/_config.js @@ -0,0 +1,12 @@ +export default { + html: ` +
4 ^ 4 = 256
+ `, + async test({ component, target, assert }) { + component.value = 3; + + assert.htmlEqual(target.innerHTML, ` +
3 ^ 4 = 81
+ `); + } +}; diff --git a/test/runtime/samples/const-tag-ordering/main.svelte b/test/runtime/samples/const-tag-ordering/main.svelte new file mode 100644 index 000000000..49fce9327 --- /dev/null +++ b/test/runtime/samples/const-tag-ordering/main.svelte @@ -0,0 +1,11 @@ + + +{#each [value] as n} +
{n} ^ 4 = {hypercubed}
+ + {@const squared = n * n} + {@const cubed = squared * n} + {@const hypercubed = cubed * n} +{/each} \ No newline at end of file diff --git a/test/runtime/samples/const-tag-shadow/_config.js b/test/runtime/samples/const-tag-shadow/_config.js new file mode 100644 index 000000000..312235f11 --- /dev/null +++ b/test/runtime/samples/const-tag-shadow/_config.js @@ -0,0 +1,43 @@ +export default { + html: ` + 7 + 11 + 15 + 7 + 19 + 23 + 27 + 19 + `, + 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, ` + 9 + 13 + 17 + 9 + 21 + 25 + 29 + 21 + `); + } +}; diff --git a/test/runtime/samples/const-tag-shadow/main.svelte b/test/runtime/samples/const-tag-shadow/main.svelte new file mode 100644 index 000000000..725121c73 --- /dev/null +++ b/test/runtime/samples/const-tag-shadow/main.svelte @@ -0,0 +1,31 @@ + + +{#each numbers as {a, b, children}} + {@const ab = a + b} + {ab} + {#each children as {a, b}} + {@const ab = a + b} + {ab} + {/each} + {ab} +{/each} \ No newline at end of file diff --git a/test/validator/samples/const-tag-conflict-1/errors.json b/test/validator/samples/const-tag-conflict-1/errors.json new file mode 100644 index 000000000..59c72ecca --- /dev/null +++ b/test/validator/samples/const-tag-conflict-1/errors.json @@ -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 + } +] diff --git a/test/validator/samples/const-tag-conflict-1/input.svelte b/test/validator/samples/const-tag-conflict-1/input.svelte new file mode 100644 index 000000000..6633eeacd --- /dev/null +++ b/test/validator/samples/const-tag-conflict-1/input.svelte @@ -0,0 +1,8 @@ + + +{#each array as item} + {@const a = item} + {@const a = item} +{/each} \ No newline at end of file diff --git a/test/validator/samples/const-tag-conflict-2/errors.json b/test/validator/samples/const-tag-conflict-2/errors.json new file mode 100644 index 000000000..11e253509 --- /dev/null +++ b/test/validator/samples/const-tag-conflict-2/errors.json @@ -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 + } +] diff --git a/test/validator/samples/const-tag-conflict-2/input.svelte b/test/validator/samples/const-tag-conflict-2/input.svelte new file mode 100644 index 000000000..2be720769 --- /dev/null +++ b/test/validator/samples/const-tag-conflict-2/input.svelte @@ -0,0 +1,7 @@ + + +{#each array as item} + {@const item = 123} +{/each} \ No newline at end of file diff --git a/test/validator/samples/const-tag-cyclical/errors.json b/test/validator/samples/const-tag-cyclical/errors.json new file mode 100644 index 000000000..143674779 --- /dev/null +++ b/test/validator/samples/const-tag-cyclical/errors.json @@ -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 + } +] diff --git a/test/validator/samples/const-tag-cyclical/input.svelte b/test/validator/samples/const-tag-cyclical/input.svelte new file mode 100644 index 000000000..2f694be80 --- /dev/null +++ b/test/validator/samples/const-tag-cyclical/input.svelte @@ -0,0 +1,8 @@ + + +{#each array as a} + {@const b = a + c} + {@const c = b + a} +{/each} \ No newline at end of file diff --git a/test/validator/samples/const-tag-out-of-scope/input.svelte b/test/validator/samples/const-tag-out-of-scope/input.svelte new file mode 100644 index 000000000..d1c2723e2 --- /dev/null +++ b/test/validator/samples/const-tag-out-of-scope/input.svelte @@ -0,0 +1,10 @@ + + +{#each array as a} + {@const b = a + 1} +
+{/each} + +{b} \ No newline at end of file diff --git a/test/validator/samples/const-tag-out-of-scope/warnings.json b/test/validator/samples/const-tag-out-of-scope/warnings.json new file mode 100644 index 000000000..c85b417fe --- /dev/null +++ b/test/validator/samples/const-tag-out-of-scope/warnings.json @@ -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 + } + } +] diff --git a/test/validator/samples/const-tag-placement-1/errors.json b/test/validator/samples/const-tag-placement-1/errors.json new file mode 100644 index 000000000..52de84e2b --- /dev/null +++ b/test/validator/samples/const-tag-placement-1/errors.json @@ -0,0 +1,9 @@ +[ + { + "code": "invalid-const-placement", + "message": "{@const} must be the immediate child of {#each}, {:then}, {:catch}, or ", + "start": { "line": 5, "column": 0, "character": 36 }, + "end": { "line": 5, "column": 18, "character": 54 }, + "pos": 36 + } +] diff --git a/test/validator/samples/const-tag-placement-1/input.svelte b/test/validator/samples/const-tag-placement-1/input.svelte new file mode 100644 index 000000000..14ea3085e --- /dev/null +++ b/test/validator/samples/const-tag-placement-1/input.svelte @@ -0,0 +1,5 @@ + + +{@const b = a + 1} \ No newline at end of file diff --git a/test/validator/samples/const-tag-placement-2/errors.json b/test/validator/samples/const-tag-placement-2/errors.json new file mode 100644 index 000000000..005b3ff8b --- /dev/null +++ b/test/validator/samples/const-tag-placement-2/errors.json @@ -0,0 +1,9 @@ +[ + { + "code": "invalid-const-placement", + "message": "{@const} must be the immediate child of {#each}, {:then}, {:catch}, or ", + "start": { "line": 6, "column": 2, "character": 46 }, + "end": { "line": 6, "column": 20, "character": 64 }, + "pos": 46 + } +] diff --git a/test/validator/samples/const-tag-placement-2/input.svelte b/test/validator/samples/const-tag-placement-2/input.svelte new file mode 100644 index 000000000..a843f9d58 --- /dev/null +++ b/test/validator/samples/const-tag-placement-2/input.svelte @@ -0,0 +1,7 @@ + + +{#if a} + {@const b = a + 1} +{/if} \ No newline at end of file diff --git a/test/validator/samples/const-tag-placement-3/errors.json b/test/validator/samples/const-tag-placement-3/errors.json new file mode 100644 index 000000000..cc0ba4d74 --- /dev/null +++ b/test/validator/samples/const-tag-placement-3/errors.json @@ -0,0 +1,9 @@ +[ + { + "code": "invalid-const-placement", + "message": "{@const} must be the immediate child of {#each}, {:then}, {:catch}, or ", + "start": { "line": 7, "column": 4, "character": 63 }, + "end": { "line": 7, "column": 18, "character": 77 }, + "pos": 63 + } +] diff --git a/test/validator/samples/const-tag-placement-3/input.svelte b/test/validator/samples/const-tag-placement-3/input.svelte new file mode 100644 index 000000000..2519482a9 --- /dev/null +++ b/test/validator/samples/const-tag-placement-3/input.svelte @@ -0,0 +1,9 @@ + + +{#each a as i} +
+ {@const b = i} +
+{/each} \ No newline at end of file diff --git a/test/validator/samples/const-tag-readonly-1/errors.json b/test/validator/samples/const-tag-readonly-1/errors.json new file mode 100644 index 000000000..ed60c529f --- /dev/null +++ b/test/validator/samples/const-tag-readonly-1/errors.json @@ -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 + } +] diff --git a/test/validator/samples/const-tag-readonly-1/input.svelte b/test/validator/samples/const-tag-readonly-1/input.svelte new file mode 100644 index 000000000..b5a37c860 --- /dev/null +++ b/test/validator/samples/const-tag-readonly-1/input.svelte @@ -0,0 +1,8 @@ + + +{#each array as a} + {@const b = a + 1} +