Skip to content

Commit

Permalink
perf: improve performance by 4x by simplified parseFile
Browse files Browse the repository at this point in the history
- previously deprecated `getTemplate()` and `getTemplateSync()` not no longer supported
- `opts` no longer support dynamic set in `parseFile()`, `renderFile()` arguments
  • Loading branch information
harttle committed Sep 27, 2021
1 parent 6b9f872 commit 24f5346
Show file tree
Hide file tree
Showing 14 changed files with 103 additions and 151 deletions.
1 change: 1 addition & 0 deletions benchmark/engines/liquid.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ const { join } = require('path')

const liquid = new Liquid({
root: join(__dirname, '../templates'),
cache: true,
extname: '.liquid'
})

Expand Down
4 changes: 3 additions & 1 deletion docs/source/tutorials/contribution-guidelines.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ title: Contribution Guideline

## Star on Github 👉 [![harttle/liquidjs](https://img.shields.io/github/stars/harttle/liquidjs?style=flat-square)][liquidjs]

Staring us is the most important and easiest way to support us: boost its rank and expose it to more people, which in turn makes it better.
Staring LiquidJS is the most important and easiest way to support us: boost its rank and expose it to more people, which in turn makes it better.

## Show Me Your Code

Expand All @@ -22,6 +22,8 @@ npm test

