From 3dc4290b56265cfafbee8d9836e912d9b8492f90 Mon Sep 17 00:00:00 2001 From: Harttle Date: Sun, 3 Jul 2022 22:21:35 +0800 Subject: [PATCH] fix: stack overflow on large number of templates, #513 --- .../navy/layout/partial/all-contributors.swig | 1 + src/cache/cache.ts | 4 + src/liquid-options.ts | 12 +-- src/parser/parser.ts | 29 +++--- src/render/render.ts | 4 +- src/util/async.ts | 94 ++++++++----------- test/e2e/issues.ts | 10 ++ test/integration/liquid/cache.ts | 8 +- test/unit/render/expression.ts | 86 ++++++++--------- test/unit/render/render.ts | 4 +- test/unit/template/filter/filter.ts | 18 ++-- test/unit/template/hash.ts | 16 ++-- test/unit/template/output.ts | 18 ++-- test/unit/template/tag.ts | 4 +- test/unit/template/value.ts | 4 +- test/unit/util/async.ts | 40 ++++---- 16 files changed, 172 insertions(+), 180 deletions(-) diff --git a/docs/themes/navy/layout/partial/all-contributors.swig b/docs/themes/navy/layout/partial/all-contributors.swig index 93efd6030..589f4e17e 100644 --- a/docs/themes/navy/layout/partial/all-contributors.swig +++ b/docs/themes/navy/layout/partial/all-contributors.swig @@ -55,6 +55,7 @@ + diff --git a/src/cache/cache.ts b/src/cache/cache.ts index 3ea13b89b..084cf3812 100644 --- a/src/cache/cache.ts +++ b/src/cache/cache.ts @@ -1,5 +1,9 @@ +import type { Template } from '../template/template' + export interface Cache { write (key: string, value: T): void | Promise; read (key: string): T | undefined | Promise; remove (key: string): void | Promise; } + +export type LiquidCache = Cache> diff --git a/src/liquid-options.ts b/src/liquid-options.ts index 45c29874c..9dd1c7a4d 100644 --- a/src/liquid-options.ts +++ b/src/liquid-options.ts @@ -1,12 +1,10 @@ import { snakeCase, forOwn, isArray, isString, isFunction } from './util/underscore' -import { Template } from './template/template' -import { Cache } from './cache/cache' +import { LiquidCache } from './cache/cache' import { LRU } from './cache/lru' import { FS } from './fs/fs' import * as fs from './fs/node' import { defaultOperators, Operators } from './render/operator' import { createTrie, Trie } from './util/operator-trie' -import { Thenable } from './util/async' import * as builtinFilters from './builtin/filters' 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 `""`. */ extname?: string; /** Whether or not to cache resolved templates. Defaults to `false`. */ - cache?: boolean | number | Cache>; + cache?: boolean | number | LiquidCache; /** Use Javascript Truthiness. Defaults to `false`. */ 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`. */ @@ -104,7 +102,7 @@ interface NormalizedOptions extends LiquidOptions { root?: string[]; partials?: string[]; layouts?: string[]; - cache?: Cache>; + cache?: LiquidCache; outputEscape?: OutputEscape; operatorsTrie?: Trie; } @@ -116,7 +114,7 @@ export interface NormalizedFullOptions extends NormalizedOptions { relativeReference: boolean; jekyllInclude: boolean; extname: string; - cache: undefined | Cache>; + cache?: LiquidCache; jsTruthy: boolean; dynamicPartials: boolean; fs: FS; @@ -180,7 +178,7 @@ export function normalize (options: LiquidOptions): NormalizedFullOptions { if (!options.hasOwnProperty('layouts')) options.layouts = options.root } if (options.hasOwnProperty('cache')) { - let cache: Cache> | undefined + let cache: LiquidCache | 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 cache = options.cache ? new LRU(1024) : undefined diff --git a/src/parser/parser.ts b/src/parser/parser.ts index 422ee3dda..ca7c3d7cb 100644 --- a/src/parser/parser.ts +++ b/src/parser/parser.ts @@ -8,17 +8,17 @@ import { Output } from '../template/output' import { HTML } from '../template/html' import { Template } from '../template/template' import { TopLevelToken } from '../tokens/toplevel-token' -import { Cache } from '../cache/cache' +import { LiquidCache } from '../cache/cache' import { Loader, LookupType } from '../fs/loader' +import { toPromise } from '../util/async' import { FS } from '../fs/fs' -import { toThenable, Thenable } from '../util/async' export default class Parser { public parseFile: (file: string, sync?: boolean, type?: LookupType, currentFile?: string) => Generator private liquid: Liquid private fs: FS - private cache: Cache> | undefined + private cache?: LiquidCache private loader: Loader public constructor (liquid: Liquid) { @@ -58,21 +58,18 @@ export default class Parser { return new ParseStream(tokens, (token, tokens) => this.parseToken(token, tokens)) } private * _parseFileCached (file: string, sync?: boolean, type: LookupType = LookupType.Root, currentFile?: string): Generator { - const key = this.loader.shouldLoadRelative(file) - ? currentFile + ',' + file - : type + ':' + file - const tpls = yield this.cache!.read(key) + const cache = this.cache! + const key = this.loader.shouldLoadRelative(file) ? currentFile + ',' + file : type + ':' + file + const tpls = yield cache.read(key) if (tpls) return tpls - const task = toThenable(this._parseFile(file, sync, type, currentFile)) - this.cache!.write(key, task) - try { - return yield task - } catch (e) { - // remove cached task if failed - this.cache!.remove(key) - } - return [] + const task = this._parseFile(file, sync, type, currentFile) + // sync mode: exec the task and cache the result + // async mode: cache the task before exec + const taskOrTpl = sync ? yield task : toPromise(task) + cache.write(key, taskOrTpl as any) + // note: concurrent tasks will be reused, cache for failed task is removed until its end + try { return yield taskOrTpl } catch (err) { cache.remove(key); throw err } } private * _parseFile (file: string, sync?: boolean, type: LookupType = LookupType.Root, currentFile?: string): Generator { const filepath = yield this.loader.lookup(file, type, sync, currentFile) diff --git a/src/render/render.ts b/src/render/render.ts index 6ef5e83a4..10aaac928 100644 --- a/src/render/render.ts +++ b/src/render/render.ts @@ -4,13 +4,13 @@ import { Template } from '../template/template' import { Emitter } from '../emitters/emitter' import { SimpleEmitter } from '../emitters/simple-emitter' import { StreamedEmitter } from '../emitters/streamed-emitter' -import { toThenable } from '../util/async' +import { toPromise } from '../util/async' import { KeepingTypeEmitter } from '../emitters/keeping-type-emitter' export class Render { public renderTemplatesToNodeStream (templates: Template[], ctx: Context): NodeJS.ReadableStream { 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)) return emitter.stream } diff --git a/src/util/async.ts b/src/util/async.ts index 095252622..8200eff2b 100644 --- a/src/util/async.ts +++ b/src/util/async.ts @@ -7,25 +7,6 @@ export interface Thenable { catch (reject: resolver): Thenable; } -function createResolvedThenable (value: T): Thenable { - const ret = { - then: (resolve: resolver) => resolve(value), - catch: () => ret - } - return ret -} - -function createRejectedThenable (err: Error): Thenable { - const ret = { - then: (resolve: resolver, reject?: resolver) => { - if (reject) return reject(err) - return ret - }, - catch: (reject: resolver) => reject(err) - } - return ret -} - function isThenable (val: any): val is Thenable { return val && isFunction(val.then) } @@ -34,48 +15,49 @@ function isAsyncIterator (val: any): val is IterableIterator { return val && isFunction(val.next) && isFunction(val.throw) && isFunction(val.return) } -// convert an async iterator to a thenable (Promise compatible) -export function toThenable (val: IteratorResult | Thenable | any): Thenable { - if (isThenable(val)) return val - if (isAsyncIterator(val)) return reduce() - return createResolvedThenable(val) - - function reduce (prev?: T): Thenable { - let state +// convert an async iterator to a Promise +export async function toPromise (val: Generator | Thenable | T): Promise { + if (!isAsyncIterator(val)) return val + let value: unknown + let done = false + let next = 'next' + do { + const state = val[next](value) + done = state.done + value = state.value + next = 'next' try { - state = val.next(prev) + if (isAsyncIterator(value)) value = toPromise(value) + if (isThenable(value)) value = await value } catch (err) { - return createRejectedThenable(err as Error) + next = 'throw' + value = err } + } while (!done) + return value as T +} - if (state.done) return createResolvedThenable(state.value) - return toThenable(state.value!).then(reduce, err => { - let state +// convert an async iterator to a value in a synchronous maner +export function toValue (val: Generator | T): T { + 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 { - state = val.throw!(err) - } catch (e) { - return createRejectedThenable(e as Error) + value = toValue(value) + } catch (err) { + next = 'throw' + value = err } - if (state.done) return createResolvedThenable(state.value) - return reduce(state.value) - }) - } + } + } while (!done) + return value } -export function toPromise (val: Generator | Thenable | T): Promise { - return Promise.resolve(toThenable(val)) -} - -// get the value of async iterator in synchronous manner -export function toValue (val: Generator | Thenable | T): T { - let ret: T - toThenable(val) - .then((x: any) => { - ret = x - return createResolvedThenable(ret) - }) - .catch((err: Error) => { - throw err - }) - return ret! -} +export const toThenable = toPromise diff --git a/test/e2e/issues.ts b/test/e2e/issues.ts index d1e058724..72570be4f 100644 --- a/test/e2e/issues.ts +++ b/test/e2e/issues.ts @@ -246,4 +246,14 @@ describe('Issues', function () { const html = await engine.parseAndRender(`{% if template contains "product" %}contains{%endif%}`, ctx) 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) + }) }) diff --git a/test/integration/liquid/cache.ts b/test/integration/liquid/cache.ts index 08a2f0e05..4dac51414 100644 --- a/test/integration/liquid/cache.ts +++ b/test/integration/liquid/cache.ts @@ -126,11 +126,13 @@ describe('LiquidOptions#cache', function () { extname: '.html', cache: true }) - try { await engine.renderFile('foo') } catch (err) {} + try { + await engine.renderFile('foo') + } catch (err) {} mock({ '/root/foo.html': 'foo' }) - const y = await engine.renderFile('foo') - expect(y).to.equal('foo') + const html = await engine.renderFile('foo') + expect(html).to.equal('foo') }) }) diff --git a/test/unit/render/expression.ts b/test/unit/render/expression.ts index 405feb4e0..5abf08583 100644 --- a/test/unit/render/expression.ts +++ b/test/unit/render/expression.ts @@ -2,7 +2,7 @@ import { Tokenizer } from '../../../src/parser/tokenizer' import { expect } from 'chai' import { Drop } from '../../../src/drop/drop' 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 { createTrie } from '../../../src/util/operator-trie' @@ -12,7 +12,7 @@ describe('Expression', function () { const create = (str: string) => new Tokenizer(str, trie).readExpression() 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'))) .catch(err => { expect(err.message).to.match(/context not defined/) @@ -22,19 +22,19 @@ describe('Expression', function () { describe('single value', function () { it('should eval literal', async function () { - expect(await toThenable(create('2.4').evaluate(ctx, false))).to.equal(2.4) - expect(await toThenable(create('"foo"').evaluate(ctx, false))).to.equal('foo') - expect(await toThenable(create('false').evaluate(ctx, false))).to.equal(false) + expect(await toPromise(create('2.4').evaluate(ctx, false))).to.equal(2.4) + expect(await toPromise(create('"foo"').evaluate(ctx, false))).to.equal('foo') + expect(await toPromise(create('false').evaluate(ctx, false))).to.equal(false) }) it('should eval range expression', async function () { const ctx = new Context({ two: 2 }) - expect(await toThenable(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('(2..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 () { - expect(await toThenable(create('2.4').evaluate(ctx, false))).to.equal(2.4) - expect(await toThenable(create('"foo"').evaluate(ctx, false))).to.equal('foo') - expect(await toThenable(create('false').evaluate(ctx, false))).to.equal(false) + expect(await toPromise(create('2.4').evaluate(ctx, false))).to.equal(2.4) + expect(await toPromise(create('"foo"').evaluate(ctx, false))).to.equal('foo') + expect(await toPromise(create('false').evaluate(ctx, false))).to.equal(false) }) it('should eval property access', async function () { @@ -43,119 +43,119 @@ describe('Expression', function () { coo: 'bar', doo: { foo: 'bar', bar: { foo: 'bar' } } }) - expect(await toThenable(create('foo.bar').evaluate(ctx, false))).to.equal('BAR') - expect(await toThenable(create('foo["bar"]').evaluate(ctx, false))).to.equal('BAR') - expect(await toThenable(create('foo[coo]').evaluate(ctx, false))).to.equal('BAR') - expect(await toThenable(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 toThenable(create('doo[coo].foo').evaluate(ctx, false))).to.equal('bar') + expect(await toPromise(create('foo.bar').evaluate(ctx, false))).to.equal('BAR') + expect(await toPromise(create('foo["bar"]').evaluate(ctx, false))).to.equal('BAR') + expect(await toPromise(create('foo[coo]').evaluate(ctx, false))).to.equal('BAR') + expect(await toPromise(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 toPromise(create('doo[coo].foo').evaluate(ctx, false))).to.equal('bar') }) }) describe('simple expression', function () { 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 () => { - 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 () => { - 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 () => { - 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 () => { - 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 () => { 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 () => { 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 () => { 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 () => { 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 () => { 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 () => { 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 () => { 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 () => { 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 () => { - 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 () { 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', () => { it('should escape quote', async function () { 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 () { 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 () { 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 () { - 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 () { - 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 () => { class TemplateDrop extends Drop { valueOf () { return 'X' } } 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 () { 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 () { - 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 () { - expect(await toThenable(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 or false and false').evaluate(ctx, false))).to.equal(true) + expect(await toPromise(create('true and false and false or true').evaluate(ctx, false))).to.equal(false) }) it('should recognize property access', async function () { 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 () { 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') }) }) }) diff --git a/test/unit/render/render.ts b/test/unit/render/render.ts index ee4316a59..3c1f14812 100644 --- a/test/unit/render/render.ts +++ b/test/unit/render/render.ts @@ -4,7 +4,7 @@ import { HTMLToken } from '../../../src/tokens/html-token' import { Render } from '../../../src/render/render' import { HTML } from '../../../src/template/html' 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 { TagToken } from '../../../src/types' @@ -18,7 +18,7 @@ describe('render', function () { it('should render html', async function () { const scope = new Context() const token = { getContent: () => '

