diff --git a/src/compiler/compile/nodes/AwaitBlock.ts b/src/compiler/compile/nodes/AwaitBlock.ts index cd57750d29..55d9d5a7d2 100644 --- a/src/compiler/compile/nodes/AwaitBlock.ts +++ b/src/compiler/compile/nodes/AwaitBlock.ts @@ -3,27 +3,71 @@ 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 get_object from '../utils/get_object'; export default class AwaitBlock extends Node { type: 'AwaitBlock'; expression: Expression; - value: string; - error: string; + value: DestructurePattern; + error: DestructurePattern; pending: PendingBlock; then: ThenBlock; catch: CatchBlock; - constructor(component, parent, scope, info) { + constructor(component: Component, parent, scope: TemplateScope, info: TemplateNode) { super(component, parent, scope, info); this.expression = new Expression(component, this, scope, info.expression); - this.value = info.value; - this.error = info.error; + this.value = info.value && new DestructurePattern(info.value); + this.error = info.error && new DestructurePattern(info.error); 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 = get_context_from_expression(this.pattern, []); + this.identifier_name = this.pattern.type === 'Identifier' ? this.pattern.name : undefined; + } +} +function get_context_from_expression(node: Pattern, result: string[]): string[] { + switch (node.type) { + case 'Identifier': + result.push(node.name); + return result; + case 'ArrayPattern': + for (const element of node.elements) { + get_context_from_expression(element, result); + } + return result; + case 'ObjectPattern': + for (const property of node.properties) { + get_context_from_expression(property.value, result); + } + return result; + case 'MemberExpression': + get_context_from_expression(get_object(node), result); + return result; + case 'RestElement': + get_context_from_expression(node.argument, result); + return result; + case 'AssignmentPattern': + get_context_from_expression(node.left, result); + return result; + } +} + diff --git a/src/compiler/compile/nodes/CatchBlock.ts b/src/compiler/compile/nodes/CatchBlock.ts index 0edc0f76d8..8b3736a2b9 100644 --- a/src/compiler/compile/nodes/CatchBlock.ts +++ b/src/compiler/compile/nodes/CatchBlock.ts @@ -1,16 +1,23 @@ 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'; export default class CatchBlock extends AbstractBlock { type: 'CatchBlock'; scope: TemplateScope; - constructor(component, parent, scope, info) { + constructor(component: Component, parent: AwaitBlock, scope: TemplateScope, info: TemplateNode) { super(component, parent, scope, info); this.scope = scope.child(); - this.scope.add(parent.error, parent.expression.dependencies, this); + if (parent.error) { + parent.error.expressions.forEach(expression => { + this.scope.add(expression, parent.expression.dependencies, this); + }); + } this.children = map_children(component, parent, this.scope, info.children); if (!info.skip) { diff --git a/src/compiler/compile/nodes/ThenBlock.ts b/src/compiler/compile/nodes/ThenBlock.ts index 7f9bbde0f0..7eefe2e6fb 100644 --- a/src/compiler/compile/nodes/ThenBlock.ts +++ b/src/compiler/compile/nodes/ThenBlock.ts @@ -1,16 +1,23 @@ 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'; export default class ThenBlock extends AbstractBlock { type: 'ThenBlock'; scope: TemplateScope; - constructor(component, parent, scope, info) { + constructor(component: Component, parent: AwaitBlock, scope: TemplateScope, info: TemplateNode) { super(component, parent, scope, info); this.scope = scope.child(); - this.scope.add(parent.value, parent.expression.dependencies, this); + if (parent.value) { + parent.value.expressions.forEach(expression => { + this.scope.add(expression, parent.expression.dependencies, this); + }); + } this.children = map_children(component, parent, this.scope, info.children); if (!info.skip) { diff --git a/src/compiler/compile/render_dom/Block.ts b/src/compiler/compile/render_dom/Block.ts index 2da77d3fbf..3deba786b4 100644 --- a/src/compiler/compile/render_dom/Block.ts +++ b/src/compiler/compile/render_dom/Block.ts @@ -46,6 +46,7 @@ export default class Block { }>; chunks: { + declarations: Array; init: Array; create: Array; claim: Array; @@ -93,6 +94,7 @@ export default class Block { this.bindings = options.bindings; this.chunks = { + declarations: [], init: [], create: [], claim: [], @@ -384,6 +386,8 @@ export default class Block { const block = dev && this.get_unique_name('block'); const body = b` + ${this.chunks.declarations} + ${Array.from(this.variables.values()).map(({ id, init }) => { return init ? b`let ${id} = ${init}` diff --git a/src/compiler/compile/render_dom/wrappers/AwaitBlock.ts b/src/compiler/compile/render_dom/wrappers/AwaitBlock.ts index 12b7fc2cef..3f293b0646 100644 --- a/src/compiler/compile/render_dom/wrappers/AwaitBlock.ts +++ b/src/compiler/compile/render_dom/wrappers/AwaitBlock.ts @@ -8,7 +8,7 @@ import FragmentWrapper from './Fragment'; import PendingBlock from '../../nodes/PendingBlock'; import ThenBlock from '../../nodes/ThenBlock'; import CatchBlock from '../../nodes/CatchBlock'; -import { Identifier } from 'estree'; +import { Identifier, Pattern } from 'estree'; class AwaitBlockBranch extends Wrapper { node: PendingBlock | ThenBlock | CatchBlock; @@ -46,6 +46,20 @@ class AwaitBlockBranch extends Wrapper { this.is_dynamic = this.block.dependencies.size > 0; } + + 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') { + replace_context(block, node.pattern); + 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}])`); + } + } + } } export default class AwaitBlockWrapper extends Wrapper { @@ -55,6 +69,9 @@ export default class AwaitBlockWrapper extends Wrapper { then: AwaitBlockBranch; catch: AwaitBlockBranch; + value: string; + error: string; + var: Identifier = { type: 'Identifier', name: 'await_block' }; constructor( @@ -71,8 +88,20 @@ export default class AwaitBlockWrapper extends Wrapper { this.not_static_content(); block.add_dependencies(this.node.expression.dependencies); - if (this.node.value) block.renderer.add_to_context(this.node.value, true); - if (this.node.error) block.renderer.add_to_context(this.node.error, true); + 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; @@ -105,17 +134,11 @@ export default class AwaitBlockWrapper extends Wrapper { this[status] = branch; }); - this.pending.block.has_update_method = is_dynamic; - this.then.block.has_update_method = is_dynamic; - this.catch.block.has_update_method = is_dynamic; - - this.pending.block.has_intro_method = has_intros; - this.then.block.has_intro_method = has_intros; - this.catch.block.has_intro_method = has_intros; - - this.pending.block.has_outro_method = has_outros; - this.then.block.has_outro_method = has_outros; - this.catch.block.has_outro_method = has_outros; + ['pending', 'then', 'catch'].forEach(status => { + this[status].block.has_update_method = is_dynamic; + this[status].block.has_intro_method = has_intros; + this[status].block.has_outro_method = has_outros; + }); if (has_outros) { block.add_outro(); @@ -139,8 +162,8 @@ export default class AwaitBlockWrapper extends Wrapper { block.maintain_context = true; - const value_index = this.node.value && block.renderer.context_lookup.get(this.node.value).index; - const error_index = this.node.error && block.renderer.context_lookup.get(this.node.error).index; + 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, @@ -205,7 +228,7 @@ export default class AwaitBlockWrapper extends Wrapper { } else { const #child_ctx = #ctx.slice(); - ${this.node.value && b`#child_ctx[${value_index}] = ${info}.resolved;`} + ${this.value && b`#child_ctx[${value_index}] = ${info}.resolved;`} ${info}.block.p(#child_ctx, #dirty); } `); @@ -219,7 +242,7 @@ export default class AwaitBlockWrapper extends Wrapper { block.chunks.update.push(b` { const #child_ctx = #ctx.slice(); - ${this.node.value && b`#child_ctx[${value_index}] = ${info}.resolved;`} + ${this.value && b`#child_ctx[${value_index}] = ${info}.resolved;`} ${info}.block.p(#child_ctx, #dirty); } `); @@ -242,7 +265,53 @@ export default class AwaitBlockWrapper extends Wrapper { `); [this.pending, this.then, this.catch].forEach(branch => { - branch.fragment.render(branch.block, null, x`#nodes` as Identifier); + 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); } } + +function replace_context(block: Block, pattern: Pattern) { + if (pattern.type === 'ObjectPattern') { + for (const property of pattern.properties) { + if (property.value.type === 'Identifier') { + // @ts-ignore + property.value = x`#ctx[${block.renderer.context_lookup.get(property.value.name).index}]`; + } else { + replace_context(block, property.value); + } + } + } else if (pattern.type === 'ArrayPattern') { + for (let i=0; i (ParserState | void); @@ -89,7 +92,7 @@ export class Parser { }, err.pos); } - error({ code, message }: { code: string; message: string }, index = this.index) { + error({ code, message }: { code: string; message: string }, index = this.index): never { error(message, { name: 'ParseError', code, @@ -170,6 +173,51 @@ 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/state/mustache.ts b/src/compiler/parse/state/mustache.ts index e5e365dddf..d46dec3797 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_identifier(); + await_block[is_then ? 'value': 'error'] = parser.read_destructure_pattern(); parser.allow_whitespace(); parser.eat('}', true); } diff --git a/src/compiler/parse/utils/bracket.ts b/src/compiler/parse/utils/bracket.ts new file mode 100644 index 0000000000..ffbb9e4952 --- /dev/null +++ b/src/compiler/parse/utils/bracket.ts @@ -0,0 +1,28 @@ +const SQUARE_BRACKET_OPEN = "[".charCodeAt(0); +const SQUARE_BRACKET_CLOSE = "]".charCodeAt(0); +const CURLY_BRACKET_OPEN = "{".charCodeAt(0); +const CURLY_BRACKET_CLOSE = "}".charCodeAt(0); + +export function is_bracket_open(code) { + return code === SQUARE_BRACKET_OPEN || code === CURLY_BRACKET_OPEN; +} + +export function is_bracket_close(code) { + return code === SQUARE_BRACKET_CLOSE || code === CURLY_BRACKET_CLOSE; +} + +export function is_bracket_pair(open, close) { + return ( + (open === SQUARE_BRACKET_OPEN && close === SQUARE_BRACKET_CLOSE) || + (open === CURLY_BRACKET_OPEN && close === CURLY_BRACKET_CLOSE) + ); +} + +export function get_bracket_close(open) { + if (open === SQUARE_BRACKET_OPEN) { + return SQUARE_BRACKET_CLOSE; + } + if (open === CURLY_BRACKET_OPEN) { + return CURLY_BRACKET_CLOSE; + } +} \ No newline at end of file diff --git a/src/compiler/utils/error.ts b/src/compiler/utils/error.ts index d13222578a..e05ea66df1 100644 --- a/src/compiler/utils/error.ts +++ b/src/compiler/utils/error.ts @@ -21,7 +21,7 @@ export default function error(message: string, props: { filename: string; start: number; end?: number; -}) { +}): never { const error = new CompileError(message); error.name = props.name; diff --git a/test/runtime/samples/await-then-destruct-array/_config.js b/test/runtime/samples/await-then-destruct-array/_config.js new file mode 100644 index 0000000000..56ec7ebd24 --- /dev/null +++ b/test/runtime/samples/await-then-destruct-array/_config.js @@ -0,0 +1,29 @@ +export default { + props: { + thePromise: new Promise(resolve => {}), + }, + + html: ` + loading... + `, + + async test({ assert, component, target }) { + let promise = Promise.resolve([1, 2]); + component.thePromise = promise; + + await promise; + assert.htmlEqual(target.innerHTML, ` +

a: 1

+

b: 2

+ `); + + promise = Promise.resolve([4, 5]); + component.thePromise = promise; + + await promise; + assert.htmlEqual(target.innerHTML, ` +

a: 4

+

b: 5

+ `); + } +}; \ No newline at end of file diff --git a/test/runtime/samples/await-then-destruct-array/main.svelte b/test/runtime/samples/await-then-destruct-array/main.svelte new file mode 100644 index 0000000000..71249207c1 --- /dev/null +++ b/test/runtime/samples/await-then-destruct-array/main.svelte @@ -0,0 +1,10 @@ + + +{#await thePromise} + loading... +{:then [ a, b ]} +

a: {a}

+

b: {b}

+{/await} \ No newline at end of file diff --git a/test/runtime/samples/await-then-destruct-object/_config.js b/test/runtime/samples/await-then-destruct-object/_config.js new file mode 100644 index 0000000000..839a5a6854 --- /dev/null +++ b/test/runtime/samples/await-then-destruct-object/_config.js @@ -0,0 +1,27 @@ +export default { + props: { + thePromise: new Promise(resolve => {}), + }, + + html: ` + loading... + `, + + async test({ assert, component, target }) { + let promise = Promise.resolve({ error: 'error message' }); + component.thePromise = promise; + + await promise; + assert.htmlEqual(target.innerHTML, ` +

error: error message

+ `); + + promise = Promise.resolve({ result: '42' }); + component.thePromise = promise; + + await promise; + assert.htmlEqual(target.innerHTML, ` +

result: 42

+ `); + } +}; \ No newline at end of file diff --git a/test/runtime/samples/await-then-destruct-object/main.svelte b/test/runtime/samples/await-then-destruct-object/main.svelte new file mode 100644 index 0000000000..9cac1e1614 --- /dev/null +++ b/test/runtime/samples/await-then-destruct-object/main.svelte @@ -0,0 +1,13 @@ + + +{#await thePromise} + loading... +{:then { result, error }} + {#if error} +

error: { error }

+ {:else} +

result: {result}

+ {/if} +{/await} \ No newline at end of file