diff --git a/CHANGELOG.md b/CHANGELOG.md index 4f3d47d73b0a..cb9bbf6b8f48 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ # Svelte changelog +## Unreleased + +* Support default values and trailing commas in destructuring `{#await}` ([#4560](https://github.com/sveltejs/svelte/issues/4560), [#4810](https://github.com/sveltejs/svelte/issues/4810)) + ## 3.22.2 * Fix compiler exception with `a11y-img-redundant-alt` and value-less `alt` attribute ([#4777](https://github.com/sveltejs/svelte/issues/4777)) diff --git a/src/compiler/compile/nodes/AwaitBlock.ts b/src/compiler/compile/nodes/AwaitBlock.ts index a7b8fb815e4e..66bc15364c48 100644 --- a/src/compiler/compile/nodes/AwaitBlock.ts +++ b/src/compiler/compile/nodes/AwaitBlock.ts @@ -3,17 +3,21 @@ import PendingBlock from './PendingBlock'; import ThenBlock from './ThenBlock'; import CatchBlock from './CatchBlock'; import Expression from './shared/Expression'; -import { Pattern } from 'estree'; import Component from '../Component'; import TemplateScope from './shared/TemplateScope'; import { TemplateNode } from '../../interfaces'; -import traverse_destructure_pattern from '../utils/traverse_destructure_pattern'; +import { Context, unpack_destructuring } from './shared/Context'; +import { Node as ESTreeNode } from 'estree'; export default class AwaitBlock extends Node { type: 'AwaitBlock'; expression: Expression; - value: DestructurePattern; - error: DestructurePattern; + + then_contexts: Context[]; + catch_contexts: Context[]; + + then_node: ESTreeNode | null; + catch_node: ESTreeNode | null; pending: PendingBlock; then: ThenBlock; @@ -24,24 +28,21 @@ export default class AwaitBlock extends Node { this.expression = new Expression(component, this, scope, info.expression); - this.value = info.value && new DestructurePattern(info.value); - this.error = info.error && new DestructurePattern(info.error); + this.then_node = info.value; + this.catch_node = info.error; + + if (this.then_node) { + this.then_contexts = []; + unpack_destructuring(this.then_contexts, info.value, node => node); + } + + if (this.catch_node) { + this.catch_contexts = []; + unpack_destructuring(this.catch_contexts, info.error, node => node); + } this.pending = new PendingBlock(component, this, scope, info.pending); this.then = new ThenBlock(component, this, scope, info.then); this.catch = new CatchBlock(component, this, scope, info.catch); } } - -export class DestructurePattern { - pattern: Pattern; - expressions: string[]; - identifier_name: string | undefined; - - constructor(pattern: Pattern) { - this.pattern = pattern; - this.expressions = []; - traverse_destructure_pattern(pattern, (node) => this.expressions.push(node.name)); - this.identifier_name = this.pattern.type === 'Identifier' ? this.pattern.name : undefined; - } -} diff --git a/src/compiler/compile/nodes/CatchBlock.ts b/src/compiler/compile/nodes/CatchBlock.ts index 8b3736a2b990..1a92f617bbeb 100644 --- a/src/compiler/compile/nodes/CatchBlock.ts +++ b/src/compiler/compile/nodes/CatchBlock.ts @@ -13,9 +13,9 @@ export default class CatchBlock extends AbstractBlock { super(component, parent, scope, info); this.scope = scope.child(); - if (parent.error) { - parent.error.expressions.forEach(expression => { - this.scope.add(expression, parent.expression.dependencies, this); + if (parent.catch_node) { + parent.catch_contexts.forEach(context => { + this.scope.add(context.key.name, parent.expression.dependencies, this); }); } this.children = map_children(component, parent, this.scope, info.children); diff --git a/src/compiler/compile/nodes/EachBlock.ts b/src/compiler/compile/nodes/EachBlock.ts index 31850f874546..6458ea002019 100644 --- a/src/compiler/compile/nodes/EachBlock.ts +++ b/src/compiler/compile/nodes/EachBlock.ts @@ -4,56 +4,8 @@ import map_children from './shared/map_children'; import TemplateScope from './shared/TemplateScope'; import AbstractBlock from './shared/AbstractBlock'; import Element from './Element'; -import { x } from 'code-red'; -import { Node, Identifier, RestElement } from 'estree'; - -interface Context { - key: Identifier; - name?: string; - modifier: (node: Node) => Node; -} - -function unpack_destructuring(contexts: Context[], node: Node, modifier: (node: Node) => Node) { - if (!node) return; - - if (node.type === 'Identifier' || (node as any).type === 'RestIdentifier') { // TODO is this right? not RestElement? - contexts.push({ - key: node as Identifier, - modifier - }); - } else if (node.type === 'ArrayPattern') { - node.elements.forEach((element, i) => { - if (element && (element as any).type === 'RestIdentifier') { - unpack_destructuring(contexts, element, node => x`${modifier(node)}.slice(${i})` as Node); - } else { - unpack_destructuring(contexts, element, node => x`${modifier(node)}[${i}]` as Node); - } - }); - } else if (node.type === 'ObjectPattern') { - const used_properties = []; - - node.properties.forEach((property, i) => { - if ((property as any).kind === 'rest') { // TODO is this right? - const replacement: RestElement = { - type: 'RestElement', - argument: property.key as Identifier - }; - - node.properties[i] = replacement as any; - - unpack_destructuring( - contexts, - property.value, - node => x`@object_without_properties(${modifier(node)}, [${used_properties}])` as Node - ); - } else { - used_properties.push(x`"${(property.key as Identifier).name}"`); - - unpack_destructuring(contexts, property.value, node => x`${modifier(node)}.${(property.key as Identifier).name}` as Node); - } - }); - } -} +import { Context, unpack_destructuring } from './shared/Context'; +import { Node } from 'estree'; export default class EachBlock extends AbstractBlock { type: 'EachBlock'; diff --git a/src/compiler/compile/nodes/ThenBlock.ts b/src/compiler/compile/nodes/ThenBlock.ts index 7eefe2e6fb59..720f88ad786b 100644 --- a/src/compiler/compile/nodes/ThenBlock.ts +++ b/src/compiler/compile/nodes/ThenBlock.ts @@ -13,9 +13,9 @@ export default class ThenBlock extends AbstractBlock { super(component, parent, scope, info); this.scope = scope.child(); - if (parent.value) { - parent.value.expressions.forEach(expression => { - this.scope.add(expression, parent.expression.dependencies, this); + if (parent.then_node) { + parent.then_contexts.forEach(context => { + this.scope.add(context.key.name, parent.expression.dependencies, this); }); } this.children = map_children(component, parent, this.scope, info.children); diff --git a/src/compiler/compile/nodes/shared/Context.ts b/src/compiler/compile/nodes/shared/Context.ts new file mode 100644 index 000000000000..797405b0fe8b --- /dev/null +++ b/src/compiler/compile/nodes/shared/Context.ts @@ -0,0 +1,58 @@ +import { x } from 'code-red'; +import { Node, Identifier, RestElement, Property } from 'estree'; + +export interface Context { + key: Identifier; + name?: string; + modifier: (node: Node) => Node; +} + +export function unpack_destructuring(contexts: Context[], node: Node, modifier: (node: Node) => Node) { + if (!node) return; + + if (node.type === 'Identifier') { + contexts.push({ + key: node as Identifier, + modifier + }); + } else if (node.type === 'RestElement') { + contexts.push({ + key: node.argument as Identifier, + modifier + }); + } else if (node.type === 'ArrayPattern') { + node.elements.forEach((element, i) => { + if (element && element.type === 'RestElement') { + unpack_destructuring(contexts, element, node => x`${modifier(node)}.slice(${i})` as Node); + } else if (element && element.type === 'AssignmentPattern') { + unpack_destructuring(contexts, element.left, node => x`${modifier(node)}[${i}] !== undefined ? ${modifier(node)}[${i}] : ${element.right}` as Node); + } else { + unpack_destructuring(contexts, element, node => x`${modifier(node)}[${i}]` as Node); + } + }); + } else if (node.type === 'ObjectPattern') { + const used_properties = []; + + node.properties.forEach((property) => { + const props: (RestElement | Property) = (property as any); + + if (props.type === 'RestElement') { + unpack_destructuring( + contexts, + props.argument, + node => x`@object_without_properties(${modifier(node)}, [${used_properties}])` as Node + ); + } else { + const key = property.key as Identifier; + const value = property.value; + + used_properties.push(x`"${(key as Identifier).name}"`); + if (value.type === 'AssignmentPattern') { + unpack_destructuring(contexts, value.left, node => x`${modifier(node)}.${key.name} !== undefined ? ${modifier(node)}.${key.name} : ${value.right}` as Node); + } else { + unpack_destructuring(contexts, value, node => x`${modifier(node)}.${key.name}` as Node); + } + } + }); + } +} diff --git a/src/compiler/compile/render_dom/wrappers/AwaitBlock.ts b/src/compiler/compile/render_dom/wrappers/AwaitBlock.ts index 1898a840d23b..ceb898bf7901 100644 --- a/src/compiler/compile/render_dom/wrappers/AwaitBlock.ts +++ b/src/compiler/compile/render_dom/wrappers/AwaitBlock.ts @@ -8,27 +8,37 @@ import FragmentWrapper from './Fragment'; import PendingBlock from '../../nodes/PendingBlock'; import ThenBlock from '../../nodes/ThenBlock'; import CatchBlock from '../../nodes/CatchBlock'; -import { Identifier } from 'estree'; -import traverse_destructure_pattern from '../../utils/traverse_destructure_pattern'; +import { Context } from '../../nodes/shared/Context'; +import { Identifier, Literal, Node } from 'estree'; + +type Status = 'pending' | 'then' | 'catch'; class AwaitBlockBranch extends Wrapper { + parent: AwaitBlockWrapper; node: PendingBlock | ThenBlock | CatchBlock; block: Block; fragment: FragmentWrapper; is_dynamic: boolean; var = null; + status: Status; + + value: string; + value_index: Literal; + value_contexts: Context[]; + is_destructured: boolean; constructor( - status: string, + status: Status, renderer: Renderer, block: Block, - parent: Wrapper, - node: AwaitBlock, + parent: AwaitBlockWrapper, + node: PendingBlock | ThenBlock | CatchBlock, strip_whitespace: boolean, next_sibling: Wrapper ) { super(renderer, block, parent, node); + this.status = status; this.block = block.child({ comment: create_debugging_comment(node, this.renderer.component), @@ -36,6 +46,8 @@ class AwaitBlockBranch extends Wrapper { type: status }); + this.add_context(parent.node[status + '_node'], parent.node[status + '_contexts']); + this.fragment = new FragmentWrapper( renderer, this.block, @@ -48,20 +60,43 @@ class AwaitBlockBranch extends Wrapper { this.is_dynamic = this.block.dependencies.size > 0; } + add_context(node: Node | null, contexts: Context[]) { + if (!node) return; + + if (node.type === 'Identifier') { + this.value = node.name; + this.renderer.add_to_context(this.value, true); + } else { + contexts.forEach(context => { + this.renderer.add_to_context(context.key.name, true); + }); + this.value = this.block.parent.get_unique_name('value').name; + this.value_contexts = contexts; + this.renderer.add_to_context(this.value, true); + this.is_destructured = true; + } + this.value_index = this.renderer.context_lookup.get(this.value).index; + } + render(block: Block, parent_node: Identifier, parent_nodes: Identifier) { this.fragment.render(block, parent_node, parent_nodes); - } - render_destructure(block: Block, value, node, index) { - if (value && node.pattern.type !== 'Identifier') { - traverse_destructure_pattern(node.pattern, (node, parent, index) => { - parent[index] = x`#ctx[${block.renderer.context_lookup.get(node.name).index}]`; - }); + if (this.is_destructured) { + this.render_destructure(); + } + } - this.block.chunks.declarations.push(b`(${node.pattern} = #ctx[${index}])`); - if (this.block.has_update_method) { - this.block.chunks.update.push(b`(${node.pattern} = #ctx[${index}])`); + render_destructure() { + const props = this.value_contexts.map(prop => b`#ctx[${this.block.renderer.context_lookup.get(prop.key.name).index}] = ${prop.modifier(x`#ctx[${this.value_index}]`)};`); + 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} } + `); + this.block.chunks.declarations.push(b`${get_context}(#ctx)`); + if (this.block.has_update_method) { + this.block.chunks.update.push(b`${get_context}(#ctx)`); } } } @@ -73,9 +108,6 @@ export default class AwaitBlockWrapper extends Wrapper { then: AwaitBlockBranch; catch: AwaitBlockBranch; - value: string; - error: string; - var: Identifier = { type: 'Identifier', name: 'await_block' }; constructor( @@ -92,26 +124,12 @@ export default class AwaitBlockWrapper extends Wrapper { this.not_static_content(); block.add_dependencies(this.node.expression.dependencies); - if (this.node.value) { - for (const ctx of this.node.value.expressions) { - block.renderer.add_to_context(ctx, true); - } - this.value = this.node.value.identifier_name || block.get_unique_name('value').name; - block.renderer.add_to_context(this.value, true); - } - if (this.node.error) { - for (const ctx of this.node.error.expressions) { - block.renderer.add_to_context(ctx, true); - } - this.error = this.node.error.identifier_name || block.get_unique_name('error').name; - block.renderer.add_to_context(this.error, true); - } let is_dynamic = false; let has_intros = false; let has_outros = false; - ['pending', 'then', 'catch'].forEach(status => { + ['pending', 'then', 'catch'].forEach((status: Status) => { const child = this.node[status]; const branch = new AwaitBlockBranch( @@ -166,9 +184,6 @@ export default class AwaitBlockWrapper extends Wrapper { block.maintain_context = true; - const value_index = this.value && block.renderer.context_lookup.get(this.value).index; - const error_index = this.error && block.renderer.context_lookup.get(this.error).index; - const info_props: any = x`{ ctx: #ctx, current: null, @@ -176,8 +191,8 @@ export default class AwaitBlockWrapper extends Wrapper { pending: ${this.pending.block.name}, then: ${this.then.block.name}, catch: ${this.catch.block.name}, - value: ${value_index}, - error: ${error_index}, + value: ${this.then.value_index}, + error: ${this.catch.value_index}, blocks: ${this.pending.block.has_outro_method && x`[,,,]`} }`; @@ -232,7 +247,7 @@ export default class AwaitBlockWrapper extends Wrapper { } else { const #child_ctx = #ctx.slice(); - ${this.value && b`#child_ctx[${value_index}] = ${info}.resolved;`} + ${this.then.value && b`#child_ctx[${this.then.value_index}] = ${info}.resolved;`} ${info}.block.p(#child_ctx, #dirty); } `); @@ -246,7 +261,7 @@ export default class AwaitBlockWrapper extends Wrapper { block.chunks.update.push(b` { const #child_ctx = #ctx.slice(); - ${this.value && b`#child_ctx[${value_index}] = ${info}.resolved;`} + ${this.then.value && b`#child_ctx[${this.then.value_index}] = ${info}.resolved;`} ${info}.block.p(#child_ctx, #dirty); } `); @@ -271,7 +286,5 @@ export default class AwaitBlockWrapper extends Wrapper { [this.pending, this.then, this.catch].forEach(branch => { branch.render(branch.block, null, x`#nodes` as Identifier); }); - this.then.render_destructure(block, this.value, this.node.value, value_index); - this.catch.render_destructure(block, this.error, this.node.error, error_index); } } diff --git a/src/compiler/compile/render_ssr/handlers/AwaitBlock.ts b/src/compiler/compile/render_ssr/handlers/AwaitBlock.ts index d6bb86f1a4d3..6a62f872bb73 100644 --- a/src/compiler/compile/render_ssr/handlers/AwaitBlock.ts +++ b/src/compiler/compile/render_ssr/handlers/AwaitBlock.ts @@ -14,7 +14,7 @@ export default function(node: AwaitBlock, renderer: Renderer, options: RenderOpt renderer.add_expression(x` function(__value) { if (@is_promise(__value)) return ${pending}; - return (function(${node.value ? node.value.pattern : ''}) { return ${then}; }(__value)); + return (function(${node.then_node ? node.then_node : ''}) { return ${then}; }(__value)); }(${node.expression.node}) `); } diff --git a/src/compiler/compile/utils/traverse_destructure_pattern.ts b/src/compiler/compile/utils/traverse_destructure_pattern.ts deleted file mode 100644 index 6c918c28c07f..000000000000 --- a/src/compiler/compile/utils/traverse_destructure_pattern.ts +++ /dev/null @@ -1,35 +0,0 @@ -import { Pattern, Identifier, RestElement } from "estree"; -import { Node } from "acorn"; - -export default function traverse_destructure_pattern( - node: Pattern, - callback: (node: Identifier, parent: Node, key: string | number) => void -) { - function traverse(node: Pattern, parent, key) { - switch (node.type) { - case "Identifier": - return callback(node, parent, key); - case "ArrayPattern": - for (let i = 0; i < node.elements.length; i++) { - const element = node.elements[i]; - traverse(element, node.elements, i); - } - break; - case "ObjectPattern": - for (let i = 0; i < node.properties.length; i++) { - const property = node.properties[i]; - if (property.type === "Property") { - traverse(property.value, property, "value"); - } else { - traverse((property as any) as RestElement, node.properties, i); - } - } - break; - case "RestElement": - return traverse(node.argument, node, 'argument'); - case "AssignmentPattern": - return traverse(node.left, node, 'left'); - } - } - traverse(node, null, null); -} diff --git a/src/compiler/parse/index.ts b/src/compiler/parse/index.ts index c21e6d6f7968..a809eeebeb31 100644 --- a/src/compiler/parse/index.ts +++ b/src/compiler/parse/index.ts @@ -5,9 +5,6 @@ import { reserved } from '../utils/names'; import full_char_code_at from '../utils/full_char_code_at'; import { TemplateNode, Ast, ParserOptions, Fragment, Style, Script } from '../interfaces'; import error from '../utils/error'; -import { is_bracket_open, is_bracket_close, is_bracket_pair, get_bracket_close } from './utils/bracket'; -import { parse_expression_at } from './acorn'; -import { Pattern } from 'estree'; type ParserState = (parser: Parser) => (ParserState | void); @@ -173,51 +170,6 @@ export class Parser { return identifier; } - read_destructure_pattern(): Pattern { - const start = this.index; - let i = this.index; - - const code = full_char_code_at(this.template, i); - if (isIdentifierStart(code, true)) { - return { type: 'Identifier', name: this.read_identifier() }; - } - - if (!is_bracket_open(code)) { - this.error({ - code: 'unexpected-token', - message: 'Expected identifier or destructure pattern', - }); - } - - const bracket_stack = [code]; - i += code <= 0xffff ? 1 : 2; - - while (i < this.template.length) { - const code = full_char_code_at(this.template, i); - if (is_bracket_open(code)) { - bracket_stack.push(code); - } else if (is_bracket_close(code)) { - if (!is_bracket_pair(bracket_stack[bracket_stack.length - 1], code)) { - this.error({ - code: 'unexpected-token', - message: `Expected ${String.fromCharCode(get_bracket_close(bracket_stack[bracket_stack.length - 1]))}` - }); - } - bracket_stack.pop(); - if (bracket_stack.length === 0) { - i += code <= 0xffff ? 1 : 2; - break; - } - } - i += code <= 0xffff ? 1 : 2; - } - - this.index = i; - - const pattern_string = this.template.slice(start, i); - return (parse_expression_at(`(${pattern_string} = 1)`, 0) as any).left as Pattern; - } - read_until(pattern: RegExp) { if (this.index >= this.template.length) this.error({ diff --git a/src/compiler/parse/read/context.ts b/src/compiler/parse/read/context.ts index fe666467f803..8d28bd20247f 100644 --- a/src/compiler/parse/read/context.ts +++ b/src/compiler/parse/read/context.ts @@ -1,202 +1,82 @@ -import { Parser } from '../index'; -import { reserved } from '../../utils/names'; - -interface Identifier { - start: number; - end: number; - type: 'Identifier'; - name: string; -} - -interface Property { - start: number; - end: number; - type: 'Property'; - kind: 'init' | 'rest'; - shorthand: boolean; - key: Identifier; - value: Context; -} - -interface Context { - start: number; - end: number; - type: 'Identifier' | 'ArrayPattern' | 'ObjectPattern' | 'RestIdentifier'; - name?: string; - elements?: Context[]; - properties?: Property[]; -} +import { Parser } from "../index"; +import { isIdentifierStart } from "acorn"; +import full_char_code_at from "../../utils/full_char_code_at"; +import { + is_bracket_open, + is_bracket_close, + is_bracket_pair, + get_bracket_close +} from "../utils/bracket"; +import { parse_expression_at } from "../acorn"; +import { Pattern } from "estree"; + +export default function read_context( + parser: Parser +): Pattern & { start: number; end: number } { + const start = parser.index; + let i = parser.index; + + const code = full_char_code_at(parser.template, i); + if (isIdentifierStart(code, true)) { + return { + type: "Identifier", + name: parser.read_identifier(), + start, + end: parser.index + }; + } -function error_on_assignment_pattern(parser: Parser) { - if (parser.eat('=')) { + if (!is_bracket_open(code)) { parser.error({ - code: 'invalid-assignment-pattern', - message: 'Assignment patterns are not supported' - }, parser.index - 1); + code: "unexpected-token", + message: "Expected identifier or destructure pattern" + }); } -} - -function error_on_rest_pattern_not_last(parser: Parser) { - parser.error({ - code: 'rest-pattern-not-last', - message: 'Rest destructuring expected to be last' - }, parser.index); -} - -export default function read_context(parser: Parser) { - const context: Context = { - start: parser.index, - end: null, - type: null - }; - if (parser.eat('[')) { - context.type = 'ArrayPattern'; - context.elements = []; - - do { - parser.allow_whitespace(); - - const lastContext = context.elements[context.elements.length - 1]; - if (lastContext && lastContext.type === 'RestIdentifier') { - error_on_rest_pattern_not_last(parser); + const bracket_stack = [code]; + i += code <= 0xffff ? 1 : 2; + + while (i < parser.template.length) { + const code = full_char_code_at(parser.template, i); + if (is_bracket_open(code)) { + bracket_stack.push(code); + } else if (is_bracket_close(code)) { + if (!is_bracket_pair(bracket_stack[bracket_stack.length - 1], code)) { + parser.error({ + code: "unexpected-token", + message: `Expected ${String.fromCharCode( + get_bracket_close(bracket_stack[bracket_stack.length - 1]) + )}` + }); } - - if (parser.template[parser.index] === ',') { - context.elements.push(null); - } else { - context.elements.push(read_context(parser)); - parser.allow_whitespace(); - } - } while (parser.eat(',')); - - error_on_assignment_pattern(parser); - parser.eat(']', true); - context.end = parser.index; - } - - else if (parser.eat('{')) { - context.type = 'ObjectPattern'; - context.properties = []; - - do { - parser.allow_whitespace(); - - if (parser.eat('...')) { - parser.allow_whitespace(); - - const start = parser.index; - const name = parser.read_identifier(); - const key: Identifier = { - start, - end: parser.index, - type: 'Identifier', - name - }; - const property: Property = { - start, - end: parser.index, - type: 'Property', - kind: 'rest', - shorthand: true, - key, - value: key - }; - - context.properties.push(property); - - parser.allow_whitespace(); - - if (parser.eat(',')) { - parser.error({ - code: `comma-after-rest`, - message: `Comma is not permitted after the rest element` - }, parser.index - 1); - } - + bracket_stack.pop(); + if (bracket_stack.length === 0) { + i += code <= 0xffff ? 1 : 2; break; } - - // TODO: DRY this out somehow - // We don't know whether we want to allow reserved words until we see whether there's a ':' after it - // Probably ideally we'd use Acorn to do all of this - const start = parser.index; - const name = parser.read_identifier(true); - const key: Identifier = { - start, - end: parser.index, - type: 'Identifier', - name - }; - parser.allow_whitespace(); - - let value: Context; - if (parser.eat(':')) { - parser.allow_whitespace(); - value = read_context(parser); - } else { - if (reserved.has(name)) { - parser.error({ - code: `unexpected-reserved-word`, - message: `'${name}' is a reserved word in JavaScript and cannot be used here` - }, start); - } - value = key; - } - - const property: Property = { - start, - end: value.end, - type: 'Property', - kind: 'init', - shorthand: value.type === 'Identifier' && value.name === name, - key, - value - }; - - context.properties.push(property); - - parser.allow_whitespace(); - } while (parser.eat(',')); - - error_on_assignment_pattern(parser); - parser.eat('}', true); - context.end = parser.index; - } - - else if (parser.eat('...')) { - const name = parser.read_identifier(); - if (name) { - context.type = 'RestIdentifier'; - context.end = parser.index; - context.name = name; - } - - else { - parser.error({ - code: 'invalid-context', - message: 'Expected a rest pattern' - }); } + i += code <= 0xffff ? 1 : 2; } - else { - const name = parser.read_identifier(); - if (name) { - context.type = 'Identifier'; - context.end = parser.index; - context.name = name; - } - - else { - parser.error({ - code: 'invalid-context', - message: 'Expected a name, array pattern or object pattern' - }); - } - - error_on_assignment_pattern(parser); + parser.index = i; + + const pattern_string = parser.template.slice(start, i); + try { + // the length of the `space_with_newline` has to be start - 1 + // because we added a `(` in front of the pattern_string, + // which shifted the entire string to right by 1 + // so we offset it by removing 1 character in the `space_with_newline` + // to achieve that, we remove the 1st space encountered, + // so it will not affect the `column` of the node + let space_with_newline = parser.template.slice(0, start).replace(/[^\n]/g, ' '); + const first_space = space_with_newline.indexOf(' '); + space_with_newline = space_with_newline.slice(0, first_space) + space_with_newline.slice(first_space + 1); + + return (parse_expression_at( + `${space_with_newline}(${pattern_string} = 1)`, + start - 1 + ) as any).left; + } catch (error) { + parser.acorn_error(error); } - - return context; } diff --git a/src/compiler/parse/state/mustache.ts b/src/compiler/parse/state/mustache.ts index 0f6608f679af..4e1d5c5fd4cc 100644 --- a/src/compiler/parse/state/mustache.ts +++ b/src/compiler/parse/state/mustache.ts @@ -196,7 +196,7 @@ export default function mustache(parser: Parser) { if (!parser.eat('}')) { parser.require_whitespace(); - await_block[is_then ? 'value': 'error'] = parser.read_destructure_pattern(); + await_block[is_then ? 'value': 'error'] = read_context(parser); parser.allow_whitespace(); parser.eat('}', true); } @@ -305,14 +305,14 @@ export default function mustache(parser: Parser) { const await_block_shorthand = type === 'AwaitBlock' && parser.eat('then'); if (await_block_shorthand) { parser.require_whitespace(); - block.value = parser.read_destructure_pattern(); + block.value = read_context(parser); parser.allow_whitespace(); } const await_block_catch_shorthand = !await_block_shorthand && type === 'AwaitBlock' && parser.eat('catch'); if (await_block_catch_shorthand) { parser.require_whitespace(); - block.error = parser.read_destructure_pattern(); + block.error = read_context(parser); parser.allow_whitespace(); } diff --git a/test/parser/samples/await-catch/output.json b/test/parser/samples/await-catch/output.json index d47d27e4cb93..c5435830184b 100644 --- a/test/parser/samples/await-catch/output.json +++ b/test/parser/samples/await-catch/output.json @@ -26,6 +26,8 @@ }, "value": null, "error": { + "start": 47, + "end": 55, "type": "Identifier", "name": "theError" }, diff --git a/test/parser/samples/await-then-catch/output.json b/test/parser/samples/await-then-catch/output.json index 01dc89f3f904..8e4b7a4c32e5 100644 --- a/test/parser/samples/await-then-catch/output.json +++ b/test/parser/samples/await-then-catch/output.json @@ -25,10 +25,14 @@ "name": "thePromise" }, "value": { + "start": 46, + "end": 54, "type": "Identifier", "name": "theValue" }, "error": { + "start": 96, + "end": 104, "type": "Identifier", "name": "theError" }, diff --git a/test/parser/samples/each-block-destructured/input.svelte b/test/parser/samples/each-block-destructured/input.svelte index 09e9885be019..5313050fa0b2 100644 --- a/test/parser/samples/each-block-destructured/input.svelte +++ b/test/parser/samples/each-block-destructured/input.svelte @@ -1,3 +1,7 @@ + + {#each animals as [key, value, ...rest]}

