support destructuring in await (#4548)

pull/4564/head
Tan Li Hau 4 years ago committed by GitHub
parent 5bb5ba4c76
commit 7d1e4e82ff
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -2,6 +2,7 @@
## Unreleased
* Allow destructuring in `{#await}` blocks ([#1851](https://github.com/sveltejs/svelte/issues/1851))
* Allow `<svelte:self>` to be used in a slot ([#2798](https://github.com/sveltejs/svelte/issues/2798))
* Expose object of unknown props in `$$restProps` ([#2930](https://github.com/sveltejs/svelte/issues/2930))
* Prevent passing named slots other than from the top level within a component ([#3385](https://github.com/sveltejs/svelte/issues/3385))

@ -3,27 +3,45 @@ 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';
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 = [];
traverse_destructure_pattern(pattern, (node) => this.expressions.push(node.name));
this.identifier_name = this.pattern.type === 'Identifier' ? this.pattern.name : undefined;
}
}

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

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

@ -46,6 +46,7 @@ export default class Block {
}>;
chunks: {
declarations: Array<Node | Node[]>;
init: Array<Node | Node[]>;
create: Array<Node | Node[]>;
claim: Array<Node | Node[]>;
@ -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}`

@ -9,6 +9,7 @@ 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';
class AwaitBlockBranch extends Wrapper {
node: PendingBlock | ThenBlock | CatchBlock;
@ -46,6 +47,23 @@ 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') {
traverse_destructure_pattern(node.pattern, (node, parent, index) => {
parent[index] = x`#ctx[${block.renderer.context_lookup.get(node.name).index}]`;
});
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 +73,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 +92,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 +138,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 +166,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 +232,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 +246,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 +269,9 @@ 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);
}
}

@ -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}) { return ${then}; }(__value));
return (function(${node.value ? node.value.pattern : ''}) { return ${then}; }(__value));
}(${node.expression.node})
`);
}

@ -0,0 +1,35 @@
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);
}

@ -5,6 +5,9 @@ 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);
@ -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({

@ -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);
}
@ -305,7 +305,7 @@ 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_identifier();
block.value = parser.read_destructure_pattern();
parser.allow_whitespace();
}

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

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

@ -25,7 +25,10 @@
"name": "thePromise"
},
"value": null,
"error": "theError",
"error": {
"type": "Identifier",
"name": "theError"
},
"pending": {
"start": 19,
"end": 39,

@ -24,8 +24,14 @@
},
"name": "thePromise"
},
"value": "theValue",
"error": "theError",
"value": {
"type": "Identifier",
"name": "theValue"
},
"error": {
"type": "Identifier",
"name": "theError"
},
"pending": {
"start": 19,
"end": 39,

@ -115,7 +115,10 @@
"value": true,
"raw": "true"
},
"value": "f",
"value": {
"type": "Identifier",
"name": "f"
},
"error": null,
"pending": {
"start": 80,
@ -198,7 +201,10 @@
"value": true,
"raw": "true"
},
"value": "f",
"value": {
"type": "Identifier",
"name": "f"
},
"error": null,
"pending": {
"start": 123,

@ -0,0 +1,61 @@
export default {
props: {
thePromise: new Promise(resolve => {})
},
html: `
loading...
`,
async test({ assert, component, target }) {
await (component.thePromise = Promise.resolve([1, 2]));
assert.htmlEqual(
target.innerHTML,
`
<p>a: 1</p>
<p>b: 2</p>
`
);
await (component.thePromise = Promise.resolve([4, 5]));
assert.htmlEqual(
target.innerHTML,
`
<p>a: 4</p>
<p>b: 5</p>
`
);
try {
await (component.thePromise = Promise.reject(['a', [6, 7]]));
} catch (e) {
// do nothing
}
assert.htmlEqual(
target.innerHTML,
`
<p>c: a</p>
<p>d: 6</p>
<p>e: 7</p>
`
);
try {
await (component.thePromise = Promise.reject(['b', [8, 9]]));
} catch (e) {
// do nothing
}
assert.htmlEqual(
target.innerHTML,
`
<p>c: b</p>
<p>d: 8</p>
<p>e: 9</p>
`
);
}
};

@ -0,0 +1,14 @@
<script>
export let thePromise;
</script>
{#await thePromise}
loading...
{:then [ a, b ]}
<p>a: {a}</p>
<p>b: {b}</p>
{:catch [c, [d, e]]}
<p>c: {c}</p>
<p>d: {d}</p>
<p>e: {e}</p>
{/await}

@ -0,0 +1,23 @@
export default {
async test({ assert, component, target }) {
await Promise.resolve();
assert.htmlEqual(
target.innerHTML,
`
<p>a: 3</p>
<p>b: 2</p>
<p>c: 3</p>
<p>a: 1</p>
<p>b: 2</p>
<p>c: 3</p>
<p>a: 3</p>
<p>b: 2</p>
<p>c: 3</p>
<p>a: 1</p>
<p>b: 2</p>
<p>c: 3</p>
`
);
}
};

@ -0,0 +1,34 @@
<script>
let object = Promise.resolve({ b: 2, c: 3 });
let array = Promise.resolve([1, 2]);
let objectReject = Promise.reject({ b: 2, c: 3 });
let arrayReject = Promise.reject([1, 2]);
</script>
{#await object then { a = 3, b = 4, c }}
<p>a: {a}</p>
<p>b: {b}</p>
<p>c: {c}</p>
{/await}
{#await array then [a, b, c = 3]}
<p>a: {a}</p>
<p>b: {b}</p>
<p>c: {c}</p>
{/await}
{#await objectReject then value}
resolved
{:catch { a = 3, b = 4, c }}
<p>a: {a}</p>
<p>b: {b}</p>
<p>c: {c}</p>
{/await}
{#await arrayReject then value}
resolved
{:catch [a, b, c = 3]}
<p>a: {a}</p>
<p>b: {b}</p>
<p>c: {c}</p>
{/await}

@ -0,0 +1,63 @@
export default {
props: {
thePromise: new Promise(resolve => {})
},
html: `
loading...
`,
async test({ assert, component, target }) {
await (component.thePromise = Promise.resolve({ error: "error message" }));
assert.htmlEqual(
target.innerHTML,
`
<p>error: error message</p>
<p>result: undefined</p>
`
);
await (component.thePromise = Promise.resolve({ result: "42" }));
assert.htmlEqual(
target.innerHTML,
`
<p>error: undefined</p>
<p>result: 42</p>
`
);
try {
await (component.thePromise = Promise.reject({
error: { message: "oops", code: "123" }
}));
} catch (e) {
// do nothing
}
assert.htmlEqual(
target.innerHTML,
`
<p>message: oops</p>
<p>code: 123</p>
`
);
try {
await (component.thePromise = Promise.reject({
error: { message: "timeout", code: "456" }
}));
} catch (e) {
// do nothing
}
assert.htmlEqual(
target.innerHTML,
`
<p>message: timeout</p>
<p>code: 456</p>
`
);
}
};

@ -0,0 +1,13 @@
<script>
export let thePromise;
</script>
{#await thePromise}
loading...
{:then { result, error }}
<p>error: {error}</p>
<p>result: {result}</p>
{:catch { error: { message, code } }}
<p>message: {message}</p>
<p>code: {code}</p>
{/await}

@ -0,0 +1,21 @@
export default {
async test({ assert, component, target }) {
await Promise.resolve();
assert.htmlEqual(
target.innerHTML,
`
<p>a: 1</p>
<p>rest: {"b":2,"c":3}</p>
<p>a: 1</p>
<p>b: 2</p>
<p>rest: [3,4,5,6]</p>
<p>a: 1</p>
<p>rest: {"b":2,"c":3}</p>
<p>a: 1</p>
<p>b: 2</p>
<p>rest: [3,4,5,6]</p>
`
);
}
};

@ -0,0 +1,32 @@
<script>
let object = Promise.resolve({ a: 1, b: 2, c: 3 });
let array = Promise.resolve([1, 2, 3, 4, 5, 6]);
let objectReject = Promise.reject({ a: 1, b: 2, c: 3 });
let arrayReject = Promise.reject([1, 2, 3, 4, 5, 6]);
</script>
{#await object then { a, ...rest }}
<p>a: {a}</p>
<p>rest: {JSON.stringify(rest)}</p>
{/await}
{#await array then [a, b, ...rest]}
<p>a: {a}</p>
<p>b: {b}</p>
<p>rest: {JSON.stringify(rest)}</p>
{/await}
{#await objectReject then value}
resolved
{:catch { a, ...rest }}
<p>a: {a}</p>
<p>rest: {JSON.stringify(rest)}</p>
{/await}
{#await arrayReject then value}
resolved
{:catch [a, b, ...rest]}
<p>a: {a}</p>
<p>b: {b}</p>
<p>rest: {JSON.stringify(rest)}</p>
{/await}
Loading…
Cancel
Save