From 908fe2ab7a278ab9ec569d6db3cdb1b82719d4ec Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Fri, 24 Nov 2017 19:02:18 -0500 Subject: [PATCH 1/9] parse await-then-catch --- src/parse/index.ts | 12 ++ src/parse/state/mustache.ts | 74 ++++++--- test/parser/index.js | 3 +- .../samples/await-then-catch/input.html | 7 + .../samples/await-then-catch/output.json | 144 ++++++++++++++++++ 5 files changed, 221 insertions(+), 19 deletions(-) create mode 100644 test/parser/samples/await-then-catch/input.html create mode 100644 test/parser/samples/await-then-catch/output.json diff --git a/src/parse/index.ts b/src/parse/index.ts index c725ab90c3..ff2714cb03 100644 --- a/src/parse/index.ts +++ b/src/parse/index.ts @@ -3,6 +3,7 @@ import fragment from './state/fragment'; import { whitespace } from '../utils/patterns'; import { trimStart, trimEnd } from '../utils/trim'; import getCodeFrame from '../utils/getCodeFrame'; +import reservedNames from '../utils/reservedNames'; import hash from './utils/hash'; import { Node, Parsed } from '../interfaces'; import CompileError from '../utils/CompileError'; @@ -139,6 +140,17 @@ export class Parser { return match[0]; } + readIdentifier() { + const start = this.index; + const identifier = this.read(/[a-zA-Z_$][a-zA-Z0-9_$]*/); + + if (reservedNames.has(identifier)) { + this.error(`'${identifier}' is a reserved word in JavaScript and cannot be used here`, start); + } + + return identifier; + } + readUntil(pattern: RegExp) { if (this.index >= this.template.length) this.error('Unexpected end of input'); diff --git a/src/parse/state/mustache.ts b/src/parse/state/mustache.ts index 3ca6003242..49d22322b1 100644 --- a/src/parse/state/mustache.ts +++ b/src/parse/state/mustache.ts @@ -5,8 +5,6 @@ import reservedNames from '../../utils/reservedNames'; import { Parser } from '../index'; import { Node } from '../../interfaces'; -const validIdentifier = /[a-zA-Z_$][a-zA-Z0-9_$]*/; - function trimWhitespace(block: Node, trimBefore: boolean, trimAfter: boolean) { const firstChild = block.children[0]; const lastChild = block.children[block.children.length - 1]; @@ -41,16 +39,20 @@ export default function mustache(parser: Parser) { let block = parser.current(); let expected; - if (block.type === 'ElseBlock') { + if (block.type === 'ElseBlock' || block.type === 'ThenBlock' || block.type === 'CatchBlock') { block.end = start; parser.stack.pop(); block = parser.current(); + + expected = 'await'; } if (block.type === 'IfBlock') { expected = 'if'; } else if (block.type === 'EachBlock') { expected = 'each'; + } else if (block.type === 'AwaitBlock') { + expected = 'await'; } else { parser.error(`Unexpected block closing tag`); } @@ -131,6 +133,50 @@ export default function mustache(parser: Parser) { }; parser.stack.push(block.else); + } else if (parser.eat('then')) { + // {{then}} is valid by itself — we need to check that a) + // we're inside an await block, and b) there's an expression + const awaitBlock = parser.current(); + if (awaitBlock.type === 'AwaitBlock') { + parser.requireWhitespace(); + awaitBlock.value = parser.readIdentifier(); + + parser.allowWhitespace(); + parser.eat('}}', true); + + const thenBlock: Node = { + start, + end: null, + type: 'ThenBlock', + children: [] + }; + + awaitBlock.then = thenBlock; + parser.stack.push(thenBlock); + } + } else if (parser.eat('catch')) { + const thenBlock = parser.current(); + if (thenBlock.type === 'ThenBlock') { + thenBlock.end = start; + parser.stack.pop(); + const awaitBlock = parser.current(); + + parser.requireWhitespace(); + awaitBlock.error = parser.readIdentifier(); + + parser.allowWhitespace(); + parser.eat('}}', true); + + const catchBlock: Node = { + start, + end: null, + type: 'CatchBlock', + children: [] + }; + + awaitBlock.catch = catchBlock; + parser.stack.push(catchBlock); + } } else if (parser.eat('#')) { // {{#if foo}} or {{#each foo}} let type; @@ -139,8 +185,10 @@ export default function mustache(parser: Parser) { type = 'IfBlock'; } else if (parser.eat('each')) { type = 'EachBlock'; + } else if (parser.eat('await')) { + type = 'AwaitBlock'; } else { - parser.error(`Expected if or each`); + parser.error(`Expected if, each or await`); } parser.requireWhitespace(); @@ -170,13 +218,8 @@ export default function mustache(parser: Parser) { do { parser.allowWhitespace(); - const start = parser.index; - const destructuredContext = parser.read(validIdentifier); - + const destructuredContext = parser.readIdentifier(); if (!destructuredContext) parser.error(`Expected name`); - if (reservedNames.has(destructuredContext)) { - parser.error(`'${destructuredContext}' is a reserved word in JavaScript and cannot be used here`, start); - } block.destructuredContexts.push(destructuredContext); parser.allowWhitespace(); @@ -188,12 +231,7 @@ export default function mustache(parser: Parser) { parser.allowWhitespace(); parser.eat(']', true); } else { - const start = parser.index; - block.context = parser.read(validIdentifier); - if (reservedNames.has(block.context)) { - parser.error(`'${block.context}' is a reserved word in JavaScript and cannot be used here`, start); - } - + block.context = parser.readIdentifier(); if (!block.context) parser.error(`Expected name`); } @@ -201,13 +239,13 @@ export default function mustache(parser: Parser) { if (parser.eat(',')) { parser.allowWhitespace(); - block.index = parser.read(validIdentifier); + block.index = parser.readIdentifier(); if (!block.index) parser.error(`Expected name`); parser.allowWhitespace(); } if (parser.eat('@')) { - block.key = parser.read(validIdentifier); + block.key = parser.readIdentifier(); if (!block.key) parser.error(`Expected name`); parser.allowWhitespace(); } diff --git a/test/parser/index.js b/test/parser/index.js index 6bdd1b6376..33e36e98cd 100644 --- a/test/parser/index.js +++ b/test/parser/index.js @@ -41,7 +41,8 @@ describe('parse', () => { assert.deepEqual(err.loc, expected.loc); assert.equal(err.pos, expected.pos); } catch (err2) { - throw err2.code === 'MODULE_NOT_FOUND' ? err : err2; + const e = err2.code === 'MODULE_NOT_FOUND' ? err : err2; + throw e; } } }); diff --git a/test/parser/samples/await-then-catch/input.html b/test/parser/samples/await-then-catch/input.html new file mode 100644 index 0000000000..36489b9043 --- /dev/null +++ b/test/parser/samples/await-then-catch/input.html @@ -0,0 +1,7 @@ +{{#await thePromise}} +

loading...

+{{then theValue}} +

the value is {{theValue}}

+{{catch theError}} +

oh no! {{theError.message}}

+{{/await}} \ No newline at end of file diff --git a/test/parser/samples/await-then-catch/output.json b/test/parser/samples/await-then-catch/output.json new file mode 100644 index 0000000000..baf2df5fdf --- /dev/null +++ b/test/parser/samples/await-then-catch/output.json @@ -0,0 +1,144 @@ +{ + "hash": 1040536517, + "html": { + "start": 0, + "end": 158, + "type": "Fragment", + "children": [ + { + "start": 0, + "end": 158, + "type": "AwaitBlock", + "expression": { + "type": "Identifier", + "start": 9, + "end": 19, + "name": "thePromise" + }, + "children": [ + { + "start": 23, + "end": 40, + "type": "Element", + "name": "p", + "attributes": [], + "children": [ + { + "start": 26, + "end": 36, + "type": "Text", + "data": "loading..." + } + ] + } + ], + "value": "theValue", + "then": { + "start": 41, + "end": 93, + "type": "ThenBlock", + "children": [ + { + "start": 58, + "end": 60, + "type": "Text", + "data": "\n\t" + }, + { + "start": 60, + "end": 92, + "type": "Element", + "name": "p", + "attributes": [], + "children": [ + { + "start": 63, + "end": 76, + "type": "Text", + "data": "the value is " + }, + { + "start": 76, + "end": 88, + "type": "MustacheTag", + "expression": { + "type": "Identifier", + "start": 78, + "end": 86, + "name": "theValue" + } + } + ] + }, + { + "start": 92, + "end": 93, + "type": "Text", + "data": "\n" + } + ] + }, + "error": "theError", + "catch": { + "start": 93, + "end": 148, + "type": "CatchBlock", + "children": [ + { + "start": 111, + "end": 113, + "type": "Text", + "data": "\n\t" + }, + { + "start": 113, + "end": 147, + "type": "Element", + "name": "p", + "attributes": [], + "children": [ + { + "start": 116, + "end": 123, + "type": "Text", + "data": "oh no! " + }, + { + "start": 123, + "end": 143, + "type": "MustacheTag", + "expression": { + "type": "MemberExpression", + "start": 125, + "end": 141, + "object": { + "type": "Identifier", + "start": 125, + "end": 133, + "name": "theError" + }, + "property": { + "type": "Identifier", + "start": 134, + "end": 141, + "name": "message" + }, + "computed": false + } + } + ] + }, + { + "start": 147, + "end": 148, + "type": "Text", + "data": "\n" + } + ] + } + } + ] + }, + "css": null, + "js": null +} \ No newline at end of file From d783993d23f90a867a76f1b90fbb6642fe4a4dc0 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Sat, 25 Nov 2017 00:01:00 -0500 Subject: [PATCH 2/9] halfway there --- src/generators/Generator.ts | 2 +- src/generators/dom/preprocess.ts | 42 ++++++ src/generators/dom/visitors/AwaitBlock.ts | 133 ++++++++++++++++++ src/generators/dom/visitors/index.ts | 2 + src/parse/state/mustache.ts | 62 ++++++-- src/shared/index.js | 8 ++ .../samples/await-then-catch/output.json | 53 ++++--- test/runtime/index.js | 4 +- .../samples/await-then-catch/_config.js | 47 +++++++ .../samples/await-then-catch/main.html | 7 + 10 files changed, 326 insertions(+), 34 deletions(-) create mode 100644 src/generators/dom/visitors/AwaitBlock.ts create mode 100644 test/runtime/samples/await-then-catch/_config.js create mode 100644 test/runtime/samples/await-then-catch/main.html diff --git a/src/generators/Generator.ts b/src/generators/Generator.ts index a8775d41b2..284f96b2c5 100644 --- a/src/generators/Generator.ts +++ b/src/generators/Generator.ts @@ -732,7 +732,7 @@ export default class Generator { } } - if (node.type === 'IfBlock') { + if (node.type === 'IfBlock' || node.type === 'AwaitBlock') { node.metadata = contextualise(node.expression, contextDependencies, indexes); } diff --git a/src/generators/dom/preprocess.ts b/src/generators/dom/preprocess.ts index 5778258155..7e0056b0ae 100644 --- a/src/generators/dom/preprocess.ts +++ b/src/generators/dom/preprocess.ts @@ -108,6 +108,48 @@ const preprocessors = { node.var = block.getUniqueName(`text`); }, + AwaitBlock: ( + generator: DomGenerator, + block: Block, + state: State, + node: Node, + inEachBlock: boolean, + elementStack: Node[], + componentStack: Node[], + stripWhitespace: boolean, + nextSibling: Node + ) => { + cannotUseInnerHTML(node); + + node.var = block.getUniqueName('await_block'); + block.addDependencies(node.metadata.dependencies); + + [ + ['pending', null], + ['then', node.value], + ['catch', node.error] + ].forEach(([status, arg]) => { + const child = node[status]; + + const context = block.getUniqueName(arg || '_'); + const contexts = new Map(block.contexts); + contexts.set(arg, context); + + child._block = block.child({ + comment: createDebuggingComment(child, generator), + name: generator.getUniqueName(`create_${status}_block`), + params: block.params.concat(context), + context, + contexts + }); + + child._state = getChildState(state); + + preprocessChildren(generator, child._block, child._state, child, inEachBlock, elementStack, componentStack, stripWhitespace, nextSibling); + generator.blocks.push(child._block); + }); + }, + IfBlock: ( generator: DomGenerator, block: Block, diff --git a/src/generators/dom/visitors/AwaitBlock.ts b/src/generators/dom/visitors/AwaitBlock.ts new file mode 100644 index 0000000000..2ebca07831 --- /dev/null +++ b/src/generators/dom/visitors/AwaitBlock.ts @@ -0,0 +1,133 @@ +import deindent from '../../../utils/deindent'; +import visit from '../visit'; +import { DomGenerator } from '../index'; +import Block from '../Block'; +import isDomNode from './shared/isDomNode'; +import { Node } from '../../../interfaces'; +import { State } from '../interfaces'; + +export default function visitAwaitBlock( + generator: DomGenerator, + block: Block, + state: State, + node: Node, + elementStack: Node[], + componentStack: Node[] +) { + const name = node.var; + + const needsAnchor = node.next ? !isDomNode(node.next, generator) : !state.parentNode || !isDomNode(node.parent, generator); + const anchor = needsAnchor + ? block.getUniqueName(`${name}_anchor`) + : (node.next && node.next.var) || 'null'; + + const params = block.params.join(', '); + + block.contextualise(node.expression); + const { snippet } = node.metadata; + + if (needsAnchor) { + block.addElement( + anchor, + `@createComment()`, + `@createComment()`, + state.parentNode + ); + } + + const promise = block.getUniqueName(`promise`); + const resolved = block.getUniqueName(`resolved`); + const status = block.getUniqueName(`status`); + const select_block_type = block.getUniqueName(`select_block_type`); + const await_block = block.getUniqueName(`await_block`); + const await_block_type = block.getUniqueName(`await_block_type`); + const token = block.getUniqueName(`token`); + const await_token = block.getUniqueName(`await_token`); + const update = block.getUniqueName(`update`); + const handle_promise = block.getUniqueName(`handle_promise`); + const value = block.getUniqueName(`value`); + const error = block.getUniqueName(`error`); + const create_pending_block = node.pending._block.name; + const create_then_block = node.then._block.name; + const create_catch_block = node.catch._block.name; + + const conditions = []; + if (node.metadata.dependencies) { + conditions.push( + `(${node.metadata.dependencies.map(dep => `'${dep}' in changed`).join(' || ')})` + ); + } + + conditions.push(`${promise} !== (${promise} = ${snippet})`); + + block.addVariable(await_block); + block.addVariable(await_block_type); + block.addVariable(await_token); + block.addVariable(promise); + block.addVariable(resolved); + + block.builders.init.addBlock(deindent` + function ${handle_promise}(${promise}, ${params}) { + var ${token} = ${await_token} = {}; + + if (@isPromise(${promise})) { + ${promise}.then(function(${value}) { + if (${token} !== ${await_token}) return; + ${await_block}.u(); + ${await_block}.d(); + ${await_block} = (${await_block_type} = ${create_then_block})(${params}, ${resolved} = ${value}, #component); + ${await_block}.c(); + ${await_block}.m(${anchor}.parentNode, ${anchor}); + }, function (${error}) { + if (${token} !== ${await_token}) return; + ${await_block}.u(); + ${await_block}.d(); + ${await_block} = (${await_block_type} = ${create_catch_block})(${params}, ${resolved} = ${error}, #component); + ${await_block}.c(); + ${await_block}.m(${anchor}.parentNode, ${anchor}); + }); + + // if we previously had a then/catch block, destroy it + if (${await_block_type} !== ${create_pending_block}) { + if (${await_block}) ${await_block}.d(); + ${await_block} = (${await_block_type} = ${create_pending_block})(${params}, ${resolved} = null, #component); + return true; + } + } else { + if (${await_block_type} !== ${create_then_block}) { + if (${await_block}) ${await_block}.d(); + ${await_block} = (${await_block_type} = ${create_then_block})(${params}, ${resolved} = promise, #component); + return true; + } + } + } + + ${handle_promise}(${promise} = ${snippet}, ${params}); + `); + + block.builders.create.addBlock(deindent` + ${await_block}.c(); + `); + + block.builders.claim.addBlock(deindent` + ${await_block}.l(${state.parentNodes}); + `); + + const targetNode = state.parentNode || '#target'; + const anchorNode = state.parentNode ? 'null' : 'anchor'; + + block.builders.mount.addBlock(deindent` + ${await_block}.m(${targetNode}, ${anchor}); + `); + + block.builders.destroy.addBlock(deindent` + ${await_token} = null; + ${await_block}.d(); + `); + + [node.pending, node.then, node.catch].forEach(status => { + status.children.forEach(child => { + visit(generator, status._block, status._state, child, elementStack, componentStack); + }); + }); +} \ No newline at end of file diff --git a/src/generators/dom/visitors/index.ts b/src/generators/dom/visitors/index.ts index 74a272d1dd..fb0da9e0bc 100644 --- a/src/generators/dom/visitors/index.ts +++ b/src/generators/dom/visitors/index.ts @@ -1,3 +1,4 @@ +import AwaitBlock from './AwaitBlock'; import EachBlock from './EachBlock'; import Element from './Element/Element'; import IfBlock from './IfBlock'; @@ -7,6 +8,7 @@ import Text from './Text'; import { Visitor } from '../interfaces'; const visitors: Record = { + AwaitBlock, EachBlock, Element, IfBlock, diff --git a/src/parse/state/mustache.ts b/src/parse/state/mustache.ts index 49d22322b1..10b6751af3 100644 --- a/src/parse/state/mustache.ts +++ b/src/parse/state/mustache.ts @@ -6,6 +6,8 @@ import { Parser } from '../index'; import { Node } from '../../interfaces'; function trimWhitespace(block: Node, trimBefore: boolean, trimAfter: boolean) { + if (!block.children) return; // AwaitBlock + const firstChild = block.children[0]; const lastChild = block.children[block.children.length - 1]; @@ -39,7 +41,7 @@ export default function mustache(parser: Parser) { let block = parser.current(); let expected; - if (block.type === 'ElseBlock' || block.type === 'ThenBlock' || block.type === 'CatchBlock') { + if (block.type === 'ElseBlock' || block.type === 'PendingBlock' || block.type === 'ThenBlock' || block.type === 'CatchBlock') { block.end = start; parser.stack.pop(); block = parser.current(); @@ -72,7 +74,7 @@ export default function mustache(parser: Parser) { } // strip leading/trailing whitespace as necessary - if (!block.children.length) parser.error(`Empty block`, block.start); + if (block.children && !block.children.length) parser.error(`Empty block`, block.start); const charBefore = parser.template[block.start - 1]; const charAfter = parser.template[parser.index]; @@ -134,10 +136,13 @@ export default function mustache(parser: Parser) { parser.stack.push(block.else); } else if (parser.eat('then')) { - // {{then}} is valid by itself — we need to check that a) - // we're inside an await block, and b) there's an expression - const awaitBlock = parser.current(); - if (awaitBlock.type === 'AwaitBlock') { + // TODO DRY out this and the next section + const pendingBlock = parser.current(); + if (pendingBlock.type === 'PendingBlock') { + pendingBlock.end = start; + parser.stack.pop(); + const awaitBlock = parser.current(); + parser.requireWhitespace(); awaitBlock.value = parser.readIdentifier(); @@ -195,13 +200,40 @@ export default function mustache(parser: Parser) { const expression = readExpression(parser); - const block: Node = { - start, - end: null, - type, - expression, - children: [], - }; + const block: Node = type === 'AwaitBlock' ? + { + start, + end: null, + type, + expression, + value: null, + error: null, + pending: { + start, + end: null, + type: 'PendingBlock', + children: [] + }, + then: { + start: null, + end: null, + type: 'ThenBlock', + children: [] + }, + catch: { + start: null, + end: null, + type: 'CatchBlock', + children: [] + }, + } : + { + start, + end: null, + type, + expression, + children: [], + }; parser.allowWhitespace(); @@ -255,6 +287,10 @@ export default function mustache(parser: Parser) { parser.current().children.push(block); parser.stack.push(block); + + if (type === 'AwaitBlock') { + parser.stack.push(block.pending); + } } else if (parser.eat('yield')) { // {{yield}} // TODO deprecate diff --git a/src/shared/index.js b/src/shared/index.js index f9d0f91998..7f01391128 100644 --- a/src/shared/index.js +++ b/src/shared/index.js @@ -187,6 +187,14 @@ export function _unmount() { this._fragment.u(); } +export function isPromise(value) { + return value && typeof value.then === 'function'; +} + +export var PENDING = {}; +export var SUCCESS = {}; +export var FAILURE = {}; + export var proto = { destroy: destroy, get: get, diff --git a/test/parser/samples/await-then-catch/output.json b/test/parser/samples/await-then-catch/output.json index baf2df5fdf..4f149e6a0c 100644 --- a/test/parser/samples/await-then-catch/output.json +++ b/test/parser/samples/await-then-catch/output.json @@ -15,24 +15,42 @@ "end": 19, "name": "thePromise" }, - "children": [ - { - "start": 23, - "end": 40, - "type": "Element", - "name": "p", - "attributes": [], - "children": [ - { - "start": 26, - "end": 36, - "type": "Text", - "data": "loading..." - } - ] - } - ], "value": "theValue", + "error": "theError", + "pending": { + "start": 21, + "end": 41, + "type": "PendingBlock", + "children": [ + { + "start": 21, + "end": 23, + "type": "Text", + "data": "\n\t" + }, + { + "start": 23, + "end": 40, + "type": "Element", + "name": "p", + "attributes": [], + "children": [ + { + "start": 26, + "end": 36, + "type": "Text", + "data": "loading..." + } + ] + }, + { + "start": 40, + "end": 41, + "type": "Text", + "data": "\n" + } + ] + }, "then": { "start": 41, "end": 93, @@ -78,7 +96,6 @@ } ] }, - "error": "theError", "catch": { "start": 93, "end": 148, diff --git a/test/runtime/index.js b/test/runtime/index.js index 122fe645b6..b4f0b6b5ea 100644 --- a/test/runtime/index.js +++ b/test/runtime/index.js @@ -51,7 +51,7 @@ describe("runtime", () => { throw new Error("Forgot to remove `solo: true` from test"); } - (config.skip ? it.skip : config.solo ? it.only : it)(`${dir} (${shared ? 'shared' : 'inline'} helpers)`, () => { + (config.skip ? it.skip : config.solo ? it.only : it)(`${dir} (${shared ? 'shared' : 'inline'} helpers)`, async () => { if (failed.has(dir)) { // this makes debugging easier, by only printing compiled output once throw new Error('skipping test, already failed'); @@ -178,7 +178,7 @@ describe("runtime", () => { } if (config.test) { - config.test(assert, component, target, window, raf); + await config.test(assert, component, target, window, raf); } else { component.destroy(); assert.equal(target.innerHTML, ""); diff --git a/test/runtime/samples/await-then-catch/_config.js b/test/runtime/samples/await-then-catch/_config.js new file mode 100644 index 0000000000..3c60bd64c9 --- /dev/null +++ b/test/runtime/samples/await-then-catch/_config.js @@ -0,0 +1,47 @@ +let fulfil; + +let thePromise = new Promise(f => { + fulfil = f; +}); + +export default { + solo: true, + + data: { + thePromise + }, + + html: ` +

loading...

+ `, + + async test(assert, component, target) { + fulfil(42); + await thePromise; + + assert.htmlEqual(target.innerHTML, ` +

the value is 42

+ `); + + let reject; + + thePromise = new Promise((f, r) => { + reject = r; + }); + + component.set({ + thePromise + }); + + assert.htmlEqual(target.innerHTML, ` +

loading...

+ `); + + reject(new Error('something broke')); + await thePromise; + + assert.htmlEqual(target.innerHTML, ` +

oh no! something broke

+ `); + } +}; \ No newline at end of file diff --git a/test/runtime/samples/await-then-catch/main.html b/test/runtime/samples/await-then-catch/main.html new file mode 100644 index 0000000000..36489b9043 --- /dev/null +++ b/test/runtime/samples/await-then-catch/main.html @@ -0,0 +1,7 @@ +{{#await thePromise}} +

loading...

+{{then theValue}} +

the value is {{theValue}}

+{{catch theError}} +

oh no! {{theError.message}}

+{{/await}} \ No newline at end of file From a2d885c8baf8e023c9f8d9b9faac23d1f98bb334 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Sat, 25 Nov 2017 08:21:23 -0500 Subject: [PATCH 3/9] basic client-side await-then-catch working --- src/generators/dom/preprocess.ts | 11 +++++ src/generators/dom/visitors/AwaitBlock.ts | 44 ++++++++++++++----- .../samples/await-then-catch/_config.js | 9 ++-- 3 files changed, 51 insertions(+), 13 deletions(-) diff --git a/src/generators/dom/preprocess.ts b/src/generators/dom/preprocess.ts index 7e0056b0ae..62768184a0 100644 --- a/src/generators/dom/preprocess.ts +++ b/src/generators/dom/preprocess.ts @@ -124,6 +124,8 @@ const preprocessors = { node.var = block.getUniqueName('await_block'); block.addDependencies(node.metadata.dependencies); + let dynamic = false; + [ ['pending', null], ['then', node.value], @@ -147,7 +149,16 @@ const preprocessors = { preprocessChildren(generator, child._block, child._state, child, inEachBlock, elementStack, componentStack, stripWhitespace, nextSibling); generator.blocks.push(child._block); + + if (child._block.dependencies.size > 0) { + dynamic = true; + block.addDependencies(child._block.dependencies); + } }); + + node.pending._block.hasUpdateMethod = dynamic; + node.then._block.hasUpdateMethod = dynamic; + node.catch._block.hasUpdateMethod = dynamic; }, IfBlock: ( diff --git a/src/generators/dom/visitors/AwaitBlock.ts b/src/generators/dom/visitors/AwaitBlock.ts index 2ebca07831..fc28091e84 100644 --- a/src/generators/dom/visitors/AwaitBlock.ts +++ b/src/generators/dom/visitors/AwaitBlock.ts @@ -51,15 +51,6 @@ export default function visitAwaitBlock( const create_then_block = node.then._block.name; const create_catch_block = node.catch._block.name; - const conditions = []; - if (node.metadata.dependencies) { - conditions.push( - `(${node.metadata.dependencies.map(dep => `'${dep}' in changed`).join(' || ')})` - ); - } - - conditions.push(`${promise} !== (${promise} = ${snippet})`); - block.addVariable(await_block); block.addVariable(await_block_type); block.addVariable(await_token); @@ -89,7 +80,10 @@ export default function visitAwaitBlock( // if we previously had a then/catch block, destroy it if (${await_block_type} !== ${create_pending_block}) { - if (${await_block}) ${await_block}.d(); + if (${await_block}) { + ${await_block}.u(); + ${await_block}.d(); + } ${await_block} = (${await_block_type} = ${create_pending_block})(${params}, ${resolved} = null, #component); return true; } @@ -120,6 +114,36 @@ export default function visitAwaitBlock( ${await_block}.m(${targetNode}, ${anchor}); `); + const conditions = []; + if (node.metadata.dependencies) { + conditions.push( + `(${node.metadata.dependencies.map(dep => `'${dep}' in changed`).join(' || ')})` + ); + } + + conditions.push( + `${promise} !== (${promise} = ${snippet})`, + `${handle_promise}(${promise}, ${params})` + ); + + if (node.pending._block.hasUpdateMethod) { + block.builders.update.addBlock(deindent` + if (${conditions.join(' && ')}) { + ${await_block}.c(); + ${await_block}.m(${anchor}.parentNode, ${anchor}); + } else { + ${await_block}.p(changed, ${params}, ${resolved}); + } + `); + } else { + block.builders.update.addBlock(deindent` + if (${conditions.join(' && ')}) { + ${await_block}.c(); + ${await_block}.m(${anchor}.parentNode, ${anchor}); + } + `); + } + block.builders.destroy.addBlock(deindent` ${await_token} = null; ${await_block}.d(); diff --git a/test/runtime/samples/await-then-catch/_config.js b/test/runtime/samples/await-then-catch/_config.js index 3c60bd64c9..9f03479d9c 100644 --- a/test/runtime/samples/await-then-catch/_config.js +++ b/test/runtime/samples/await-then-catch/_config.js @@ -5,8 +5,6 @@ let thePromise = new Promise(f => { }); export default { - solo: true, - data: { thePromise }, @@ -38,7 +36,12 @@ export default { `); reject(new Error('something broke')); - await thePromise; + + try { + await thePromise; + } catch (err) { + // do nothing + } assert.htmlEqual(target.innerHTML, `

oh no! something broke

From 8f525875399a1fb74e60fdd0314c135160a51f54 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Sat, 25 Nov 2017 08:37:59 -0500 Subject: [PATCH 4/9] show output using cjs format, so stack traces make sense --- test/runtime/index.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/test/runtime/index.js b/test/runtime/index.js index b4f0b6b5ea..3e3d070cfe 100644 --- a/test/runtime/index.js +++ b/test/runtime/index.js @@ -88,7 +88,7 @@ describe("runtime", () => { } } catch (err) { failed.add(dir); - showOutput(cwd, { shared }, svelte); // eslint-disable-line no-console + showOutput(cwd, { shared, format: 'cjs' }, svelte); // eslint-disable-line no-console throw err; } } @@ -134,7 +134,7 @@ describe("runtime", () => { try { SvelteComponent = require(`./samples/${dir}/main.html`); } catch (err) { - showOutput(cwd, { shared, hydratable: hydrate }, svelte); // eslint-disable-line no-console + showOutput(cwd, { shared, format: 'cjs', hydratable: hydrate }, svelte); // eslint-disable-line no-console throw err; } @@ -188,12 +188,12 @@ describe("runtime", () => { config.error(assert, err); } else { failed.add(dir); - showOutput(cwd, { shared, hydratable: hydrate }, svelte); // eslint-disable-line no-console + showOutput(cwd, { shared, format: 'cjs', hydratable: hydrate }, svelte); // eslint-disable-line no-console throw err; } } - if (config.show) showOutput(cwd, { shared, hydratable: hydrate }, svelte); + if (config.show) showOutput(cwd, { shared, format: 'cjs', hydratable: hydrate }, svelte); }); } From 56b167b44fe3bfb93cc0e542ff24a76ee1a9d14b Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Sat, 25 Nov 2017 09:13:58 -0500 Subject: [PATCH 5/9] await-then-catch with non-promise --- src/generators/Generator.ts | 12 +++++++++++- src/generators/dom/visitors/AwaitBlock.ts | 8 ++++++-- .../await-then-catch-non-promise/_config.js | 19 +++++++++++++++++++ .../await-then-catch-non-promise/main.html | 7 +++++++ 4 files changed, 43 insertions(+), 3 deletions(-) create mode 100644 test/runtime/samples/await-then-catch-non-promise/_config.js create mode 100644 test/runtime/samples/await-then-catch-non-promise/main.html diff --git a/src/generators/Generator.ts b/src/generators/Generator.ts index 284f96b2c5..5fd82e274b 100644 --- a/src/generators/Generator.ts +++ b/src/generators/Generator.ts @@ -732,7 +732,17 @@ export default class Generator { } } - if (node.type === 'IfBlock' || node.type === 'AwaitBlock') { + if (node.type === 'AwaitBlock') { + node.metadata = contextualise(node.expression, contextDependencies, indexes); + + contextDependencies = new Map(contextDependencies); + contextDependencies.set(node.value, node.metadata.dependencies); + contextDependencies.set(node.error, node.metadata.dependencies); + + contextDependenciesStack.push(contextDependencies); + } + + if (node.type === 'IfBlock') { node.metadata = contextualise(node.expression, contextDependencies, indexes); } diff --git a/src/generators/dom/visitors/AwaitBlock.ts b/src/generators/dom/visitors/AwaitBlock.ts index fc28091e84..b4f887167c 100644 --- a/src/generators/dom/visitors/AwaitBlock.ts +++ b/src/generators/dom/visitors/AwaitBlock.ts @@ -88,9 +88,13 @@ export default function visitAwaitBlock( return true; } } else { + ${resolved} = ${promise}; if (${await_block_type} !== ${create_then_block}) { - if (${await_block}) ${await_block}.d(); - ${await_block} = (${await_block_type} = ${create_then_block})(${params}, ${resolved} = promise, #component); + if (${await_block}) { + ${await_block}.u(); + ${await_block}.d(); + } + ${await_block} = (${await_block_type} = ${create_then_block})(${params}, ${resolved}, #component); return true; } } diff --git a/test/runtime/samples/await-then-catch-non-promise/_config.js b/test/runtime/samples/await-then-catch-non-promise/_config.js new file mode 100644 index 0000000000..56b4b59f1b --- /dev/null +++ b/test/runtime/samples/await-then-catch-non-promise/_config.js @@ -0,0 +1,19 @@ +export default { + data: { + thePromise: 'not actually a promise' + }, + + html: ` +

the value is not actually a promise

+ `, + + test(assert, component, target) { + component.set({ + thePromise: 'still not a promise' + }); + + assert.htmlEqual(target.innerHTML, ` +

the value is still not a promise

+ `); + } +}; \ No newline at end of file diff --git a/test/runtime/samples/await-then-catch-non-promise/main.html b/test/runtime/samples/await-then-catch-non-promise/main.html new file mode 100644 index 0000000000..36489b9043 --- /dev/null +++ b/test/runtime/samples/await-then-catch-non-promise/main.html @@ -0,0 +1,7 @@ +{{#await thePromise}} +

loading...

+{{then theValue}} +

the value is {{theValue}}

+{{catch theError}} +

oh no! {{theError.message}}

+{{/await}} \ No newline at end of file From 8a0813e96b08235dbbbb5d6151a1e30f76e16c89 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Sat, 25 Nov 2017 10:34:44 -0500 Subject: [PATCH 6/9] SSR await-then-catch --- src/generators/server-side-rendering/index.ts | 31 +++++++++----- .../server-side-rendering/preprocess.ts | 10 +++++ .../visitors/AwaitBlock.ts | 40 +++++++++++++++++++ .../server-side-rendering/visitors/index.ts | 2 + src/parse/state/mustache.ts | 3 +- .../samples/ssr-no-oncreate-etc/expected.js | 12 ------ 6 files changed, 76 insertions(+), 22 deletions(-) create mode 100644 src/generators/server-side-rendering/visitors/AwaitBlock.ts diff --git a/src/generators/server-side-rendering/index.ts b/src/generators/server-side-rendering/index.ts index 2accc2bc7e..e08c10ff14 100644 --- a/src/generators/server-side-rendering/index.ts +++ b/src/generators/server-side-rendering/index.ts @@ -159,16 +159,29 @@ export default function ssr( }; }; - var escaped = { - '"': '"', - "'": '&##39;', - '&': '&', - '<': '<', - '>': '>' - }; + ${ + // TODO this is a bit hacky + /__escape/.test(generator.renderCode) && deindent` + var escaped = { + '"': '"', + "'": '&##39;', + '&': '&', + '<': '<', + '>': '>' + }; + + function __escape(html) { + return String(html).replace(/["'&<>]/g, match => escaped[match]); + } + ` + } - function __escape(html) { - return String(html).replace(/["'&<>]/g, match => escaped[match]); + ${ + /__isPromise/.test(generator.renderCode) && deindent` + function __isPromise(value) { + return value && typeof value.then === 'function'; + } + ` } `.replace(/(@+|#+|%+)(\w*(?:-\w*)?)/g, (match: string, sigil: string, name: string) => { if (sigil === '@') return generator.alias(name); diff --git a/src/generators/server-side-rendering/preprocess.ts b/src/generators/server-side-rendering/preprocess.ts index 0734a135f2..7e9cb7365d 100644 --- a/src/generators/server-side-rendering/preprocess.ts +++ b/src/generators/server-side-rendering/preprocess.ts @@ -10,6 +10,16 @@ const preprocessors = { RawMustacheTag: noop, Text: noop, + AwaitBlock: ( + generator: SsrGenerator, + node: Node, + elementStack: Node[] + ) => { + preprocessChildren(generator, node.pending, elementStack); + preprocessChildren(generator, node.then, elementStack); + preprocessChildren(generator, node.catch, elementStack); + }, + IfBlock: ( generator: SsrGenerator, node: Node, diff --git a/src/generators/server-side-rendering/visitors/AwaitBlock.ts b/src/generators/server-side-rendering/visitors/AwaitBlock.ts new file mode 100644 index 0000000000..6251eb15c0 --- /dev/null +++ b/src/generators/server-side-rendering/visitors/AwaitBlock.ts @@ -0,0 +1,40 @@ +import visit from '../visit'; +import { SsrGenerator } from '../index'; +import Block from '../Block'; +import { Node } from '../../../interfaces'; + +export default function visitAwaitBlock( + generator: SsrGenerator, + block: Block, + node: Node +) { + block.contextualise(node.expression); + const { dependencies, snippet } = node.metadata; + + // TODO should this be the generator's job? It's duplicated between + // here and the equivalent DOM compiler visitor + const contexts = new Map(block.contexts); + contexts.set(node.value, '__value'); + + const contextDependencies = new Map(block.contextDependencies); + contextDependencies.set(node.value, dependencies); + + const childBlock = block.child({ + contextDependencies, + contexts + }); + + generator.append('${(function(__value) { if(__isPromise(__value)) return `'); + + node.pending.children.forEach((child: Node) => { + visit(generator, childBlock, child); + }); + + generator.append('`; return `'); + + node.then.children.forEach((child: Node) => { + visit(generator, childBlock, child); + }); + + generator.append(`\`;}(${snippet})) }`); +} diff --git a/src/generators/server-side-rendering/visitors/index.ts b/src/generators/server-side-rendering/visitors/index.ts index 0ed4a58e22..468239a949 100644 --- a/src/generators/server-side-rendering/visitors/index.ts +++ b/src/generators/server-side-rendering/visitors/index.ts @@ -1,3 +1,4 @@ +import AwaitBlock from './AwaitBlock'; import Comment from './Comment'; import EachBlock from './EachBlock'; import Element from './Element'; @@ -7,6 +8,7 @@ import RawMustacheTag from './RawMustacheTag'; import Text from './Text'; export default { + AwaitBlock, Comment, EachBlock, Element, diff --git a/src/parse/state/mustache.ts b/src/parse/state/mustache.ts index 10b6751af3..b59cf92b8f 100644 --- a/src/parse/state/mustache.ts +++ b/src/parse/state/mustache.ts @@ -209,7 +209,7 @@ export default function mustache(parser: Parser) { value: null, error: null, pending: { - start, + start: null, end: null, type: 'PendingBlock', children: [] @@ -289,6 +289,7 @@ export default function mustache(parser: Parser) { parser.stack.push(block); if (type === 'AwaitBlock') { + block.pending.start = parser.index; parser.stack.push(block.pending); } } else if (parser.eat('yield')) { diff --git a/test/js/samples/ssr-no-oncreate-etc/expected.js b/test/js/samples/ssr-no-oncreate-etc/expected.js index 51c10c7656..e8e5330377 100644 --- a/test/js/samples/ssr-no-oncreate-etc/expected.js +++ b/test/js/samples/ssr-no-oncreate-etc/expected.js @@ -22,16 +22,4 @@ SvelteComponent.renderCss = function() { }; }; -var escaped = { - '"': '"', - "'": ''', - '&': '&', - '<': '<', - '>': '>' -}; - -function __escape(html) { - return String(html).replace(/["'&<>]/g, match => escaped[match]); -} - module.exports = SvelteComponent; \ No newline at end of file From ccef13a2d5bf22157822088a06b403f12c7cb3b6 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Sat, 25 Nov 2017 10:56:27 -0500 Subject: [PATCH 7/9] ditch async/await in tests, so that they run in node 6 --- test/runtime/index.js | 153 +++++++++--------- .../samples/await-then-catch/_config.js | 49 +++--- 2 files changed, 102 insertions(+), 100 deletions(-) diff --git a/test/runtime/index.js b/test/runtime/index.js index 3e3d070cfe..b572ec81bc 100644 --- a/test/runtime/index.js +++ b/test/runtime/index.js @@ -51,7 +51,7 @@ describe("runtime", () => { throw new Error("Forgot to remove `solo: true` from test"); } - (config.skip ? it.skip : config.solo ? it.only : it)(`${dir} (${shared ? 'shared' : 'inline'} helpers)`, async () => { + (config.skip ? it.skip : config.solo ? it.only : it)(`${dir} (${shared ? 'shared' : 'inline'} helpers)`, () => { if (failed.has(dir)) { // this makes debugging easier, by only printing compiled output once throw new Error('skipping test, already failed'); @@ -105,95 +105,98 @@ describe("runtime", () => { const window = env(); - try { - // set of hacks to support transition tests - transitionManager.running = false; - transitionManager.transitions = []; - - const raf = { - time: 0, - callback: null, - tick: now => { - raf.time = now; - if (raf.callback) raf.callback(); - } - }; - window.performance = { now: () => raf.time }; - global.requestAnimationFrame = cb => { - let called = false; - raf.callback = () => { - if (!called) { - called = true; - cb(); + return Promise.resolve() + .then(() => { + // set of hacks to support transition tests + transitionManager.running = false; + transitionManager.transitions = []; + + const raf = { + time: 0, + callback: null, + tick: now => { + raf.time = now; + if (raf.callback) raf.callback(); } }; - }; - - global.window = window; + window.performance = { now: () => raf.time }; + global.requestAnimationFrame = cb => { + let called = false; + raf.callback = () => { + if (!called) { + called = true; + cb(); + } + }; + }; - try { - SvelteComponent = require(`./samples/${dir}/main.html`); - } catch (err) { - showOutput(cwd, { shared, format: 'cjs', hydratable: hydrate }, svelte); // eslint-disable-line no-console - throw err; - } + global.window = window; - global.window = window; + try { + SvelteComponent = require(`./samples/${dir}/main.html`); + } catch (err) { + showOutput(cwd, { shared, format: 'cjs', hydratable: hydrate }, svelte); // eslint-disable-line no-console + throw err; + } - // Put the constructor on window for testing - window.SvelteComponent = SvelteComponent; + global.window = window; - const target = window.document.querySelector("main"); + // Put the constructor on window for testing + window.SvelteComponent = SvelteComponent; - const warnings = []; - const warn = console.warn; - console.warn = warning => { - warnings.push(warning); - }; + const target = window.document.querySelector("main"); - const options = Object.assign({}, { - target, - hydrate, - data: config.data - }, config.options || {}); + const warnings = []; + const warn = console.warn; + console.warn = warning => { + warnings.push(warning); + }; - const component = new SvelteComponent(options); + const options = Object.assign({}, { + target, + hydrate, + data: config.data + }, config.options || {}); - console.warn = warn; + const component = new SvelteComponent(options); - if (config.error) { - unintendedError = true; - throw new Error("Expected a runtime error"); - } + console.warn = warn; - if (config.warnings) { - assert.deepEqual(warnings, config.warnings); - } else if (warnings.length) { - unintendedError = true; - throw new Error("Received unexpected warnings"); - } + if (config.error) { + unintendedError = true; + throw new Error("Expected a runtime error"); + } - if (config.html) { - assert.htmlEqual(target.innerHTML, config.html); - } + if (config.warnings) { + assert.deepEqual(warnings, config.warnings); + } else if (warnings.length) { + unintendedError = true; + throw new Error("Received unexpected warnings"); + } - if (config.test) { - await config.test(assert, component, target, window, raf); - } else { - component.destroy(); - assert.equal(target.innerHTML, ""); - } - } catch (err) { - if (config.error && !unintendedError) { - config.error(assert, err); - } else { - failed.add(dir); - showOutput(cwd, { shared, format: 'cjs', hydratable: hydrate }, svelte); // eslint-disable-line no-console - throw err; - } - } + if (config.html) { + assert.htmlEqual(target.innerHTML, config.html); + } - if (config.show) showOutput(cwd, { shared, format: 'cjs', hydratable: hydrate }, svelte); + if (config.test) { + return config.test(assert, component, target, window, raf); + } else { + component.destroy(); + assert.equal(target.innerHTML, ""); + } + }) + .catch(err => { + if (config.error && !unintendedError) { + config.error(assert, err); + } else { + failed.add(dir); + showOutput(cwd, { shared, format: 'cjs', hydratable: hydrate }, svelte); // eslint-disable-line no-console + throw err; + } + }) + .then(() => { + if (config.show) showOutput(cwd, { shared, format: 'cjs', hydratable: hydrate }, svelte); + }); }); } diff --git a/test/runtime/samples/await-then-catch/_config.js b/test/runtime/samples/await-then-catch/_config.js index 9f03479d9c..ed467b0382 100644 --- a/test/runtime/samples/await-then-catch/_config.js +++ b/test/runtime/samples/await-then-catch/_config.js @@ -13,38 +13,37 @@ export default {

loading...

`, - async test(assert, component, target) { + test(assert, component, target) { fulfil(42); - await thePromise; - assert.htmlEqual(target.innerHTML, ` -

the value is 42

- `); + return thePromise + .then(() => { + assert.htmlEqual(target.innerHTML, ` +

the value is 42

+ `); - let reject; + let reject; - thePromise = new Promise((f, r) => { - reject = r; - }); + thePromise = new Promise((f, r) => { + reject = r; + }); - component.set({ - thePromise - }); + component.set({ + thePromise + }); - assert.htmlEqual(target.innerHTML, ` -

loading...

- `); + assert.htmlEqual(target.innerHTML, ` +

loading...

+ `); - reject(new Error('something broke')); + reject(new Error('something broke')); - try { - await thePromise; - } catch (err) { - // do nothing - } - - assert.htmlEqual(target.innerHTML, ` -

oh no! something broke

- `); + return thePromise.catch(() => {}); + }) + .then(() => { + assert.htmlEqual(target.innerHTML, ` +

oh no! something broke

+ `); + }); } }; \ No newline at end of file From dc97fa76c8d9a1132dbfd6e70d196c934a9498b9 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Sat, 25 Nov 2017 16:12:24 -0500 Subject: [PATCH 8/9] less code --- src/generators/dom/visitors/AwaitBlock.ts | 48 ++++++++++------------- 1 file changed, 21 insertions(+), 27 deletions(-) diff --git a/src/generators/dom/visitors/AwaitBlock.ts b/src/generators/dom/visitors/AwaitBlock.ts index b4f887167c..b4372831a8 100644 --- a/src/generators/dom/visitors/AwaitBlock.ts +++ b/src/generators/dom/visitors/AwaitBlock.ts @@ -37,14 +37,13 @@ export default function visitAwaitBlock( const promise = block.getUniqueName(`promise`); const resolved = block.getUniqueName(`resolved`); - const status = block.getUniqueName(`status`); - const select_block_type = block.getUniqueName(`select_block_type`); const await_block = block.getUniqueName(`await_block`); const await_block_type = block.getUniqueName(`await_block_type`); const token = block.getUniqueName(`token`); const await_token = block.getUniqueName(`await_token`); - const update = block.getUniqueName(`update`); const handle_promise = block.getUniqueName(`handle_promise`); + const replace_await_block = block.getUniqueName(`replace_await_block`); + const old_block = block.getUniqueName(`old_block`); const value = block.getUniqueName(`value`); const error = block.getUniqueName(`error`); const create_pending_block = node.pending._block.name; @@ -58,43 +57,39 @@ export default function visitAwaitBlock( block.addVariable(resolved); block.builders.init.addBlock(deindent` + function ${replace_await_block}(${token}, type, ${value}, ${params}) { + if (${token} !== ${await_token}) return; + + var ${old_block} = ${await_block}; + ${await_block} = (${await_block_type} = type)(${params}, ${resolved} = ${value}, #component); + + if (${old_block}) { + ${old_block}.u(); + ${old_block}.d(); + ${await_block}.c(); + ${await_block}.m(${anchor}.parentNode, ${anchor}); + } + } + function ${handle_promise}(${promise}, ${params}) { var ${token} = ${await_token} = {}; if (@isPromise(${promise})) { ${promise}.then(function(${value}) { - if (${token} !== ${await_token}) return; - ${await_block}.u(); - ${await_block}.d(); - ${await_block} = (${await_block_type} = ${create_then_block})(${params}, ${resolved} = ${value}, #component); - ${await_block}.c(); - ${await_block}.m(${anchor}.parentNode, ${anchor}); + ${replace_await_block}(${token}, ${create_then_block}, ${value}, ${params}); }, function (${error}) { - if (${token} !== ${await_token}) return; - ${await_block}.u(); - ${await_block}.d(); - ${await_block} = (${await_block_type} = ${create_catch_block})(${params}, ${resolved} = ${error}, #component); - ${await_block}.c(); - ${await_block}.m(${anchor}.parentNode, ${anchor}); + ${replace_await_block}(${token}, ${create_catch_block}, ${error}, ${params}); }); // if we previously had a then/catch block, destroy it if (${await_block_type} !== ${create_pending_block}) { - if (${await_block}) { - ${await_block}.u(); - ${await_block}.d(); - } - ${await_block} = (${await_block_type} = ${create_pending_block})(${params}, ${resolved} = null, #component); + ${replace_await_block}(${token}, ${create_pending_block}, null, ${params}); return true; } } else { ${resolved} = ${promise}; if (${await_block_type} !== ${create_then_block}) { - if (${await_block}) { - ${await_block}.u(); - ${await_block}.d(); - } - ${await_block} = (${await_block_type} = ${create_then_block})(${params}, ${resolved}, #component); + ${replace_await_block}(${token}, ${create_then_block}, ${resolved}, ${params}); return true; } } @@ -133,8 +128,7 @@ export default function visitAwaitBlock( if (node.pending._block.hasUpdateMethod) { block.builders.update.addBlock(deindent` if (${conditions.join(' && ')}) { - ${await_block}.c(); - ${await_block}.m(${anchor}.parentNode, ${anchor}); + // nothing } else { ${await_block}.p(changed, ${params}, ${resolved}); } From 844e89f277f44d462aadd602a48d7b9f3fb92729 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Sun, 3 Dec 2017 09:33:56 -0500 Subject: [PATCH 9/9] correctly mount await block that has an anchor --- src/generators/dom/visitors/AwaitBlock.ts | 2 +- .../await-then-catch-multiple/_config.js | 53 +++++++++++++++++++ .../await-then-catch-multiple/main.html | 15 ++++++ 3 files changed, 69 insertions(+), 1 deletion(-) create mode 100644 test/runtime/samples/await-then-catch-multiple/_config.js create mode 100644 test/runtime/samples/await-then-catch-multiple/main.html diff --git a/src/generators/dom/visitors/AwaitBlock.ts b/src/generators/dom/visitors/AwaitBlock.ts index b4372831a8..c0d5c25b85 100644 --- a/src/generators/dom/visitors/AwaitBlock.ts +++ b/src/generators/dom/visitors/AwaitBlock.ts @@ -110,7 +110,7 @@ export default function visitAwaitBlock( const anchorNode = state.parentNode ? 'null' : 'anchor'; block.builders.mount.addBlock(deindent` - ${await_block}.m(${targetNode}, ${anchor}); + ${await_block}.m(${targetNode}, ${anchorNode}); `); const conditions = []; diff --git a/test/runtime/samples/await-then-catch-multiple/_config.js b/test/runtime/samples/await-then-catch-multiple/_config.js new file mode 100644 index 0000000000..2866fea883 --- /dev/null +++ b/test/runtime/samples/await-then-catch-multiple/_config.js @@ -0,0 +1,53 @@ +let fulfil; + +let thePromise = new Promise(f => { + fulfil = f; +}); + +export default { + data: { + thePromise + }, + + html: ` +

loading...

+

loading...

+ `, + + test(assert, component, target) { + fulfil(42); + + return thePromise + .then(() => { + assert.htmlEqual(target.innerHTML, ` +

the value is 42

+

the value is 42

+ `); + + let reject; + + thePromise = new Promise((f, r) => { + reject = r; + }); + + component.set({ + thePromise + }); + + assert.htmlEqual(target.innerHTML, ` +

loading...

+

loading...

+ `); + + reject(new Error('something broke')); + + return thePromise.catch(() => {}); + }) + .then(() => { + assert.htmlEqual(target.innerHTML, ` +

oh no! something broke

+

oh no! something broke

+ `); + }); + } +}; \ No newline at end of file diff --git a/test/runtime/samples/await-then-catch-multiple/main.html b/test/runtime/samples/await-then-catch-multiple/main.html new file mode 100644 index 0000000000..eb071c27a8 --- /dev/null +++ b/test/runtime/samples/await-then-catch-multiple/main.html @@ -0,0 +1,15 @@ +{{#await thePromise}} +

loading...

+{{then theValue}} +

the value is {{theValue}}

+{{catch theError}} +

oh no! {{theError.message}}

+{{/await}} + +{{#await thePromise}} +

loading...

+{{then theValue}} +

the value is {{theValue}}

+{{catch theError}} +

oh no! {{theError.message}}

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