fix: stack overflow on large number of templates, #513

This commit is contained in:
Harttle
2022-07-08 01:46:51 +08:00
parent 2f87708989
commit 3dc4290b56
16 changed files with 172 additions and 180 deletions
+1
View File
@@ -55,6 +55,7 @@
<td align="center"><a href="https://github.com/ameyaapte1"><img src="https://avatars.githubusercontent.com/u/16054747?v=4?s=100" width="100px;" alt=""/></a></td> <td align="center"><a href="https://github.com/ameyaapte1"><img src="https://avatars.githubusercontent.com/u/16054747?v=4?s=100" width="100px;" alt=""/></a></td>
<td align="center"><a href="https://github.com/tbdrz"><img src="https://avatars.githubusercontent.com/u/50599116?v=4?s=100" width="100px;" alt=""/></a></td> <td align="center"><a href="https://github.com/tbdrz"><img src="https://avatars.githubusercontent.com/u/50599116?v=4?s=100" width="100px;" alt=""/></a></td>
<td align="center"><a href="http://santialbo.com"><img src="https://avatars.githubusercontent.com/u/1557563?v=4?s=100" width="100px;" alt=""/></a></td> <td align="center"><a href="http://santialbo.com"><img src="https://avatars.githubusercontent.com/u/1557563?v=4?s=100" width="100px;" alt=""/></a></td>
<td align="center"><a href="https://github.com/YahangWu"><img src="https://avatars.githubusercontent.com/u/12295975?v=4?s=100" width="100px;" alt=""/></a></td>
</tr> </tr>
</table> </table>
+4
View File
@@ -1,5 +1,9 @@
import type { Template } from '../template/template'
export interface Cache<T> { export interface Cache<T> {
write (key: string, value: T): void | Promise<void>; write (key: string, value: T): void | Promise<void>;
read (key: string): T | undefined | Promise<T | undefined>; read (key: string): T | undefined | Promise<T | undefined>;
remove (key: string): void | Promise<void>; remove (key: string): void | Promise<void>;
} }
export type LiquidCache = Cache<Template[] | Promise<Template[]>>
+5 -7
View File
@@ -1,12 +1,10 @@
import { snakeCase, forOwn, isArray, isString, isFunction } from './util/underscore' import { snakeCase, forOwn, isArray, isString, isFunction } from './util/underscore'
import { Template } from './template/template' import { LiquidCache } from './cache/cache'
import { Cache } from './cache/cache'
import { LRU } from './cache/lru' import { LRU } from './cache/lru'
import { FS } from './fs/fs' import { FS } from './fs/fs'
import * as fs from './fs/node' import * as fs from './fs/node'
import { defaultOperators, Operators } from './render/operator' import { defaultOperators, Operators } from './render/operator'
import { createTrie, Trie } from './util/operator-trie' import { createTrie, Trie } from './util/operator-trie'
import { Thenable } from './util/async'
import * as builtinFilters from './builtin/filters' import * as builtinFilters from './builtin/filters'
import { assert, FilterImplOptions } from './types' import { assert, FilterImplOptions } from './types'
@@ -32,7 +30,7 @@ export interface LiquidOptions {
/** Add a extname (if filepath doesn't include one) before template file lookup. Eg: setting to `".html"` will allow including file by basename. Defaults to `""`. */ /** Add a extname (if filepath doesn't include one) before template file lookup. Eg: setting to `".html"` will allow including file by basename. Defaults to `""`. */
extname?: string; extname?: string;
/** Whether or not to cache resolved templates. Defaults to `false`. */ /** Whether or not to cache resolved templates. Defaults to `false`. */
cache?: boolean | number | Cache<Thenable<Template[]>>; cache?: boolean | number | LiquidCache;
/** Use Javascript Truthiness. Defaults to `false`. */ /** Use Javascript Truthiness. Defaults to `false`. */
jsTruthy?: boolean; jsTruthy?: boolean;
/** If set, treat the `filepath` parameter in `{%include filepath %}` and `{%layout filepath%}` as a variable, otherwise as a literal value. Defaults to `true`. */ /** If set, treat the `filepath` parameter in `{%include filepath %}` and `{%layout filepath%}` as a variable, otherwise as a literal value. Defaults to `true`. */
@@ -104,7 +102,7 @@ interface NormalizedOptions extends LiquidOptions {
root?: string[]; root?: string[];
partials?: string[]; partials?: string[];
layouts?: string[]; layouts?: string[];
cache?: Cache<Thenable<Template[]>>; cache?: LiquidCache;
outputEscape?: OutputEscape; outputEscape?: OutputEscape;
operatorsTrie?: Trie; operatorsTrie?: Trie;
} }
@@ -116,7 +114,7 @@ export interface NormalizedFullOptions extends NormalizedOptions {
relativeReference: boolean; relativeReference: boolean;
jekyllInclude: boolean; jekyllInclude: boolean;
extname: string; extname: string;
cache: undefined | Cache<Thenable<Template[]>>; cache?: LiquidCache;
jsTruthy: boolean; jsTruthy: boolean;
dynamicPartials: boolean; dynamicPartials: boolean;
fs: FS; fs: FS;
@@ -180,7 +178,7 @@ export function normalize (options: LiquidOptions): NormalizedFullOptions {
if (!options.hasOwnProperty('layouts')) options.layouts = options.root if (!options.hasOwnProperty('layouts')) options.layouts = options.root
} }
if (options.hasOwnProperty('cache')) { if (options.hasOwnProperty('cache')) {
let cache: Cache<Thenable<Template[]>> | undefined let cache: LiquidCache | undefined
if (typeof options.cache === 'number') cache = options.cache > 0 ? new LRU(options.cache) : undefined if (typeof options.cache === 'number') cache = options.cache > 0 ? new LRU(options.cache) : undefined
else if (typeof options.cache === 'object') cache = options.cache else if (typeof options.cache === 'object') cache = options.cache
else cache = options.cache ? new LRU(1024) : undefined else cache = options.cache ? new LRU(1024) : undefined
+13 -16
View File
@@ -8,17 +8,17 @@ import { Output } from '../template/output'
import { HTML } from '../template/html' import { HTML } from '../template/html'
import { Template } from '../template/template' import { Template } from '../template/template'
import { TopLevelToken } from '../tokens/toplevel-token' import { TopLevelToken } from '../tokens/toplevel-token'
import { Cache } from '../cache/cache' import { LiquidCache } from '../cache/cache'
import { Loader, LookupType } from '../fs/loader' import { Loader, LookupType } from '../fs/loader'
import { toPromise } from '../util/async'
import { FS } from '../fs/fs' import { FS } from '../fs/fs'
import { toThenable, Thenable } from '../util/async'
export default class Parser { export default class Parser {
public parseFile: (file: string, sync?: boolean, type?: LookupType, currentFile?: string) => Generator<unknown, Template[], Template[] | string> public parseFile: (file: string, sync?: boolean, type?: LookupType, currentFile?: string) => Generator<unknown, Template[], Template[] | string>
private liquid: Liquid private liquid: Liquid
private fs: FS private fs: FS
private cache: Cache<Thenable<Template[]>> | undefined private cache?: LiquidCache
private loader: Loader private loader: Loader
public constructor (liquid: Liquid) { public constructor (liquid: Liquid) {
@@ -58,21 +58,18 @@ export default class Parser {
return new ParseStream(tokens, (token, tokens) => this.parseToken(token, tokens)) return new ParseStream(tokens, (token, tokens) => this.parseToken(token, tokens))
} }
private * _parseFileCached (file: string, sync?: boolean, type: LookupType = LookupType.Root, currentFile?: string): Generator<unknown, Template[], Template[]> { private * _parseFileCached (file: string, sync?: boolean, type: LookupType = LookupType.Root, currentFile?: string): Generator<unknown, Template[], Template[]> {
const key = this.loader.shouldLoadRelative(file) const cache = this.cache!
? currentFile + ',' + file const key = this.loader.shouldLoadRelative(file) ? currentFile + ',' + file : type + ':' + file
: type + ':' + file const tpls = yield cache.read(key)
const tpls = yield this.cache!.read(key)
if (tpls) return tpls if (tpls) return tpls
const task = toThenable(this._parseFile(file, sync, type, currentFile)) const task = this._parseFile(file, sync, type, currentFile)
this.cache!.write(key, task) // sync mode: exec the task and cache the result
try { // async mode: cache the task before exec
return yield task const taskOrTpl = sync ? yield task : toPromise(task)
} catch (e) { cache.write(key, taskOrTpl as any)
// remove cached task if failed // note: concurrent tasks will be reused, cache for failed task is removed until its end
this.cache!.remove(key) try { return yield taskOrTpl } catch (err) { cache.remove(key); throw err }
}
return []
} }
private * _parseFile (file: string, sync?: boolean, type: LookupType = LookupType.Root, currentFile?: string): Generator<unknown, Template[], string> { private * _parseFile (file: string, sync?: boolean, type: LookupType = LookupType.Root, currentFile?: string): Generator<unknown, Template[], string> {
const filepath = yield this.loader.lookup(file, type, sync, currentFile) const filepath = yield this.loader.lookup(file, type, sync, currentFile)
+2 -2
View File
@@ -4,13 +4,13 @@ import { Template } from '../template/template'
import { Emitter } from '../emitters/emitter' import { Emitter } from '../emitters/emitter'
import { SimpleEmitter } from '../emitters/simple-emitter' import { SimpleEmitter } from '../emitters/simple-emitter'
import { StreamedEmitter } from '../emitters/streamed-emitter' import { StreamedEmitter } from '../emitters/streamed-emitter'
import { toThenable } from '../util/async' import { toPromise } from '../util/async'
import { KeepingTypeEmitter } from '../emitters/keeping-type-emitter' import { KeepingTypeEmitter } from '../emitters/keeping-type-emitter'
export class Render { export class Render {
public renderTemplatesToNodeStream (templates: Template[], ctx: Context): NodeJS.ReadableStream { public renderTemplatesToNodeStream (templates: Template[], ctx: Context): NodeJS.ReadableStream {
const emitter = new StreamedEmitter() const emitter = new StreamedEmitter()
Promise.resolve().then(() => toThenable(this.renderTemplates(templates, ctx, emitter))) Promise.resolve().then(() => toPromise(this.renderTemplates(templates, ctx, emitter)))
.then(() => emitter.end(), err => emitter.error(err)) .then(() => emitter.end(), err => emitter.error(err))
return emitter.stream return emitter.stream
} }
+38 -56
View File
@@ -7,25 +7,6 @@ export interface Thenable<T> {
catch (reject: resolver): Thenable<T>; catch (reject: resolver): Thenable<T>;
} }
function createResolvedThenable<T> (value: T): Thenable<T> {
const ret = {
then: (resolve: resolver) => resolve(value),
catch: () => ret
}
return ret
}
function createRejectedThenable<T> (err: Error): Thenable<T> {
const ret = {
then: (resolve: resolver, reject?: resolver) => {
if (reject) return reject(err)
return ret
},
catch: (reject: resolver) => reject(err)
}
return ret
}
function isThenable<T> (val: any): val is Thenable<T> { function isThenable<T> (val: any): val is Thenable<T> {
return val && isFunction(val.then) return val && isFunction(val.then)
} }
@@ -34,48 +15,49 @@ function isAsyncIterator (val: any): val is IterableIterator<any> {
return val && isFunction(val.next) && isFunction(val.throw) && isFunction(val.return) return val && isFunction(val.next) && isFunction(val.throw) && isFunction(val.return)
} }
// convert an async iterator to a thenable (Promise compatible) // convert an async iterator to a Promise
export function toThenable<T> (val: IteratorResult<unknown, T> | Thenable<T> | any): Thenable<T> { export async function toPromise<T> (val: Generator<unknown, T, unknown> | Thenable<T> | T): Promise<T> {
if (isThenable(val)) return val if (!isAsyncIterator(val)) return val
if (isAsyncIterator(val)) return reduce() let value: unknown
return createResolvedThenable(val) let done = false
let next = 'next'
function reduce<T> (prev?: T): Thenable<T> { do {
let state const state = val[next](value)
done = state.done
value = state.value
next = 'next'
try { try {
state = val.next(prev) if (isAsyncIterator(value)) value = toPromise(value)
if (isThenable(value)) value = await value
} catch (err) { } catch (err) {
return createRejectedThenable(err as Error) next = 'throw'
value = err
} }
} while (!done)
return value as T
}
if (state.done) return createResolvedThenable(state.value) // convert an async iterator to a value in a synchronous maner
return toThenable(state.value!).then(reduce, err => { export function toValue<T> (val: Generator<unknown, T, unknown> | T): T {
let state if (!isAsyncIterator(val)) return val
let value: any
let done = false
let next = 'next'
do {
const state = val[next](value)
done = state.done
value = state.value
next = 'next'
if (isAsyncIterator(value)) {
try { try {
state = val.throw!(err) value = toValue(value)
} catch (e) { } catch (err) {
return createRejectedThenable(e as Error) next = 'throw'
value = err
} }
if (state.done) return createResolvedThenable(state.value) }
return reduce(state.value) } while (!done)
}) return value
}
} }
export function toPromise<T> (val: Generator<unknown, T, unknown> | Thenable<T> | T): Promise<T> { export const toThenable = toPromise
return Promise.resolve(toThenable(val))
}
// get the value of async iterator in synchronous manner
export function toValue<T> (val: Generator<unknown, T, unknown> | Thenable<T> | T): T {
let ret: T
toThenable(val)
.then((x: any) => {
ret = x
return createResolvedThenable(ret)
})
.catch((err: Error) => {
throw err
})
return ret!
}
+10
View File
@@ -246,4 +246,14 @@ describe('Issues', function () {
const html = await engine.parseAndRender(`{% if template contains "product" %}contains{%endif%}`, ctx) const html = await engine.parseAndRender(`{% if template contains "product" %}contains{%endif%}`, ctx)
expect(html).to.equal('contains') expect(html).to.equal('contains')
}) })
it('#513 should support large number of templates [async]', async () => {
const engine = new Liquid()
const html = await engine.parseAndRender(`{% for i in (1..10000) %}{{ i }}{% endfor %}`)
expect(html).to.have.lengthOf(38894)
})
it('#513 should support large number of templates [sync]', () => {
const engine = new Liquid()
const html = engine.parseAndRenderSync(`{% for i in (1..10000) %}{{ i }}{% endfor %}`)
expect(html).to.have.lengthOf(38894)
})
}) })
+5 -3
View File
@@ -126,11 +126,13 @@ describe('LiquidOptions#cache', function () {
extname: '.html', extname: '.html',
cache: true cache: true
}) })
try { await engine.renderFile('foo') } catch (err) {} try {
await engine.renderFile('foo')
} catch (err) {}
mock({ '/root/foo.html': 'foo' }) mock({ '/root/foo.html': 'foo' })
const y = await engine.renderFile('foo') const html = await engine.renderFile('foo')
expect(y).to.equal('foo') expect(html).to.equal('foo')
}) })
}) })
+43 -43
View File
@@ -2,7 +2,7 @@ import { Tokenizer } from '../../../src/parser/tokenizer'
import { expect } from 'chai' import { expect } from 'chai'
import { Drop } from '../../../src/drop/drop' import { Drop } from '../../../src/drop/drop'
import { Context } from '../../../src/context/context' import { Context } from '../../../src/context/context'
import { toThenable } from '../../../src/util/async' import { toPromise } from '../../../src/util/async'
import { defaultOperators } from '../../../src/render/operator' import { defaultOperators } from '../../../src/render/operator'
import { createTrie } from '../../../src/util/operator-trie' import { createTrie } from '../../../src/util/operator-trie'
@@ -12,7 +12,7 @@ describe('Expression', function () {
const create = (str: string) => new Tokenizer(str, trie).readExpression() const create = (str: string) => new Tokenizer(str, trie).readExpression()
it('should throw when context not defined', done => { it('should throw when context not defined', done => {
toThenable(create('foo').evaluate(undefined!, false)) toPromise(create('foo').evaluate(undefined!, false))
.then(() => done(new Error('should not resolved'))) .then(() => done(new Error('should not resolved')))
.catch(err => { .catch(err => {
expect(err.message).to.match(/context not defined/) expect(err.message).to.match(/context not defined/)
@@ -22,19 +22,19 @@ describe('Expression', function () {
describe('single value', function () { describe('single value', function () {
it('should eval literal', async function () { it('should eval literal', async function () {
expect(await toThenable(create('2.4').evaluate(ctx, false))).to.equal(2.4) expect(await toPromise(create('2.4').evaluate(ctx, false))).to.equal(2.4)
expect(await toThenable(create('"foo"').evaluate(ctx, false))).to.equal('foo') expect(await toPromise(create('"foo"').evaluate(ctx, false))).to.equal('foo')
expect(await toThenable(create('false').evaluate(ctx, false))).to.equal(false) expect(await toPromise(create('false').evaluate(ctx, false))).to.equal(false)
}) })
it('should eval range expression', async function () { it('should eval range expression', async function () {
const ctx = new Context({ two: 2 }) const ctx = new Context({ two: 2 })
expect(await toThenable(create('(2..4)').evaluate(ctx, false))).to.deep.equal([2, 3, 4]) expect(await toPromise(create('(2..4)').evaluate(ctx, false))).to.deep.equal([2, 3, 4])
expect(await toThenable(create('(two..4)').evaluate(ctx, false))).to.deep.equal([2, 3, 4]) expect(await toPromise(create('(two..4)').evaluate(ctx, false))).to.deep.equal([2, 3, 4])
}) })
it('should eval literal', async function () { it('should eval literal', async function () {
expect(await toThenable(create('2.4').evaluate(ctx, false))).to.equal(2.4) expect(await toPromise(create('2.4').evaluate(ctx, false))).to.equal(2.4)
expect(await toThenable(create('"foo"').evaluate(ctx, false))).to.equal('foo') expect(await toPromise(create('"foo"').evaluate(ctx, false))).to.equal('foo')
expect(await toThenable(create('false').evaluate(ctx, false))).to.equal(false) expect(await toPromise(create('false').evaluate(ctx, false))).to.equal(false)
}) })
it('should eval property access', async function () { it('should eval property access', async function () {
@@ -43,119 +43,119 @@ describe('Expression', function () {
coo: 'bar', coo: 'bar',
doo: { foo: 'bar', bar: { foo: 'bar' } } doo: { foo: 'bar', bar: { foo: 'bar' } }
}) })
expect(await toThenable(create('foo.bar').evaluate(ctx, false))).to.equal('BAR') expect(await toPromise(create('foo.bar').evaluate(ctx, false))).to.equal('BAR')
expect(await toThenable(create('foo["bar"]').evaluate(ctx, false))).to.equal('BAR') expect(await toPromise(create('foo["bar"]').evaluate(ctx, false))).to.equal('BAR')
expect(await toThenable(create('foo[coo]').evaluate(ctx, false))).to.equal('BAR') expect(await toPromise(create('foo[coo]').evaluate(ctx, false))).to.equal('BAR')
expect(await toThenable(create('foo[doo.foo]').evaluate(ctx, false))).to.equal('BAR') expect(await toPromise(create('foo[doo.foo]').evaluate(ctx, false))).to.equal('BAR')
expect(await toThenable(create('foo[doo["foo"]]').evaluate(ctx, false))).to.equal('BAR') expect(await toPromise(create('foo[doo["foo"]]').evaluate(ctx, false))).to.equal('BAR')
expect(await toThenable(create('doo[coo].foo').evaluate(ctx, false))).to.equal('bar') expect(await toPromise(create('doo[coo].foo').evaluate(ctx, false))).to.equal('bar')
}) })
}) })
describe('simple expression', function () { describe('simple expression', function () {
it('should return false for "1==2"', async () => { it('should return false for "1==2"', async () => {
expect(await toThenable(create('1==2').evaluate(ctx, false))).to.equal(false) expect(await toPromise(create('1==2').evaluate(ctx, false))).to.equal(false)
}) })
it('should return true for "1<2"', async () => { it('should return true for "1<2"', async () => {
expect(await toThenable(create('1<2').evaluate(ctx, false))).to.equal(true) expect(await toPromise(create('1<2').evaluate(ctx, false))).to.equal(true)
}) })
it('should return true for "1 < 2"', async () => { it('should return true for "1 < 2"', async () => {
expect(await toThenable(create('1 < 2').evaluate(ctx, false))).to.equal(true) expect(await toPromise(create('1 < 2').evaluate(ctx, false))).to.equal(true)
}) })
it('should return true for "1 < 2"', async () => { it('should return true for "1 < 2"', async () => {
expect(await toThenable(create('1 < 2').evaluate(ctx, false))).to.equal(true) expect(await toPromise(create('1 < 2').evaluate(ctx, false))).to.equal(true)
}) })
it('should return true for "2 <= 2"', async () => { it('should return true for "2 <= 2"', async () => {
expect(await toThenable(create('2 <= 2').evaluate(ctx, false))).to.equal(true) expect(await toPromise(create('2 <= 2').evaluate(ctx, false))).to.equal(true)
}) })
it('should return true for "one <= two"', async () => { it('should return true for "one <= two"', async () => {
const ctx = new Context({ one: 1, two: 2 }) const ctx = new Context({ one: 1, two: 2 })
expect(await toThenable(create('one <= two').evaluate(ctx, false))).to.equal(true) expect(await toPromise(create('one <= two').evaluate(ctx, false))).to.equal(true)
}) })
it('should return false for "x contains "x""', async () => { it('should return false for "x contains "x""', async () => {
const ctx = new Context({ x: 'XXX' }) const ctx = new Context({ x: 'XXX' })
expect(await toThenable(create('x contains "x"').evaluate(ctx, false))).to.equal(false) expect(await toPromise(create('x contains "x"').evaluate(ctx, false))).to.equal(false)
}) })
it('should return true for "x contains "X""', async () => { it('should return true for "x contains "X""', async () => {
const ctx = new Context({ x: 'XXX' }) const ctx = new Context({ x: 'XXX' })
expect(await toThenable(create('x contains "X"').evaluate(ctx, false))).to.equal(true) expect(await toPromise(create('x contains "X"').evaluate(ctx, false))).to.equal(true)
}) })
it('should return false for "1 contains "x""', async () => { it('should return false for "1 contains "x""', async () => {
const ctx = new Context({ x: 'XXX' }) const ctx = new Context({ x: 'XXX' })
expect(await toThenable(create('1 contains "x"').evaluate(ctx, false))).to.equal(false) expect(await toPromise(create('1 contains "x"').evaluate(ctx, false))).to.equal(false)
}) })
it('should return false for "y contains "x""', async () => { it('should return false for "y contains "x""', async () => {
const ctx = new Context({ x: 'XXX' }) const ctx = new Context({ x: 'XXX' })
expect(await toThenable(create('y contains "x"').evaluate(ctx, false))).to.equal(false) expect(await toPromise(create('y contains "x"').evaluate(ctx, false))).to.equal(false)
}) })
it('should return false for "z contains "x""', async () => { it('should return false for "z contains "x""', async () => {
const ctx = new Context({ x: 'XXX' }) const ctx = new Context({ x: 'XXX' })
expect(await toThenable(create('z contains "x"').evaluate(ctx, false))).to.equal(false) expect(await toPromise(create('z contains "x"').evaluate(ctx, false))).to.equal(false)
}) })
it('should return true for "(1..5) contains 3"', async () => { it('should return true for "(1..5) contains 3"', async () => {
const ctx = new Context({ x: 'XXX' }) const ctx = new Context({ x: 'XXX' })
expect(await toThenable(create('(1..5) contains 3').evaluate(ctx, false))).to.equal(true) expect(await toPromise(create('(1..5) contains 3').evaluate(ctx, false))).to.equal(true)
}) })
it('should return false for "(1..5) contains 6"', async () => { it('should return false for "(1..5) contains 6"', async () => {
const ctx = new Context({ x: 'XXX' }) const ctx = new Context({ x: 'XXX' })
expect(await toThenable(create('(1..5) contains 6').evaluate(ctx, false))).to.equal(false) expect(await toPromise(create('(1..5) contains 6').evaluate(ctx, false))).to.equal(false)
}) })
it('should return true for ""<=" == "<=""', async () => { it('should return true for ""<=" == "<=""', async () => {
expect(await toThenable(create('"<=" == "<="').evaluate(ctx, false))).to.equal(true) expect(await toPromise(create('"<=" == "<="').evaluate(ctx, false))).to.equal(true)
}) })
}) })
it('should allow space in quoted value', async function () { it('should allow space in quoted value', async function () {
const ctx = new Context({ space: ' ' }) const ctx = new Context({ space: ' ' })
expect(await toThenable(create('" " == space').evaluate(ctx, false))).to.equal(true) expect(await toPromise(create('" " == space').evaluate(ctx, false))).to.equal(true)
}) })
describe('escape', () => { describe('escape', () => {
it('should escape quote', async function () { it('should escape quote', async function () {
const ctx = new Context({ quote: '"' }) const ctx = new Context({ quote: '"' })
expect(await toThenable(create('"\\"" == quote').evaluate(ctx, false))).to.equal(true) expect(await toPromise(create('"\\"" == quote').evaluate(ctx, false))).to.equal(true)
}) })
it('should escape square bracket', async function () { it('should escape square bracket', async function () {
const ctx = new Context({ obj: { ']': 'bracket' } }) const ctx = new Context({ obj: { ']': 'bracket' } })
expect(await toThenable(create('obj["]"] == "bracket"').evaluate(ctx, false))).to.equal(true) expect(await toPromise(create('obj["]"] == "bracket"').evaluate(ctx, false))).to.equal(true)
}) })
}) })
describe('complex expression', function () { describe('complex expression', function () {
it('should support value or value', async function () { it('should support value or value', async function () {
expect(await toThenable(create('false or true').evaluate(ctx, false))).to.equal(true) expect(await toPromise(create('false or true').evaluate(ctx, false))).to.equal(true)
}) })
it('should support < and contains', async function () { it('should support < and contains', async function () {
expect(await toThenable(create('1 < 2 and x contains "x"').evaluate(ctx, false))).to.equal(false) expect(await toPromise(create('1 < 2 and x contains "x"').evaluate(ctx, false))).to.equal(false)
}) })
it('should support < or contains', async function () { it('should support < or contains', async function () {
expect(await toThenable(create('1 < 2 or x contains "x"').evaluate(ctx, false))).to.equal(true) expect(await toPromise(create('1 < 2 or x contains "x"').evaluate(ctx, false))).to.equal(true)
}) })
it('should support Drops for "x contains "x""', async () => { it('should support Drops for "x contains "x""', async () => {
class TemplateDrop extends Drop { class TemplateDrop extends Drop {
valueOf () { return 'X' } valueOf () { return 'X' }
} }
const ctx = new Context({ x: 'XXX', X: new TemplateDrop() }) const ctx = new Context({ x: 'XXX', X: new TemplateDrop() })
expect(await toThenable(create('x contains X').evaluate(ctx, false))).to.equal(true) expect(await toPromise(create('x contains X').evaluate(ctx, false))).to.equal(true)
}) })
it('should support value and !=', async function () { it('should support value and !=', async function () {
const ctx = new Context({ empty: '' }) const ctx = new Context({ empty: '' })
expect(await toThenable(create('empty and empty != ""').evaluate(ctx, false))).to.equal(false) expect(await toPromise(create('empty and empty != ""').evaluate(ctx, false))).to.equal(false)
}) })
it('should recognize quoted value', async function () { it('should recognize quoted value', async function () {
expect(await toThenable(create('">"').evaluate(ctx, false))).to.equal('>') expect(await toPromise(create('">"').evaluate(ctx, false))).to.equal('>')
}) })
it('should evaluate from right to left', async function () { it('should evaluate from right to left', async function () {
expect(await toThenable(create('true or false and false').evaluate(ctx, false))).to.equal(true) expect(await toPromise(create('true or false and false').evaluate(ctx, false))).to.equal(true)
expect(await toThenable(create('true and false and false or true').evaluate(ctx, false))).to.equal(false) expect(await toPromise(create('true and false and false or true').evaluate(ctx, false))).to.equal(false)
}) })
it('should recognize property access', async function () { it('should recognize property access', async function () {
const ctx = new Context({ obj: { foo: true } }) const ctx = new Context({ obj: { foo: true } })
expect(await toThenable(create('obj["foo"] and true').evaluate(ctx, false))).to.equal(true) expect(await toPromise(create('obj["foo"] and true').evaluate(ctx, false))).to.equal(true)
}) })
it('should allow nested property access', async function () { it('should allow nested property access', async function () {
const ctx = new Context({ obj: { foo: 'FOO' }, keys: { "what's this": 'foo' } }) const ctx = new Context({ obj: { foo: 'FOO' }, keys: { "what's this": 'foo' } })
expect(await toThenable(create('obj[keys["what\'s this"]]').evaluate(ctx, false))).to.equal('FOO') expect(await toPromise(create('obj[keys["what\'s this"]]').evaluate(ctx, false))).to.equal('FOO')
}) })
}) })
}) })
+2 -2
View File
@@ -4,7 +4,7 @@ import { HTMLToken } from '../../../src/tokens/html-token'
import { Render } from '../../../src/render/render' import { Render } from '../../../src/render/render'
import { HTML } from '../../../src/template/html' import { HTML } from '../../../src/template/html'
import { SimpleEmitter } from '../../../src/emitters/simple-emitter' import { SimpleEmitter } from '../../../src/emitters/simple-emitter'
import { toThenable } from '../../../src/util/async' import { toPromise } from '../../../src/util/async'
import { Tag } from '../../../src/template/tag/tag' import { Tag } from '../../../src/template/tag/tag'
import { TagToken } from '../../../src/types' import { TagToken } from '../../../src/types'
@@ -18,7 +18,7 @@ describe('render', function () {
it('should render html', async function () { it('should render html', async function () {
const scope = new Context() const scope = new Context()
const token = { getContent: () => '<p>' } as HTMLToken const token = { getContent: () => '<p>' } as HTMLToken
const html = await toThenable(render.renderTemplates([new HTML(token)], scope, new SimpleEmitter())) const html = await toPromise(render.renderTemplates([new HTML(token)], scope, new SimpleEmitter()))
return expect(html).to.equal('<p>') return expect(html).to.equal('<p>')
}) })
}) })
+9 -9
View File
@@ -2,7 +2,7 @@ import * as chai from 'chai'
import * as sinon from 'sinon' import * as sinon from 'sinon'
import * as sinonChai from 'sinon-chai' import * as sinonChai from 'sinon-chai'
import { Context } from '../../../../src/context/context' import { Context } from '../../../../src/context/context'
import { toThenable } from '../../../../src/util/async' import { toPromise } from '../../../../src/util/async'
import { NumberToken } from '../../../../src/tokens/number-token' import { NumberToken } from '../../../../src/tokens/number-token'
import { QuotedToken } from '../../../../src/tokens/quoted-token' import { QuotedToken } from '../../../../src/tokens/quoted-token'
import { IdentifierToken } from '../../../../src/tokens/identifier-token' import { IdentifierToken } from '../../../../src/tokens/identifier-token'
@@ -25,46 +25,46 @@ describe('filter', function () {
}) })
it('should render input if filter not registered', async function () { it('should render input if filter not registered', async function () {
expect(await toThenable(filters.create('undefined', []).render('foo', ctx))).to.equal('foo') expect(await toPromise(filters.create('undefined', []).render('foo', ctx))).to.equal('foo')
}) })
it('should call filter impl with correct arguments', async function () { it('should call filter impl with correct arguments', async function () {
const spy = sinon.spy() const spy = sinon.spy()
filters.set('foo', spy) filters.set('foo', spy)
const thirty = new NumberToken(new IdentifierToken('30', 0, 2), undefined) const thirty = new NumberToken(new IdentifierToken('30', 0, 2), undefined)
await toThenable(filters.create('foo', [thirty]).render('foo', ctx)) await toPromise(filters.create('foo', [thirty]).render('foo', ctx))
expect(spy).to.have.been.calledWith('foo', 30) expect(spy).to.have.been.calledWith('foo', 30)
}) })
it('should call filter impl with correct this', async function () { it('should call filter impl with correct this', async function () {
const spy = sinon.spy() const spy = sinon.spy()
filters.set('foo', spy) filters.set('foo', spy)
const thirty = new NumberToken(new IdentifierToken('33', 0, 2), undefined) const thirty = new NumberToken(new IdentifierToken('33', 0, 2), undefined)
await toThenable(filters.create('foo', [thirty]).render('foo', ctx)) await toPromise(filters.create('foo', [thirty]).render('foo', ctx))
expect(spy).to.have.been.calledOn(sinon.match.has('context', ctx)) expect(spy).to.have.been.calledOn(sinon.match.has('context', ctx))
expect(spy).to.have.been.calledOn(sinon.match.has('liquid', liquid)) expect(spy).to.have.been.calledOn(sinon.match.has('liquid', liquid))
}) })
it('should render a simple filter', async function () { it('should render a simple filter', async function () {
filters.set('upcase', x => x.toUpperCase()) filters.set('upcase', x => x.toUpperCase())
expect(await toThenable(filters.create('upcase', []).render('foo', ctx))).to.equal('FOO') expect(await toPromise(filters.create('upcase', []).render('foo', ctx))).to.equal('FOO')
}) })
it('should render filters with argument', async function () { it('should render filters with argument', async function () {
filters.set('add', (a, b) => a + b) filters.set('add', (a, b) => a + b)
const two = new NumberToken(new IdentifierToken('2', 0, 1), undefined) const two = new NumberToken(new IdentifierToken('2', 0, 1), undefined)
expect(await toThenable(filters.create('add', [two]).render(3, ctx))).to.equal(5) expect(await toPromise(filters.create('add', [two]).render(3, ctx))).to.equal(5)
}) })
it('should render filters with multiple arguments', async function () { it('should render filters with multiple arguments', async function () {
filters.set('add', (a, b, c) => a + b + c) filters.set('add', (a, b, c) => a + b + c)
const two = new NumberToken(new IdentifierToken('2', 0, 1), undefined) const two = new NumberToken(new IdentifierToken('2', 0, 1), undefined)
const c = new QuotedToken('"c"', 0, 3) const c = new QuotedToken('"c"', 0, 3)
expect(await toThenable(filters.create('add', [two, c]).render(3, ctx))).to.equal('5c') expect(await toPromise(filters.create('add', [two, c]).render(3, ctx))).to.equal('5c')
}) })
it('should pass Objects/Drops as it is', async function () { it('should pass Objects/Drops as it is', async function () {
filters.set('name', a => a.constructor.name) filters.set('name', a => a.constructor.name)
class Foo {} class Foo {}
expect(await toThenable(filters.create('name', []).render(new Foo(), ctx))).to.equal('Foo') expect(await toPromise(filters.create('name', []).render(new Foo(), ctx))).to.equal('Foo')
}) })
it('should not throw when filter name illegal', function () { it('should not throw when filter name illegal', function () {
@@ -76,6 +76,6 @@ describe('filter', function () {
it('should support key value pairs', async function () { it('should support key value pairs', async function () {
filters.set('add', (a, b) => b[0] + ':' + (a + b[1])) filters.set('add', (a, b) => b[0] + ':' + (a + b[1]))
const two = new NumberToken(new IdentifierToken('2', 0, 1), undefined) const two = new NumberToken(new IdentifierToken('2', 0, 1), undefined)
expect(await toThenable((filters.create('add', [['num', two]]).render(3, ctx)))).to.equal('num:5') expect(await toPromise((filters.create('add', [['num', two]]).render(3, ctx)))).to.equal('num:5')
}) })
}) })
+8 -8
View File
@@ -1,5 +1,5 @@
import * as chai from 'chai' import * as chai from 'chai'
import { toThenable } from '../../../src/util/async' import { toPromise } from '../../../src/util/async'
import { Hash } from '../../../src/template/tag/hash' import { Hash } from '../../../src/template/tag/hash'
import { Context } from '../../../src/context/context' import { Context } from '../../../src/context/context'
@@ -7,34 +7,34 @@ const expect = chai.expect
describe('Hash', function () { describe('Hash', function () {
it('should parse "reverse"', async function () { it('should parse "reverse"', async function () {
const hash = await toThenable(new Hash('reverse').render(new Context({ foo: 3 }))) const hash = await toPromise(new Hash('reverse').render(new Context({ foo: 3 })))
expect(hash).to.haveOwnProperty('reverse') expect(hash).to.haveOwnProperty('reverse')
expect(hash.reverse).to.be.true expect(hash.reverse).to.be.true
}) })
it('should parse "num:foo"', async function () { it('should parse "num:foo"', async function () {
const hash = await toThenable(new Hash('num:foo').render(new Context({ foo: 3 }))) const hash = await toPromise(new Hash('num:foo').render(new Context({ foo: 3 })))
expect(hash.num).to.equal(3) expect(hash.num).to.equal(3)
}) })
it('should parse "num:3"', async function () { it('should parse "num:3"', async function () {
const hash = await toThenable(new Hash('num:3').render(new Context())) const hash = await toPromise(new Hash('num:3').render(new Context()))
expect(hash.num).to.equal(3) expect(hash.num).to.equal(3)
}) })
it('should parse "num: arr[0]"', async function () { it('should parse "num: arr[0]"', async function () {
const hash = await toThenable(new Hash('num:3').render(new Context({ arr: [3] }))) const hash = await toPromise(new Hash('num:3').render(new Context({ arr: [3] })))
expect(hash.num).to.equal(3) expect(hash.num).to.equal(3)
}) })
it('should parse "num: 2.3"', async function () { it('should parse "num: 2.3"', async function () {
const hash = await toThenable(new Hash('num:2.3').render(new Context())) const hash = await toPromise(new Hash('num:2.3').render(new Context()))
expect(hash.num).to.equal(2.3) expect(hash.num).to.equal(2.3)
}) })
it('should parse "num:bar.coo"', async function () { it('should parse "num:bar.coo"', async function () {
const pending = new Hash('num:bar.coo').render(new Context({ bar: { coo: 3 } })) const pending = new Hash('num:bar.coo').render(new Context({ bar: { coo: 3 } }))
const hash = await toThenable(pending) const hash = await toPromise(pending)
expect(hash.num).to.equal(3) expect(hash.num).to.equal(3)
}) })
it('should parse "num1:2.3 reverse,num2:bar.coo\n num3: arr[0]"', async function () { it('should parse "num1:2.3 reverse,num2:bar.coo\n num3: arr[0]"', async function () {
const ctx = new Context({ bar: { coo: 3 }, arr: [4] }) const ctx = new Context({ bar: { coo: 3 }, arr: [4] })
const hash = await toThenable(new Hash('num1:2.3 reverse,num2:bar.coo\n num3: arr[0]').render(ctx)) const hash = await toPromise(new Hash('num1:2.3 reverse,num2:bar.coo\n num3: arr[0]').render(ctx))
expect(hash).to.deep.equal({ expect(hash).to.deep.equal({
num1: 2.3, num1: 2.3,
reverse: true, reverse: true,
+9 -9
View File
@@ -1,5 +1,5 @@
import * as chai from 'chai' import * as chai from 'chai'
import { toThenable } from '../../../src/util/async' import { toPromise } from '../../../src/util/async'
import { Context } from '../../../src/context/context' import { Context } from '../../../src/context/context'
import { Output } from '../../../src/template/output' import { Output } from '../../../src/template/output'
import { OutputToken } from '../../../src/tokens/output-token' import { OutputToken } from '../../../src/tokens/output-token'
@@ -21,25 +21,25 @@ describe('Output', function () {
foo: { obj: { arr: ['a', 2] } } foo: { obj: { arr: ['a', 2] } }
}) })
const output = new Output({ content: 'foo' } as OutputToken, liquid) const output = new Output({ content: 'foo' } as OutputToken, liquid)
await toThenable(output.render(scope, emitter)) await toPromise(output.render(scope, emitter))
return expect(emitter.html).to.equal('[object Object]') return expect(emitter.html).to.equal('[object Object]')
}) })
it('should skip function property', async function () { it('should skip function property', async function () {
const scope = new Context({ obj: { foo: 'foo', bar: (x: any) => x } }) const scope = new Context({ obj: { foo: 'foo', bar: (x: any) => x } })
const output = new Output({ content: 'obj' } as OutputToken, liquid) const output = new Output({ content: 'obj' } as OutputToken, liquid)
await toThenable(output.render(scope, emitter)) await toPromise(output.render(scope, emitter))
return expect(emitter.html).to.equal('[object Object]') return expect(emitter.html).to.equal('[object Object]')
}) })
it('should respect to .toString()', async () => { it('should respect to .toString()', async () => {
const scope = new Context({ obj: { toString: () => 'FOO' } }) const scope = new Context({ obj: { toString: () => 'FOO' } })
const output = new Output({ content: 'obj' } as OutputToken, liquid) const output = new Output({ content: 'obj' } as OutputToken, liquid)
await toThenable(output.render(scope, emitter)) await toPromise(output.render(scope, emitter))
return expect(emitter.html).to.equal('FOO') return expect(emitter.html).to.equal('FOO')
}) })
it('should respect to .toString()', async () => { it('should respect to .toString()', async () => {
const scope = new Context({ obj: { toString: () => 'FOO' } }) const scope = new Context({ obj: { toString: () => 'FOO' } })
const output = new Output({ content: 'obj' } as OutputToken, liquid) const output = new Output({ content: 'obj' } as OutputToken, liquid)
await toThenable(output.render(scope, emitter)) await toPromise(output.render(scope, emitter))
return expect(emitter.html).to.equal('FOO') return expect(emitter.html).to.equal('FOO')
}) })
context('when keepOutputType is enabled', () => { context('when keepOutputType is enabled', () => {
@@ -62,7 +62,7 @@ describe('Output', function () {
foo: 42 foo: 42
}, { ...defaultOptions, keepOutputType: true }) }, { ...defaultOptions, keepOutputType: true })
const output = new Output({ content: 'foo' } as OutputToken, liquid) const output = new Output({ content: 'foo' } as OutputToken, liquid)
await toThenable(output.render(scope, emitter)) await toPromise(output.render(scope, emitter))
return expect(emitter.html).to.equal(42) return expect(emitter.html).to.equal(42)
}) })
it('should respect output variable boolean type', async () => { it('should respect output variable boolean type', async () => {
@@ -70,7 +70,7 @@ describe('Output', function () {
foo: true foo: true
}, { ...defaultOptions, keepOutputType: true }) }, { ...defaultOptions, keepOutputType: true })
const output = new Output({ content: 'foo' } as OutputToken, liquid) const output = new Output({ content: 'foo' } as OutputToken, liquid)
await toThenable(output.render(scope, emitter)) await toPromise(output.render(scope, emitter))
return expect(emitter.html).to.equal(true) return expect(emitter.html).to.equal(true)
}) })
it('should respect output variable object type', async () => { it('should respect output variable object type', async () => {
@@ -78,7 +78,7 @@ describe('Output', function () {
foo: 'test' foo: 'test'
}, { ...defaultOptions, keepOutputType: true }) }, { ...defaultOptions, keepOutputType: true })
const output = new Output({ content: 'foo' } as OutputToken, liquid) const output = new Output({ content: 'foo' } as OutputToken, liquid)
await toThenable(output.render(scope, emitter)) await toPromise(output.render(scope, emitter))
return expect(emitter.html).to.equal('test') return expect(emitter.html).to.equal('test')
}) })
it('should respect output variable string type', async () => { it('should respect output variable string type', async () => {
@@ -86,7 +86,7 @@ describe('Output', function () {
foo: { a: { b: 42 } } foo: { a: { b: 42 } }
}, { ...defaultOptions, keepOutputType: true }) }, { ...defaultOptions, keepOutputType: true })
const output = new Output({ content: 'foo' } as OutputToken, liquid) const output = new Output({ content: 'foo' } as OutputToken, liquid)
await toThenable(output.render(scope, emitter)) await toPromise(output.render(scope, emitter))
return expect(emitter.html).to.deep.equal({ a: { b: 42 } }) return expect(emitter.html).to.deep.equal({ a: { b: 42 } })
}) })
}) })
+2 -2
View File
@@ -4,7 +4,7 @@ import { Context } from '../../../src/context/context'
import * as sinon from 'sinon' import * as sinon from 'sinon'
import * as sinonChai from 'sinon-chai' import * as sinonChai from 'sinon-chai'
import { TagToken } from '../../../src/tokens/tag-token' import { TagToken } from '../../../src/tokens/tag-token'
import { toThenable } from '../../../src/util/async' import { toPromise } from '../../../src/util/async'
chai.use(sinonChai) chai.use(sinonChai)
const expect = chai.expect const expect = chai.expect
@@ -20,7 +20,7 @@ describe('Tag', function () {
args: '', args: '',
name: 'foo' name: 'foo'
} as TagToken } as TagToken
await toThenable(new Tag(token, [], { await toPromise(new Tag(token, [], {
tags: { tags: {
get: () => ({ render: spy }) get: () => ({ render: spy })
} }
+2 -2
View File
@@ -1,7 +1,7 @@
import * as chai from 'chai' import * as chai from 'chai'
import { Liquid } from '../../../src/liquid' import { Liquid } from '../../../src/liquid'
import { QuotedToken } from '../../../src/tokens/quoted-token' import { QuotedToken } from '../../../src/tokens/quoted-token'
import { toThenable } from '../../../src/util/async' import { toPromise } from '../../../src/util/async'
import * as sinonChai from 'sinon-chai' import * as sinonChai from 'sinon-chai'
import * as sinon from 'sinon' import * as sinon from 'sinon'
import { Context } from '../../../src/context/context' import { Context } from '../../../src/context/context'
@@ -36,7 +36,7 @@ describe('Value', function () {
const scope = new Context({ const scope = new Context({
foo: { bar: 'bar' } foo: { bar: 'bar' }
}) })
await toThenable(tpl.value(scope, false)) await toPromise(tpl.value(scope, false))
expect(date).to.have.been.calledWith('bar', 'b') expect(date).to.have.been.calledWith('bar', 'b')
expect(time).to.have.been.calledWith('y', 2) expect(time).to.have.been.calledWith('y', 2)
}) })
+19 -21
View File
@@ -1,4 +1,4 @@
import { toThenable, toPromise, toValue } from '../../../src/util/async' import { toPromise, toValue } from '../../../src/util/async'
import { expect, use } from 'chai' import { expect, use } from 'chai'
import * as chaiAsPromised from 'chai-as-promised' import * as chaiAsPromised from 'chai-as-promised'
@@ -13,47 +13,45 @@ describe('utils/async', () => {
const result = await toPromise(foo()) const result = await toPromise(foo())
expect(result).to.equal('foo') expect(result).to.equal('foo')
}) })
})
describe('#toThenable()', function () {
it('should support iterable with single return statement', async () => { it('should support iterable with single return statement', async () => {
function * foo () { function * foo () {
return 'foo' return 'foo'
} }
const result = await toThenable(foo()) const result = await toPromise(foo())
expect(result).to.equal('foo') expect(result).to.equal('foo')
}) })
it('should support promise', async () => { it('should support promise', async () => {
function foo () { function foo () {
return Promise.resolve('foo') return Promise.resolve('foo')
} }
const result = await toThenable(foo()) const result = await toPromise(foo())
expect(result).to.equal('foo') expect(result).to.equal('foo')
}) })
it('should resolve dependency', async () => { it('should resolve dependency', async () => {
function * foo () { function * foo (): Generator<Generator<string>> {
return yield bar() return yield bar()
} }
function * bar () { function * bar (): Generator<string> {
return 'bar' return 'bar'
} }
const result = await toThenable(foo()) const result = await toPromise(foo())
expect(result).to.equal('bar') expect(result).to.equal('bar')
}) })
it('should support promise dependency', async () => { it('should support promise dependency', async () => {
function * foo () { function * foo (): Generator<Promise<string>> {
return yield Promise.resolve('foo') return yield Promise.resolve('foo')
} }
const result = await toThenable(foo()) const result = await toPromise(foo())
expect(result).to.equal('foo') expect(result).to.equal('foo')
}) })
it('should reject Promise if dependency throws syncly', done => { it('should reject Promise if dependency throws syncly', done => {
function * foo () { function * foo (): Generator<Generator<never>> {
return yield bar() return yield bar()
} }
function * bar (): IterableIterator<any> { function * bar (): Generator<never> {
throw new Error('bar') throw new Error('bar')
} }
toThenable(foo()).catch(err => { toPromise(foo()).catch(err => {
expect(err.message).to.equal('bar') expect(err.message).to.equal('bar')
done() done()
return 0 as any return 0 as any
@@ -70,43 +68,43 @@ describe('utils/async', () => {
ret += 'foo' ret += 'foo'
return ret return ret
} }
function * bar (): IterableIterator<any> { function * bar (): Generator<never> {
throw new Error('bar') throw new Error('bar')
} }
const ret = await toThenable(foo()) const ret = await toPromise(foo())
expect(ret).to.equal('barfoo') expect(ret).to.equal('barfoo')
}) })
}) })
describe('#toValue()', function () { describe('#toValue()', function () {
it('should throw Error if dependency throws syncly', () => { it('should throw Error if dependency throws syncly', () => {
function * foo () { function * foo (): Generator<Generator<never>> {
return yield bar() return yield bar()
} }
function * bar (): IterableIterator<any> { function * bar (): Generator<never> {
throw new Error('bar') throw new Error('bar')
} }
expect(() => toValue(foo())).to.throw('bar') expect(() => toValue(foo())).to.throw('bar')
}) })
it('should resume yield after catch', () => { it('should resume yield after catch', () => {
function * foo () { function * foo (): Generator<unknown, never, never> {
try { try {
yield bar() yield bar()
} catch (e) {} } catch (e) {}
return yield 'foo' return yield 'foo'
} }
function * bar (): IterableIterator<any> { function * bar (): Generator<never> {
throw new Error('bar') throw new Error('bar')
} }
expect(toValue(foo())).to.equal('foo') expect(toValue(foo())).to.equal('foo')
}) })
it('should resume return after catch', () => { it('should resume return after catch', () => {
function * foo () { function * foo (): Generator<Generator<never>, string> {
try { try {
yield bar() yield bar()
} catch (e) {} } catch (e) {}
return 'foo' return 'foo'
} }
function * bar (): IterableIterator<any> { function * bar (): Generator<never> {
throw new Error('bar') throw new Error('bar')
} }
expect(toValue(foo())).to.equal('foo') expect(toValue(foo())).to.equal('foo')