From 99c1068802fbe96e1becbad883dbdd4e2b668f80 Mon Sep 17 00:00:00 2001 From: Peter Jaszkowiak Date: Thu, 10 May 2018 00:13:20 -0600 Subject: [PATCH] Implement compiler in Rust - speed up compile times by orders of magnitude - native bindings with JS fallback - fully compatible (except unsafe) - Will attempt to compile on install - If that fails, will try precompiled version - If that fails, will fall back to JS version - Add more tests to catch previously untested bugs - Make the extra tokens algorithm more forgiving - Add benchmarks for compilation - Build binaries with CI - benchchpress-rs in a separate repo with a git submodule here --- .eslintignore | 3 +- .gitmodules | 3 + lib/compiler/tokenizer.js | 71 +++++---- lib/precompile.js | 42 +++-- package.json | 6 +- rust/benchpress-rs | 1 + tests/bench/compilation.js | 36 +++++ tests/bench/index.js | 17 ++- tests/compile-render.spec.js | 60 +++++--- tests/data.json | 8 +- tests/express.spec.js | 144 ++++++++++-------- tests/lib/utils.js | 19 ++- tests/named-blocks.spec.js | 20 --- tests/native.spec.js | 10 ++ tests/object-keys-error.spec.js | 49 +++--- tests/precompile.spec.js | 75 +++++---- tests/render.spec.js | 67 ++++---- tests/templates.spec.js | 98 +++++++----- tests/templates/expected/extra-tokens.html | 4 +- tests/templates/expected/invalid-between.html | 8 + tests/templates/expected/mixed-syntax.html | 4 +- tests/templates/expected/no-extra-tokens.html | 7 + tests/templates/source/invalid-between.tpl | 8 + tests/templates/source/no-extra-tokens.tpl | 12 ++ tests/unsafe.spec.js | 15 ++ 25 files changed, 493 insertions(+), 294 deletions(-) create mode 100644 .gitmodules create mode 160000 rust/benchpress-rs create mode 100644 tests/bench/compilation.js delete mode 100644 tests/named-blocks.spec.js create mode 100644 tests/native.spec.js create mode 100644 tests/templates/expected/invalid-between.html create mode 100644 tests/templates/expected/no-extra-tokens.html create mode 100644 tests/templates/source/invalid-between.tpl create mode 100644 tests/templates/source/no-extra-tokens.tpl diff --git a/.eslintignore b/.eslintignore index f64ce8e..36f40e2 100644 --- a/.eslintignore +++ b/.eslintignore @@ -1,4 +1,5 @@ *.jst build coverage -.nyc_output \ No newline at end of file +.nyc_output +node_modules \ No newline at end of file diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..00bdcc6 --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "rust/benchpress-rs"] + path = rust/benchpress-rs + url = https://github.com/benchpressjs/benchpress-rs diff --git a/lib/compiler/tokenizer.js b/lib/compiler/tokenizer.js index 49ce9bc..306d2f6 100644 --- a/lib/compiler/tokenizer.js +++ b/lib/compiler/tokenizer.js @@ -11,47 +11,40 @@ function getTopLevelTokens() { .sort((a, b) => a.priority - b.priority); } -function findOpensAndCloses(output) { - const opens = []; - const closes = []; - output.forEach((token) => { - if (token.tokenType.startsWith('Open')) { - opens.push(token); - } else if (token.tokenType === 'Close') { - closes.push(token); - } - }); - - return [opens, closes]; -} - -function removeExtraCloses(output) { +function removeExtraCloses(input) { const remove = new Set(); - const closeSubject = /^$/; + const closeSubject = /^$/; + + let opens = 0; + let closes = 0; const expectedSubjects = []; // try to find a Close with no corresponding Open - output.forEach((token, index) => { + input.forEach((token, index) => { if (token.tokenType.startsWith('Open')) { + opens += 1; + expectedSubjects.push( (token.subject && token.subject.path) || (token.test && (token.test.raw || token.test.path)) ); } else if (token.tokenType === 'Close') { - const expectedSubject = expectedSubjects[expectedSubjects.length - 1]; - expectedSubjects.pop(); + closes += 1; + + const expectedSubject = expectedSubjects.pop(); if (!expectedSubject) { remove.add(token); } else { const matches = token.raw.match(closeSubject); - if (matches && matches[1] !== expectedSubject) { + if (matches && !expectedSubject.startsWith(matches[1])) { remove.add(token); + expectedSubjects.push(expectedSubject); } else { // search for a close within close proximity // that has the expected subject - for (let i = index + 1; i < output.length; i += 1) { - const tok = output[i]; + for (let i = index + 1; i < input.length; i += 1) { + const tok = input[i]; if (tok.tokenType.startsWith('Open')) { break; } @@ -60,6 +53,7 @@ function removeExtraCloses(output) { if (m && m[1] === expectedSubject) { // found one ahead, so remove the current one remove.add(token); + expectedSubjects.push(expectedSubject); break; } } @@ -69,7 +63,30 @@ function removeExtraCloses(output) { } }); - return output.filter(token => !remove.has(token)); + if (closes > opens) { + let diff = closes - opens; + + /* eslint-disable no-console */ + console.warn('Found extra token(s):'); + + const output = input.map((token) => { + if (remove.has(token) && diff > 0) { + console.warn(token.raw); + + diff -= 1; + return new Text(token.raw); + } + + return token; + }); + + console.warn('These tokens will be passed through as text, but you should remove them to prevent issues in the future.'); + /* eslint-enable no-console */ + + return output; + } + + return input; } /** @@ -122,15 +139,9 @@ function tokenizer(input) { output.push(new Text(text)); } - const [opens, closes] = findOpensAndCloses(output); - // if there are more closes than opens // intelligently remove extra ones - if (closes.length > opens.length) { - return removeExtraCloses(output, opens, closes); - } - - return output; + return removeExtraCloses(output); } module.exports = tokenizer; diff --git a/lib/precompile.js b/lib/precompile.js index c426d12..9d9d108 100644 --- a/lib/precompile.js +++ b/lib/precompile.js @@ -9,15 +9,6 @@ const compiler = require('./compiler/compiler'); const blocks = require('./compiler/blocks'); const codegen = require('./compiler/codegen'); -function compile(source, opts) { - const prefixed = prefixer(source); - const tokens = tokenizer(prefixed); - const parsed = parser(tokens); - const fnAst = compiler(parsed, opts); - const ast = blocks(fnAst); - return codegen(ast, { minified: opts.minify }); -} - function wrap(compiled) { return ` (function (factory) { @@ -34,6 +25,17 @@ function wrap(compiled) { `; } +function compileFallback(source, opts) { + const prefixed = prefixer(source); + const tokens = tokenizer(prefixed); + const parsed = parser(tokens); + const fnAst = compiler(parsed, opts); + const ast = blocks(fnAst); + const code = codegen(ast, { minified: opts.minify }); + + return wrap(code); +} + function minify(wrapped) { const result = uglifyjs.minify(wrapped); @@ -44,6 +46,17 @@ function minify(wrapped) { return result.code; } +const compile = (() => { + try { + // eslint-disable-next-line global-require, import/no-unresolved + return require('../../rust/benchpress-rs').compile; + } catch (e) { + // eslint-disable-next-line no-console + console.warn('[benchpressjs] Unable to build or find a suitable native module, falling back to JS version'); + return compileFallback; + } +})(); + /** * Precompile a benchpress template * - `precompiled(source, options): Promise` @@ -54,6 +67,7 @@ function minify(wrapped) { * @param {Object} options * @param {boolean} [options.minify = false] - Output minified code * @param {boolean} [options.unsafe = false] - Disable safety checks, will throw on misshapen data + * @param {boolean} [options.native = true] - Use the native Rust compiler if available * @param {function} [callback] - (err, output) * @returns {Promise} - output code */ @@ -71,9 +85,12 @@ function precompile(source, options, callback) { throw Error('source must be a string'); } - const compiled = compile(source, opts); - const wrapped = wrap(compiled); - return opts.minify ? minify(wrapped) : wrapped; + // benchpress-rs doesn't support unsafe yet + const compiled = (opts.unsafe || opts.native === false ? compileFallback : compile)( + source, + opts + ); + return opts.minify ? minify(compiled) : compiled; }); if (callback) { @@ -89,6 +106,7 @@ function precompile(source, options, callback) { precompile.defaults = { minify: false, unsafe: false, + native: true, }; module.exports = precompile; diff --git a/package.json b/package.json index df4b7a2..a657a90 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "benchpressjs", - "version": "1.2.1", + "version": "1.2.2-0", "author": "psychobunny ", "description": "An ultralight and super fast templating framework", "scripts": { @@ -8,7 +8,9 @@ "test": "nyc --reporter=html --reporter=text mocha -R spec -- tests", "coverage": "nyc report --reporter=text-lcov | coveralls", "docs": "documentation build lib/benchpress.js lib/precompile.js lib/compile-render.js lib/express.js -f md -o docs/api.md --shallow", - "prepare": "grunt build uglify" + "prepare": "grunt build uglify && cd rust/benchpress-rs && npm run build 2>&1 | tee -a build.log || exit 0", + "prepublishOnly": "grunt", + "install": "cd rust/benchpress-rs && npm install" }, "repository": "git://github.com/benchpressjs/benchpressjs", "main": "build/lib/benchpress", diff --git a/rust/benchpress-rs b/rust/benchpress-rs new file mode 160000 index 0000000..85dfe5a --- /dev/null +++ b/rust/benchpress-rs @@ -0,0 +1 @@ +Subproject commit 85dfe5a8915b0794d6627d823b51212057974131 diff --git a/tests/bench/compilation.js b/tests/bench/compilation.js new file mode 100644 index 0000000..ee8df6d --- /dev/null +++ b/tests/bench/compilation.js @@ -0,0 +1,36 @@ +'use strict'; + +const path = require('path'); +const fs = require('fs'); +const async = require('async'); + +const benchpress = require('../../build/lib/benchpress'); + +const templatePaths = ['categories.tpl', 'topic.tpl'].map(name => path.join(__dirname, name)); + +function prep(callback) { + async.waterfall([ + next => async.map( + templatePaths, + (templatePath, cb) => fs.readFile(templatePath, 'utf8', cb), + next + ), + ([categories, topics], next) => { + function bench(deferred) { + return benchpress.precompile(categories, { native: false }) + .then(() => benchpress.precompile(topics, { native: false })) + .then(() => deferred.resolve(), err => deferred.reject(err)); + } + + function benchNative(deferred) { + return benchpress.precompile(categories, { native: true }) + .then(() => benchpress.precompile(topics, { native: true })) + .then(() => deferred.resolve(), err => deferred.reject(err)); + } + + next(null, { bench, benchNative }); + }, + ], callback); +} + +module.exports = prep; diff --git a/tests/bench/index.js b/tests/bench/index.js index de633ab..b1a26a0 100644 --- a/tests/bench/index.js +++ b/tests/bench/index.js @@ -6,14 +6,18 @@ const Benchmark = require('benchmark'); const benchpress = require('../../build/lib/benchpress'); const categories = require('./categories'); const topic = require('./topic'); +const compilation = require('./compilation'); +Benchmark.options.defer = true; +Benchmark.options.minSamples = 100; const suite = new Benchmark.Suite(); function benchmark(done) { async.parallel([ categories, topic, - ], (err, [cats, top]) => { + compilation, + ], (err, [cats, top, comp]) => { const cache = { categories: cats.template, topic: top.template, @@ -26,12 +30,10 @@ function benchmark(done) { const output = []; suite - .add('categories', cats.bench, { - defer: true, - }) - .add('topic', top.bench, { - defer: true, - }) + .add('categories', cats.bench) + .add('topic', top.bench) + .add('compilation', comp.bench) + .add('native compilation', comp.benchNative) .on('cycle', (event) => { output.push(event.target.toString()); }) @@ -43,4 +45,5 @@ function benchmark(done) { }); }); } + module.exports = benchmark; diff --git a/tests/compile-render.spec.js b/tests/compile-render.spec.js index e0dde5b..e9767b3 100644 --- a/tests/compile-render.spec.js +++ b/tests/compile-render.spec.js @@ -11,34 +11,48 @@ const mainData = require('./data.json'); const source = fs.readFileSync(path.join(__dirname, 'templates/source/loop-inside-if-else.tpl')).toString(); const expected = fs.readFileSync(path.join(__dirname, 'templates/expected/loop-inside-if-else.html')).toString(); -describe('compileRender', () => { - it('should work', () => - Benchpress.compileRender(source, mainData) - .then(output => equalsIgnoreWhitespace(expected, output)) - ); - - it('should work with block', () => - Benchpress.compileRender(source, mainData, 'rooms') - .then(output => equalsIgnoreWhitespace(expected, output)) - ); -}); - -describe('compileParse', () => { - it('should work', (done) => { - Benchpress.compileParse(source, mainData, (err, output) => { - assert.ifError(err); +[true, false].forEach((native) => { + const type = native ? 'native' : 'fallback'; - equalsIgnoreWhitespace(expected, output); - done(); + describe(`compileRender (${type})`, () => { + before(() => { + Benchpress.precompile.defaults.native = native; + Benchpress.flush(); }); + + it('should work', () => + Benchpress.compileRender(source, mainData) + .then(output => equalsIgnoreWhitespace(output, expected)) + ); + + it('should work with block', () => + Benchpress.compileRender(source, mainData, 'rooms') + .then(output => equalsIgnoreWhitespace(output, expected)) + ); }); - it('should work with block', (done) => { - Benchpress.compileParse(source, 'rooms', mainData, (err, output) => { - assert.ifError(err); + describe(`compileParse (${type})`, () => { + before(() => { + Benchpress.precompile.defaults.native = native; + Benchpress.flush(); + }); + + it('should work', (done) => { + Benchpress.compileParse(source, mainData, (err, output) => { + assert.ifError(err); + + equalsIgnoreWhitespace(output, expected); + done(); + }); + }); + + it('should work with block', (done) => { + Benchpress.compileParse(source, 'rooms', mainData, (err, output) => { + assert.ifError(err); - equalsIgnoreWhitespace(expected, output); - done(); + equalsIgnoreWhitespace(output, expected); + done(); + }); }); }); }); diff --git a/tests/data.json b/tests/data.json index b9bd5aa..369142a 100644 --- a/tests/data.json +++ b/tests/data.json @@ -286,5 +286,11 @@ "four": null, "five": "" }, - "arrayOfBools": [true, false] + "arrayOfBools": [true, false], + "relative_path": "", + "configJSON": "{}", + "template": { + "name": "header" + }, + "userJSON": "{}" } \ No newline at end of file diff --git a/tests/express.spec.js b/tests/express.spec.js index 93685b7..82f1c8f 100644 --- a/tests/express.spec.js +++ b/tests/express.spec.js @@ -6,88 +6,98 @@ const express = require('express'); const async = require('async'); const assert = require('assert'); -const benchpress = require('../build/lib/benchpress'); +const Benchpress = require('../build/lib/benchpress'); const { compileTemplate, equalsIgnoreWhitespace } = require('./lib/utils'); const data = require('./data.json'); -const app = express(); - const templatesDir = path.join(__dirname, 'templates/build'); +const name = 'basic'; +const sourcePath = path.join(__dirname, `templates/source/${name}.tpl`); +const compiledPath = path.join(templatesDir, `${name}.jst`); +const expectedPath = path.join(__dirname, `templates/expected/${name}.html`); -app.engine('jst', benchpress.__express); -app.set('view engine', 'jst'); -app.set('views', templatesDir); - -describe('express', () => { - const name = 'basic'; - const sourcePath = path.join(__dirname, `templates/source/${name}.tpl`); - const compiledPath = path.join(templatesDir, `${name}.jst`); - const expectedPath = path.join(__dirname, `templates/expected/${name}.html`); - - it('app.render should work first time', (done) => { - async.waterfall([ - next => compileTemplate(sourcePath, compiledPath, next), - next => fs.readFile(expectedPath, 'utf8', next), - (expected, next) => { - app.render(name, data, (err, rendered) => next(err, rendered, expected)); - }, - (rendered, expected, next) => { - equalsIgnoreWhitespace(rendered, expected); - next(); - }, - ], done); - }); +[true, false].forEach((native) => { + const type = native ? 'native' : 'fallback'; - it('app.render should work from cache', (done) => { - assert.ok(benchpress.cache[compiledPath]); - - async.waterfall([ - next => fs.readFile(expectedPath, 'utf8', next), - (expected, next) => { - app.render(name, data, (err, rendered) => next(err, rendered, expected)); - }, - (rendered, expected, next) => { - equalsIgnoreWhitespace(rendered, expected); - next(); - }, - ], done); - }); + let app; + + describe(`express (${type})`, () => { + before(() => { + Benchpress.precompile.defaults.native = native; + Benchpress.flush(); - it('should catch errors in render', (done) => { - const error = new Error(); - benchpress.cache[compiledPath] = () => { throw error; }; + app = express(); - app.render(name, data, (err) => { - assert.strictEqual(err, error); - assert.ok(err.message.startsWith('Render failed')); - done(); + app.engine('jst', Benchpress.__express); + app.set('view engine', 'jst'); + app.set('views', templatesDir); }); - }); - it('should catch errors in evaluate', (done) => { - const id = 'some-random-name'; - const tempPath = path.join(templatesDir, `${id}.jst`); + it('app.render should work first time', (done) => { + async.waterfall([ + next => compileTemplate(sourcePath, compiledPath, next), + next => fs.readFile(expectedPath, 'utf8', next), + (expected, next) => { + app.render(name, data, (err, rendered) => next(err, rendered, expected)); + }, + (rendered, expected, next) => { + equalsIgnoreWhitespace(rendered, expected); + next(); + }, + ], done); + }); - async.series([ - next => fs.writeFile(tempPath, 'throw Error()', next), - (next) => { - app.render(id, data, (err) => { - assert.ok(err); - assert.ok(err.message.startsWith('Evaluate failed')); + it('app.render should work from cache', (done) => { + assert.ok(Benchpress.cache[compiledPath]); + async.waterfall([ + next => fs.readFile(expectedPath, 'utf8', next), + (expected, next) => { + app.render(name, data, (err, rendered) => next(err, rendered, expected)); + }, + (rendered, expected, next) => { + equalsIgnoreWhitespace(rendered, expected); next(); - }); - }, - next => fs.unlink(tempPath, next), - ], done); - }); + }, + ], done); + }); + + it('should catch errors in render', (done) => { + const error = new Error(); + Benchpress.cache[compiledPath] = () => { throw error; }; + + app.render(name, data, (err) => { + assert.strictEqual(err, error); + assert.ok(err.message.startsWith('Render failed')); + done(); + }); + }); + + it('should catch errors in evaluate', (done) => { + const id = 'some-random-name'; + const tempPath = path.join(templatesDir, `${id}.jst`); + + async.series([ + next => fs.writeFile(tempPath, 'throw Error()', next), + (next) => { + app.render(id, data, (err) => { + assert.ok(err); + assert.ok(err.message.startsWith('Evaluate failed')); + + next(); + }); + }, + next => fs.unlink(tempPath, next), + ], done); + }); - it('should fail if file does not exist', (done) => { - const id = 'file-that-does-not-exist'; - app.render(id, data, (err) => { - assert.ok(err); + it('should fail if file does not exist', (done) => { + const id = 'file-that-does-not-exist'; + app.render(id, data, (err) => { + assert.ok(err); - done(); + done(); + }); }); }); }); diff --git a/tests/lib/utils.js b/tests/lib/utils.js index c0c474f..4f3fb80 100644 --- a/tests/lib/utils.js +++ b/tests/lib/utils.js @@ -8,7 +8,16 @@ const assert = require('assert'); const benchpress = require('../../build/lib/benchpress'); -function prepare(sourceDir, expectedDir) { +let cache = null; +function prepare() { + if (cache) { + return cache; + } + + const templatesDir = path.join(__dirname, '../templates'); + const sourceDir = path.join(templatesDir, 'source'); + const expectedDir = path.join(templatesDir, 'expected'); + const [sourceArr, expectedArr] = [sourceDir, expectedDir] .map(dir => fs.readdirSync(dir).map(file => [ file.replace(/(\.tpl|\.html|\.hbs)$/, ''), @@ -32,7 +41,9 @@ function prepare(sourceDir, expectedDir) { return prev; }, {}); - return [source, expected, missing]; + cache = [source, expected, missing]; + + return cache; } function collapseWhitespace(str) { @@ -45,8 +56,8 @@ function collapseWhitespace(str) { function compileTemplate(src, dest, callback) { async.waterfall([ - next => fs.readFile(src, next), - (file, next) => benchpress.precompile({ source: file.toString() }, next), + next => fs.readFile(src, 'utf8', next), + (source, next) => benchpress.precompile({ source }, next), (code, next) => mkdirp(path.dirname(dest), err => next(err, code)), (code, next) => fs.writeFile(dest, code, next), ], callback); diff --git a/tests/named-blocks.spec.js b/tests/named-blocks.spec.js deleted file mode 100644 index 47f77ef..0000000 --- a/tests/named-blocks.spec.js +++ /dev/null @@ -1,20 +0,0 @@ -'use strict'; - -const fs = require('fs'); -const path = require('path'); - -const Benchpress = require('../build/lib/benchpress'); -const mainData = require('./data.json'); -const { equalsIgnoreWhitespace } = require('./lib/utils'); - -const source = fs.readFileSync(path.join(__dirname, './templates/source/loop-inside-if-else.tpl')).toString(); -const expected = fs.readFileSync(path.join(__dirname, './templates/expected/loop-inside-if-else.html')).toString(); - -describe('named-blocks', () => { - it('should work', () => { - const blockName = 'rooms'; - - return Benchpress.compileRender(source, mainData, blockName) - .then(output => equalsIgnoreWhitespace(output, expected)); - }); -}); diff --git a/tests/native.spec.js b/tests/native.spec.js new file mode 100644 index 0000000..18887d5 --- /dev/null +++ b/tests/native.spec.js @@ -0,0 +1,10 @@ +'use strict'; + +const assert = require('assert'); + +describe('native', () => { + it('is available', () => { + // eslint-disable-next-line global-require + assert.doesNotThrow(() => require('../rust/benchpress-rs')); + }); +}); diff --git a/tests/object-keys-error.spec.js b/tests/object-keys-error.spec.js index 607a20c..8e96d14 100644 --- a/tests/object-keys-error.spec.js +++ b/tests/object-keys-error.spec.js @@ -8,32 +8,39 @@ const Benchpress = require('../build/lib/benchpress'); const { equalsIgnoreWhitespace } = require('./lib/utils'); const mainData = require('./data.json'); -describe('each Object.keys("")', () => { - const keys = Object.keys; - - before(() => { - Object.keys = (obj) => { - assert.equal(typeof obj, 'object'); - return keys(obj); - }; - }); +[true, false].forEach((native) => { + const type = native ? 'native' : 'fallback'; + + describe(`each Object.keys("") (${type})`, () => { + const keys = Object.keys; - it('ES5 behavior is correct', () => { - assert.throws(() => { - Object.keys(''); + before(() => { + Benchpress.precompile.defaults.native = native; + Benchpress.flush(); + + Object.keys = (obj) => { + assert.equal(typeof obj, 'object'); + return keys(obj); + }; }); - }); - it('should work with ES5 behavior', () => { - const source = fs.readFileSync(path.join(__dirname, 'templates/source/object-keys-error.tpl')).toString(); - const expected = fs.readFileSync(path.join(__dirname, 'templates/expected/object-keys-error.html')).toString(); + it('ES5 behavior is correct', () => { + assert.throws(() => { + Object.keys(''); + }); + }); + + it('should work with ES5 behavior', () => { + const source = fs.readFileSync(path.join(__dirname, 'templates/source/object-keys-error.tpl')).toString(); + const expected = fs.readFileSync(path.join(__dirname, 'templates/expected/object-keys-error.html')).toString(); - return Benchpress.compileRender(source, mainData).then((output) => { - equalsIgnoreWhitespace(expected, output); + return Benchpress.compileRender(source, mainData).then((output) => { + equalsIgnoreWhitespace(expected, output); + }); }); - }); - after(() => { - Object.keys = keys; + after(() => { + Object.keys = keys; + }); }); }); diff --git a/tests/precompile.spec.js b/tests/precompile.spec.js index 231d662..fb6b119 100644 --- a/tests/precompile.spec.js +++ b/tests/precompile.spec.js @@ -8,44 +8,53 @@ const Benchpress = require('../build/lib/benchpress'); const tplPath = path.join(__dirname, './templates/source/conditional-with-else-inside-loop.tpl'); const template = fs.readFileSync(tplPath).toString(); -describe('precompile', () => { - it('should work with Promise usage', () => - Benchpress.precompile(template, {}) - .then((code) => { - assert(code); - assert(code.length); - }) - ); +[true, false].forEach((native) => { + const type = native ? 'native' : 'fallback'; + + describe(`precompile (${type})`, () => { + before(() => { + Benchpress.precompile.defaults.native = native; + Benchpress.flush(); + }); + + it('should work with Promise usage', () => + Benchpress.precompile(template, {}) + .then((code) => { + assert(code); + assert(code.length); + }) + ); - it('should work with callback usage', (done) => { - Benchpress.precompile(template, {}, (err, code) => { - assert.ifError(err); + it('should work with callback usage', (done) => { + Benchpress.precompile(template, {}, (err, code) => { + assert.ifError(err); - assert(code); - assert(code.length); - done(); + assert(code); + assert(code.length); + done(); + }); }); - }); - it('should work with old arguments', (done) => { - Benchpress.precompile({ source: template }, (err, code) => { - assert.ifError(err); + it('should work with old arguments', (done) => { + Benchpress.precompile({ source: template }, (err, code) => { + assert.ifError(err); - assert(code); - assert(code.length); - done(); + assert(code); + assert(code.length); + done(); + }); }); - }); - it('should work with minify on', () => - Benchpress.precompile(template, { minify: true }) - .then((minified) => { - assert(minified); - - return Benchpress.precompile(template, { minify: false }) - .then((code) => { - assert(minified.length < code.length); - }); - }) - ); + it('should work with minify on', () => + Benchpress.precompile(template, { minify: true }) + .then((minified) => { + assert(minified); + + return Benchpress.precompile(template, { minify: false }) + .then((code) => { + assert(minified.length < code.length); + }); + }) + ); + }); }); diff --git a/tests/render.spec.js b/tests/render.spec.js index ca513d8..5e91142 100644 --- a/tests/render.spec.js +++ b/tests/render.spec.js @@ -11,41 +11,48 @@ const name = 'loop-inside-if-else'; const source = fs.readFileSync(path.join(__dirname, 'templates/source/loop-inside-if-else.tpl')).toString(); const expected = fs.readFileSync(path.join(__dirname, 'templates/expected/loop-inside-if-else.html')).toString(); -describe('', () => { - before(() => { - const cache = {}; - - return Benchpress.precompile(source) - .then((code) => { - cache[name] = Benchpress.evaluate(code); - return Benchpress.registerLoader(n => Promise.resolve(cache[n])); - }); - }); +[true, false].forEach((native) => { + const type = native ? 'native' : 'fallback'; - describe('render', () => { - it('should work', () => - Benchpress.render(name, mainData) - .then(output => equalsIgnoreWhitespace(expected, output)) - ); + describe('', () => { + before(() => { + Benchpress.precompile.defaults.native = native; + Benchpress.flush(); - it('should work with block', () => - Benchpress.render(name, mainData, 'rooms') - .then(output => equalsIgnoreWhitespace(expected, output)) - ); - }); + const cache = {}; - describe('parse', () => { - it('should work', (done) => { - Benchpress.parse(name, mainData, (output) => { - equalsIgnoreWhitespace(expected, output); - done(); - }); + return Benchpress.precompile(source) + .then((code) => { + cache[name] = Benchpress.evaluate(code); + return Benchpress.registerLoader(n => Promise.resolve(cache[n])); + }); }); - it('should work with block', (done) => { - Benchpress.parse(name, 'rooms', mainData, (output) => { - equalsIgnoreWhitespace(expected, output); - done(); + describe(`render (${type})`, () => { + it('should work', () => + Benchpress.render(name, mainData) + .then(output => equalsIgnoreWhitespace(output, expected)) + ); + + it('should work with block', () => + Benchpress.render(name, mainData, 'rooms') + .then(output => equalsIgnoreWhitespace(output, expected)) + ); + }); + + describe(`parse (${type})`, () => { + it('should work', (done) => { + Benchpress.parse(name, mainData, (output) => { + equalsIgnoreWhitespace(output, expected); + done(); + }); + }); + + it('should work with block', (done) => { + Benchpress.parse(name, 'rooms', mainData, (output) => { + equalsIgnoreWhitespace(output, expected); + done(); + }); }); }); }); diff --git a/tests/templates.spec.js b/tests/templates.spec.js index 87dfccd..8e7f9fb 100644 --- a/tests/templates.spec.js +++ b/tests/templates.spec.js @@ -36,48 +36,68 @@ function logFailure({ name, source, code, expected, output, err }) { } } -const templatesDir = path.join(__dirname, 'templates'); -const sourceDir = path.join(templatesDir, 'source'); -const expectedDir = path.join(templatesDir, 'expected'); +[true, false].forEach((native) => { + const type = native ? 'native' : 'fallback'; -describe('templates', () => { - const [source, expected, missing] = prepare(sourceDir, expectedDir); + describe(`templates (${type})`, () => { + before(() => { + Benchpress.precompile.defaults.native = native; + Benchpress.flush(); + }); - if (missing.length) { - // eslint-disable-next-line no-console - console.warn(`[templates.js] Missing expected files: ${JSON.stringify(missing, null, 2)}`); - } + const [source, expected, missing] = prepare(); + + if (missing.length) { + // eslint-disable-next-line no-console + console.warn(`[templates.js] Missing expected files: ${JSON.stringify(missing, null, 2)}`); + } - const keys = Object.keys(source); - - keys.forEach((name) => { - it(name, () => - Benchpress.precompile(source[name], {}) - .catch((err) => { - logFailure({ - source: source[name], - expected: expected[name], - name, - err: err.message, - }); - throw err; - }) - .then((code) => { - const template = Benchpress.evaluate(code); - const output = Benchpress.runtime(Benchpress.helpers, mainData, template); - const expect = expected[name]; - - logFailure({ - source: source[name], - expected: expect, - code, - output, - name, - }); - - equalsIgnoreWhitespace(output, expect); - }) - ); + const keys = Object.keys(source); + + keys.forEach((name) => { + it(name, () => + Benchpress.precompile(source[name], {}) + .catch((err) => { + logFailure({ + source: source[name], + expected: expected[name], + name, + err: err.message, + }); + throw err; + }) + .then((code) => { + let template = null; + let output = ''; + let err = null; + + try { + template = Benchpress.evaluate(code); + + try { + output = Benchpress.runtime(Benchpress.helpers, mainData, template); + } catch (e) { + err = e; + } + } catch (e) { + err = e; + } + + const expect = expected[name]; + + logFailure({ + source: source[name], + expected: expect, + code, + output, + name, + err, + }); + + equalsIgnoreWhitespace(output, expect); + }) + ); + }); }); }); diff --git a/tests/templates/expected/extra-tokens.html b/tests/templates/expected/extra-tokens.html index 2a689a5..94d5f9b 100644 --- a/tests/templates/expected/extra-tokens.html +++ b/tests/templates/expected/extra-tokens.html @@ -3,10 +3,10 @@