**Commit Message**: Please align to [the Angular Commit Message Guidelines](https://github.com/angular/angular.js/blob/master/DEVELOPERS.md#commits), especially note the [type identifier](https://github.com/angular/angular.js/blob/master/DEVELOPERS.md#type), on which semantic-release bot depends.

**Backward-Compatibility**: please be backward-compatible. LiquidJS is used by multiple layers of softwares, including underlying libraries, compilers, site generators and Web servers. It's not easy to do a major upgrade for most of them.

## Financial Support

LiquidJS is Open Source and Free and **without** capitalists support and **without** any ADs. To help it live and thrive, consider contribute on [Open Collective][oc] or [Patreon][pt]. To acknowledge your contribution, your name and avatar will be listed here and on [Github README][liquidjs].
Expand Down
2 changes: 2 additions & 0 deletions docs/source/zh-cn/tutorials/contribution-guidelines.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@ npm test

**提交消息**:请遵守 [Angular 提交消息规范](https://github.com/angular/angular.js/blob/master/DEVELOPERS.md#commits),尤其注意 [type 标识](https://github.com/angular/angular.js/blob/master/DEVELOPERS.md#type),semantic-release 机器人依赖这个标识自动发布。

**向后兼容**:请考虑向后(之前的旧的版本)兼容。LiquidJS 被用于很多层的软件,包括底层库、编译器、站点生成器、 Web 服务器。对多数最终用户来说,驱动或请求整个系统做一次主版本升级是很难办到的。

## 成为赞助者!

LiquidJS 是开源的、免费的,并且 **没有** 商业支持,也 **没有** 任何广告。如果你喜欢 LiquidJS 或你的公司在使用 LiquidJS,请考虑通过 [Open Collective][oc][Patreon][pt] 赞助,作为感谢你的名字和头像(或 Logo)会展示在这里和 [Github README][liquidjs]
Expand Down
7 changes: 1 addition & 6 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,13 +13,8 @@
"scripts": {
"lint": "eslint \"**/*.ts\" .",
"check": "npm test && npm run lint",
"unit": "mocha \"test/unit/**/*.ts\"",
"integration": "mocha \"test/integration/**/*.ts\"",
"e2e": "mocha \"test/e2e/**/*.ts\"",
"test": "nyc mocha \"test/**/*.ts\"",
"benchmark:prepare": "cd benchmark && npm ci",
"benchmark": "cd benchmark && npm start",
"benchmark:engines": "cd benchmark && npm run engines",
"benchmark": "cd benchmark && npm ci && npm start",
"build": "npm run build:dist && npm run build:docs",
"build:dist": "rm -rf dist && rollup -c rollup.config.ts && ls -lh dist",
"build:docs": "bin/build-docs.sh"
Expand Down
2 changes: 1 addition & 1 deletion src/builtin/tags/include.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ export default {
ctx.setRegister('blockMode', BlockMode.OUTPUT)
const scope = yield hash.render(ctx)
if (withVar) scope[filepath] = evalToken(withVar, ctx)
const templates = yield liquid._parseFile(filepath, ctx.opts, ctx.sync)
const templates = yield liquid.parseFileImpl(filepath, ctx.sync)
ctx.push(scope)
yield renderer.renderTemplates(templates, ctx, emitter)
ctx.pop()
Expand Down
2 changes: 1 addition & 1 deletion src/builtin/tags/layout.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ export default {
: evalToken(this.file, ctx))
: file.getText()
assert(filepath, () => `file "${file.getText()}"("${filepath}") not available`)
const templates = yield liquid._parseFile(filepath, ctx.opts, ctx.sync)
const templates = yield liquid.parseFileImpl(filepath, ctx.sync)

// render remaining contents and store rendered results
ctx.setRegister('blockMode', BlockMode.STORE)
Expand Down
4 changes: 2 additions & 2 deletions src/builtin/tags/render.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,12 +64,12 @@ export default {
scope['forloop'] = new ForloopDrop(collection.length)
for (const item of collection) {
scope[alias] = item
const templates = yield liquid._parseFile(filepath, childCtx.opts, childCtx.sync)
const templates = yield liquid.parseFileImpl(filepath, childCtx.sync)
yield renderer.renderTemplates(templates, childCtx, emitter)
scope.forloop.next()
}
} else {
const templates = yield liquid._parseFile(filepath, childCtx.opts, childCtx.sync)
const templates = yield liquid.parseFileImpl(filepath, childCtx.sync)
yield renderer.renderTemplates(templates, childCtx, emitter)
}
}
Expand Down
114 changes: 58 additions & 56 deletions src/liquid.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,13 +24,15 @@ export class Liquid {
public parser: Parser
public filters: FilterMap
public tags: TagMap
private parseFileImpl: (file: string, sync?: boolean) => Iterator<Template[]>

public constructor (opts: LiquidOptions = {}) {
this.options = applyDefault(normalize(opts))
this.parser = new Parser(this)
this.renderer = new Render()
this.filters = new FilterMap(this.options.strictFilters, this)
this.tags = new TagMap()
this.parseFileImpl = this.options.cache ? this._parseFileCached : this._parseFile

forOwn(builtinTags, (conf: TagImplOptions, name: string) => this.registerTag(snakeCase(name), conf))
forOwn(builtinFilters, (handler: FilterImplOptions, name: string) => this.registerFilter(snakeCase(name), handler))
Expand All @@ -41,64 +43,61 @@ export class Liquid {
return this.parser.parse(tokens)
}

public _render (tpl: Template[], scope?: object, opts?: LiquidOptions, sync?: boolean): IterableIterator<any> {
const options = { ...this.options, ...normalize(opts) }
const ctx = new Context(scope, options, sync)
const emitter = new Emitter(options.keepOutputType)
public _render (tpl: Template[], scope?: object, sync?: boolean): IterableIterator<any> {
const ctx = new Context(scope, this.options, sync)
const emitter = new Emitter(this.options.keepOutputType)
return this.renderer.renderTemplates(tpl, ctx, emitter)
}
public async render (tpl: Template[], scope?: object, opts?: LiquidOptions): Promise<any> {
return toPromise(this._render(tpl, scope, opts, false))
public async render (tpl: Template[], scope?: object): Promise<any> {
return toPromise(this._render(tpl, scope, false))
}
public renderSync (tpl: Template[], scope?: object, opts?: LiquidOptions): any {
return toValue(this._render(tpl, scope, opts, true))
public renderSync (tpl: Template[], scope?: object): any {
return toValue(this._render(tpl, scope, true))
}

public _parseAndRender (html: string, scope?: object, opts?: LiquidOptions, sync?: boolean): IterableIterator<any> {
public _parseAndRender (html: string, scope?: object, sync?: boolean): IterableIterator<any> {
const tpl = this.parse(html)
return this._render(tpl, scope, opts, sync)
return this._render(tpl, scope, sync)
}
public async parseAndRender (html: string, scope?: object, opts?: LiquidOptions): Promise<any> {
return toPromise(this._parseAndRender(html, scope, opts, false))
public async parseAndRender (html: string, scope?: object): Promise<any> {
return toPromise(this._parseAndRender(html, scope, false))
}
public parseAndRenderSync (html: string, scope?: object, opts?: LiquidOptions): any {
return toValue(this._parseAndRender(html, scope, opts, true))
public parseAndRenderSync (html: string, scope?: object): any {
return toValue(this._parseAndRender(html, scope, true))
}

public * _parseFile (file: string, opts?: LiquidOptions, sync?: boolean) {
const options = { ...this.options, ...normalize(opts) }
const paths = options.root.map(root => options.fs.resolve(root, file, options.extname))
if (options.fs.fallback !== undefined) {
const filepath = options.fs.fallback(file)
if (filepath !== undefined) paths.push(filepath)
}
private * _parseFileCached (file: string, sync?: boolean) {
const cache = this.options.cache!
let tpls = yield cache.read(file)
if (tpls) return tpls

for (const filepath of paths) {
const { cache } = options
if (cache) {
const tpls = yield cache.read(filepath)
if (tpls) return tpls
}
if (!(sync ? options.fs.existsSync(filepath) : yield options.fs.exists(filepath))) continue
const tpl = this.parse(sync ? options.fs.readFileSync(filepath) : yield options.fs.readFile(filepath), filepath)
if (cache) cache.write(filepath, tpl)
tpls = yield this._parseFile(file, sync)
cache.write(file, tpls)
return tpls
}
private * _parseFile (file: string, sync?: boolean) {
const { fs, root } = this.options

for (const filepath of this.lookupFiles(file, this.options)) {
if (!(sync ? fs.existsSync(filepath) : yield fs.exists(filepath))) continue
const tpl = this.parse(sync ? fs.readFileSync(filepath) : yield fs.readFile(filepath), filepath)
return tpl
}
throw this.lookupError(file, options.root)
throw this.lookupError(file, root)
}
public async parseFile (file: string, opts?: LiquidOptions): Promise<Template[]> {
return toPromise(this._parseFile(file, opts, false))
public async parseFile (file: string): Promise<Template[]> {
return toPromise(this.parseFileImpl(file, false))
}
public parseFileSync (file: string, opts?: LiquidOptions): Template[] {
return toValue(this._parseFile(file, opts, true))
public parseFileSync (file: string): Template[] {
return toValue(this.parseFileImpl(file, true))
}
public async renderFile (file: string, ctx?: object, opts?: LiquidOptions) {
const templates = await this.parseFile(file, opts)
return this.render(templates, ctx, opts)
public async renderFile (file: string, ctx?: object) {
const templates = await this.parseFile(file)
return this.render(templates, ctx)
}
public renderFileSync (file: string, ctx?: object, opts?: LiquidOptions) {
const templates = this.parseFileSync(file, opts)
return this.renderSync(templates, ctx, opts)
public renderFileSync (file: string, ctx?: object) {
const templates = this.parseFileSync(file)
return this.renderSync(templates, ctx)
}

public _evalValue (str: string, ctx: Context): IterableIterator<any> {
Expand All @@ -123,9 +122,25 @@ export class Liquid {
}
public express () {
const self = this // eslint-disable-line
let firstCall = true

return function (this: any, filePath: string, ctx: object, callback: (err: Error | null, rendered: string) => void) {
const opts = { root: [...normalizeStringArray(this.root), ...self.options.root] }
self.renderFile(filePath, ctx, opts).then(html => callback(null, html) as any, callback as any)
if (firstCall) {
firstCall = false
self.options.root.unshift(...normalizeStringArray(this.root))
}
self.renderFile(filePath, ctx).then(html => callback(null, html) as any, callback as any)
}
}

private * lookupFiles (file: string, options: NormalizedFullOptions) {
const { root, fs, extname } = options
for (const dir of root) {
yield fs.resolve(dir, file, extname)
}
if (fs.fallback !== undefined) {
const filepath = fs.fallback(file)
if (filepath !== undefined) yield filepath
}
}

Expand All @@ -135,17 +150,4 @@ export class Liquid {
err.code = 'ENOENT'
return err
}

/**
* @deprecated use parseFile instead
*/
public async getTemplate (file: string, opts?: LiquidOptions): Promise<Template[]> {
return this.parseFile(file, opts)
}
/**
* @deprecated use parseFileSync instead
*/
public getTemplateSync (file: string, opts?: LiquidOptions): Template[] {
return this.parseFileSync(file, opts)
}
}
7 changes: 2 additions & 5 deletions test/integration/builtin/tags/render.ts
Original file line number Diff line number Diff line change
Expand Up @@ -76,15 +76,12 @@ describe('tags/render', function () {
})

it('should be able to access globals', async function () {
liquid = new Liquid({ root: '/', extname: '.html', globals: { name: 'Harttle' } })
mock({
'/hash.html': 'InParent: {{name}} {% render "user.html" %}',
'/user.html': 'InChild: {{name}}'
})
const html = await liquid.renderFile('hash.html', {
name: 'harttle'
}, {
globals: { name: 'Harttle' }
})
const html = await liquid.renderFile('hash', { name: 'harttle' })
expect(html).to.equal('InParent: harttle InChild: Harttle')
})

Expand Down
56 changes: 0 additions & 56 deletions test/integration/liquid/cache.ts
Original file line number Diff line number Diff line change
Expand Up @@ -132,34 +132,6 @@ describe('LiquidOptions#cache', function () {
const y = await engine.renderFile('foo')
expect(y).to.equal('foo')
})
it('should respect passed in cache=false option', async function () {
const engine = new Liquid({
root: '/root/',
extname: '.html',
cache: true
})
mock({ '/root/files/foo.html': 'foo' })
const x = await engine.renderFile('files/foo')
expect(x).to.equal('foo')
mock({ '/root/files/foo.html': 'bar' })
const y = await engine.renderFile('files/foo')
expect(y).to.equal('foo')
const z = await engine.renderFile('files/foo', undefined, { cache: false })
expect(z).to.equal('bar')
})
it('should use cache when passing in other options', async function () {
const engine = new Liquid({
root: '/root/',
extname: '.html',
cache: true
})
mock({ '/root/files/foo.html': 'foo' })
const x = await engine.renderFile('files/foo')
expect(x).to.equal('foo')
mock({ '/root/files/foo.html': 'bar' })
const y = await engine.renderFile('files/foo', undefined, { greedy: true })
expect(y).to.equal('foo')
})
})

describe('#renderFileSync', function () {
Expand Down Expand Up @@ -202,33 +174,5 @@ describe('LiquidOptions#cache', function () {
const y = await engine.renderFile('foo')
expect(y).to.equal('foo')
})
it('should respect passed in cache=false option', async function () {
const engine = new Liquid({
root: '/root/',
extname: '.html',
cache: true
})
mock({ '/root/files/foo.html': 'foo' })
const x = engine.renderFileSync('files/foo')
expect(x).to.equal('foo')
mock({ '/root/files/foo.html': 'bar' })
const y = engine.renderFileSync('files/foo')
expect(y).to.equal('foo')
const z = engine.renderFileSync('files/foo', undefined, { cache: false })
expect(z).to.equal('bar')
})
it('should use cache when passing in other options', async function () {
const engine = new Liquid({
root: '/root/',
extname: '.html',
cache: true
})
mock({ '/root/files/foo.html': 'foo' })
const x = engine.renderFileSync('files/foo')
expect(x).to.equal('foo')
mock({ '/root/files/foo.html': 'bar' })
const y = engine.renderFileSync('files/foo', undefined, { greedy: true })
expect(y).to.equal('foo')
})
})
})
4 changes: 2 additions & 2 deletions test/integration/liquid/liquid.ts
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,7 @@ describe('Liquid', function () {
root: ['/root/'],
extname: '.html'
})
const tpls = await engine.getTemplate('mocha')
const tpls = await engine.parseFileSync('mocha')
expect(tpls.length).to.gte(1)
expect(tpls[0].token.getText()).to.contain('module.exports')
})
Expand Down Expand Up @@ -126,7 +126,7 @@ describe('Liquid', function () {
root: ['/boo', '/root/'],
extname: '.html'
})
return expect(() => engine.getTemplateSync('/not/exist.html'))
return expect(() => engine.parseFileSync('/not/exist.html'))
.to.throw(/Failed to lookup "\/not\/exist.html" in "\/boo,\/root\/"/)
})
})
Expand Down
Loading

0 comments on commit 24f5346

Please sign in to comment.