{key}: {value}

{/each} diff --git a/test/parser/samples/each-block-destructured/output.json b/test/parser/samples/each-block-destructured/output.json index 425c609a2ccd..69e165c0a4c4 100644 --- a/test/parser/samples/each-block-destructured/output.json +++ b/test/parser/samples/each-block-destructured/output.json @@ -1,24 +1,31 @@ { "html": { - "start": 0, - "end": 71, + "start": 41, + "end": 112, "type": "Fragment", "children": [ { - "start": 0, - "end": 71, + "start": 39, + "end": 41, + "type": "Text", + "raw": "\n\n", + "data": "\n\n" + }, + { + "start": 41, + "end": 112, "type": "EachBlock", "expression": { "type": "Identifier", - "start": 7, - "end": 14, + "start": 48, + "end": 55, "loc": { "start": { - "line": 1, + "line": 5, "column": 7 }, "end": { - "line": 1, + "line": 5, "column": 14 } }, @@ -26,27 +33,27 @@ }, "children": [ { - "start": 42, - "end": 63, + "start": 83, + "end": 104, "type": "Element", "name": "p", "attributes": [], "children": [ { - "start": 45, - "end": 50, + "start": 86, + "end": 91, "type": "MustacheTag", "expression": { "type": "Identifier", - "start": 46, - "end": 49, + "start": 87, + "end": 90, "loc": { "start": { - "line": 2, + "line": 6, "column": 5 }, "end": { - "line": 2, + "line": 6, "column": 8 } }, @@ -54,27 +61,27 @@ } }, { - "start": 50, - "end": 52, + "start": 91, + "end": 93, "type": "Text", "raw": ": ", "data": ": " }, { - "start": 52, - "end": 59, + "start": 93, + "end": 100, "type": "MustacheTag", "expression": { "type": "Identifier", - "start": 53, - "end": 58, + "start": 94, + "end": 99, "loc": { "start": { - "line": 2, + "line": 6, "column": 12 }, "end": { - "line": 2, + "line": 6, "column": 17 } }, @@ -85,31 +92,177 @@ } ], "context": { - "start": 18, - "end": 39, "type": "ArrayPattern", + "start": 59, + "end": 80, + "loc": { + "start": { + "line": 5, + "column": 19 + }, + "end": { + "line": 5, + "column": 40 + } + }, "elements": [ { - "start": 19, - "end": 22, "type": "Identifier", + "start": 60, + "end": 63, + "loc": { + "start": { + "line": 5, + "column": 20 + }, + "end": { + "line": 5, + "column": 23 + } + }, "name": "key" }, { - "start": 24, - "end": 29, "type": "Identifier", + "start": 65, + "end": 70, + "loc": { + "start": { + "line": 5, + "column": 25 + }, + "end": { + "line": 5, + "column": 30 + } + }, "name": "value" }, { - "start": 31, - "end": 38, - "type": "RestIdentifier", - "name": "rest" + "type": "RestElement", + "start": 72, + "end": 79, + "loc": { + "start": { + "line": 5, + "column": 32 + }, + "end": { + "line": 5, + "column": 39 + } + }, + "argument": { + "type": "Identifier", + "start": 75, + "end": 79, + "loc": { + "start": { + "line": 5, + "column": 35 + }, + "end": { + "line": 5, + "column": 39 + } + }, + "name": "rest" + } } ] } } ] + }, + "instance": { + "type": "Script", + "start": 0, + "end": 39, + "context": "default", + "content": { + "type": "Program", + "start": 8, + "end": 30, + "loc": { + "start": { + "line": 1, + "column": 0 + }, + "end": { + "line": 3, + "column": 0 + } + }, + "body": [ + { + "type": "ExportNamedDeclaration", + "start": 10, + "end": 29, + "loc": { + "start": { + "line": 2, + "column": 1 + }, + "end": { + "line": 2, + "column": 20 + } + }, + "declaration": { + "type": "VariableDeclaration", + "start": 17, + "end": 29, + "loc": { + "start": { + "line": 2, + "column": 8 + }, + "end": { + "line": 2, + "column": 20 + } + }, + "declarations": [ + { + "type": "VariableDeclarator", + "start": 21, + "end": 28, + "loc": { + "start": { + "line": 2, + "column": 12 + }, + "end": { + "line": 2, + "column": 19 + } + }, + "id": { + "type": "Identifier", + "start": 21, + "end": 28, + "loc": { + "start": { + "line": 2, + "column": 12 + }, + "end": { + "line": 2, + "column": 19 + } + }, + "name": "animals" + }, + "init": null + } + ], + "kind": "let" + }, + "specifiers": [], + "source": null + } + ], + "sourceType": "module" + } } } \ No newline at end of file diff --git a/test/parser/samples/no-error-if-before-closing/output.json b/test/parser/samples/no-error-if-before-closing/output.json index e30c302e0f95..708128a42eb9 100644 --- a/test/parser/samples/no-error-if-before-closing/output.json +++ b/test/parser/samples/no-error-if-before-closing/output.json @@ -116,6 +116,8 @@ "raw": "true" }, "value": { + "start": 97, + "end": 98, "type": "Identifier", "name": "f" }, @@ -202,6 +204,8 @@ "raw": "true" }, "value": { + "start": 137, + "end": 138, "type": "Identifier", "name": "f" }, diff --git a/test/runtime/samples/each-block-destructured-default/_config.js b/test/runtime/samples/each-block-destructured-default/_config.js new file mode 100644 index 000000000000..133fd685321a --- /dev/null +++ b/test/runtime/samples/each-block-destructured-default/_config.js @@ -0,0 +1,22 @@ +export default { + props: { + animalEntries: [ + { animal: 'raccoon', class: 'mammal', species: 'P. lotor', kilogram: 25 }, + { animal: 'eagle', class: 'bird', kilogram: 5.4 } + ] + }, + + html: ` +

raccoon - P. lotor - 25kg

+

eagle - unknown - 5.4kg

+ `, + + + + test({ assert, component, target }) { + component.animalEntries = [{ animal: 'cow', class: 'mammal', species: '‎B. taurus' }]; + assert.htmlEqual(target.innerHTML, ` +

cow - ‎B. taurus - 50kg

+ `); + }, +}; diff --git a/test/runtime/samples/each-block-destructured-default/main.svelte b/test/runtime/samples/each-block-destructured-default/main.svelte new file mode 100644 index 000000000000..a91b45299e5c --- /dev/null +++ b/test/runtime/samples/each-block-destructured-default/main.svelte @@ -0,0 +1,7 @@ + + +{#each animalEntries as { animal, species = 'unknown', kilogram: weight = 50 , ...props } } +

{animal} - {species} - {weight}kg

+{/each} diff --git a/test/validator/samples/each-block-destructured-object-rest-comma-after/errors.json b/test/validator/samples/each-block-destructured-object-rest-comma-after/errors.json index 549b6960eb33..df899b7702ad 100644 --- a/test/validator/samples/each-block-destructured-object-rest-comma-after/errors.json +++ b/test/validator/samples/each-block-destructured-object-rest-comma-after/errors.json @@ -1,5 +1,5 @@ [{ - "code": "comma-after-rest", + "code": "parse-error", "message": "Comma is not permitted after the rest element", "pos": 100, "start": { diff --git a/test/validator/samples/each-block-invalid-context-destructured-object/errors.json b/test/validator/samples/each-block-invalid-context-destructured-object/errors.json index 085021ff5ac0..c96e3d2c8cc7 100644 --- a/test/validator/samples/each-block-invalid-context-destructured-object/errors.json +++ b/test/validator/samples/each-block-invalid-context-destructured-object/errors.json @@ -1,6 +1,6 @@ [{ - "code": "unexpected-reserved-word", - "message": "'case' is a reserved word in JavaScript and cannot be used here", + "code": "parse-error", + "message": "Unexpected keyword 'case'", "start": { "line": 1, "column": 18, diff --git a/test/validator/samples/each-block-invalid-context-destructured/errors.json b/test/validator/samples/each-block-invalid-context-destructured/errors.json index afe99ee219bc..62d6f62e87c2 100644 --- a/test/validator/samples/each-block-invalid-context-destructured/errors.json +++ b/test/validator/samples/each-block-invalid-context-destructured/errors.json @@ -1,6 +1,6 @@ [{ - "code": "unexpected-reserved-word", - "message": "'case' is a reserved word in JavaScript and cannot be used here", + "code": "parse-error", + "message": "Unexpected token", "start": { "line": 1, "column": 17,