mirror of
https://github.com/harttle/liquidjs.git
synced 2026-09-16 04:40:39 -07:00
fix: cache ongoing parseFile() calls, fixes #416
This commit is contained in:
+3
-3
@@ -9,8 +9,8 @@ if [ ! -f $FILE_LATEST ]; then
|
|||||||
curl $URL_LATEST > $FILE_LATEST
|
curl $URL_LATEST > $FILE_LATEST
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if [ ! -f $FILE_LOCAL ]; then
|
# if [ ! -f $FILE_LOCAL ]; then
|
||||||
BUNDLES=cjs npm run build:dist
|
BUNDLES=cjs npm run build:dist
|
||||||
fi
|
# fi
|
||||||
|
|
||||||
exec node benchmark/diff.js $FILE_LOCAL $FILE_LATEST
|
exec node benchmark/diff.js $FILE_LOCAL $FILE_LATEST
|
||||||
|
|||||||
+2
-2
@@ -12,14 +12,14 @@
|
|||||||
"types": "dist/liquid.d.ts",
|
"types": "dist/liquid.d.ts",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"lint": "eslint \"**/*.ts\" .",
|
"lint": "eslint \"**/*.ts\" .",
|
||||||
"check": "npm test && npm run lint",
|
"check": "npm run build && npm test && npm run lint",
|
||||||
"test": "nyc mocha \"test/**/*.ts\"",
|
"test": "nyc mocha \"test/**/*.ts\"",
|
||||||
"test:e2e": "mocha \"test/e2e/**/*.ts\"",
|
"test:e2e": "mocha \"test/e2e/**/*.ts\"",
|
||||||
"perf": "cd benchmark && npm ci && npm start",
|
"perf": "cd benchmark && npm ci && npm start",
|
||||||
"perf:diff": "bin/perf-diff.sh",
|
"perf:diff": "bin/perf-diff.sh",
|
||||||
"perf:engines": "cd benchmark && npm run engines",
|
"perf:engines": "cd benchmark && npm run engines",
|
||||||
"build": "npm run build:dist && npm run build:docs",
|
"build": "npm run build:dist && npm run build:docs",
|
||||||
"build:dist": "rollup -c rollup.config.ts && ls -lh dist",
|
"build:dist": "rollup -c rollup.config.ts",
|
||||||
"build:docs": "bin/build-docs.sh"
|
"build:docs": "bin/build-docs.sh"
|
||||||
},
|
},
|
||||||
"bin": {
|
"bin": {
|
||||||
|
|||||||
Vendored
+1
@@ -1,4 +1,5 @@
|
|||||||
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>;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ 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'
|
||||||
|
|
||||||
export interface LiquidOptions {
|
export interface LiquidOptions {
|
||||||
/** A directory or an array of directories from where to resolve layout and include templates, and the filename passed to `.renderFile()`. If it's an array, the files are looked up in the order they occur in the array. Defaults to `["."]` */
|
/** A directory or an array of directories from where to resolve layout and include templates, and the filename passed to `.renderFile()`. If it's an array, the files are looked up in the order they occur in the array. Defaults to `["."]` */
|
||||||
@@ -19,7 +20,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<Template[]>;
|
cache?: boolean | number | Cache<Thenable<Template[]>>;
|
||||||
/** 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`. */
|
||||||
@@ -68,7 +69,7 @@ interface NormalizedOptions extends LiquidOptions {
|
|||||||
root?: string[];
|
root?: string[];
|
||||||
partials?: string[];
|
partials?: string[];
|
||||||
layouts?: string[];
|
layouts?: string[];
|
||||||
cache?: Cache<Template[]>;
|
cache?: Cache<Thenable<Template[]>>;
|
||||||
operatorsTrie?: Trie;
|
operatorsTrie?: Trie;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -78,7 +79,7 @@ export interface NormalizedFullOptions extends NormalizedOptions {
|
|||||||
layouts: string[];
|
layouts: string[];
|
||||||
relativeReference: boolean;
|
relativeReference: boolean;
|
||||||
extname: string;
|
extname: string;
|
||||||
cache: undefined | Cache<Template[]>;
|
cache: undefined | Cache<Thenable<Template[]>>;
|
||||||
jsTruthy: boolean;
|
jsTruthy: boolean;
|
||||||
dynamicPartials: boolean;
|
dynamicPartials: boolean;
|
||||||
fs: FS;
|
fs: FS;
|
||||||
@@ -142,10 +143,10 @@ export function normalize (options?: LiquidOptions): NormalizedOptions {
|
|||||||
options.layouts = normalizeDirectoryList(options.layouts)
|
options.layouts = normalizeDirectoryList(options.layouts)
|
||||||
}
|
}
|
||||||
if (options.hasOwnProperty('cache')) {
|
if (options.hasOwnProperty('cache')) {
|
||||||
let cache: Cache<Template[]> | undefined
|
let cache: Cache<Thenable<Template[]>> | 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<Template[]>(1024) : undefined
|
else cache = options.cache ? new LRU(1024) : undefined
|
||||||
options.cache = cache
|
options.cache = cache
|
||||||
}
|
}
|
||||||
if (options.hasOwnProperty('operators')) {
|
if (options.hasOwnProperty('operators')) {
|
||||||
|
|||||||
+13
-7
@@ -11,13 +11,14 @@ import { TopLevelToken } from '../tokens/toplevel-token'
|
|||||||
import { Cache } from '../cache/cache'
|
import { Cache } from '../cache/cache'
|
||||||
import { Loader, LookupType } from '../fs/loader'
|
import { Loader, LookupType } from '../fs/loader'
|
||||||
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) => Iterator<Template[]>
|
public parseFile: (file: string, sync?: boolean, type?: LookupType, currentFile?: string) => Iterator<Template[]>
|
||||||
|
|
||||||
private liquid: Liquid
|
private liquid: Liquid
|
||||||
private fs: FS
|
private fs: FS
|
||||||
private cache: Cache<Template[]> | undefined
|
private cache: Cache<Thenable<Template[]>> | undefined
|
||||||
private loader: Loader
|
private loader: Loader
|
||||||
|
|
||||||
public constructor (liquid: Liquid) {
|
public constructor (liquid: Liquid) {
|
||||||
@@ -60,14 +61,19 @@ export default class Parser {
|
|||||||
const key = this.loader.shouldLoadRelative(file)
|
const key = this.loader.shouldLoadRelative(file)
|
||||||
? currentFile + ',' + file
|
? currentFile + ',' + file
|
||||||
: type + ':' + file
|
: type + ':' + file
|
||||||
let templates = yield this.cache!.read(key)
|
const tpls = yield this.cache!.read(key)
|
||||||
if (templates) return templates
|
if (tpls) return tpls
|
||||||
|
|
||||||
templates = yield this._parseFile(file, sync, type, currentFile)
|
const task = toThenable(this._parseFile(file, sync, type, currentFile))
|
||||||
this.cache!.write(key, templates)
|
this.cache!.write(key, task)
|
||||||
return templates
|
try {
|
||||||
|
return yield task
|
||||||
|
} catch (e) {
|
||||||
|
// remove cached task if failed
|
||||||
|
this.cache!.remove(key)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
private * _parseFile (file: string, sync?: boolean, type: LookupType = LookupType.Root, currentFile?: string) {
|
private * _parseFile (file: string, sync?: boolean, type: LookupType = LookupType.Root, currentFile?: string): IterableIterator<any> {
|
||||||
const filepath = yield this.loader.lookup(file, type, sync, currentFile)
|
const filepath = yield this.loader.lookup(file, type, sync, currentFile)
|
||||||
return this.liquid.parse(sync ? this.fs.readFileSync(filepath) : yield this.fs.readFile(filepath), filepath)
|
return this.liquid.parse(sync ? this.fs.readFileSync(filepath) : yield this.fs.readFile(filepath), filepath)
|
||||||
}
|
}
|
||||||
|
|||||||
+14
-12
@@ -2,12 +2,12 @@ import { isFunction } from './underscore'
|
|||||||
|
|
||||||
type resolver = (x?: any) => any
|
type resolver = (x?: any) => any
|
||||||
|
|
||||||
interface Thenable {
|
export interface Thenable<T> {
|
||||||
then (resolve: resolver, reject?: resolver): Thenable;
|
then (resolve: resolver, reject?: resolver): Thenable<T>;
|
||||||
catch (reject: resolver): Thenable;
|
catch (reject: resolver): Thenable<T>;
|
||||||
}
|
}
|
||||||
|
|
||||||
function createResolvedThenable (value: any): Thenable {
|
function createResolvedThenable<T> (value: T): Thenable<T> {
|
||||||
const ret = {
|
const ret = {
|
||||||
then: (resolve: resolver) => resolve(value),
|
then: (resolve: resolver) => resolve(value),
|
||||||
catch: () => ret
|
catch: () => ret
|
||||||
@@ -15,7 +15,7 @@ function createResolvedThenable (value: any): Thenable {
|
|||||||
return ret
|
return ret
|
||||||
}
|
}
|
||||||
|
|
||||||
function createRejectedThenable (err: Error): Thenable {
|
function createRejectedThenable<T> (err: Error): Thenable<T> {
|
||||||
const ret = {
|
const ret = {
|
||||||
then: (resolve: resolver, reject?: resolver) => {
|
then: (resolve: resolver, reject?: resolver) => {
|
||||||
if (reject) return reject(err)
|
if (reject) return reject(err)
|
||||||
@@ -26,7 +26,7 @@ function createRejectedThenable (err: Error): Thenable {
|
|||||||
return ret
|
return ret
|
||||||
}
|
}
|
||||||
|
|
||||||
function isThenable (val: any): val is Thenable {
|
function isThenable<T> (val: any): val is Thenable<T> {
|
||||||
return val && isFunction(val.then)
|
return val && isFunction(val.then)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -34,13 +34,15 @@ 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)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type Task<T> = Thenable<T>
|
||||||
|
|
||||||
// convert an async iterator to a thenable (Promise compatible)
|
// convert an async iterator to a thenable (Promise compatible)
|
||||||
export function toThenable (val: IterableIterator<any> | Thenable | any): Thenable {
|
export function toThenable<T> (val: IterableIterator<T> | Thenable<T> | any): Thenable<T> {
|
||||||
if (isThenable(val)) return val
|
if (isThenable(val)) return val
|
||||||
if (isAsyncIterator(val)) return reduce()
|
if (isAsyncIterator(val)) return reduce()
|
||||||
return createResolvedThenable(val)
|
return createResolvedThenable(val)
|
||||||
|
|
||||||
function reduce (prev?: any): Thenable {
|
function reduce<T> (prev?: T): Thenable<T> {
|
||||||
let state
|
let state
|
||||||
try {
|
try {
|
||||||
state = (val as IterableIterator<any>).next(prev)
|
state = (val as IterableIterator<any>).next(prev)
|
||||||
@@ -62,13 +64,13 @@ export function toThenable (val: IterableIterator<any> | Thenable | any): Thenab
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function toPromise (val: IterableIterator<any> | Thenable | any): Promise<any> {
|
export function toPromise<T> (val: IterableIterator<T> | Thenable<T> | T): Promise<T> {
|
||||||
return Promise.resolve(toThenable(val))
|
return Promise.resolve(toThenable(val))
|
||||||
}
|
}
|
||||||
|
|
||||||
// get the value of async iterator in synchronous manner
|
// get the value of async iterator in synchronous manner
|
||||||
export function toValue (val: IterableIterator<any> | Thenable | any) {
|
export function toValue<T> (val: IterableIterator<T> | Thenable<T> | T): T {
|
||||||
let ret: any
|
let ret: T
|
||||||
toThenable(val)
|
toThenable(val)
|
||||||
.then((x: any) => {
|
.then((x: any) => {
|
||||||
ret = x
|
ret = x
|
||||||
@@ -77,5 +79,5 @@ export function toValue (val: IterableIterator<any> | Thenable | any) {
|
|||||||
.catch((err: Error) => {
|
.catch((err: Error) => {
|
||||||
throw err
|
throw err
|
||||||
})
|
})
|
||||||
return ret
|
return ret!
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,9 +1,12 @@
|
|||||||
import { Liquid } from '../../src/liquid'
|
import { Liquid } from '../../src/liquid'
|
||||||
import { expect, use } from 'chai'
|
import { expect, use } from 'chai'
|
||||||
import * as chaiAsPromised from 'chai-as-promised'
|
import * as chaiAsPromised from 'chai-as-promised'
|
||||||
|
import * as sinon from 'sinon'
|
||||||
|
import * as sinonChai from 'sinon-chai'
|
||||||
const LiquidUMD = require('../../dist/liquid.browser.umd.js').Liquid
|
const LiquidUMD = require('../../dist/liquid.browser.umd.js').Liquid
|
||||||
|
|
||||||
use(chaiAsPromised)
|
use(chaiAsPromised)
|
||||||
|
use(sinonChai)
|
||||||
|
|
||||||
describe('Issues', function () {
|
describe('Issues', function () {
|
||||||
it('#221 unicode blanks are not properly treated', async () => {
|
it('#221 unicode blanks are not properly treated', async () => {
|
||||||
@@ -129,4 +132,25 @@ describe('Issues', function () {
|
|||||||
const html = await engine.renderSync(tpl)
|
const html = await engine.renderSync(tpl)
|
||||||
expect(html).to.equal('/tmp/foo.liquid')
|
expect(html).to.equal('/tmp/foo.liquid')
|
||||||
})
|
})
|
||||||
|
it('#416 Templates imported by {% render %} not cached for concurrent async render', async () => {
|
||||||
|
const readFile = sinon.spy(() => Promise.resolve('HELLO'))
|
||||||
|
const exists = sinon.spy(() => 'HELLO')
|
||||||
|
const engine = new Liquid({
|
||||||
|
cache: true,
|
||||||
|
extname: '.liquid',
|
||||||
|
root: '~',
|
||||||
|
fs: {
|
||||||
|
exists,
|
||||||
|
resolve: (root: string, file: string, ext: string) => root + '#' + file + ext,
|
||||||
|
sep: '#',
|
||||||
|
readFile
|
||||||
|
} as any
|
||||||
|
})
|
||||||
|
|
||||||
|
await Promise.all(Array(5).fill(0).map(
|
||||||
|
x => engine.parseAndRender("{% render 'template' %}")
|
||||||
|
))
|
||||||
|
expect(exists).to.be.calledOnce
|
||||||
|
expect(readFile).to.be.calledOnce
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -155,10 +155,7 @@ describe('LiquidOptions#cache', function () {
|
|||||||
cache: true
|
cache: true
|
||||||
})
|
})
|
||||||
mock({ '/root/foo.html': 'foo' })
|
mock({ '/root/foo.html': 'foo' })
|
||||||
mock({ '/root/bar.html': 'bar' })
|
|
||||||
|
|
||||||
expect(engine.renderFileSync('foo')).to.equal('foo')
|
expect(engine.renderFileSync('foo')).to.equal('foo')
|
||||||
expect(engine.renderFileSync('bar')).to.equal('bar')
|
|
||||||
mock({ '/root/foo.html': 'bar' })
|
mock({ '/root/foo.html': 'bar' })
|
||||||
expect(engine.renderFileSync('foo')).to.equal('foo')
|
expect(engine.renderFileSync('foo')).to.equal('foo')
|
||||||
})
|
})
|
||||||
|
|||||||
Reference in New Issue
Block a user