From 26f767e873742a4ea6f9214e01b34ca9c95f811b Mon Sep 17 00:00:00 2001 From: Simon H <5968653+dummdidumm@users.noreply.github.com> Date: Sun, 9 Jan 2022 10:01:40 +0100 Subject: [PATCH 01/27] [docs] add note about the AST not being public API (#7113) --- site/content/docs/04-compile-time.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/site/content/docs/04-compile-time.md b/site/content/docs/04-compile-time.md index 6ba5dabacc..1ee41eeb93 100644 --- a/site/content/docs/04-compile-time.md +++ b/site/content/docs/04-compile-time.md @@ -178,7 +178,7 @@ ast: object = svelte.parse( --- -The `parse` function parses a component, returning only its abstract syntax tree. Unlike compiling with the `generate: false` option, this will not perform any validation or other analysis of the component beyond parsing it. +The `parse` function parses a component, returning only its abstract syntax tree. Unlike compiling with the `generate: false` option, this will not perform any validation or other analysis of the component beyond parsing it. Note that the returned AST is not considered public API, so breaking changes could occur at any point in time. ```js From 895b4c67210b37b671089a334998c942371d015e Mon Sep 17 00:00:00 2001 From: Yuichiro Yamashita Date: Sun, 9 Jan 2022 23:12:57 +0900 Subject: [PATCH 02/27] [fix] hydrate correct elements when using `@html` (#6946) --- src/runtime/internal/dom.ts | 2 +- .../samples/raw-with-empty-line-at-top/_after.html | 6 ++++++ .../samples/raw-with-empty-line-at-top/_before.html | 6 ++++++ .../samples/raw-with-empty-line-at-top/main.svelte | 11 +++++++++++ 4 files changed, 24 insertions(+), 1 deletion(-) create mode 100644 test/hydration/samples/raw-with-empty-line-at-top/_after.html create mode 100644 test/hydration/samples/raw-with-empty-line-at-top/_before.html create mode 100644 test/hydration/samples/raw-with-empty-line-at-top/main.svelte diff --git a/src/runtime/internal/dom.ts b/src/runtime/internal/dom.ts index a8b542053b..972b1ff537 100644 --- a/src/runtime/internal/dom.ts +++ b/src/runtime/internal/dom.ts @@ -501,7 +501,7 @@ export function claim_html_tag(nodes) { } init_claim_info(nodes); - const html_tag_nodes = nodes.splice(start_index, end_index + 1); + const html_tag_nodes = nodes.splice(start_index, end_index - start_index + 1); detach(html_tag_nodes[0]); detach(html_tag_nodes[html_tag_nodes.length - 1]); const claimed_nodes = html_tag_nodes.slice(1, html_tag_nodes.length - 1); diff --git a/test/hydration/samples/raw-with-empty-line-at-top/_after.html b/test/hydration/samples/raw-with-empty-line-at-top/_after.html new file mode 100644 index 0000000000..28ee6452d3 --- /dev/null +++ b/test/hydration/samples/raw-with-empty-line-at-top/_after.html @@ -0,0 +1,6 @@ + +
before
+
+ +a b c +
after
diff --git a/test/hydration/samples/raw-with-empty-line-at-top/_before.html b/test/hydration/samples/raw-with-empty-line-at-top/_before.html new file mode 100644 index 0000000000..28ee6452d3 --- /dev/null +++ b/test/hydration/samples/raw-with-empty-line-at-top/_before.html @@ -0,0 +1,6 @@ + +
before
+
+ +a b c +
after
diff --git a/test/hydration/samples/raw-with-empty-line-at-top/main.svelte b/test/hydration/samples/raw-with-empty-line-at-top/main.svelte new file mode 100644 index 0000000000..2d08a3f418 --- /dev/null +++ b/test/hydration/samples/raw-with-empty-line-at-top/main.svelte @@ -0,0 +1,11 @@ + + +
before
+
+ +{#each content as c} + {@html c} +{/each} +
after
From 1143b5615a7a6f45f1025d61e5df5e985d886412 Mon Sep 17 00:00:00 2001 From: Ben McCann <322311+benmccann@users.noreply.github.com> Date: Sun, 9 Jan 2022 06:15:07 -0800 Subject: [PATCH 03/27] update changelog --- CHANGELOG.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 39dc47b387..ee465f58b2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,10 @@ # Svelte changelog +## Unreleased + +* Add style directives ([RFC #42](https://github.com/sveltejs/rfcs/pull/42)) +* Fix hydration when using `@html` ([#7115](https://github.com/sveltejs/svelte/issues/7115)) + ## 3.45.0 * Fix non-boolean attribute rendering in SSR to render truthy values as-is ([#6121](https://github.com/sveltejs/svelte/issues/6121)) From 806cba2b755a93ac2b083cf87fb50e8b84947dae Mon Sep 17 00:00:00 2001 From: Hofer Ivan Date: Tue, 11 Jan 2022 20:54:07 +0100 Subject: [PATCH 04/27] [fix] ensure style manager instances don't conflict with each other (#7114) --- src/runtime/internal/dom.ts | 2 +- src/runtime/internal/style_manager.ts | 35 ++++++++++++++++----------- 2 files changed, 22 insertions(+), 15 deletions(-) diff --git a/src/runtime/internal/dom.ts b/src/runtime/internal/dom.ts index 972b1ff537..287c16b5fc 100644 --- a/src/runtime/internal/dom.ts +++ b/src/runtime/internal/dom.ts @@ -157,7 +157,7 @@ export function get_root_for_style(node: Node): ShadowRoot | Document { export function append_empty_stylesheet(node: Node) { const style_element = element('style') as HTMLStyleElement; append_stylesheet(get_root_for_style(node), style_element); - return style_element; + return style_element.sheet as CSSStyleSheet; } function append_stylesheet(node: ShadowRoot | Document, style: HTMLStyleElement) { diff --git a/src/runtime/internal/style_manager.ts b/src/runtime/internal/style_manager.ts index 0993b3bf18..6907e5af02 100644 --- a/src/runtime/internal/style_manager.ts +++ b/src/runtime/internal/style_manager.ts @@ -1,12 +1,14 @@ import { append_empty_stylesheet, get_root_for_style } from './dom'; import { raf } from './environment'; -interface ExtendedDoc extends Document { - __svelte_stylesheet: CSSStyleSheet; - __svelte_rules: Record; +interface StyleInformation { + stylesheet: CSSStyleSheet; + rules: Record; } -const active_docs = new Set(); +// we need to store the information for multiple documents because a Svelte application could also contain iframes +// https://github.com/sveltejs/svelte/issues/3624 +const managed_styles = new Map(); let active = 0; // https://github.com/darkskyapp/string-hash/blob/master/index.js @@ -18,6 +20,12 @@ function hash(str: string) { return hash >>> 0; } +function create_style_information(doc: Document | ShadowRoot, node: Element & ElementCSSInlineStyle) { + const info = { stylesheet: append_empty_stylesheet(node), rules: {} }; + managed_styles.set(doc, info); + return info; +} + export function create_rule(node: Element & ElementCSSInlineStyle, a: number, b: number, duration: number, delay: number, ease: (t: number) => number, fn: (t: number, u: number) => string, uid: number = 0) { const step = 16.666 / duration; let keyframes = '{\n'; @@ -29,13 +37,12 @@ export function create_rule(node: Element & ElementCSSInlineStyle, a: number, b: const rule = keyframes + `100% {${fn(b, 1 - b)}}\n}`; const name = `__svelte_${hash(rule)}_${uid}`; - const doc = get_root_for_style(node) as ExtendedDoc; - active_docs.add(doc); - const stylesheet = doc.__svelte_stylesheet || (doc.__svelte_stylesheet = append_empty_stylesheet(node).sheet as CSSStyleSheet); - const current_rules = doc.__svelte_rules || (doc.__svelte_rules = {}); + const doc = get_root_for_style(node); + + const { stylesheet, rules } = managed_styles.get(doc) || create_style_information(doc, node); - if (!current_rules[name]) { - current_rules[name] = true; + if (!rules[name]) { + rules[name] = true; stylesheet.insertRule(`@keyframes ${name} ${rule}`, stylesheet.cssRules.length); } @@ -63,12 +70,12 @@ export function delete_rule(node: Element & ElementCSSInlineStyle, name?: string export function clear_rules() { raf(() => { if (active) return; - active_docs.forEach(doc => { - const stylesheet = doc.__svelte_stylesheet; + managed_styles.forEach(info => { + const { stylesheet } = info; let i = stylesheet.cssRules.length; while (i--) stylesheet.deleteRule(i); - doc.__svelte_rules = {}; + info.rules = {}; }); - active_docs.clear(); + managed_styles.clear(); }); } From d5fde793c161f5ee0da28ef6ee161b82e94da613 Mon Sep 17 00:00:00 2001 From: Ben McCann <322311+benmccann@users.noreply.github.com> Date: Tue, 11 Jan 2022 11:55:59 -0800 Subject: [PATCH 05/27] update changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index ee465f58b2..d7d8b69678 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,7 @@ * Add style directives ([RFC #42](https://github.com/sveltejs/rfcs/pull/42)) * Fix hydration when using `@html` ([#7115](https://github.com/sveltejs/svelte/issues/7115)) +* Fix style manager conflicts when using multiple Svelte instances ([#7026](https://github.com/sveltejs/svelte/issues/7026)) ## 3.45.0 From b5aaa6641b7ce1a6c64f9593b5afe0c4ba457546 Mon Sep 17 00:00:00 2001 From: Tan Li Hau Date: Wed, 12 Jan 2022 06:10:09 +0800 Subject: [PATCH 06/27] [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 e6db1854d2..ae39c18cd9 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 54263c3eb9..f6e9d17ea3 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 1efc1a3038..39f6fa374e 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 1a92f617bb..ba6a4b77a6 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 0000000000..157361fb67 --- /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 7f55ee3a60..0000000000 --- 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 a8312b2462..e1e3107d92 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 a7bc986e9b..9d88b136cd 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 fe94a24e4e..cd18859603 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 720f88ad78..6aee3f916f 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 2e2fa8b5e0..b038cf62a7 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 7c6fc39185..c54540f249 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 df694ed45e..eae8640ca0 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 0000000000..77bc26fe9c --- /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 b1d0816aac..8fe53088bd 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 94cd539082..c585cb3f72 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 637d32676c..9c07267223 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 5a5a217bce..b77b2472df 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 daf41e10c3..a50f74fc04 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 0000000000..cd4dc7f033 --- /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 e09abc817c..eecc8ddf5b 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 0d05836e82..008c023e11 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 2b3ca5fbcb..626365e3e2 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 568942c3f5..09f3293301 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 0000000000..2c156609cd --- /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 3e54ea55a3..63dc29eec6 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 584f9d6e9a..da9016a9df 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 0d39529b59..944bdb4c58 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 69da37b2d9..e0a29f18de 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 0000000000..cb40e7c456 --- /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 0000000000..ab450822c6 --- /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 0000000000..cb40e7c456 --- /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 0000000000..f8eed87115 --- /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 0000000000..68ba82f245 --- /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 0000000000..66399a06fa --- /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 0000000000..1854c3fba9 --- /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 0000000000..5cbb2da587 --- /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 0000000000..cce3b0c1f7 --- /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 0000000000..00f8b31540 --- /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 0000000000..7f0391d5e6 --- /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 0000000000..00f8b31540 --- /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 0000000000..004f81cd83 --- /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 0000000000..f8f4a5c85e --- /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 0000000000..49fce93274 --- /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 0000000000..f8f4a5c85e --- /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 0000000000..49fce93274 --- /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 0000000000..312235f113 --- /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 0000000000..725121c733 --- /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 0000000000..59c72eccaf --- /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 0000000000..6633eeacdf --- /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 0000000000..11e2535091 --- /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 0000000000..2be7207691 --- /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 0000000000..1436747798 --- /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 0000000000..2f694be808 --- /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 0000000000..d1c2723e22 --- /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 0000000000..c85b417fe4 --- /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 0000000000..52de84e2be --- /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 0000000000..14ea3085e7 --- /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 0000000000..005b3ff8b7 --- /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 0000000000..a843f9d58f --- /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 0000000000..cc0ba4d74d --- /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 0000000000..2519482a98 --- /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 0000000000..ed60c529fd --- /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 0000000000..b5a37c8608 --- /dev/null +++ b/test/validator/samples/const-tag-readonly-1/input.svelte @@ -0,0 +1,8 @@ + + +{#each array as a} + {@const b = a + 1} +