Test

testing

- +

Test

-

testing

+

testing

\ No newline at end of file diff --git a/tests/templates/expected/invalid-between.html b/tests/templates/expected/invalid-between.html new file mode 100644 index 0000000..d66cab5 --- /dev/null +++ b/tests/templates/expected/invalid-between.html @@ -0,0 +1,8 @@ + diff --git a/tests/templates/expected/mixed-syntax.html b/tests/templates/expected/mixed-syntax.html index 2a689a5..94d5f9b 100644 --- a/tests/templates/expected/mixed-syntax.html +++ b/tests/templates/expected/mixed-syntax.html @@ -3,10 +3,10 @@

Test

testing

- +

Test

-

testing

+

testing

\ No newline at end of file diff --git a/tests/templates/expected/no-extra-tokens.html b/tests/templates/expected/no-extra-tokens.html new file mode 100644 index 0000000..428b90c --- /dev/null +++ b/tests/templates/expected/no-extra-tokens.html @@ -0,0 +1,7 @@ +above stuff + + + + + Human is human + diff --git a/tests/templates/source/invalid-between.tpl b/tests/templates/source/invalid-between.tpl new file mode 100644 index 0000000..a2f57cf --- /dev/null +++ b/tests/templates/source/invalid-between.tpl @@ -0,0 +1,8 @@ + diff --git a/tests/templates/source/no-extra-tokens.tpl b/tests/templates/source/no-extra-tokens.tpl new file mode 100644 index 0000000..f54bac1 --- /dev/null +++ b/tests/templates/source/no-extra-tokens.tpl @@ -0,0 +1,12 @@ + +above stuff + + + + {animals.name} is human + + + + +other stuff + diff --git a/tests/unsafe.spec.js b/tests/unsafe.spec.js index 2ba3e7f..0bc8c42 100644 --- a/tests/unsafe.spec.js +++ b/tests/unsafe.spec.js @@ -11,6 +11,7 @@ const mainData = require('./data.json'); describe('unsafe', () => { before(() => { benchpress.precompile.defaults.unsafe = true; + benchpress.precompile.defaults.native = false; }); it('should throw if property does not exist', () => { @@ -47,6 +48,20 @@ describe('unsafe', () => { }); }); + it('should fall back if native is enabled', () => { + benchpress.precompile.defaults.native = true; + + const source = fs.readFileSync(path.join(__dirname, 'templates/source/loop-nested-with-conditional.tpl')).toString(); + const expected = fs.readFileSync(path.join(__dirname, 'templates/expected/loop-nested-with-conditional.html')).toString(); + + return benchpress.precompile(source, {}).then((compiled) => { + const template = benchpress.evaluate(compiled); + const output = template(benchpress.helpers, mainData); + + equalsIgnoreWhitespace(expected, output); + }); + }); + after(() => { benchpress.precompile.defaults.unsafe = false; });