' } 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('

') }) }) diff --git a/test/unit/template/filter/filter.ts b/test/unit/template/filter/filter.ts index f1b84838c..7d4dbba71 100644 --- a/test/unit/template/filter/filter.ts +++ b/test/unit/template/filter/filter.ts @@ -2,7 +2,7 @@ import * as chai from 'chai' import * as sinon from 'sinon' import * as sinonChai from 'sinon-chai' 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 { QuotedToken } from '../../../../src/tokens/quoted-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 () { - 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 () { const spy = sinon.spy() filters.set('foo', spy) 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) }) it('should call filter impl with correct this', async function () { const spy = sinon.spy() filters.set('foo', spy) 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('liquid', liquid)) }) it('should render a simple filter', async function () { 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 () { filters.set('add', (a, b) => a + b) 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 () { filters.set('add', (a, b, c) => a + b + c) const two = new NumberToken(new IdentifierToken('2', 0, 1), undefined) 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 () { filters.set('name', a => a.constructor.name) 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 () { @@ -76,6 +76,6 @@ describe('filter', function () { it('should support key value pairs', async function () { filters.set('add', (a, b) => b[0] + ':' + (a + b[1])) 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') }) }) diff --git a/test/unit/template/hash.ts b/test/unit/template/hash.ts index 5e12ac8ea..0a8977997 100644 --- a/test/unit/template/hash.ts +++ b/test/unit/template/hash.ts @@ -1,5 +1,5 @@ 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 { Context } from '../../../src/context/context' @@ -7,34 +7,34 @@ const expect = chai.expect describe('Hash', 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.reverse).to.be.true }) 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) }) 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) }) 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) }) 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) }) it('should parse "num:bar.coo"', async function () { 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) }) 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 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({ num1: 2.3, reverse: true, diff --git a/test/unit/template/output.ts b/test/unit/template/output.ts index 64d15aa30..270b76f5f 100644 --- a/test/unit/template/output.ts +++ b/test/unit/template/output.ts @@ -1,5 +1,5 @@ import * as chai from 'chai' -import { toThenable } from '../../../src/util/async' +import { toPromise } from '../../../src/util/async' import { Context } from '../../../src/context/context' import { Output } from '../../../src/template/output' import { OutputToken } from '../../../src/tokens/output-token' @@ -21,25 +21,25 @@ describe('Output', function () { foo: { obj: { arr: ['a', 2] } } }) 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]') }) it('should skip function property', async function () { const scope = new Context({ obj: { foo: 'foo', bar: (x: any) => x } }) 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]') }) it('should respect to .toString()', async () => { const scope = new Context({ obj: { toString: () => 'FOO' } }) 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') }) it('should respect to .toString()', async () => { const scope = new Context({ obj: { toString: () => 'FOO' } }) 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') }) context('when keepOutputType is enabled', () => { @@ -62,7 +62,7 @@ describe('Output', function () { foo: 42 }, { ...defaultOptions, keepOutputType: true }) 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) }) it('should respect output variable boolean type', async () => { @@ -70,7 +70,7 @@ describe('Output', function () { foo: true }, { ...defaultOptions, keepOutputType: true }) 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) }) it('should respect output variable object type', async () => { @@ -78,7 +78,7 @@ describe('Output', function () { foo: 'test' }, { ...defaultOptions, keepOutputType: true }) 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') }) it('should respect output variable string type', async () => { @@ -86,7 +86,7 @@ describe('Output', function () { foo: { a: { b: 42 } } }, { ...defaultOptions, keepOutputType: true }) 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 } }) }) }) diff --git a/test/unit/template/tag.ts b/test/unit/template/tag.ts index 1821ceb9f..23cbadbfd 100644 --- a/test/unit/template/tag.ts +++ b/test/unit/template/tag.ts @@ -4,7 +4,7 @@ import { Context } from '../../../src/context/context' import * as sinon from 'sinon' import * as sinonChai from 'sinon-chai' import { TagToken } from '../../../src/tokens/tag-token' -import { toThenable } from '../../../src/util/async' +import { toPromise } from '../../../src/util/async' chai.use(sinonChai) const expect = chai.expect @@ -20,7 +20,7 @@ describe('Tag', function () { args: '', name: 'foo' } as TagToken - await toThenable(new Tag(token, [], { + await toPromise(new Tag(token, [], { tags: { get: () => ({ render: spy }) } diff --git a/test/unit/template/value.ts b/test/unit/template/value.ts index 86faa611c..fa2b9358e 100644 --- a/test/unit/template/value.ts +++ b/test/unit/template/value.ts @@ -1,7 +1,7 @@ import * as chai from 'chai' import { Liquid } from '../../../src/liquid' 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 sinon from 'sinon' import { Context } from '../../../src/context/context' @@ -36,7 +36,7 @@ describe('Value', function () { const scope = new Context({ foo: { bar: 'bar' } }) - await toThenable(tpl.value(scope, false)) + await toPromise(tpl.value(scope, false)) expect(date).to.have.been.calledWith('bar', 'b') expect(time).to.have.been.calledWith('y', 2) }) diff --git a/test/unit/util/async.ts b/test/unit/util/async.ts index 9952bf8c2..9cd83a3ee 100644 --- a/test/unit/util/async.ts +++ b/test/unit/util/async.ts @@ -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 * as chaiAsPromised from 'chai-as-promised' @@ -13,47 +13,45 @@ describe('utils/async', () => { const result = await toPromise(foo()) expect(result).to.equal('foo') }) - }) - describe('#toThenable()', function () { it('should support iterable with single return statement', async () => { function * foo () { return 'foo' } - const result = await toThenable(foo()) + const result = await toPromise(foo()) expect(result).to.equal('foo') }) it('should support promise', async () => { function foo () { return Promise.resolve('foo') } - const result = await toThenable(foo()) + const result = await toPromise(foo()) expect(result).to.equal('foo') }) it('should resolve dependency', async () => { - function * foo () { + function * foo (): Generator> { return yield bar() } - function * bar () { + function * bar (): Generator { return 'bar' } - const result = await toThenable(foo()) + const result = await toPromise(foo()) expect(result).to.equal('bar') }) it('should support promise dependency', async () => { - function * foo () { + function * foo (): Generator> { return yield Promise.resolve('foo') } - const result = await toThenable(foo()) + const result = await toPromise(foo()) expect(result).to.equal('foo') }) it('should reject Promise if dependency throws syncly', done => { - function * foo () { + function * foo (): Generator> { return yield bar() } - function * bar (): IterableIterator { + function * bar (): Generator { throw new Error('bar') } - toThenable(foo()).catch(err => { + toPromise(foo()).catch(err => { expect(err.message).to.equal('bar') done() return 0 as any @@ -70,43 +68,43 @@ describe('utils/async', () => { ret += 'foo' return ret } - function * bar (): IterableIterator { + function * bar (): Generator { throw new Error('bar') } - const ret = await toThenable(foo()) + const ret = await toPromise(foo()) expect(ret).to.equal('barfoo') }) }) describe('#toValue()', function () { it('should throw Error if dependency throws syncly', () => { - function * foo () { + function * foo (): Generator> { return yield bar() } - function * bar (): IterableIterator { + function * bar (): Generator { throw new Error('bar') } expect(() => toValue(foo())).to.throw('bar') }) it('should resume yield after catch', () => { - function * foo () { + function * foo (): Generator { try { yield bar() } catch (e) {} return yield 'foo' } - function * bar (): IterableIterator { + function * bar (): Generator { throw new Error('bar') } expect(toValue(foo())).to.equal('foo') }) it('should resume return after catch', () => { - function * foo () { + function * foo (): Generator, string> { try { yield bar() } catch (e) {} return 'foo' } - function * bar (): IterableIterator { + function * bar (): Generator { throw new Error('bar') } expect(toValue(foo())).to.equal('foo')