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;
});