Compare commits

..

1 Commits

Author SHA1 Message Date
Jakob Linskeseder 88f789e419 Add possibility to precompile as ES module 2023-09-04 23:35:36 +02:00
142 changed files with 23997 additions and 21698 deletions
+23
View File
@@ -0,0 +1,23 @@
.rvmrc
.DS_Store
/tmp/
*.sublime-project
*.sublime-workspace
npm-debug.log
.idea
yarn-error.log
node_modules
/handlebars-release.tgz
.nyc_output
# Generated files
/coverage/
/dist/
/tests/integration/*/dist/
# Third-party or files that must remain unchanged
/spec/expected/
/spec/vendor
# JS-Snippets
src/*.js
+64
View File
@@ -0,0 +1,64 @@
module.exports = {
extends: ['eslint:recommended', 'plugin:compat/recommended', 'prettier'],
globals: {
self: false,
},
env: {
node: true,
es2020: true,
},
parserOptions: {
sourceType: 'module',
},
rules: {
'no-console': 'warn',
// temporarily disabled until the violating places are fixed.
'no-func-assign': 'off',
'no-sparse-arrays': 'off',
// Best Practices //
//----------------//
'default-case': 'warn',
'guard-for-in': 'warn',
'no-alert': 'error',
'no-caller': 'error',
'no-div-regex': 'warn',
'no-eval': 'error',
'no-extend-native': 'error',
'no-extra-bind': 'error',
'no-floating-decimal': 'error',
'no-implied-eval': 'error',
'no-iterator': 'error',
'no-labels': 'error',
'no-lone-blocks': 'error',
'no-loop-func': 'error',
'no-multi-str': 'warn',
'no-global-assign': 'error',
'no-new': 'error',
'no-new-func': 'error',
'no-new-wrappers': 'error',
'no-octal-escape': 'error',
'no-process-env': 'error',
'no-proto': 'error',
'no-return-assign': 'error',
'no-script-url': 'error',
'no-self-compare': 'error',
'no-sequences': 'error',
'no-throw-literal': 'error',
'no-unused-expressions': 'error',
'no-warning-comments': 'warn',
'no-with': 'error',
radix: 'error',
// Variables //
//-----------//
'no-label-var': 'error',
'no-undef-init': 'error',
'no-use-before-define': ['error', 'nofunc'],
// ECMAScript 6 //
//--------------//
'no-var': 'error',
},
};
+1 -3
View File
@@ -1,6 +1,4 @@
# Upgrade to Prettier 2.7 # Upgrade to Prettier 2.7
3d228334530860a6e3f99dc10777c84bf22292c1 3d228334530860a6e3f99dc10777c84bf22292c1
# Format markdown files with Prettier # Format markdown files with Prettier
dfe2eaaf20f0b679d94e5a799757c4394d80f1cc dfe2eaaf20f0b679d94e5a799757c4394d80f1cc
# migrate to oxlint and oxfmt
0c1d00282ca619c3416ad819bdf53c0852b32415
+2 -2
View File
@@ -7,6 +7,6 @@ Generally we like to see pull requests that
- [ ] Are focused on a single change (i.e. avoid large refactoring or style adjustments in untouched code if not the primary goal of the pull request) - [ ] Are focused on a single change (i.e. avoid large refactoring or style adjustments in untouched code if not the primary goal of the pull request)
- [ ] Have good commit messages - [ ] Have good commit messages
- [ ] Have tests - [ ] Have tests
- [ ] Have the [typings](https://www.typescriptlang.org/docs/handbook/declaration-files/introduction.html) (types/index.d.ts) updated on every API change. If you need help, updating those, please mention that in the PR description. - [ ] Have the [typings](https://www.typescriptlang.org/docs/handbook/declaration-files/introduction.html) (lib/handlebars.d.ts) updated on every API change. If you need help, updating those, please mention that in the PR description.
- [ ] Don't significantly decrease the current code coverage (see coverage/lcov-report/index.html) - [ ] Don't significantly decrease the current code coverage (see coverage/lcov-report/index.html)
- [ ] Please target the `master` branch in the PR. - [ ] Currently, the `4.x`-branch contains the latest version. Please target that branch in the PR.
+1 -1
View File
@@ -1,7 +1,7 @@
version: 2 version: 2
updates: updates:
- package-ecosystem: npm - package-ecosystem: npm
directory: '/' directory: "/"
open-pull-requests-limit: 0 open-pull-requests-limit: 0
schedule: schedule:
interval: weekly interval: weekly
+41 -20
View File
@@ -12,12 +12,12 @@ jobs:
runs-on: 'ubuntu-latest' runs-on: 'ubuntu-latest'
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v6 uses: actions/checkout@v2
- name: Setup Node.js - name: Setup Node.js
uses: actions/setup-node@v6 uses: actions/setup-node@v2
with: with:
node-version: '24' node-version: '18'
- name: Install dependencies - name: Install dependencies
run: npm ci run: npm ci
@@ -25,6 +25,31 @@ jobs:
- name: Lint - name: Lint
run: npm run lint run: npm run lint
dependencies:
name: Test (dependencies)
runs-on: 'ubuntu-latest'
steps:
- name: Checkout
uses: actions/checkout@v2
- name: Setup Node.js
uses: actions/setup-node@v2
with:
# Node 14 ships with npm v6, which doesn't install peer-dependencies by default.
# Starting with npm v7 (which is shipped with Node >= 16), peer-dependencies are
# automatically installed. So this test (check for unmet peer-dependencies) only
# works with Node <= 14.
node-version: '14'
# Simulate an installation by a dependent package
- name: Install dependencies
run: |
rm package-lock.json
npm install --production
- name: Check dependency tree
run: npm ls
test: test:
name: Test (Node) name: Test (Node)
runs-on: ${{ matrix.operating-system }} runs-on: ${{ matrix.operating-system }}
@@ -33,16 +58,16 @@ jobs:
matrix: matrix:
operating-system: ['ubuntu-latest', 'windows-latest'] operating-system: ['ubuntu-latest', 'windows-latest']
# https://nodejs.org/en/about/releases/ # https://nodejs.org/en/about/releases/
node-version: ['20', '22', '24'] node-version: ['12', '14', '16', '18', '20']
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v6 uses: actions/checkout@v2
with: with:
submodules: true submodules: true
- name: Setup Node.js - name: Setup Node.js
uses: actions/setup-node@v6 uses: actions/setup-node@v2
with: with:
node-version: ${{ matrix.node-version }} node-version: ${{ matrix.node-version }}
@@ -52,27 +77,25 @@ jobs:
- name: Test - name: Test
run: npm run test run: npm run test
- name: Test (Publish)
if: matrix.node-version != '20'
run: npx vitest run --project publish
- name: Test (Integration) - name: Test (Integration)
if: matrix.operating-system == 'ubuntu-latest' run: |
run: npm run test:integration cd ./tests/integration/rollup-test && ./test.sh && cd -
cd ./tests/integration/webpack-babel-test && ./test.sh && cd -
cd ./tests/integration/webpack-test && ./test.sh && cd -
browser: browser:
name: Test (Browser) name: Test (Browser)
runs-on: ubuntu-latest runs-on: 'ubuntu-22.04'
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v6 uses: actions/checkout@v2
with: with:
submodules: true submodules: true
- name: Setup Node.js - name: Setup Node.js
uses: actions/setup-node@v6 uses: actions/setup-node@v2
with: with:
node-version: '24' node-version: '18'
- name: Install dependencies - name: Install dependencies
run: npm ci run: npm ci
@@ -83,9 +106,7 @@ jobs:
npx playwright install npx playwright install
- name: Build - name: Build
run: npm run build run: npx grunt prepare
- name: Test - name: Test
run: | run: npm run test:browser
npm run test:browser-smoke
npm run test:browser
+4 -5
View File
@@ -15,14 +15,14 @@ jobs:
environment: 'builds.handlebarsjs.com.s3.amazonaws.com' environment: 'builds.handlebarsjs.com.s3.amazonaws.com'
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v6 uses: actions/checkout@v2
with: with:
submodules: true submodules: true
- name: Setup Node.js - name: Setup Node.js
uses: actions/setup-node@v6 uses: actions/setup-node@v2
with: with:
node-version: '24' node-version: '18'
- name: Install dependencies - name: Install dependencies
run: npm ci run: npm ci
@@ -33,7 +33,6 @@ jobs:
git config --global user.name "handlebars-lang" git config --global user.name "handlebars-lang"
npm run publish:aws npm run publish:aws
env: env:
S3_BUCKET_NAME: 'builds.handlebarsjs.com' S3_BUCKET_NAME: "builds.handlebarsjs.com"
S3_REGION: 'us-east-1'
S3_ACCESS_KEY_ID: ${{ secrets.S3_ACCESS_KEY_ID }} S3_ACCESS_KEY_ID: ${{ secrets.S3_ACCESS_KEY_ID }}
S3_SECRET_ACCESS_KEY: ${{ secrets.S3_SECRET_ACCESS_KEY }} S3_SECRET_ACCESS_KEY: ${{ secrets.S3_SECRET_ACCESS_KEY }}
-1
View File
@@ -14,6 +14,5 @@ node_modules
# Generated files # Generated files
/coverage/ /coverage/
/dist/ /dist/
/tests/bench/results/
/tests/integration/*/dist/ /tests/integration/*/dist/
/spec/tmp/* /spec/tmp/*
-28
View File
@@ -1,28 +0,0 @@
{
"$schema": "https://raw.githubusercontent.com/nicolo-ribaudo/oxfmt-config-schema/refs/heads/main/schema.json",
"singleQuote": true,
"tabWidth": 2,
"semi": true,
"trailingComma": "es5",
"printWidth": 80,
"ignorePatterns": [
".rvmrc",
".DS_Store",
"/tmp/",
"*.sublime-project",
"*.sublime-workspace",
"npm-debug.log",
"sauce_connect.log*",
".idea",
"yarn-error.log",
"/coverage/",
".nyc_output/",
"/dist/",
"/tests/integration/*/dist/",
"/spec/expected/",
"/spec/mustache",
"/spec/vendor",
"*.handlebars",
"*.hbs"
]
}
-150
View File
@@ -1,150 +0,0 @@
{
"$schema": "https://raw.githubusercontent.com/oxc-project/oxc/main/npm/oxlint/configuration_schema.json",
"plugins": ["eslint", "typescript", "unicorn", "oxc", "node", "vitest"],
"categories": {
"correctness": "error"
},
"rules": {
"no-console": "warn",
"no-func-assign": "off",
"no-sparse-arrays": "off",
"default-case": "warn",
"guard-for-in": "warn",
"no-alert": "error",
"no-caller": "error",
"no-div-regex": "warn",
"no-eval": "error",
"no-extend-native": "error",
"no-extra-bind": "error",
"no-implied-eval": "error",
"no-iterator": "error",
"no-labels": "error",
"no-lone-blocks": "error",
"no-loop-func": "error",
"no-multi-str": "warn",
"no-global-assign": "error",
"no-new": "error",
"no-new-func": "error",
"no-new-wrappers": "error",
"no-proto": "error",
"no-return-assign": "error",
"no-script-url": "error",
"no-self-compare": "error",
"no-sequences": "error",
"no-throw-literal": "error",
"no-unused-expressions": "error",
"no-warning-comments": "warn",
"no-with": "error",
"radix": "error",
"no-label-var": "error",
"no-use-before-define": ["error", { "functions": false }],
"no-var": "error",
"node/no-process-env": "error"
},
"ignorePatterns": [
"tmp/",
"dist/",
"coverage/",
".nyc_output/",
"handlebars-release.tgz",
"tests/integration/*/dist/",
"spec/expected/",
"spec/mustache",
"spec/vendor",
"node_modules",
"types/"
],
"overrides": [
{
"files": ["lib/**/*.js"],
"env": {
"node": false,
"browser": true
}
},
{
"files": ["spec/**/*.js"],
"globals": {
"CompilerContext": "readonly",
"Handlebars": "writable",
"handlebarsEnv": "readonly",
"expectTemplate": "readonly",
"suite": "readonly",
"test": "readonly",
"testBoth": "readonly",
"raises": "readonly",
"deepEqual": "readonly",
"start": "readonly",
"stop": "readonly",
"ok": "readonly",
"vi": "readonly",
"strictEqual": "readonly",
"define": "readonly",
"expect": "readonly",
"beforeEach": "readonly",
"afterEach": "readonly",
"describe": "readonly",
"it": "readonly"
},
"rules": {
"no-var": "off",
"dot-notation": "off",
"vitest/no-conditional-tests": "off"
}
},
{
"files": ["tasks/**/*.js"],
"rules": {
"node/no-process-env": "off",
"prefer-const": "warn",
"dot-notation": "error"
}
},
{
"files": ["tasks/tests/**/*.js"],
"globals": {
"describe": "readonly",
"it": "readonly",
"expect": "readonly",
"beforeEach": "readonly",
"afterEach": "readonly",
"vi": "readonly"
}
},
{
"files": ["tests/bench/**/*.mjs"],
"rules": {
"no-console": "off"
}
},
{
"files": ["tests/integration/multi-nodejs-test/**/*.js"],
"rules": {
"no-console": "off",
"no-var": "off"
}
},
{
"files": ["tests/browser/**/*.js"],
"env": {
"browser": true
}
},
{
"files": [
"tests/integration/webpack-babel-test/src/**/*.js",
"tests/integration/webpack-test/src/**/*.js"
],
"env": {
"browser": true
},
"rules": {
"no-var": "off"
}
}
]
}
+21
View File
@@ -0,0 +1,21 @@
.rvmrc
.DS_Store
/tmp/
*.sublime-project
*.sublime-workspace
npm-debug.log
sauce_connect.log*
.idea
yarn-error.log
node_modules
/handlebars-release.tgz
.nyc_output
# Generated files
/coverage/
/dist/
/tests/integration/*/dist/
# Third-party or files that must remain unchanged
/spec/expected/
/spec/vendor
-8
View File
@@ -1,8 +0,0 @@
{
"$schema": "https://swc.rs/schema.json",
"module": {
"type": "commonjs",
"importInterop": "swc"
},
"sourceMaps": "inline"
}
+32 -12
View File
@@ -18,8 +18,11 @@ Documentation issues on the [handlebarsjs.com](https://handlebarsjs.com) site sh
## Branches ## Branches
- The branch `master` contains the current development version (v5). - The branch `4.x` contains the currently released version. Bugfixes should be made in this branch.
- The branch `4.x` contains the previous stable version. Only critical bugfixes are backported there. - The branch `master` contains the next version. A release date is not yet specified. Maintainers
should merge the branch `4.x` into the master branch regularly.
- The branch `3.x` contains the legacy version `3.x`. Bugfixes are applied separately (if needed). The branch will not
be merged with any of the other branches.
## Pull Requests ## Pull Requests
@@ -35,13 +38,21 @@ Generally we like to see pull requests that
## Building ## Building
To build Handlebars.js you'll need Node.js installed. To build Handlebars.js you'll need a few things installed.
- Node.js
- [Grunt](http://gruntjs.com/getting-started)
Before building, you need to make sure that the Git submodule `spec/mustache` is included (i.e. the directory `spec/mustache` should not be empty). To include it, if using Git version 1.6.5 or newer, use `git clone --recursive` rather than `git clone`. Or, if you already cloned without `--recursive`, use `git submodule update --init`. Before building, you need to make sure that the Git submodule `spec/mustache` is included (i.e. the directory `spec/mustache` should not be empty). To include it, if using Git version 1.6.5 or newer, use `git clone --recursive` rather than `git clone`. Or, if you already cloned without `--recursive`, use `git submodule update --init`.
Project dependencies may be installed via `npm install`. Project dependencies may be installed via `npm install`.
To build Handlebars.js from scratch, run `npm run build` in the root of the project. That will compile CJS modules via SWC and bundle UMD distributions via rspack, outputting results to the dist/ folder. To run tests, use `npm test`. To build Handlebars.js from scratch, you'll want to run `grunt`
in the root of the project. That will build Handlebars and output the
results to the dist/ folder. To re-run tests, run `grunt test` or `npm test`.
You can also run our set of benchmarks with `grunt bench`.
The `grunt dev` implements watching for tests and allows for in browser testing at `http://localhost:9999/spec/`.
If you notice any problems, please report them to the GitHub issue tracker at If you notice any problems, please report them to the GitHub issue tracker at
[http://github.com/handlebars-lang/handlebars.js/issues](http://github.com/handlebars-lang/handlebars.js/issues). [http://github.com/handlebars-lang/handlebars.js/issues](http://github.com/handlebars-lang/handlebars.js/issues).
@@ -70,16 +81,21 @@ npm test
## Linting and Formatting ## Linting and Formatting
Handlebars uses `oxlint` for linting, `oxfmt` for formatting, and `eslint` (with `eslint-plugin-compat`) for browser API compatibility checks. Handlebars uses `eslint` to enforce best-practices and `prettier` to auto-format files.
Committed files are linted and formatted in a pre-commit hook. We do linting and formatting in two phases:
- Committed files are linted and formatted in a pre-commit hook. In this stage eslint-errors are forbidden,
while warnings are allowed.
- The GitHub CI job also lints all files and checks if they are formatted correctly. In this stage, warnings
are forbidden.
You can use the following scripts to make sure that the CI job does not fail: You can use the following scripts to make sure that the CI job does not fail:
- **npm run lint** will run all linters and fail on warnings - **npm run lint** will run `eslint` and fail on warnings
- **npm run format** will format all files - **npm run format** will run `prettier` on all files
- **npm run check-before-pull-request** will perform all checks that our CI job does, excluding integration tests. - **npm run check-before-pull-request** will perform all most checks that our CI job does in its build-job, excluding the "integration-test".
- **npm run test:integration** will run integration tests (bundler compatibility with webpack, rollup, etc.) - **npm run test:integration** will run integration tests (using old NodeJS versions and integrations with webpack, babel and so on)
These tests only work on Linux. These tests only work on a Linux-machine with `nvm` installed (for running tests in multiple versions of NodeJS).
## Releasing the latest version ## Releasing the latest version
@@ -97,8 +113,12 @@ A full release may be completed with the following:
``` ```
npm ci npm ci
npm run build npx grunt
npm publish npm publish
cd dist/components/
gem build handlebars-source.gemspec
gem push handlebars-source-*.gem
``` ```
After the release, you should check that all places have really been updated. Especially verify that the `latest`-tags After the release, you should check that all places have really been updated. Especially verify that the `latest`-tags
+176
View File
@@ -0,0 +1,176 @@
/* eslint-disable no-process-env */
module.exports = function (grunt) {
grunt.initConfig({
pkg: grunt.file.readJSON('package.json'),
clean: ['tmp', 'dist', 'tests/integration/**/node_modules'],
copy: {
dist: {
options: {
processContent: function (content) {
return (
grunt.template.process(
'/**!\n\n @license magnet:?xt=urn:btih:d3d9a9a6595521f9666a5e94cc830dab83b65699&dn=expat.txt Expat\n <%= pkg.name %> v<%= pkg.version %>\n\n<%= grunt.file.read("LICENSE") %>\n*/\n'
) +
content +
'\n// @license-end\n'
);
},
},
files: [{ expand: true, cwd: 'dist/', src: ['*.js'], dest: 'dist/' }],
},
components: {
files: [
{
expand: true,
cwd: 'components/',
src: ['**'],
dest: 'dist/components',
},
{
expand: true,
cwd: 'dist/',
src: ['*.js'],
dest: 'dist/components',
},
],
},
},
babel: {
options: {
sourceMaps: 'inline',
loose: ['es6.modules'],
auxiliaryCommentBefore: 'istanbul ignore next',
},
cjs: {
files: [
{
cwd: 'lib/',
expand: true,
src: '**/!(index).js',
dest: 'dist/cjs/',
},
],
},
},
webpack: {
options: {
context: __dirname,
output: {
path: 'dist/',
library: 'Handlebars',
libraryTarget: 'umd',
},
},
handlebars: {
entry: './dist/cjs/handlebars.js',
output: {
filename: 'handlebars.js',
},
},
runtime: {
entry: './dist/cjs/handlebars.runtime.js',
output: {
filename: 'handlebars.runtime.js',
},
},
},
uglify: {
options: {
mangle: true,
compress: true,
preserveComments: /(?:^!|@(?:license|preserve|cc_on))/,
},
dist: {
files: [
{
cwd: 'dist/',
expand: true,
src: ['handlebars*.js', '!*.min.js'],
dest: 'dist/',
rename: function (dest, src) {
return dest + src.replace(/\.js$/, '.min.js');
},
},
],
},
},
concat: {
tests: {
src: ['spec/!(require).js'],
dest: 'tmp/tests.js',
},
},
connect: {
server: {
options: {
base: '.',
hostname: '*',
port: 9999,
},
},
},
shell: {
integrationTests: {
command: './tests/integration/run-integration-tests.sh',
},
},
watch: {
scripts: {
options: {
atBegin: true,
},
files: ['src/*', 'lib/**/*.js', 'spec/**/*.js'],
tasks: ['on-file-change'],
},
},
});
// Load tasks from npm
grunt.loadNpmTasks('grunt-contrib-clean');
grunt.loadNpmTasks('grunt-contrib-concat');
grunt.loadNpmTasks('grunt-contrib-connect');
grunt.loadNpmTasks('grunt-contrib-copy');
grunt.loadNpmTasks('grunt-contrib-uglify');
grunt.loadNpmTasks('grunt-contrib-watch');
grunt.loadNpmTasks('grunt-babel');
grunt.loadNpmTasks('grunt-shell');
grunt.loadNpmTasks('grunt-webpack');
grunt.task.loadTasks('tasks');
grunt.registerTask('node', ['babel:cjs']);
grunt.registerTask('globals', ['webpack']);
grunt.registerTask('release', 'Build final packages', [
'uglify',
'test:min',
'copy:dist',
'copy:components',
]);
grunt.registerTask('on-file-change', ['build', 'concat:tests', 'test']);
// === Primary tasks ===
grunt.registerTask('dev', ['clean', 'connect', 'watch']);
grunt.registerTask('default', ['clean', 'build', 'test', 'release']);
grunt.registerTask('test', ['test:bin', 'test:cov']);
grunt.registerTask('bench', ['metrics']);
grunt.registerTask('prepare', ['build', 'concat:tests']);
grunt.registerTask(
'build',
'Builds a distributable version of the current project',
['node', 'globals']
);
grunt.registerTask('integration-tests', [
'default',
'shell:integrationTests',
]);
};
+65 -95
View File
@@ -5,7 +5,8 @@
[![Bundle size](https://badgen.net/bundlephobia/minzip/handlebars?label=minified%20%2B%20gzipped)](https://bundlephobia.com/package/handlebars) [![Bundle size](https://badgen.net/bundlephobia/minzip/handlebars?label=minified%20%2B%20gzipped)](https://bundlephobia.com/package/handlebars)
[![Install size](https://packagephobia.com/badge?p=handlebars)](https://packagephobia.com/result?p=handlebars) [![Install size](https://packagephobia.com/badge?p=handlebars)](https://packagephobia.com/result?p=handlebars)
# Handlebars.js Handlebars.js
=============
Handlebars provides the power necessary to let you build **semantic templates** effectively with no frustration. Handlebars provides the power necessary to let you build **semantic templates** effectively with no frustration.
Handlebars is largely compatible with Mustache templates. In most cases it is possible to swap out Mustache with Handlebars and continue using your current templates. Handlebars is largely compatible with Mustache templates. In most cases it is possible to swap out Mustache with Handlebars and continue using your current templates.
@@ -13,12 +14,13 @@ Handlebars is largely compatible with Mustache templates. In most cases it is po
Checkout the official Handlebars docs site at Checkout the official Handlebars docs site at
[handlebarsjs.com](https://handlebarsjs.com) and try our [live demo](https://handlebarsjs.com/playground.html). [handlebarsjs.com](https://handlebarsjs.com) and try our [live demo](https://handlebarsjs.com/playground.html).
## Installing Installing
----------
See our [installation documentation](https://handlebarsjs.com/guide/installation/). See our [installation documentation](https://handlebarsjs.com/installation/).
## Usage
Usage
-----
In general, the syntax of Handlebars.js templates is a superset In general, the syntax of Handlebars.js templates is a superset
of Mustache templates. For basic syntax, check out the [Mustache of Mustache templates. For basic syntax, check out the [Mustache
manpage](https://mustache.github.io/mustache.5.html). manpage](https://mustache.github.io/mustache.5.html).
@@ -28,20 +30,13 @@ the template into a function. The generated function takes a context
argument, which will be used to render the template. argument, which will be used to render the template.
```js ```js
var source = var source = "<p>Hello, my name is {{name}}. I am from {{hometown}}. I have " +
'<p>Hello, my name is {{name}}. I am from {{hometown}}. I have ' + "{{kids.length}} kids:</p>" +
'{{kids.length}} kids:</p>' + "<ul>{{#kids}}<li>{{name}} is {{age}}</li>{{/kids}}</ul>";
'<ul>{{#kids}}<li>{{name}} is {{age}}</li>{{/kids}}</ul>';
var template = Handlebars.compile(source); var template = Handlebars.compile(source);
var data = { var data = { "name": "Alan", "hometown": "Somewhere, TX",
name: 'Alan', "kids": [{"name": "Jimmy", "age": "12"}, {"name": "Sally", "age": "4"}]};
hometown: 'Somewhere, TX',
kids: [
{ name: 'Jimmy', age: '12' },
{ name: 'Sally', age: '4' },
],
};
var result = template(data); var result = template(data);
// Would render: // Would render:
@@ -54,12 +49,13 @@ var result = template(data);
Full documentation and more examples are at [handlebarsjs.com](https://handlebarsjs.com/). Full documentation and more examples are at [handlebarsjs.com](https://handlebarsjs.com/).
## Precompiling Templates Precompiling Templates
----------------------
Handlebars allows templates to be precompiled and included as javascript code rather than the handlebars template allowing for faster startup time. Full details are located [here](https://handlebarsjs.com/guide/installation/precompilation.html). Handlebars allows templates to be precompiled and included as javascript code rather than the handlebars template allowing for faster startup time. Full details are located [here](https://handlebarsjs.com/installation/precompilation.html).
## Differences Between Handlebars.js and Mustache
Differences Between Handlebars.js and Mustache
----------------------------------------------
Handlebars.js adds a couple of additional features to make writing Handlebars.js adds a couple of additional features to make writing
templates easier and also changes a tiny detail of how partials work. templates easier and also changes a tiny detail of how partials work.
@@ -74,13 +70,14 @@ Block expressions have the same syntax as mustache sections but should not be co
### Compatibility ### Compatibility
There are a few Mustache behaviors that Handlebars does not implement. There are a few Mustache behaviors that Handlebars does not implement.
- Handlebars deviates from Mustache slightly in that it does not perform recursive lookup by default. The compile time `compat` flag must be set to enable this functionality. Users should note that there is a performance cost for enabling this flag. The exact cost varies by template, but it's recommended that performance sensitive operations should avoid this mode and instead opt for explicit path references. - Handlebars deviates from Mustache slightly in that it does not perform recursive lookup by default. The compile time `compat` flag must be set to enable this functionality. Users should note that there is a performance cost for enabling this flag. The exact cost varies by template, but it's recommended that performance sensitive operations should avoid this mode and instead opt for explicit path references.
- The optional Mustache-style lambdas are not supported. Instead Handlebars provides its own lambda resolution that follows the behaviors of helpers. - The optional Mustache-style lambdas are not supported. Instead Handlebars provides its own lambda resolution that follows the behaviors of helpers.
- Handlebars does not allow space between the opening `{{` and a command character such as `#`, `/` or `>`. The command character must immediately follow the braces, so for example `{{> partial }}` is allowed but `{{ > partial }}` is not. - Handlebars does not allow space between the opening `{{` and a command character such as `#`, `/` or `>`. The command character must immediately follow the braces, so for example `{{> partial }}` is allowed but `{{ > partial }}` is not.
- Alternative delimiters are not supported. - Alternative delimiters are not supported.
## Supported Environments
Supported Environments
----------------------
Handlebars has been designed to work in any ECMAScript 2020 environment. This includes Handlebars has been designed to work in any ECMAScript 2020 environment. This includes
@@ -92,7 +89,9 @@ Handlebars has been designed to work in any ECMAScript 2020 environment. This in
If you need to support older environments, use Handlebars version 4. If you need to support older environments, use Handlebars version 4.
## Performance
Performance
-----------
In a rough performance test, precompiled Handlebars.js templates (in In a rough performance test, precompiled Handlebars.js templates (in
the original version of Handlebars.js) rendered in about half the the original version of Handlebars.js) rendered in about half the
@@ -104,44 +103,9 @@ does have some big performance advantages. Justin Marney, a.k.a.
rewritten Handlebars (current version) is faster than the old version, rewritten Handlebars (current version) is faster than the old version,
with many performance tests being 5 to 7 times faster than the Mustache equivalent. with many performance tests being 5 to 7 times faster than the Mustache equivalent.
### Benchmarks
The project includes a comprehensive benchmark suite (powered by [tinybench](https://github.com/tinylibs/tinybench)) that measures compilation, execution, precompilation, and end-to-end performance across templates of varying size and complexity. Upgrading
---------
```bash
# Run benchmarks (auto-labels with current git branch)
npm run bench
# Run with a custom label
npm run bench -- --label my-optimization
# Filter templates by name (regex, case-insensitive)
npm run bench -- --grep "complex|recursive"
# Run only specific sections (regex, case-insensitive)
npm run bench -- --section precompil
npm run bench -- --section "compilation|precompil"
# Compare results
npm run bench:compare
# Or specify files explicitly
npm run bench:compare -- bench/results/bench-*-main.md bench/results/bench-*-feat.md
```
Results are saved as timestamped Markdown files in `bench/results/`. Each report includes ops/sec, avg latency, p50/p75/p99 percentiles, and sample counts.
Typical workflow for comparing branches:
```bash
git checkout main && npm run bench
git checkout my-feature && npm run bench
npm run bench:compare
```
When run without arguments, `bench:compare` auto-selects two result files: if a file labelled "main" exists it is always used as the baseline, otherwise the older file is the baseline. The comparison uses p75 latency for the diff to filter outliers, and marks changes with `!` (>2%) and `!!` (>5%).
## Upgrading
See [release-notes.md](https://github.com/handlebars-lang/handlebars.js/blob/master/release-notes.md) for upgrade notes. See [release-notes.md](https://github.com/handlebars-lang/handlebars.js/blob/master/release-notes.md) for upgrade notes.
@@ -150,49 +114,55 @@ If you are using Handlebars in production, please regularly look for issues labe
If this label is applied to an issue, it means that the requested change is probably not a breaking change, If this label is applied to an issue, it means that the requested change is probably not a breaking change,
but since Handlebars is widely in use by a lot of people, there's always a chance that it breaks somebody's build. but since Handlebars is widely in use by a lot of people, there's always a chance that it breaks somebody's build.
## Known Issues
Known Issues
------------
See [FAQ.md](https://github.com/handlebars-lang/handlebars.js/blob/master/FAQ.md) for known issues and common pitfalls. See [FAQ.md](https://github.com/handlebars-lang/handlebars.js/blob/master/FAQ.md) for known issues and common pitfalls.
## Handlebars in the Wild
- [apiDoc](https://github.com/apidoc/apidoc) apiDoc uses handlebars as parsing engine for api documentation view generation. Handlebars in the Wild
- [Assemble](https://assemble.io), by [@jonschlinkert](https://github.com/jonschlinkert) and [@doowb](https://github.com/doowb), is a static site generator that uses Handlebars.js as its template engine. ----------------------
- [CoSchedule](https://coschedule.com) An editorial calendar for WordPress that uses Handlebars.js.
- [Ember.js](https://www.emberjs.com) makes Handlebars.js the primary way to structure your views, also with automatic data binding support.
- [express-handlebars](https://github.com/express-handlebars/express-handlebars) A Handlebars view engine for Express which doesn't suck.
- [express-hbs](https://github.com/TryGhost/express-hbs) Express Handlebars template engine with inheritance, partials, i18n and async helpers.
- [Ghost](https://ghost.org/) Just a blogging platform.
- [handlebars-action](https://github.com/marketplace/actions/handlebars-action) A GitHub action to transform files in your repository with Handlebars templating.
- [handlebars_assets](https://github.com/leshill/handlebars_assets) A Rails Asset Pipeline gem from Les Hill (@leshill).
- [handlebars-helpers](https://github.com/assemble/handlebars-helpers) is an extensive library with 100+ handlebars helpers.
- [handlebars-layouts](https://github.com/shannonmoeller/handlebars-layouts) is a set of helpers which implement extensible and embeddable layout blocks as seen in other popular templating languages.
- [handlebars-loader](https://github.com/pcardune/handlebars-loader) A handlebars template loader for webpack.
- [handlebars-wax](https://github.com/shannonmoeller/handlebars-wax) The missing Handlebars API. Effortless registration of data, partials, helpers, and decorators using file-system globs, modules, and plain-old JavaScript objects.
- [hbs](https://github.com/pillarjs/hbs) An Express.js view engine adapter for Handlebars.js, from Don Park.
- [html-bundler-webpack-plugin](https://github.com/webdiscus/html-bundler-webpack-plugin) The webpack plugin to compile templates, [supports Handlebars](https://github.com/webdiscus/html-bundler-webpack-plugin#using-template-handlebars).
- [incremental-bars](https://github.com/atomictag/incremental-bars) adds support for [incremental-dom](https://github.com/google/incremental-dom) as template target to Handlebars.
- [jQuery plugin](https://71104.github.io/jquery-handlebars/) allows you to use Handlebars.js with [jQuery](http://jquery.com/).
- [just-handlebars-helpers](https://github.com/leapfrogtechnology/just-handlebars-helpers) A fully tested lightweight package with common Handlebars helpers.
- [koa-hbs](https://github.com/jwilm/koa-hbs) [koa](https://github.com/koajs/koa) generator based renderer for Handlebars.js.
- [Marionette.Handlebars](https://github.com/hashchange/marionette.handlebars) adds support for Handlebars and Mustache templates to Marionette.
- [openVALIDATION](https://github.com/openvalidation/openvalidation) a natural language compiler for validation rules. Generates program code in Java, JavaScript, C#, Python and Rust with handlebars.
- [Plop](https://plopjs.com/) is a micro-generator framework that makes it easy to create files with a level of uniformity.
- [promised-handlebars](https://github.com/nknapp/promised-handlebars) is a wrapper for Handlebars that allows helpers to return Promises.
- [sammy.js](https://github.com/quirkey/sammy) by Aaron Quint, a.k.a. quirkey, supports Handlebars.js as one of its template plugins.
- [Swag](https://github.com/elving/swag) by [@elving](https://github.com/elving) is a growing collection of helpers for handlebars.js. Give your handlebars.js templates some swag son!
- [SproutCore](https://www.sproutcore.com) uses Handlebars.js as its main templating engine, extending it with automatic data binding support.
- [vite-plugin-handlebars](https://github.com/alexlafroscia/vite-plugin-handlebars) A package for Vite 2. Allows for running your HTML files through the Handlebars compiler.
- [YUI](https://yuilibrary.com/yui/docs/handlebars/) implements a port of handlebars.
## External Resources * [apiDoc](https://github.com/apidoc/apidoc) apiDoc uses handlebars as parsing engine for api documentation view generation.
* [Assemble](https://assemble.io), by [@jonschlinkert](https://github.com/jonschlinkert) and [@doowb](https://github.com/doowb), is a static site generator that uses Handlebars.js as its template engine.
* [CoSchedule](https://coschedule.com) An editorial calendar for WordPress that uses Handlebars.js.
* [Ember.js](https://www.emberjs.com) makes Handlebars.js the primary way to structure your views, also with automatic data binding support.
* [express-handlebars](https://github.com/express-handlebars/express-handlebars) A Handlebars view engine for Express which doesn't suck.
* [express-hbs](https://github.com/TryGhost/express-hbs) Express Handlebars template engine with inheritance, partials, i18n and async helpers.
* [Ghost](https://ghost.org/) Just a blogging platform.
* [handlebars-action](https://github.com/marketplace/actions/handlebars-action) A GitHub action to transform files in your repository with Handlebars templating.
* [handlebars_assets](https://github.com/leshill/handlebars_assets) A Rails Asset Pipeline gem from Les Hill (@leshill).
* [handlebars-helpers](https://github.com/assemble/handlebars-helpers) is an extensive library with 100+ handlebars helpers.
* [handlebars-layouts](https://github.com/shannonmoeller/handlebars-layouts) is a set of helpers which implement extensible and embeddable layout blocks as seen in other popular templating languages.
* [handlebars-loader](https://github.com/pcardune/handlebars-loader) A handlebars template loader for webpack.
* [handlebars-wax](https://github.com/shannonmoeller/handlebars-wax) The missing Handlebars API. Effortless registration of data, partials, helpers, and decorators using file-system globs, modules, and plain-old JavaScript objects.
* [hbs](https://github.com/pillarjs/hbs) An Express.js view engine adapter for Handlebars.js, from Don Park.
* [html-bundler-webpack-plugin](https://github.com/webdiscus/html-bundler-webpack-plugin) The webpack plugin to compile templates, [supports Handlebars](https://github.com/webdiscus/html-bundler-webpack-plugin#using-template-handlebars).
* [incremental-bars](https://github.com/atomictag/incremental-bars) adds support for [incremental-dom](https://github.com/google/incremental-dom) as template target to Handlebars.
* [jblotus](https://github.com/jblotus) created [http://tryhandlebarsjs.com](http://tryhandlebarsjs.com) for anyone who would like to try out Handlebars.js in their browser.
* [jQuery plugin](https://71104.github.io/jquery-handlebars/) allows you to use Handlebars.js with [jQuery](http://jquery.com/).
* [just-handlebars-helpers](https://github.com/leapfrogtechnology/just-handlebars-helpers) A fully tested lightweight package with common Handlebars helpers.
* [koa-hbs](https://github.com/jwilm/koa-hbs) [koa](https://github.com/koajs/koa) generator based renderer for Handlebars.js.
* [Marionette.Handlebars](https://github.com/hashchange/marionette.handlebars) adds support for Handlebars and Mustache templates to Marionette.
* [openVALIDATION](https://github.com/openvalidation/openvalidation) a natural language compiler for validation rules. Generates program code in Java, JavaScript, C#, Python and Rust with handlebars.
* [Plop](https://plopjs.com/) is a micro-generator framework that makes it easy to create files with a level of uniformity.
* [promised-handlebars](https://github.com/nknapp/promised-handlebars) is a wrapper for Handlebars that allows helpers to return Promises.
* [sammy.js](https://github.com/quirkey/sammy) by Aaron Quint, a.k.a. quirkey, supports Handlebars.js as one of its template plugins.
* [Swag](https://github.com/elving/swag) by [@elving](https://github.com/elving) is a growing collection of helpers for handlebars.js. Give your handlebars.js templates some swag son!
* [SproutCore](https://www.sproutcore.com) uses Handlebars.js as its main templating engine, extending it with automatic data binding support.
* [vite-plugin-handlebars](https://github.com/alexlafroscia/vite-plugin-handlebars) A package for Vite 2. Allows for running your HTML files through the Handlebars compiler.
* [YUI](https://yuilibrary.com/yui/docs/handlebars/) implements a port of handlebars.
- [Gist about Synchronous and asynchronous loading of external handlebars templates](https://gist.github.com/2287070) External Resources
------------------
* [Gist about Synchronous and asynchronous loading of external handlebars templates](https://gist.github.com/2287070)
Have a project using Handlebars? Send us a [pull request][pull-request]! Have a project using Handlebars? Send us a [pull request][pull-request]!
## License License
-------
Handlebars.js is released under the MIT license. Handlebars.js is released under the MIT license.
[pull-request]: https://github.com/handlebars-lang/handlebars.js/pull/new/master [pull-request]: https://github.com/handlebars-lang/handlebars.js/pull/new/master
+18 -23
View File
@@ -1,15 +1,7 @@
#!/usr/bin/env node #!/usr/bin/env node
import { createRequire } from 'node:module'; const yargs = require('yargs')
import yargs from 'yargs';
const require = createRequire(import.meta.url);
const Precompiler = require('../dist/cjs/precompiler');
const parser = yargs(process.argv.slice(2))
.usage('Precompile handlebar templates.\nUsage: $0 [template|directory]...') .usage('Precompile handlebar templates.\nUsage: $0 [template|directory]...')
.help(false)
.version(false)
.option('f', { .option('f', {
type: 'string', type: 'string',
description: 'Output File', description: 'Output File',
@@ -19,10 +11,11 @@ const parser = yargs(process.argv.slice(2))
type: 'string', type: 'string',
description: 'Source Map File', description: 'Source Map File',
}) })
.option('a', { .option('esm', {
type: 'boolean', type: 'string',
description: 'Exports amd style (require.js)', description:
alias: 'amd', 'Exports ECMAScript module style, path to Handlebars module (eg. "handlebars/lib/handlebars")',
default: null,
}) })
.option('c', { .option('c', {
type: 'string', type: 'string',
@@ -30,11 +23,18 @@ const parser = yargs(process.argv.slice(2))
alias: 'commonjs', alias: 'commonjs',
default: null, default: null,
}) })
.option('a', {
type: 'boolean',
description: 'Exports AMD style (require.js)',
alias: 'amd',
deprecated: true,
})
.option('h', { .option('h', {
type: 'string', type: 'string',
description: 'Path to handlebar.js (only valid for amd-style)', description: 'Path to handlebar.js (only valid for AMD-style)',
alias: 'handlebarPath', alias: 'handlebarPath',
default: '', default: '',
deprecated: true,
}) })
.option('k', { .option('k', {
type: 'string', type: 'string',
@@ -113,24 +113,19 @@ const parser = yargs(process.argv.slice(2))
}) })
.wrap(120); .wrap(120);
const argv = parser.parseSync(); const argv = yargs.argv;
argv.files = argv._; argv.files = argv._;
delete argv._; delete argv._;
const Precompiler = require('../dist/cjs/precompiler');
Precompiler.loadTemplates(argv, function (err, opts) { Precompiler.loadTemplates(argv, function (err, opts) {
if (err) { if (err) {
throw err; throw err;
} }
if (opts.help || (!opts.templates.length && !opts.version)) { if (opts.help || (!opts.templates.length && !opts.version)) {
parser.showHelp('log'); yargs.showHelp();
} else { } else {
// cli() is async (returns a Promise), so errors would become unhandled Precompiler.cli(opts);
// rejections. Re-throw via nextTick to surface them as uncaught exceptions.
Promise.resolve(Precompiler.cli(opts)).catch((error) => {
process.nextTick(() => {
throw error;
});
});
} }
}); });
+2
View File
@@ -297,6 +297,7 @@ The `Handlebars.JavaScriptCompiler` object has a number of methods that may be c
- `nameLookup(parent, name, type)` - `nameLookup(parent, name, type)`
Used to generate the code to resolve a given path component. Used to generate the code to resolve a given path component.
- `parent` is the existing code in the path resolution - `parent` is the existing code in the path resolution
- `name` is the current path component - `name` is the current path component
- `type` is the type of name being evaluated. May be one of `context`, `data`, `helper`, `decorator`, or `partial`. - `type` is the type of name being evaluated. May be one of `context`, `data`, `helper`, `decorator`, or `partial`.
@@ -311,6 +312,7 @@ The `Handlebars.JavaScriptCompiler` object has a number of methods that may be c
- `appendToBuffer(source, location, explicit)` - `appendToBuffer(source, location, explicit)`
Allows for code buffer emitting code. Defaults behavior is string concatenation. Allows for code buffer emitting code. Defaults behavior is string concatenation.
- `source` is the source code whose result is to be appending - `source` is the source code whose result is to be appending
- `location` is the location of the source in the source map. - `location` is the location of the source in the source map.
- `explicit` is a flag signaling that the emit operation must occur, vs. the lazy evaled options otherwise. - `explicit` is a flag signaling that the emit operation must occur, vs. the lazy evaled options otherwise.
-17
View File
@@ -1,17 +0,0 @@
import compat from 'eslint-plugin-compat';
export default [
{
// Ignore everything except lib/
ignores: ['**', '!lib/**'],
},
{
// Only check browser API compat in the runtime library code.
// All other linting is handled by oxlint.
...compat.configs['flat/recommended'],
files: ['lib/**/*.js'],
linterOptions: {
reportUnusedDisableDirectives: 'off',
},
},
];
+7
View File
@@ -0,0 +1,7 @@
/* eslint-env node */
module.exports = {
env: {
// Handlebars should run natively in the browser
node: false,
},
};
+1 -4
View File
@@ -19,10 +19,7 @@ function create() {
hb.Utils = Utils; hb.Utils = Utils;
hb.escapeExpression = Utils.escapeExpression; hb.escapeExpression = Utils.escapeExpression;
// Spread into a plain object so that runtime functions (e.g. checkRevision) hb.VM = runtime;
// can be overridden by consumers. ES module namespace objects are sealed with
// getter-only properties per spec, which would prevent monkey-patching.
hb.VM = { ...runtime };
hb.template = function (spec) { hb.template = function (spec) {
return runtime.template(spec, hb); return runtime.template(spec, hb);
}; };
+4 -10
View File
@@ -4,24 +4,18 @@ import { isArray } from '../utils';
let SourceNode; let SourceNode;
try { try {
/* v8 ignore next */ /* istanbul ignore next */
if (typeof define !== 'function' || !define.amd) { if (typeof define !== 'function' || !define.amd) {
// We don't support this in AMD environments. For these environments, we assume that // We don't support this in AMD environments. For these environments, we assume that
// they are running on the browser and thus have no need for the source-map library. // they are running on the browser and thus have no need for the source-map library.
// The variable indirection prevents bundlers from statically resolving and bundling let SourceMap = require('source-map'); // eslint-disable-line no-undef
// source-map (which requires Node-built-ins). The stub SourceNode below handles
// browser/bundled usage. Bundlers may emit a "Critical dependency" warning — this
// is expected and harmless.
let mod = 'source-map';
let SourceMap = require(mod);
SourceNode = SourceMap.SourceNode; SourceNode = SourceMap.SourceNode;
} }
// oxlint-disable-next-line no-unused-vars -- Babel 5 requires named catch param
} catch (err) { } catch (err) {
/* NOP */ /* NOP */
} }
/* v8 ignore next -- tested but not covered due to dist build */ /* istanbul ignore if: tested but not covered in istanbul due to dist build */
if (!SourceNode) { if (!SourceNode) {
SourceNode = function (line, column, srcFile, chunks) { SourceNode = function (line, column, srcFile, chunks) {
this.src = ''; this.src = '';
@@ -29,7 +23,7 @@ if (!SourceNode) {
this.add(chunks); this.add(chunks);
} }
}; };
/* v8 ignore next */ /* istanbul ignore next */
SourceNode.prototype = { SourceNode.prototype = {
add: function (chunks) { add: function (chunks) {
if (isArray(chunks)) { if (isArray(chunks)) {
+4 -2
View File
@@ -1,3 +1,5 @@
/* eslint-disable new-cap */
import { Exception } from '@handlebars/parser'; import { Exception } from '@handlebars/parser';
import { isArray, indexOf, extend } from '../utils'; import { isArray, indexOf, extend } from '../utils';
import AST from './ast'; import AST from './ast';
@@ -72,7 +74,7 @@ Compiler.prototype = {
}, },
compileProgram: function (program) { compileProgram: function (program) {
let childCompiler = new this.compiler(), let childCompiler = new this.compiler(), // eslint-disable-line new-cap
result = childCompiler.compile(program, this.options), result = childCompiler.compile(program, this.options),
guid = this.guid++; guid = this.guid++;
@@ -85,7 +87,7 @@ Compiler.prototype = {
}, },
accept: function (node) { accept: function (node) {
/* v8 ignore next -- Sanity code */ /* istanbul ignore next: Sanity code */
if (!this[node.type]) { if (!this[node.type]) {
throw new Exception('Unknown type: ' + node.type, node); throw new Exception('Unknown type: ' + node.type, node);
} }
+15 -16
View File
@@ -112,7 +112,7 @@ JavaScriptCompiler.prototype = {
this.source.currentLocation = firstLoc; this.source.currentLocation = firstLoc;
this.pushSource(''); this.pushSource('');
/* v8 ignore next */ /* istanbul ignore next */
if (this.stackSlot || this.inlineStack.length || this.compileStack.length) { if (this.stackSlot || this.inlineStack.length || this.compileStack.length) {
throw new Exception('Compile completed with content left on stack'); throw new Exception('Compile completed with content left on stack');
} }
@@ -158,22 +158,21 @@ JavaScriptCompiler.prototype = {
}; };
if (this.decorators) { if (this.decorators) {
ret.main_d = this.decorators; ret.main_d = this.decorators; // eslint-disable-line camelcase
ret.useDecorators = true; ret.useDecorators = true;
} }
let { programs, decorators } = this.context; let { programs, decorators } = this.context;
for (i = 0, l = programs.length; i < l; i++) { for (i = 0, l = programs.length; i < l; i++) {
ret[i] = programs[i]; if (programs[i]) {
if (decorators[i]) { ret[i] = programs[i];
ret[i + '_d'] = decorators[i]; if (decorators[i]) {
ret.useDecorators = true; ret[i + '_d'] = decorators[i];
ret.useDecorators = true;
}
} }
} }
// Release AST/compiler references only needed during compilation for dedup
this.context.environments.length = 0;
if (this.environment.usePartial) { if (this.environment.usePartial) {
ret.usePartial = true; ret.usePartial = true;
} }
@@ -829,13 +828,13 @@ JavaScriptCompiler.prototype = {
for (let i = 0, l = children.length; i < l; i++) { for (let i = 0, l = children.length; i < l; i++) {
child = children[i]; child = children[i];
compiler = new this.compiler(); compiler = new this.compiler(); // eslint-disable-line new-cap
let existing = this.matchExistingProgram(child); let existing = this.matchExistingProgram(child);
if (existing == null) { if (existing == null) {
// Placeholder to prevent name conflicts for nested children this.context.programs.push(''); // Placeholder to prevent name conflicts for nested children
let index = this.context.programs.push('') - 1; let index = this.context.programs.length;
child.index = index; child.index = index;
child.name = 'program' + index; child.name = 'program' + index;
this.context.programs[index] = compiler.compile( this.context.programs[index] = compiler.compile(
@@ -925,7 +924,7 @@ JavaScriptCompiler.prototype = {
createdStack, createdStack,
usedLiteral; usedLiteral;
/* v8 ignore next */ /* istanbul ignore next */
if (!this.isInline()) { if (!this.isInline()) {
throw new Exception('replaceStack on non-inline'); throw new Exception('replaceStack on non-inline');
} }
@@ -973,7 +972,7 @@ JavaScriptCompiler.prototype = {
this.inlineStack = []; this.inlineStack = [];
for (let i = 0, len = inlineStack.length; i < len; i++) { for (let i = 0, len = inlineStack.length; i < len; i++) {
let entry = inlineStack[i]; let entry = inlineStack[i];
/* v8 ignore next */ /* istanbul ignore if */
if (entry instanceof Literal) { if (entry instanceof Literal) {
this.compileStack.push(entry); this.compileStack.push(entry);
} else { } else {
@@ -995,7 +994,7 @@ JavaScriptCompiler.prototype = {
return item.value; return item.value;
} else { } else {
if (!inline) { if (!inline) {
/* v8 ignore next */ /* istanbul ignore next */
if (!this.stackSlot) { if (!this.stackSlot) {
throw new Exception('Invalid stack pop'); throw new Exception('Invalid stack pop');
} }
@@ -1009,7 +1008,7 @@ JavaScriptCompiler.prototype = {
let stack = this.isInline() ? this.inlineStack : this.compileStack, let stack = this.isInline() ? this.inlineStack : this.compileStack,
item = stack[stack.length - 1]; item = stack[stack.length - 1];
/* v8 ignore next */ /* istanbul ignore if */
if (item instanceof Literal) { if (item instanceof Literal) {
return item.value; return item.value;
} else { } else {
+1 -2
View File
@@ -20,8 +20,7 @@ export function moveHelperToHooks(instance, helperName, keepHelper) {
if (instance.helpers[helperName]) { if (instance.helpers[helperName]) {
instance.hooks[helperName] = instance.helpers[helperName]; instance.hooks[helperName] = instance.helpers[helperName];
if (!keepHelper) { if (!keepHelper) {
// Using delete is slow delete instance.helpers[helperName];
instance.helpers[helperName] = undefined;
} }
} }
} }
+7 -17
View File
@@ -1,5 +1,5 @@
import { Exception } from '@handlebars/parser'; import { Exception } from '@handlebars/parser';
import { createFrame, isArray, isFunction, isMap, isSet } from '../utils'; import { createFrame, isArray, isFunction } from '../utils';
export default function (instance) { export default function (instance) {
instance.registerHelper('each', function (context, options) { instance.registerHelper('each', function (context, options) {
@@ -21,7 +21,7 @@ export default function (instance) {
data = createFrame(options.data); data = createFrame(options.data);
} }
function execIteration(field, value, index, last) { function execIteration(field, index, last) {
if (data) { if (data) {
data.key = field; data.key = field;
data.index = index; data.index = index;
@@ -31,7 +31,7 @@ export default function (instance) {
ret = ret =
ret + ret +
fn(value, { fn(context[field], {
data: data, data: data,
blockParams: [context[field], field], blockParams: [context[field], field],
}); });
@@ -41,19 +41,9 @@ export default function (instance) {
if (isArray(context)) { if (isArray(context)) {
for (let j = context.length; i < j; i++) { for (let j = context.length; i < j; i++) {
if (i in context) { if (i in context) {
execIteration(i, context[i], i, i === context.length - 1); execIteration(i, i, i === context.length - 1);
} }
} }
} else if (isMap(context)) {
const j = context.size;
for (const [key, value] of context) {
execIteration(key, value, i++, i === j);
}
} else if (isSet(context)) {
const j = context.size;
for (const value of context) {
execIteration(i, value, i++, i === j);
}
} else if (typeof Symbol === 'function' && context[Symbol.iterator]) { } else if (typeof Symbol === 'function' && context[Symbol.iterator]) {
const newContext = []; const newContext = [];
const iterator = context[Symbol.iterator](); const iterator = context[Symbol.iterator]();
@@ -62,7 +52,7 @@ export default function (instance) {
} }
context = newContext; context = newContext;
for (let j = context.length; i < j; i++) { for (let j = context.length; i < j; i++) {
execIteration(i, context[i], i, i === context.length - 1); execIteration(i, i, i === context.length - 1);
} }
} else { } else {
let priorKey; let priorKey;
@@ -72,13 +62,13 @@ export default function (instance) {
// the last iteration without have to scan the object twice and create // the last iteration without have to scan the object twice and create
// an intermediate keys array. // an intermediate keys array.
if (priorKey !== undefined) { if (priorKey !== undefined) {
execIteration(priorKey, context[priorKey], i - 1); execIteration(priorKey, i - 1);
} }
priorKey = key; priorKey = key;
i++; i++;
}); });
if (priorKey !== undefined) { if (priorKey !== undefined) {
execIteration(priorKey, context[priorKey], i - 1, true); execIteration(priorKey, i - 1, true);
} }
} }
} }
@@ -0,0 +1,11 @@
import { extend } from '../utils';
/**
* Create a new object with "null"-prototype to avoid truthy results on prototype properties.
* The resulting object can be used with "object[property]" to check if a property exists
* @param {...object} sources a varargs parameter of source objects that will be merged
* @returns {object}
*/
export function createNewLookupObject(...sources) {
return extend(Object.create(null), ...sources);
}
+17 -15
View File
@@ -1,30 +1,32 @@
import { extend } from '../utils'; import { createNewLookupObject } from './create-new-lookup-object';
import logger from '../logger'; import logger from '../logger';
const loggedProperties = Object.create(null); const loggedProperties = Object.create(null);
export function createProtoAccessControl(runtimeOptions) { export function createProtoAccessControl(runtimeOptions) {
// Create an object with "null"-prototype to avoid truthy results on let defaultMethodWhiteList = Object.create(null);
// prototype properties. defaultMethodWhiteList['constructor'] = false;
const propertyWhiteList = Object.create(null); defaultMethodWhiteList['__defineGetter__'] = false;
// eslint-disable-next-line no-proto defaultMethodWhiteList['__defineSetter__'] = false;
propertyWhiteList['__proto__'] = false; defaultMethodWhiteList['__lookupGetter__'] = false;
extend(propertyWhiteList, runtimeOptions.allowedProtoProperties);
const methodWhiteList = Object.create(null); let defaultPropertyWhiteList = Object.create(null);
methodWhiteList['constructor'] = false; // eslint-disable-next-line no-proto
methodWhiteList['__defineGetter__'] = false; defaultPropertyWhiteList['__proto__'] = false;
methodWhiteList['__defineSetter__'] = false;
methodWhiteList['__lookupGetter__'] = false;
extend(methodWhiteList, runtimeOptions.allowedProtoMethods);
return { return {
properties: { properties: {
whitelist: propertyWhiteList, whitelist: createNewLookupObject(
defaultPropertyWhiteList,
runtimeOptions.allowedProtoProperties
),
defaultValue: runtimeOptions.allowProtoPropertiesByDefault, defaultValue: runtimeOptions.allowProtoPropertiesByDefault,
}, },
methods: { methods: {
whitelist: methodWhiteList, whitelist: createNewLookupObject(
defaultMethodWhiteList,
runtimeOptions.allowedProtoMethods
),
defaultValue: runtimeOptions.allowProtoMethodsByDefault, defaultValue: runtimeOptions.allowProtoMethodsByDefault,
}, },
}; };
+1 -1
View File
@@ -1,7 +1,7 @@
export default function (Handlebars) { export default function (Handlebars) {
let $Handlebars = globalThis.Handlebars; let $Handlebars = globalThis.Handlebars;
/* v8 ignore next */ /* istanbul ignore next */
Handlebars.noConflict = function () { Handlebars.noConflict = function () {
if (globalThis.Handlebars === Handlebars) { if (globalThis.Handlebars === Handlebars) {
globalThis.Handlebars = $Handlebars; globalThis.Handlebars = $Handlebars;
+18 -18
View File
@@ -47,7 +47,7 @@ export function checkRevision(compilerInfo) {
} }
export function template(templateSpec, env) { export function template(templateSpec, env) {
/* v8 ignore next */ /* istanbul ignore next */
if (!env) { if (!env) {
throw new Exception('No environment passed to template'); throw new Exception('No environment passed to template');
} }
@@ -71,10 +71,17 @@ export function template(templateSpec, env) {
} }
partial = env.VM.resolvePartial.call(this, partial, context, options); partial = env.VM.resolvePartial.call(this, partial, context, options);
options.hooks = this.hooks; let extendedOptions = Utils.extend({}, options, {
options.protoAccessControl = this.protoAccessControl; hooks: this.hooks,
protoAccessControl: this.protoAccessControl,
});
let result = env.VM.invokePartial.call(this, partial, context, options); let result = env.VM.invokePartial.call(
this,
partial,
context,
extendedOptions
);
if (result == null && env.compile) { if (result == null && env.compile) {
options.partials[options.name] = env.compile( options.partials[options.name] = env.compile(
@@ -82,7 +89,7 @@ export function template(templateSpec, env) {
templateSpec.compilerOptions, templateSpec.compilerOptions,
env env
); );
result = options.partials[options.name](context, options); result = options.partials[options.name](context, extendedOptions);
} }
if (result != null) { if (result != null) {
if (options.indent) { if (options.indent) {
@@ -117,10 +124,6 @@ export function template(templateSpec, env) {
return container.lookupProperty(obj, name); return container.lookupProperty(obj, name);
}, },
lookupProperty: function (parent, propertyName) { lookupProperty: function (parent, propertyName) {
if (Utils.isMap(parent)) {
return parent.get(propertyName);
}
let result = parent[propertyName]; let result = parent[propertyName];
if (result == null) { if (result == null) {
return result; return result;
@@ -248,9 +251,8 @@ export function template(templateSpec, env) {
function _setup(options) { function _setup(options) {
if (!options.partial) { if (!options.partial) {
let mergedHelpers = {}; let mergedHelpers = Utils.extend({}, env.helpers, options.helpers);
addHelpers(mergedHelpers, env.helpers, container); wrapHelpersToPassLookupProperty(mergedHelpers, container);
addHelpers(mergedHelpers, options.helpers, container);
container.helpers = mergedHelpers; container.helpers = mergedHelpers;
if (templateSpec.usePartial) { if (templateSpec.usePartial) {
@@ -411,10 +413,9 @@ function executeDecorators(fn, prog, container, depths, data, blockParams) {
return prog; return prog;
} }
function addHelpers(mergedHelpers, helpers, container) { function wrapHelpersToPassLookupProperty(mergedHelpers, container) {
if (!helpers) return; Object.keys(mergedHelpers).forEach((helperName) => {
Object.keys(helpers).forEach((helperName) => { let helper = mergedHelpers[helperName];
let helper = helpers[helperName];
mergedHelpers[helperName] = passLookupPropertyOption(helper, container); mergedHelpers[helperName] = passLookupPropertyOption(helper, container);
}); });
} }
@@ -422,7 +423,6 @@ function addHelpers(mergedHelpers, helpers, container) {
function passLookupPropertyOption(helper, container) { function passLookupPropertyOption(helper, container) {
const lookupProperty = container.lookupProperty; const lookupProperty = container.lookupProperty;
return wrapHelper(helper, (options) => { return wrapHelper(helper, (options) => {
options.lookupProperty = lookupProperty; return Utils.extend({ lookupProperty }, options);
return options;
}); });
} }
+5 -9
View File
@@ -35,18 +35,14 @@ export function isFunction(value) {
return typeof value === 'function'; return typeof value === 'function';
} }
function testTag(name) { /* istanbul ignore next */
const tag = '[object ' + name + ']'; export const isArray =
return function (value) { Array.isArray ||
function (value) {
return value && typeof value === 'object' return value && typeof value === 'object'
? toString.call(value) === tag ? toString.call(value) === '[object Array]'
: false; : false;
}; };
}
export const isArray = Array.isArray;
export const isMap = testTag('Map');
export const isSet = testTag('Set');
// Older IE versions do not directly support indexOf so we must implement our own, sadly. // Older IE versions do not directly support indexOf so we must implement our own, sadly.
export function indexOf(array, value) { export function indexOf(array, value) {
+2 -1
View File
@@ -1,5 +1,6 @@
// USAGE: // USAGE:
// var handlebars = require('handlebars'); // var handlebars = require('handlebars');
/* eslint-env node */
/* eslint-disable no-var */ /* eslint-disable no-var */
// var local = handlebars.create(); // var local = handlebars.create();
@@ -18,7 +19,7 @@ function extension(module, filename) {
var templateString = fs.readFileSync(filename, 'utf8'); var templateString = fs.readFileSync(filename, 'utf8');
module.exports = handlebars.compile(templateString); module.exports = handlebars.compile(templateString);
} }
/* v8 ignore next 4 */ /* istanbul ignore else */
if (typeof require !== 'undefined' && require.extensions) { if (typeof require !== 'undefined' && require.extensions) {
require.extensions['.handlebars'] = extension; require.extensions['.handlebars'] = extension;
require.extensions['.hbs'] = extension; require.extensions['.hbs'] = extension;
+10 -8
View File
@@ -1,7 +1,8 @@
/* eslint-env node */
/* eslint-disable no-console */ /* eslint-disable no-console */
import Async from 'neo-async'; import Async from 'neo-async';
import fs from 'fs'; import fs from 'fs';
import Handlebars from './handlebars'; import * as Handlebars from './handlebars';
import { basename } from 'path'; import { basename } from 'path';
import { SourceMapConsumer, SourceNode } from 'source-map'; import { SourceMapConsumer, SourceNode } from 'source-map';
@@ -94,7 +95,7 @@ function loadFiles(opts, callback) {
opts.hasDirectory = true; opts.hasDirectory = true;
fs.readdir(path, function (err, children) { fs.readdir(path, function (err, children) {
/* v8 ignore next -- Race condition that being too lazy to test */ /* istanbul ignore next : Race condition that being too lazy to test */
if (err) { if (err) {
return callback(err); return callback(err);
} }
@@ -113,7 +114,7 @@ function loadFiles(opts, callback) {
}); });
} else { } else {
fs.readFile(path, 'utf8', function (err, data) { fs.readFile(path, 'utf8', function (err, data) {
/* v8 ignore next -- Race condition that being too lazy to test */ /* istanbul ignore next : Race condition that being too lazy to test */
if (err) { if (err) {
return callback(err); return callback(err);
} }
@@ -152,7 +153,7 @@ function loadFiles(opts, callback) {
); );
} }
module.exports.cli = async function (opts) { module.exports.cli = function (opts) {
if (opts.version) { if (opts.version) {
console.log(Handlebars.VERSION); console.log(Handlebars.VERSION);
return; return;
@@ -208,6 +209,8 @@ module.exports.cli = async function (opts) {
); );
} else if (opts.commonjs) { } else if (opts.commonjs) {
output.add('var Handlebars = require("' + opts.commonjs + '");'); output.add('var Handlebars = require("' + opts.commonjs + '");');
} else if (opts.esm) {
output.add(`import handlebars from "${opts.esm}";`);
} else { } else {
output.add('(function() {\n'); output.add('(function() {\n');
} }
@@ -221,7 +224,7 @@ module.exports.cli = async function (opts) {
output.add('{};\n'); output.add('{};\n');
} }
for (const template of opts.templates) { opts.templates.forEach(function (template) {
let options = { let options = {
knownHelpers: known, knownHelpers: known,
knownHelpersOnly: opts.o, knownHelpersOnly: opts.o,
@@ -238,12 +241,11 @@ module.exports.cli = async function (opts) {
// If we are generating a source map, we have to reconstruct the SourceNode object // If we are generating a source map, we have to reconstruct the SourceNode object
if (opts.map) { if (opts.map) {
let consumer = await new SourceMapConsumer(precompiled.map); let consumer = new SourceMapConsumer(precompiled.map);
precompiled = SourceNode.fromStringWithSourceMap( precompiled = SourceNode.fromStringWithSourceMap(
precompiled.code, precompiled.code,
consumer consumer
); );
consumer.destroy();
} }
if (opts.simple) { if (opts.simple) {
@@ -265,7 +267,7 @@ module.exports.cli = async function (opts) {
');\n', ');\n',
]); ]);
} }
} });
// Output the content // Output the content
if (!opts.simple) { if (!opts.simple) {
+9
View File
@@ -0,0 +1,9 @@
module.exports = {
'check-coverage': true,
branches: 100,
lines: 100,
functions: 100,
statements: 100,
exclude: ['**/spec/**'],
reporter: 'html',
};
+20352 -17314
View File
File diff suppressed because it is too large Load Diff
+105 -95
View File
@@ -2,21 +2,107 @@
"name": "handlebars", "name": "handlebars",
"version": "5.0.0-alpha.1", "version": "5.0.0-alpha.1",
"description": "Handlebars provides the power necessary to let you build semantic templates effectively with no frustration", "description": "Handlebars provides the power necessary to let you build semantic templates effectively with no frustration",
"homepage": "https://handlebarsjs.com/",
"keywords": [ "keywords": [
"handlebars", "handlebars",
"html",
"mustache", "mustache",
"template" "template",
"html"
], ],
"homepage": "https://handlebarsjs.com/",
"license": "MIT",
"author": "Yehuda Katz",
"repository": { "repository": {
"type": "git", "type": "git",
"url": "https://github.com/handlebars-lang/handlebars.js.git" "url": "https://github.com/handlebars-lang/handlebars.js.git"
}, },
"author": "Yehuda Katz",
"license": "MIT",
"readmeFilename": "README.markdown",
"engines": {
"node": ">=12"
},
"dependencies": {
"@handlebars/parser": "^2.1.0",
"neo-async": "^2.6.2",
"source-map": "^0.6.1",
"yargs": "^16.2.0"
},
"peerDependencies": {
"uglify-js": "^3.1.4"
},
"devDependencies": {
"@definitelytyped/dtslint": "^0.0.100",
"@playwright/test": "^1.36.1",
"aws-sdk": "^2.1.49",
"babel-loader": "^5.0.0",
"babel-runtime": "^5.1.10",
"benchmark": "~1.0",
"chai": "^4.2.0",
"chai-diff": "^1.0.1",
"concurrently": "^5.0.0",
"dirty-chai": "^2.0.1",
"dustjs-linkedin": "^2.0.2",
"eslint": "^8.25.0",
"eslint-config-prettier": "^8.9.0",
"eslint-plugin-compat": "4.0",
"fs-extra": "^8.1.0",
"grunt": "^1.0.4",
"grunt-babel": "^5.0.0",
"grunt-cli": "^1",
"grunt-contrib-clean": "^1",
"grunt-contrib-concat": "^1",
"grunt-contrib-connect": "^3.0.0",
"grunt-contrib-copy": "^1",
"grunt-contrib-requirejs": "^1",
"grunt-contrib-uglify": "^1",
"grunt-contrib-watch": "^1.1.0",
"grunt-shell": "^4.0.0",
"grunt-webpack": "^1.0.8",
"husky": "^3.1.0",
"lint-staged": "^9.5.0",
"mocha": "^5",
"mock-stdin": "^0.3.0",
"mustache": "^2.1.3",
"nyc": "^14.1.1",
"prettier": "^3.0.0",
"semver": "^5.0.1",
"sinon": "^7.5.0",
"typescript": "^3.4.3",
"uglify-js": "^3.1.4",
"underscore": "^1.5.1",
"webpack": "^1.12.6",
"webpack-dev-server": "^1.12.1"
},
"main": "lib/index.js",
"types": "types/index.d.ts",
"browser": "./dist/cjs/handlebars.js",
"bin": { "bin": {
"handlebars": "bin/handlebars.mjs" "handlebars": "bin/handlebars.js"
},
"scripts": {
"build": "grunt build",
"release": "grunt release",
"publish:aws": "npm run test:tasks && grunt && grunt publish-to-aws",
"format": "prettier --write '**/*.{js,css,json,md}' && eslint --fix .",
"lint": "npm run lint:eslint && npm run lint:prettier && npm run lint:types",
"lint:eslint": "eslint --max-warnings 0 .",
"lint:prettier": "prettier --check '**/*.js'",
"lint:types": "dtslint types",
"test": "npm run test:mocha",
"test:mocha": "grunt build && grunt test",
"test:tasks": "mocha tasks/tests/",
"test:browser": "playwright test --config tests/browser/playwright.config.js",
"test:integration": "grunt integration-tests",
"test:serve": "grunt connect:server:keepalive",
"--- combined tasks ---": "",
"check-before-pull-request": "concurrently --kill-others-on-fail npm:lint npm:test"
},
"jspm": {
"main": "handlebars",
"directories": {
"lib": "dist/cjs"
},
"buildConfig": {
"minify": true
}
}, },
"files": [ "files": [
"bin", "bin",
@@ -28,85 +114,6 @@
"types/*.d.ts", "types/*.d.ts",
"runtime.d.ts" "runtime.d.ts"
], ],
"main": "lib/index.js",
"browser": "./dist/cjs/handlebars.js",
"types": "types/index.d.ts",
"scripts": {
"clean": "node -e \"require('fs').rmSync('dist',{recursive:true,force:true});require('fs').rmSync('tmp',{recursive:true,force:true})\"",
"build:cjs": "swc lib --out-dir dist/cjs --strip-leading-paths --ignore \"**/index.js\"",
"build:bundles": "rspack build",
"build": "npm run clean && npm run build:cjs && npm run build:bundles",
"release": "npm run build",
"publish:aws": "npm run build && npm run test:tasks && node tasks/publish-to-aws.js",
"format": "oxfmt --write . && oxlint --fix .",
"lint": "npm run lint:oxlint && npm run lint:format && npm run lint:types && npm run lint:compat",
"lint:oxlint": "oxlint --max-warnings 0 .",
"lint:format": "oxfmt --check .",
"lint:types": "tstyche",
"lint:compat": "eslint",
"test": "npm run build && vitest run --project node --project tasks --project rspack --coverage",
"test:browser": "vitest run --project browser",
"test:unit": "vitest run --project node",
"test:tasks": "vitest run --project tasks",
"test:publish": "npm run build && vitest run --project publish",
"test:browser-smoke": "playwright test --config tests/browser/playwright.config.js",
"test:serve": "npx serve -l 9999 .",
"test:integration": "npm run build && ./tests/integration/run-integration-tests.sh",
"bench": "node tests/bench/perf.mjs",
"bench:compare": "node tests/bench/compare.mjs",
"bench:size": "node tests/bench/size.mjs",
"--- combined tasks ---": "",
"check-before-pull-request": "concurrently --kill-others-on-fail npm:lint npm:test"
},
"dependencies": {
"@handlebars/parser": "^2.1.0",
"neo-async": "^2.6.2",
"source-map": "^0.7.6",
"yargs": "^18.0.0"
},
"devDependencies": {
"@aws-sdk/client-s3": "^3.1011.0",
"@playwright/test": "^1.58.2",
"@rspack/cli": "^1.7.8",
"@rspack/core": "^1.7.8",
"@swc/cli": "^0.8.0",
"@swc/core": "^1.15.18",
"@vitest/browser": "^4.0.18",
"@vitest/browser-playwright": "^4.0.18",
"@vitest/coverage-v8": "^4.0.18",
"cli-testlab": "^6.0.0",
"concurrently": "^5.0.0",
"eslint": "^10.0.3",
"eslint-plugin-compat": "^7.0.1",
"fs-extra": "^8.1.0",
"husky": "^3.1.0",
"lint-staged": "^16.3.2",
"mock-stdin": "^0.3.0",
"oxfmt": "^0.36.0",
"oxlint": "^1.51.0",
"semver": "^5.0.1",
"tinybench": "^6.0.0",
"tstyche": "^6.2.0",
"typescript": "^5.9.3",
"uglify-js": "^3.19.3",
"vitest": "^4.0.18"
},
"peerDependencies": {
"uglify-js": "^3.19.3"
},
"husky": {
"hooks": {
"pre-commit": "lint-staged"
}
},
"lint-staged": {
"*.{js,css,json,md}": [
"oxfmt --write"
],
"*.js": [
"oxlint --fix"
]
},
"browserslist": [ "browserslist": [
"last 2 versions", "last 2 versions",
"Firefox ESR", "Firefox ESR",
@@ -114,16 +121,19 @@
"not IE 11", "not IE 11",
"maintained node versions" "maintained node versions"
], ],
"engines": { "husky": {
"node": ">=20" "hooks": {
}, "pre-commit": "lint-staged"
"jspm": {
"main": "handlebars",
"directories": {
"lib": "dist/cjs"
},
"buildConfig": {
"minify": true
} }
},
"lint-staged": {
"*.{js,css,json,md}": [
"prettier --write",
"git add"
],
"*.js": [
"eslint --fix",
"git add"
]
} }
} }
+6
View File
@@ -0,0 +1,6 @@
module.exports = {
tabWidth: 2,
semi: true,
singleQuote: true,
trailingComma: 'es5',
};
+1
View File
@@ -356,6 +356,7 @@ Breaking changes:
Compatibility notes: Compatibility notes:
- Compiler revision increased - 06b7224 - Compiler revision increased - 06b7224
- This means that template compiled with versions prior to 4.3.0 will not work with runtimes >= 4.3.0 - This means that template compiled with versions prior to 4.3.0 will not work with runtimes >= 4.3.0
The increase was done because the "helperMissing" and "blockHelperMissing" are now moved from the helpers The increase was done because the "helperMissing" and "blockHelperMissing" are now moved from the helpers
to the internal "container.hooks" object, so old templates will not be able to call them anymore. We suggest to the internal "container.hooks" object, so old templates will not be able to call them anymore. We suggest
-88
View File
@@ -1,88 +0,0 @@
const { rspack } = require('@rspack/core');
const path = require('path');
const fs = require('fs');
const pkg = require('./package.json');
const license = fs.readFileSync(path.resolve(__dirname, 'LICENSE'), 'utf8');
const banner = `/*!
@license magnet:?xt=urn:btih:d3d9a9a6595521f9666a5e94cc830dab83b65699&dn=expat.txt Expat
${pkg.name} v${pkg.version}
${license}
*/`;
function createConfig(entry, filename, minimize) {
const plugins = [];
if (!minimize) {
// For non-minified builds, use BannerPlugin to add the license header
plugins.push(new rspack.BannerPlugin({ banner, raw: true }));
}
return {
mode: minimize ? 'production' : 'none',
context: __dirname,
entry,
output: {
path: path.resolve(__dirname, 'dist'),
filename,
library: {
name: 'Handlebars',
type: 'umd',
export: 'default',
},
globalObject: 'this',
clean: false,
},
module: {
rules: [
{
test: /\.js$/,
exclude: /node_modules/,
use: {
loader: 'builtin:swc-loader',
options: {
jsc: {
parser: { syntax: 'ecmascript' },
},
},
},
},
],
},
optimization: {
minimize,
minimizer: minimize
? [
new rspack.SwcJsMinimizerRspackPlugin({
extractComments: false,
minimizerOptions: {
compress: { passes: 2 },
mangle: true,
format: {
comments: false,
// Prepend the license banner in the minified output
preamble: banner,
},
},
}),
]
: [],
},
plugins,
target: ['web', 'browserslist'],
devtool: false,
};
}
module.exports = [
createConfig('./lib/handlebars.js', 'handlebars.js', false),
createConfig('./lib/handlebars.runtime.js', 'handlebars.runtime.js', false),
createConfig('./lib/handlebars.js', 'handlebars.min.js', true),
createConfig(
'./lib/handlebars.runtime.js',
'handlebars.runtime.min.js',
true
),
];
+4 -2
View File
@@ -1,3 +1,5 @@
import Handlebars = require('handlebars'); import Handlebars = require('handlebars')
declare module 'handlebars/runtime' {} declare module "handlebars/runtime" {
}
+37
View File
@@ -0,0 +1,37 @@
module.exports = {
globals: {
CompilerContext: true,
Handlebars: true,
handlebarsEnv: true,
shouldCompileTo: true,
shouldCompileToWithPartials: true,
shouldThrow: true,
expectTemplate: true,
compileWithPartials: true,
suite: true,
equal: true,
equals: true,
test: true,
testBoth: true,
raises: true,
deepEqual: true,
start: true,
stop: true,
ok: true,
sinon: true,
strictEqual: true,
define: true,
expect: true,
chai: true,
},
env: {
mocha: true,
},
rules: {
// Disabling for tests, for now.
'no-path-concat': 'off',
'no-var': 'off',
'dot-notation': 'off',
},
};
+68 -64
View File
@@ -7,100 +7,102 @@ describe('ast', function () {
describe('BlockStatement', function () { describe('BlockStatement', function () {
it('should throw on mustache mismatch', function () { it('should throw on mustache mismatch', function () {
expect(function () { shouldThrow(
handlebarsEnv.parse('\n {{#foo}}{{/bar}}'); function () {
}).toThrow("foo doesn't match bar - 2:5"); handlebarsEnv.parse('\n {{#foo}}{{/bar}}');
},
Handlebars.Exception,
"foo doesn't match bar - 2:5"
);
}); });
}); });
describe('helpers', function () { describe('helpers', function () {
describe('#helperExpression', function () { describe('#helperExpression', function () {
it('should handle mustache statements', function () { it('should handle mustache statements', function () {
expect( equals(
AST.helpers.helperExpression({ AST.helpers.helperExpression({
type: 'MustacheStatement', type: 'MustacheStatement',
params: [], params: [],
hash: undefined, hash: undefined,
}) }),
).toBe(false); false
expect( );
equals(
AST.helpers.helperExpression({ AST.helpers.helperExpression({
type: 'MustacheStatement', type: 'MustacheStatement',
params: [1], params: [1],
hash: undefined, hash: undefined,
}) }),
).toBe(true); true
expect( );
equals(
AST.helpers.helperExpression({ AST.helpers.helperExpression({
type: 'MustacheStatement', type: 'MustacheStatement',
params: [], params: [],
hash: {}, hash: {},
}) }),
).toBe(true);
});
it('should handle block statements', function () {
expect(
AST.helpers.helperExpression({
type: 'BlockStatement',
params: [],
hash: undefined,
})
).toBe(false);
expect(
AST.helpers.helperExpression({
type: 'BlockStatement',
params: [1],
hash: undefined,
})
).toBe(true);
expect(
AST.helpers.helperExpression({
type: 'BlockStatement',
params: [],
hash: {},
})
).toBe(true);
});
it('should handle subexpressions', function () {
expect(AST.helpers.helperExpression({ type: 'SubExpression' })).toBe(
true true
); );
}); });
it('should handle block statements', function () {
equals(
AST.helpers.helperExpression({
type: 'BlockStatement',
params: [],
hash: undefined,
}),
false
);
equals(
AST.helpers.helperExpression({
type: 'BlockStatement',
params: [1],
hash: undefined,
}),
true
);
equals(
AST.helpers.helperExpression({
type: 'BlockStatement',
params: [],
hash: {},
}),
true
);
});
it('should handle subexpressions', function () {
equals(AST.helpers.helperExpression({ type: 'SubExpression' }), true);
});
it('should work with non-helper nodes', function () { it('should work with non-helper nodes', function () {
expect(AST.helpers.helperExpression({ type: 'Program' })).toBe(false); equals(AST.helpers.helperExpression({ type: 'Program' }), false);
expect(AST.helpers.helperExpression({ type: 'PartialStatement' })).toBe( equals(
AST.helpers.helperExpression({ type: 'PartialStatement' }),
false false
); );
expect(AST.helpers.helperExpression({ type: 'ContentStatement' })).toBe( equals(
AST.helpers.helperExpression({ type: 'ContentStatement' }),
false false
); );
expect(AST.helpers.helperExpression({ type: 'CommentStatement' })).toBe( equals(
AST.helpers.helperExpression({ type: 'CommentStatement' }),
false false
); );
expect(AST.helpers.helperExpression({ type: 'PathExpression' })).toBe( equals(AST.helpers.helperExpression({ type: 'PathExpression' }), false);
false
);
expect(AST.helpers.helperExpression({ type: 'StringLiteral' })).toBe( equals(AST.helpers.helperExpression({ type: 'StringLiteral' }), false);
false equals(AST.helpers.helperExpression({ type: 'NumberLiteral' }), false);
); equals(AST.helpers.helperExpression({ type: 'BooleanLiteral' }), false);
expect(AST.helpers.helperExpression({ type: 'NumberLiteral' })).toBe( equals(
false AST.helpers.helperExpression({ type: 'UndefinedLiteral' }),
);
expect(AST.helpers.helperExpression({ type: 'BooleanLiteral' })).toBe(
false
);
expect(AST.helpers.helperExpression({ type: 'UndefinedLiteral' })).toBe(
false
);
expect(AST.helpers.helperExpression({ type: 'NullLiteral' })).toBe(
false false
); );
equals(AST.helpers.helperExpression({ type: 'NullLiteral' }), false);
expect(AST.helpers.helperExpression({ type: 'Hash' })).toBe(false); equals(AST.helpers.helperExpression({ type: 'Hash' }), false);
expect(AST.helpers.helperExpression({ type: 'HashPair' })).toBe(false); equals(AST.helpers.helperExpression({ type: 'HashPair' }), false);
}); });
}); });
}); });
@@ -109,12 +111,13 @@ describe('ast', function () {
var ast, body; var ast, body;
function testColumns(node, firstLine, lastLine, firstColumn, lastColumn) { function testColumns(node, firstLine, lastLine, firstColumn, lastColumn) {
expect(node.loc.start.line).toBe(firstLine); equals(node.loc.start.line, firstLine);
expect(node.loc.start.column).toBe(firstColumn); equals(node.loc.start.column, firstColumn);
expect(node.loc.end.line).toBe(lastLine); equals(node.loc.end.line, lastLine);
expect(node.loc.end.column).toBe(lastColumn); equals(node.loc.end.column, lastColumn);
} }
/* eslint-disable no-multi-spaces */
ast = Handlebars.parse( ast = Handlebars.parse(
'line 1 {{line1Token}}\n' + // 1 'line 1 {{line1Token}}\n' + // 1
' line 2 {{line2token}}\n' + // 2 ' line 2 {{line2token}}\n' + // 2
@@ -128,6 +131,7 @@ describe('ast', function () {
'{{else}}\n' + // 10 '{{else}}\n' + // 10
'{{/open}}' '{{/open}}'
); // 11 ); // 11
/* eslint-enable no-multi-spaces */
body = ast.body; body = ast.body;
it('gets ContentNode line numbers', function () { it('gets ContentNode line numbers', function () {
+6 -7
View File
@@ -1,3 +1,9 @@
global.handlebarsEnv = null;
beforeEach(function () {
global.handlebarsEnv = Handlebars.create();
});
describe('basic context', function () { describe('basic context', function () {
it('most basic', function () { it('most basic', function () {
expectTemplate('{{foo}}').withInput({ foo: 'foo' }).toCompileTo('foo'); expectTemplate('{{foo}}').withInput({ foo: 'foo' }).toCompileTo('foo');
@@ -381,13 +387,6 @@ describe('basic context', function () {
.toCompileTo('Goodbye beautiful world!'); .toCompileTo('Goodbye beautiful world!');
}); });
it('nested paths with Map', function () {
expectTemplate('Goodbye {{alan/expression}} world!')
.withInput({ alan: new Map([['expression', 'beautiful']]) })
.withMessage('Nested paths access nested objects')
.toCompileTo('Goodbye beautiful world!');
});
it('nested paths with empty string value', function () { it('nested paths with empty string value', function () {
expectTemplate('Goodbye {{alan/expression}} world!') expectTemplate('Goodbye {{alan/expression}} world!')
.withInput({ alan: { expression: '' } }) .withInput({ alan: { expression: '' } })
+26 -22
View File
@@ -381,26 +381,26 @@ describe('blocks', function () {
var run; var run;
expectTemplate('{{*decorator "success"}}') expectTemplate('{{*decorator "success"}}')
.withDecorator('decorator', function (fn, props, container, options) { .withDecorator('decorator', function (fn, props, container, options) {
expect(options.args[0]).toBe('success'); equals(options.args[0], 'success');
run = true; run = true;
return fn; return fn;
}) })
.withInput({ foo: 'success' }) .withInput({ foo: 'success' })
.toCompileTo(''); .toCompileTo('');
expect(run).toBe(true); equals(run, true);
}); });
it('should fail when accessing variables from root', function () { it('should fail when accessing variables from root', function () {
var run; var run;
expectTemplate('{{*decorator foo}}') expectTemplate('{{*decorator foo}}')
.withDecorator('decorator', function (fn, props, container, options) { .withDecorator('decorator', function (fn, props, container, options) {
expect(options.args[0]).toBeUndefined(); equals(options.args[0], undefined);
run = true; run = true;
return fn; return fn;
}) })
.withInput({ foo: 'fail' }) .withInput({ foo: 'fail' })
.toCompileTo(''); .toCompileTo('');
expect(run).toBe(true); equals(run, true);
}); });
describe('registration', function () { describe('registration', function () {
@@ -411,9 +411,9 @@ describe('blocks', function () {
return 'fail'; return 'fail';
}); });
expect(handlebarsEnv.decorators.foo).toBeTruthy(); equals(!!handlebarsEnv.decorators.foo, true);
handlebarsEnv.unregisterDecorator('foo'); handlebarsEnv.unregisterDecorator('foo');
expect(handlebarsEnv.decorators.foo).toBeUndefined(); equals(handlebarsEnv.decorators.foo, undefined);
}); });
it('allows multiple globals', function () { it('allows multiple globals', function () {
@@ -424,28 +424,32 @@ describe('blocks', function () {
bar: function () {}, bar: function () {},
}); });
expect(handlebarsEnv.decorators.foo).toBeTruthy(); equals(!!handlebarsEnv.decorators.foo, true);
expect(handlebarsEnv.decorators.bar).toBeTruthy(); equals(!!handlebarsEnv.decorators.bar, true);
handlebarsEnv.unregisterDecorator('foo'); handlebarsEnv.unregisterDecorator('foo');
handlebarsEnv.unregisterDecorator('bar'); handlebarsEnv.unregisterDecorator('bar');
expect(handlebarsEnv.decorators.foo).toBeUndefined(); equals(handlebarsEnv.decorators.foo, undefined);
expect(handlebarsEnv.decorators.bar).toBeUndefined(); equals(handlebarsEnv.decorators.bar, undefined);
}); });
it('fails with multiple and args', function () { it('fails with multiple and args', function () {
expect(function () { shouldThrow(
handlebarsEnv.registerDecorator( function () {
{ handlebarsEnv.registerDecorator(
world: function () { {
return 'world!'; world: function () {
return 'world!';
},
testHelper: function () {
return 'found it!';
},
}, },
testHelper: function () { {}
return 'found it!'; );
}, },
}, Error,
{} 'Arg not supported with multiple decorators'
); );
}).toThrow('Arg not supported with multiple decorators');
}); });
}); });
}); });
+29 -69
View File
@@ -257,13 +257,17 @@ describe('builtin helpers', function () {
// Object property iteration order is undefined according to ECMA spec, // Object property iteration order is undefined according to ECMA spec,
// so we need to check both possible orders // so we need to check both possible orders
// @see http://stackoverflow.com/questions/280713/elements-order-in-a-for-in-loop // @see http://stackoverflow.com/questions/280713/elements-order-in-a-for-in-loop
var actual = CompilerContext.compile(string)(hash); var actual = compileWithPartials(string, hash);
var expected1 = var expected1 =
'&lt;b&gt;#1&lt;/b&gt;. goodbye! 2. GOODBYE! cruel world!'; '&lt;b&gt;#1&lt;/b&gt;. goodbye! 2. GOODBYE! cruel world!';
var expected2 = var expected2 =
'2. GOODBYE! &lt;b&gt;#1&lt;/b&gt;. goodbye! cruel world!'; '2. GOODBYE! &lt;b&gt;#1&lt;/b&gt;. goodbye! cruel world!';
expect([expected1, expected2]).toContain(actual); equals(
actual === expected1 || actual === expected2,
true,
'each with object argument iterates over the contents when not empty'
);
expectTemplate(string) expectTemplate(string)
.withInput({ .withInput({
@@ -504,50 +508,6 @@ describe('builtin helpers', function () {
); );
}); });
it('each on Map', function () {
var map = new Map([
[1, 'one'],
[2, 'two'],
[3, 'three'],
]);
expectTemplate('{{#each map}}{{@key}}(i{{@index}}) {{.}} {{/each}}')
.withInput({ map: map })
.toCompileTo('1(i0) one 2(i1) two 3(i2) three ');
expectTemplate('{{#each map}}{{#if @first}}{{.}}{{/if}}{{/each}}')
.withInput({ map: map })
.toCompileTo('one');
expectTemplate('{{#each map}}{{#if @last}}{{.}}{{/if}}{{/each}}')
.withInput({ map: map })
.toCompileTo('three');
expectTemplate('{{#each map}}{{.}}{{/each}}not-in-each')
.withInput({ map: new Map() })
.toCompileTo('not-in-each');
});
it('each on Set', function () {
var set = new Set([1, 2, 3]);
expectTemplate('{{#each set}}{{@key}}(i{{@index}}) {{.}} {{/each}}')
.withInput({ set: set })
.toCompileTo('0(i0) 1 1(i1) 2 2(i2) 3 ');
expectTemplate('{{#each set}}{{#if @first}}{{.}}{{/if}}{{/each}}')
.withInput({ set: set })
.toCompileTo('1');
expectTemplate('{{#each set}}{{#if @last}}{{.}}{{/if}}{{/each}}')
.withInput({ set: set })
.toCompileTo('3');
expectTemplate('{{#each set}}{{.}}{{/each}}not-in-each')
.withInput({ set: new Set() })
.toCompileTo('not-in-each');
});
if (global.Symbol && global.Symbol.iterator) { if (global.Symbol && global.Symbol.iterator) {
it('each on iterable', function () { it('each on iterable', function () {
function Iterator(arr) { function Iterator(arr) {
@@ -626,8 +586,8 @@ describe('builtin helpers', function () {
.withInput({ blah: 'whee' }) .withInput({ blah: 'whee' })
.withMessage('log should not display') .withMessage('log should not display')
.toCompileTo(''); .toCompileTo('');
expect(levelArg).toBe(1); equals(1, levelArg, 'should call log with 1');
expect(logArg).toBe('whee'); equals('whee', logArg, "should call log with 'whee'");
}); });
it('should call logger at data level', function () { it('should call logger at data level', function () {
@@ -642,21 +602,21 @@ describe('builtin helpers', function () {
.withRuntimeOptions({ data: { level: '03' } }) .withRuntimeOptions({ data: { level: '03' } })
.withCompileOptions({ data: true }) .withCompileOptions({ data: true })
.toCompileTo(''); .toCompileTo('');
expect(levelArg).toBe('03'); equals('03', levelArg);
expect(logArg).toBe('whee'); equals('whee', logArg);
}); });
it('should output to info', function () { it('should output to info', function () {
var called; var called;
console.info = function (info) { console.info = function (info) {
expect(info).toBe('whee'); equals('whee', info);
called = true; called = true;
console.info = $info; console.info = $info;
console.log = $log; console.log = $log;
}; };
console.log = function (log) { console.log = function (log) {
expect(log).toBe('whee'); equals('whee', log);
called = true; called = true;
console.info = $info; console.info = $info;
console.log = $log; console.log = $log;
@@ -665,14 +625,14 @@ describe('builtin helpers', function () {
expectTemplate('{{log blah}}') expectTemplate('{{log blah}}')
.withInput({ blah: 'whee' }) .withInput({ blah: 'whee' })
.toCompileTo(''); .toCompileTo('');
expect(called).toBe(true); equals(true, called);
}); });
it('should log at data level', function () { it('should log at data level', function () {
var called; var called;
console.error = function (log) { console.error = function (log) {
expect(log).toBe('whee'); equals('whee', log);
called = true; called = true;
console.error = $error; console.error = $error;
}; };
@@ -682,7 +642,7 @@ describe('builtin helpers', function () {
.withRuntimeOptions({ data: { level: '03' } }) .withRuntimeOptions({ data: { level: '03' } })
.withCompileOptions({ data: true }) .withCompileOptions({ data: true })
.toCompileTo(''); .toCompileTo('');
expect(called).toBe(true); equals(true, called);
}); });
it('should handle missing logger', function () { it('should handle missing logger', function () {
@@ -690,7 +650,7 @@ describe('builtin helpers', function () {
console.error = undefined; console.error = undefined;
console.log = function (log) { console.log = function (log) {
expect(log).toBe('whee'); equals('whee', log);
called = true; called = true;
console.log = $log; console.log = $log;
}; };
@@ -700,14 +660,14 @@ describe('builtin helpers', function () {
.withRuntimeOptions({ data: { level: '03' } }) .withRuntimeOptions({ data: { level: '03' } })
.withCompileOptions({ data: true }) .withCompileOptions({ data: true })
.toCompileTo(''); .toCompileTo('');
expect(called).toBe(true); equals(true, called);
}); });
it('should handle string log levels', function () { it('should handle string log levels', function () {
var called; var called;
console.error = function (log) { console.error = function (log) {
expect(log).toBe('whee'); equals('whee', log);
called = true; called = true;
}; };
@@ -716,7 +676,7 @@ describe('builtin helpers', function () {
.withRuntimeOptions({ data: { level: 'error' } }) .withRuntimeOptions({ data: { level: 'error' } })
.withCompileOptions({ data: true }) .withCompileOptions({ data: true })
.toCompileTo(''); .toCompileTo('');
expect(called).toBe(true); equals(true, called);
called = false; called = false;
@@ -725,21 +685,21 @@ describe('builtin helpers', function () {
.withRuntimeOptions({ data: { level: 'ERROR' } }) .withRuntimeOptions({ data: { level: 'ERROR' } })
.withCompileOptions({ data: true }) .withCompileOptions({ data: true })
.toCompileTo(''); .toCompileTo('');
expect(called).toBe(true); equals(true, called);
}); });
it('should handle hash log levels', function () { it('should handle hash log levels', function () {
var called; var called;
console.error = function (log) { console.error = function (log) {
expect(log).toBe('whee'); equals('whee', log);
called = true; called = true;
}; };
expectTemplate('{{log blah level="error"}}') expectTemplate('{{log blah level="error"}}')
.withInput({ blah: 'whee' }) .withInput({ blah: 'whee' })
.toCompileTo(''); .toCompileTo('');
expect(called).toBe(true); equals(true, called);
}); });
it('should handle hash log levels', function () { it('should handle hash log levels', function () {
@@ -757,16 +717,16 @@ describe('builtin helpers', function () {
expectTemplate('{{log blah level="debug"}}') expectTemplate('{{log blah level="debug"}}')
.withInput({ blah: 'whee' }) .withInput({ blah: 'whee' })
.toCompileTo(''); .toCompileTo('');
expect(called).toBe(false); equals(false, called);
}); });
it('should pass multiple log arguments', function () { it('should pass multiple log arguments', function () {
var called; var called;
console.info = console.log = function (log1, log2, log3) { console.info = console.log = function (log1, log2, log3) {
expect(log1).toBe('whee'); equals('whee', log1);
expect(log2).toBe('foo'); equals('foo', log2);
expect(log3).toBe(1); equals(1, log3);
called = true; called = true;
console.log = $log; console.log = $log;
}; };
@@ -774,20 +734,20 @@ describe('builtin helpers', function () {
expectTemplate('{{log blah "foo" 1}}') expectTemplate('{{log blah "foo" 1}}')
.withInput({ blah: 'whee' }) .withInput({ blah: 'whee' })
.toCompileTo(''); .toCompileTo('');
expect(called).toBe(true); equals(true, called);
}); });
it('should pass zero log arguments', function () { it('should pass zero log arguments', function () {
var called; var called;
console.info = console.log = function () { console.info = console.log = function () {
expect(arguments.length).toBe(0); expect(arguments.length).to.equal(0);
called = true; called = true;
console.log = $log; console.log = $log;
}; };
expectTemplate('{{log}}').withInput({ blah: 'whee' }).toCompileTo(''); expectTemplate('{{log}}').withInput({ blah: 'whee' }).toCompileTo('');
expect(called).toBe(true); expect(called).to.be.true();
}); });
/* eslint-enable no-console */ /* eslint-enable no-console */
}); });
+110 -65
View File
@@ -10,56 +10,69 @@ describe('compiler', function () {
} }
it('should treat as equal', function () { it('should treat as equal', function () {
expect(compile('foo').equals(compile('foo'))).toBe(true); equal(compile('foo').equals(compile('foo')), true);
expect(compile('{{foo}}').equals(compile('{{foo}}'))).toBe(true); equal(compile('{{foo}}').equals(compile('{{foo}}')), true);
expect(compile('{{foo.bar}}').equals(compile('{{foo.bar}}'))).toBe(true); equal(compile('{{foo.bar}}').equals(compile('{{foo.bar}}')), true);
expect( equal(
compile('{{foo.bar baz "foo" true false bat=1}}').equals( compile('{{foo.bar baz "foo" true false bat=1}}').equals(
compile('{{foo.bar baz "foo" true false bat=1}}') compile('{{foo.bar baz "foo" true false bat=1}}')
) ),
).toBe(true); true
expect( );
equal(
compile('{{foo.bar (baz bat=1)}}').equals( compile('{{foo.bar (baz bat=1)}}').equals(
compile('{{foo.bar (baz bat=1)}}') compile('{{foo.bar (baz bat=1)}}')
) ),
).toBe(true); true
expect( );
compile('{{#foo}} {{/foo}}').equals(compile('{{#foo}} {{/foo}}')) equal(
).toBe(true); compile('{{#foo}} {{/foo}}').equals(compile('{{#foo}} {{/foo}}')),
true
);
}); });
it('should treat as not equal', function () { it('should treat as not equal', function () {
expect(compile('foo').equals(compile('bar'))).toBe(false); equal(compile('foo').equals(compile('bar')), false);
expect(compile('{{foo}}').equals(compile('{{bar}}'))).toBe(false); equal(compile('{{foo}}').equals(compile('{{bar}}')), false);
expect(compile('{{foo.bar}}').equals(compile('{{bar.bar}}'))).toBe(false); equal(compile('{{foo.bar}}').equals(compile('{{bar.bar}}')), false);
expect( equal(
compile('{{foo.bar baz bat=1}}').equals( compile('{{foo.bar baz bat=1}}').equals(
compile('{{foo.bar bar bat=1}}') compile('{{foo.bar bar bat=1}}')
) ),
).toBe(false); false
expect( );
equal(
compile('{{foo.bar (baz bat=1)}}').equals( compile('{{foo.bar (baz bat=1)}}').equals(
compile('{{foo.bar (bar bat=1)}}') compile('{{foo.bar (bar bat=1)}}')
) ),
).toBe(false); false
expect( );
compile('{{#foo}} {{/foo}}').equals(compile('{{#bar}} {{/bar}}')) equal(
).toBe(false); compile('{{#foo}} {{/foo}}').equals(compile('{{#bar}} {{/bar}}')),
expect( false
compile('{{#foo}} {{/foo}}').equals(compile('{{#foo}} {{foo}}{{/foo}}')) );
).toBe(false); equal(
compile('{{#foo}} {{/foo}}').equals(
compile('{{#foo}} {{foo}}{{/foo}}')
),
false
);
}); });
}); });
describe('#compile', function () { describe('#compile', function () {
it('should fail with invalid input', function () { it('should fail with invalid input', function () {
expect(function () { shouldThrow(
Handlebars.compile(null); function () {
}).toThrow( Handlebars.compile(null);
},
Error,
'You must pass a string or Handlebars AST to Handlebars.compile. You passed null' 'You must pass a string or Handlebars AST to Handlebars.compile. You passed null'
); );
expect(function () { shouldThrow(
Handlebars.compile({}); function () {
}).toThrow( Handlebars.compile({});
},
Error,
'You must pass a string or Handlebars AST to Handlebars.compile. You passed [object Object]' 'You must pass a string or Handlebars AST to Handlebars.compile. You passed [object Object]'
); );
}); });
@@ -67,52 +80,71 @@ describe('compiler', function () {
it('should include the location in the error (row and column)', function () { it('should include the location in the error (row and column)', function () {
try { try {
Handlebars.compile(' \n {{#if}}\n{{/def}}')(); Handlebars.compile(' \n {{#if}}\n{{/def}}')();
expect.unreachable('Statement must throw exception'); equal(
true,
false,
'Statement must throw exception. This line should not be executed.'
);
} catch (err) { } catch (err) {
expect(err.message).toBe("if doesn't match def - 2:5"); equal(
err.message,
"if doesn't match def - 2:5",
'Checking error message'
);
if (Object.getOwnPropertyDescriptor(err, 'column').writable) { if (Object.getOwnPropertyDescriptor(err, 'column').writable) {
// In Safari 8, the column-property is read-only. This means that even if it is set with defineProperty, // In Safari 8, the column-property is read-only. This means that even if it is set with defineProperty,
// its value won't change (https://github.com/jquery/esprima/issues/1290#issuecomment-132455482) // its value won't change (https://github.com/jquery/esprima/issues/1290#issuecomment-132455482)
// Since this was neither working in Handlebars 3 nor in 4.0.5, we only check the column for other browsers. // Since this was neither working in Handlebars 3 nor in 4.0.5, we only check the column for other browsers.
expect(err.column).toBe(5); equal(err.column, 5, 'Checking error column');
} }
expect(err.lineNumber).toBe(2); equal(err.lineNumber, 2, 'Checking error row');
} }
}); });
it('should include the location as enumerable property', function () { it('should include the location as enumerable property', function () {
try { try {
Handlebars.compile(' \n {{#if}}\n{{/def}}')(); Handlebars.compile(' \n {{#if}}\n{{/def}}')();
expect.unreachable('Statement must throw exception'); equal(
true,
false,
'Statement must throw exception. This line should not be executed.'
);
} catch (err) { } catch (err) {
expect(Object.prototype.propertyIsEnumerable.call(err, 'column')).toBe( equal(
true Object.prototype.propertyIsEnumerable.call(err, 'column'),
true,
'Checking error column'
); );
} }
}); });
it('can utilize AST instance', function () { it('can utilize AST instance', function () {
expect( equal(
Handlebars.compile({ Handlebars.compile({
type: 'Program', type: 'Program',
body: [{ type: 'ContentStatement', value: 'Hello' }], body: [{ type: 'ContentStatement', value: 'Hello' }],
})() })(),
).toBe('Hello'); 'Hello'
);
}); });
it('can pass through an empty string', function () { it('can pass through an empty string', function () {
expect(Handlebars.compile('')()).toBe(''); equal(Handlebars.compile('')(), '');
}); });
it('throws on desupported options', function () { it('throws on desupported options', function () {
expect(function () { shouldThrow(
Handlebars.compile('Dudes', { trackIds: true }); function () {
}).toThrow( Handlebars.compile('Dudes', { trackIds: true });
},
Error,
'TrackIds and stringParams are no longer supported. See Github #1145' 'TrackIds and stringParams are no longer supported. See Github #1145'
); );
expect(function () { shouldThrow(
Handlebars.compile('Dudes', { stringParams: true }); function () {
}).toThrow( Handlebars.compile('Dudes', { stringParams: true });
},
Error,
'TrackIds and stringParams are no longer supported. See Github #1145' 'TrackIds and stringParams are no longer supported. See Github #1145'
); );
}); });
@@ -120,41 +152,54 @@ describe('compiler', function () {
it('should not modify the options.data property(GH-1327)', function () { it('should not modify the options.data property(GH-1327)', function () {
var options = { data: [{ a: 'foo' }, { a: 'bar' }] }; var options = { data: [{ a: 'foo' }, { a: 'bar' }] };
Handlebars.compile('{{#each data}}{{@index}}:{{a}} {{/each}}', options)(); Handlebars.compile('{{#each data}}{{@index}}:{{a}} {{/each}}', options)();
expect(options).toStrictEqual({ data: [{ a: 'foo' }, { a: 'bar' }] }); equal(
JSON.stringify(options, 0, 2),
JSON.stringify({ data: [{ a: 'foo' }, { a: 'bar' }] }, 0, 2)
);
}); });
it('should not modify the options.knownHelpers property(GH-1327)', function () { it('should not modify the options.knownHelpers property(GH-1327)', function () {
var options = { knownHelpers: {} }; var options = { knownHelpers: {} };
Handlebars.compile('{{#each data}}{{@index}}:{{a}} {{/each}}', options)(); Handlebars.compile('{{#each data}}{{@index}}:{{a}} {{/each}}', options)();
expect(options).toStrictEqual({ knownHelpers: {} }); equal(
JSON.stringify(options, 0, 2),
JSON.stringify({ knownHelpers: {} }, 0, 2)
);
}); });
}); });
describe('#precompile', function () { describe('#precompile', function () {
it('should fail with invalid input', function () { it('should fail with invalid input', function () {
expect(function () { shouldThrow(
Handlebars.precompile(null); function () {
}).toThrow( Handlebars.precompile(null);
},
Error,
'You must pass a string or Handlebars AST to Handlebars.compile. You passed null' 'You must pass a string or Handlebars AST to Handlebars.compile. You passed null'
); );
expect(function () { shouldThrow(
Handlebars.precompile({}); function () {
}).toThrow( Handlebars.precompile({});
},
Error,
'You must pass a string or Handlebars AST to Handlebars.compile. You passed [object Object]' 'You must pass a string or Handlebars AST to Handlebars.compile. You passed [object Object]'
); );
}); });
it('can utilize AST instance', function () { it('can utilize AST instance', function () {
expect( equal(
Handlebars.precompile({ /return "Hello"/.test(
type: 'Program', Handlebars.precompile({
body: [{ type: 'ContentStatement', value: 'Hello' }], type: 'Program',
}) body: [{ type: 'ContentStatement', value: 'Hello' }],
).toMatch(/return "Hello"/); })
),
true
);
}); });
it('can pass through an empty string', function () { it('can pass through an empty string', function () {
expect(Handlebars.precompile('')).toMatch(/return ""/); equal(/return ""/.test(Handlebars.precompile('')), true);
}); });
}); });
}); });
-6
View File
@@ -1,6 +0,0 @@
// Pre-setup for browser tests. Must run before the main setup file
// imports the Handlebars library, so that noConflict() captures this value.
globalThis.Handlebars = 'no-conflict';
// Polyfill Node.js 'global' for specs that reference it at module level
globalThis.global = globalThis;
-27
View File
@@ -1,27 +0,0 @@
import './common.js';
import Handlebars from '../../lib/handlebars.js';
globalThis.Handlebars = Handlebars;
globalThis.CompilerContext = {
browser: true,
compile: function (template, options) {
var templateSpec = handlebarsEnv.precompile(template, options);
return handlebarsEnv.template(safeEval(templateSpec));
},
compileWithPartial: function (template, options) {
return handlebarsEnv.compile(template, options);
},
};
function safeEval(templateSpec) {
/* eslint-disable no-eval, no-console */
try {
return eval('(' + templateSpec + ')');
} catch (err) {
console.error(templateSpec);
throw err;
}
/* eslint-enable no-eval, no-console */
}
+11
View File
@@ -3,9 +3,20 @@ require('./common');
var fs = require('fs'), var fs = require('fs'),
vm = require('vm'); vm = require('vm');
var chai = require('chai');
var dirtyChai = require('dirty-chai');
chai.use(dirtyChai);
global.expect = chai.expect;
global.sinon = require('sinon');
global.Handlebars = 'no-conflict'; global.Handlebars = 'no-conflict';
var filename = 'dist/handlebars.js'; var filename = 'dist/handlebars.js';
if (global.minimizedTest) {
filename = 'dist/handlebars.min.js';
}
var distHandlebars = fs.readFileSync( var distHandlebars = fs.readFileSync(
require.resolve('../../' + filename), require.resolve('../../' + filename),
'utf-8' 'utf-8'
+129 -23
View File
@@ -1,4 +1,125 @@
var global = globalThis; var global = (function () {
return this;
})();
var AssertError;
if (Error.captureStackTrace) {
AssertError = function AssertError(message, caller) {
Error.prototype.constructor.call(this, message);
this.message = message;
if (Error.captureStackTrace) {
Error.captureStackTrace(this, caller || AssertError);
}
};
AssertError.prototype = new Error();
} else {
AssertError = Error;
}
/**
* @deprecated Use "expectTemplate(template)...toCompileTo(output)" instead
*/
global.shouldCompileTo = function (string, hashOrArray, expected, message) {
shouldCompileToWithPartials(string, hashOrArray, false, expected, message);
};
/**
* @deprecated Use "expectTemplate(template)...toCompileTo(output)" instead
*/
global.shouldCompileToWithPartials = function shouldCompileToWithPartials(
string,
hashOrArray,
partials,
expected,
message
) {
var result = compileWithPartials(string, hashOrArray, partials);
if (result !== expected) {
throw new AssertError(
"'" + result + "' should === '" + expected + "': " + message,
shouldCompileToWithPartials
);
}
};
/**
* @deprecated Use "expectTemplate(template)...toCompileTo(output)" instead
*/
global.compileWithPartials = function (string, hashOrArray, partials) {
var template, ary, options;
if (hashOrArray && hashOrArray.hash) {
ary = [hashOrArray.hash, hashOrArray];
delete hashOrArray.hash;
} else if (Object.prototype.toString.call(hashOrArray) === '[object Array]') {
ary = [];
ary.push(hashOrArray[0]); // input
ary.push({ helpers: hashOrArray[1], partials: hashOrArray[2] });
options =
typeof hashOrArray[3] === 'object'
? hashOrArray[3]
: { compat: hashOrArray[3] };
if (hashOrArray[4] != null) {
options.data = !!hashOrArray[4];
ary[1].data = hashOrArray[4];
}
} else {
ary = [hashOrArray];
}
template = CompilerContext[partials ? 'compileWithPartial' : 'compile'](
string,
options
);
return template.apply(this, ary);
};
/**
* @deprecated Use chai's expect-style API instead (`expect(actualValue).to.equal(expectedValue)`)
* @see https://www.chaijs.com/api/bdd/
*/
global.equals = global.equal = function equals(a, b, msg) {
if (a !== b) {
throw new AssertError(
"'" + a + "' should === '" + b + "'" + (msg ? ': ' + msg : ''),
equals
);
}
};
/**
* @deprecated Use chai's expect-style API instead (`expect(actualValue).to.equal(expectedValue)`)
* @see https://www.chaijs.com/api/bdd/#method_throw
*/
global.shouldThrow = function (callback, type, msg) {
var failed;
try {
callback();
failed = true;
} catch (caught) {
if (type && !(caught instanceof type)) {
throw new AssertError('Type failure: ' + caught);
}
if (
msg &&
!(msg.test ? msg.test(caught.message) : msg === caught.message)
) {
throw new AssertError(
'Throw mismatch: Expected ' +
caught.message +
' to match ' +
msg +
'\n\n' +
caught.stack,
shouldThrow
);
}
}
if (failed) {
throw new AssertError('It failed to throw', shouldThrow);
}
};
global.expectTemplate = function (templateAsString) { global.expectTemplate = function (templateAsString) {
return new HandlebarsTestBench(templateAsString); return new HandlebarsTestBench(templateAsString);
@@ -79,29 +200,18 @@ HandlebarsTestBench.prototype.withMessage = function (message) {
}; };
HandlebarsTestBench.prototype.toCompileTo = function (expectedOutputAsString) { HandlebarsTestBench.prototype.toCompileTo = function (expectedOutputAsString) {
expect(this._compileAndExecute()).toBe(expectedOutputAsString); expect(this._compileAndExecute()).to.equal(
expectedOutputAsString,
this.message
);
}; };
// see chai "to.throw" (https://www.chaijs.com/api/bdd/#method_throw)
HandlebarsTestBench.prototype.toThrow = function (errorLike, errMsgMatcher) { HandlebarsTestBench.prototype.toThrow = function (errorLike, errMsgMatcher) {
var self = this; var self = this;
var caught; expect(function () {
try {
self._compileAndExecute(); self._compileAndExecute();
} catch (e) { }).to.throw(errorLike, errMsgMatcher, this.message);
caught = e;
}
expect(caught).toBeDefined();
if (typeof errorLike === 'function') {
expect(caught).toBeInstanceOf(errorLike);
if (errMsgMatcher) {
expect(caught.message).toMatch(errMsgMatcher);
}
} else if (errorLike) {
// errorLike is a string or regex message matcher (single-argument form)
expect(caught.message).toMatch(errorLike);
}
}; };
HandlebarsTestBench.prototype._compileAndExecute = function () { HandlebarsTestBench.prototype._compileAndExecute = function () {
@@ -127,7 +237,3 @@ HandlebarsTestBench.prototype._combineRuntimeOptions = function () {
combinedRuntimeOptions.decorators = this.decorators; combinedRuntimeOptions.decorators = this.decorators;
return combinedRuntimeOptions; return combinedRuntimeOptions;
}; };
beforeEach(function () {
global.handlebarsEnv = Handlebars.create();
});
+8
View File
@@ -1,5 +1,13 @@
require('./common'); require('./common');
var chai = require('chai');
var dirtyChai = require('dirty-chai');
chai.use(dirtyChai);
global.expect = chai.expect;
global.sinon = require('sinon');
global.Handlebars = require('../../lib'); global.Handlebars = require('../../lib');
global.CompilerContext = { global.CompilerContext = {
+63
View File
@@ -0,0 +1,63 @@
/* eslint-disable no-console */
var fs = require('fs'),
Mocha = require('mocha'),
path = require('path');
var errors = 0,
testDir = path.dirname(__dirname),
grep = process.argv[2];
// Lazy hack, but whatever
if (grep === '--min') {
global.minimizedTest = true;
grep = undefined;
}
var files = fs
.readdirSync(testDir)
.filter(function (name) {
return /.*\.js$/.test(name);
})
.map(function (name) {
return testDir + path.sep + name;
});
if (global.minimizedTest) {
run('./runtime', function () {
run('./browser', function () {
/* eslint-disable no-process-exit */
process.exit(errors);
/* eslint-enable no-process-exit */
});
});
} else {
run('./runtime', function () {
run('./browser', function () {
run('./node', function () {
/* eslint-disable no-process-exit */
process.exit(errors);
/* eslint-enable no-process-exit */
});
});
});
}
function run(env, callback) {
var mocha = new Mocha();
mocha.ui('bdd');
mocha.files = files.slice();
if (grep) {
mocha.grep(grep);
}
files.forEach(function (name) {
delete require.cache[name];
});
console.log('Running env: ' + env);
require(env);
mocha.run(function (errorCount) {
errors += errorCount;
callback();
});
}
+73
View File
@@ -0,0 +1,73 @@
require('./common');
var fs = require('fs'),
vm = require('vm');
var chai = require('chai');
var dirtyChai = require('dirty-chai');
chai.use(dirtyChai);
global.expect = chai.expect;
global.sinon = require('sinon');
global.Handlebars = 'no-conflict';
var filename = 'dist/handlebars.runtime.js';
if (global.minimizedTest) {
filename = 'dist/handlebars.runtime.min.js';
}
vm.runInThisContext(
fs.readFileSync(__dirname + '/../../' + filename),
filename
);
var parse = require('@handlebars/parser').parse;
var compiler = require('../../dist/cjs/handlebars/compiler/compiler');
var JavaScriptCompiler = require('../../dist/cjs/handlebars/compiler/javascript-compiler');
global.CompilerContext = {
browser: true,
compile: function (template, options) {
// Hack the compiler on to the environment for these specific tests
handlebarsEnv.precompile = function (
precompileTemplate,
precompileOptions
) {
return compiler.precompile(
precompileTemplate,
precompileOptions,
handlebarsEnv
);
};
handlebarsEnv.parse = parse;
handlebarsEnv.Compiler = compiler.Compiler;
handlebarsEnv.JavaScriptCompiler = JavaScriptCompiler;
var templateSpec = handlebarsEnv.precompile(template, options);
return handlebarsEnv.template(safeEval(templateSpec));
},
compileWithPartial: function (template, options) {
// Hack the compiler on to the environment for these specific tests
handlebarsEnv.compile = function (compileTemplate, compileOptions) {
return compiler.compile(compileTemplate, compileOptions, handlebarsEnv);
};
handlebarsEnv.parse = parse;
handlebarsEnv.Compiler = compiler.Compiler;
handlebarsEnv.JavaScriptCompiler = JavaScriptCompiler;
return handlebarsEnv.compile(template, options);
},
};
function safeEval(templateSpec) {
/* eslint-disable no-eval, no-console */
try {
return eval('(' + templateSpec + ')');
} catch (err) {
console.error(templateSpec);
throw err;
}
/* eslint-enable no-eval, no-console */
}
+8 -6
View File
@@ -1,12 +1,15 @@
Precompile handlebar templates. Precompile handlebar templates.
Usage: handlebars.mjs [template|directory]... Usage: handlebars.js [template|directory]...
Options: Options:
--help Outputs this message [boolean]
-f, --output Output File [string] -f, --output Output File [string]
--map Source Map File [string] --map Source Map File [string]
-a, --amd Exports amd style (require.js) [boolean] --esm Exports ECMAScript module style, path to Handlebars module (eg. "handlebars/lib/handlebars")
[string] [default: null]
-c, --commonjs Exports CommonJS style, path to Handlebars module [string] [default: null] -c, --commonjs Exports CommonJS style, path to Handlebars module [string] [default: null]
-h, --handlebarPath Path to handlebar.js (only valid for amd-style) [string] [default: ""] -a, --amd Exports AMD style (require.js) [deprecated] [boolean]
-h, --handlebarPath Path to handlebar.js (only valid for AMD-style) [deprecated] [string] [default: ""]
-k, --known Known helpers [string] -k, --known Known helpers [string]
-o, --knownOnly Known helpers only [boolean] -o, --knownOnly Known helpers only [boolean]
-m, --min Minimize output [boolean] -m, --min Minimize output [boolean]
@@ -21,5 +24,4 @@ Options:
-d, --data Include data when compiling [boolean] -d, --data Include data when compiling [boolean]
-e, --extension Template extension. [string] [default: "handlebars"] -e, --extension Template extension. [string] [default: "handlebars"]
-b, --bom Removes the BOM (Byte Order Mark) from the beginning of the templates. [boolean] -b, --bom Removes the BOM (Byte Order Mark) from the beginning of the templates. [boolean]
-v, --version Prints the current compiler version [boolean] -v, --version Show version number [boolean]
--help Outputs this message [boolean]
+4 -4
View File
@@ -1,6 +1,6 @@
define(['handlebars.runtime'], function(Handlebars) { define(['handlebars.runtime'], function(Handlebars) {
Handlebars = Handlebars["default"]; var template = Handlebars.template, templates = Handlebars.templates = Handlebars.templates || {}; Handlebars = Handlebars["default"]; var template = Handlebars.template, templates = Handlebars.templates = Handlebars.templates || {};
return templates['known.helpers'] = template({"0":function(container,depth0,helpers,partials,data) { return templates['known.helpers'] = template({"1":function(container,depth0,helpers,partials,data) {
var stack1, lookupProperty = container.lookupProperty || function(parent, propertyName) { var stack1, lookupProperty = container.lookupProperty || function(parent, propertyName) {
if (Object.prototype.hasOwnProperty.call(parent, propertyName)) { if (Object.prototype.hasOwnProperty.call(parent, propertyName)) {
return parent[propertyName]; return parent[propertyName];
@@ -8,8 +8,8 @@ return parent[propertyName];
return undefined return undefined
}; };
return " <div>Some known helper</div>\n" return " <div>Some known helper</div>\n"
+ ((stack1 = lookupProperty(helpers,"anotherHelper").call(depth0 != null ? depth0 : (container.nullContext || {}),true,{"name":"anotherHelper","hash":{},"fn":container.program(1, data, 0),"inverse":container.noop,"data":data,"loc":{"start":{"line":3,"column":4},"end":{"line":5,"column":22}}})) != null ? stack1 : ""); + ((stack1 = lookupProperty(helpers,"anotherHelper").call(depth0 != null ? depth0 : (container.nullContext || {}),true,{"name":"anotherHelper","hash":{},"fn":container.program(2, data, 0),"inverse":container.noop,"data":data,"loc":{"start":{"line":3,"column":4},"end":{"line":5,"column":22}}})) != null ? stack1 : "");
},"1":function(container,depth0,helpers,partials,data) { },"2":function(container,depth0,helpers,partials,data) {
return " <div>Another known helper</div>\n"; return " <div>Another known helper</div>\n";
},"compiler":[8,">= 4.3.0"],"main":function(container,depth0,helpers,partials,data) { },"compiler":[8,">= 4.3.0"],"main":function(container,depth0,helpers,partials,data) {
var stack1, lookupProperty = container.lookupProperty || function(parent, propertyName) { var stack1, lookupProperty = container.lookupProperty || function(parent, propertyName) {
@@ -18,7 +18,7 @@ return parent[propertyName];
} }
return undefined return undefined
}; };
return ((stack1 = lookupProperty(helpers,"someHelper").call(depth0 != null ? depth0 : (container.nullContext || {}),true,{"name":"someHelper","hash":{},"fn":container.program(0, data, 0),"inverse":container.noop,"data":data,"loc":{"start":{"line":1,"column":0},"end":{"line":6,"column":15}}})) != null ? stack1 : ""); return ((stack1 = lookupProperty(helpers,"someHelper").call(depth0 != null ? depth0 : (container.nullContext || {}),true,{"name":"someHelper","hash":{},"fn":container.program(1, data, 0),"inverse":container.noop,"data":data,"loc":{"start":{"line":1,"column":0},"end":{"line":6,"column":15}}})) != null ? stack1 : "");
},"useData":true}); },"useData":true});
}); });
+22 -18
View File
@@ -53,7 +53,7 @@ describe('helpers', function () {
runWithIdentityHelper('{{{{identity}}}}{{{{/identity}}}}', ''); runWithIdentityHelper('{{{{identity}}}}{{{{/identity}}}}', '');
}); });
it.skip('helper for nested raw block works if nested raw blocks are broken', function () { xit('helper for nested raw block works if nested raw blocks are broken', function () {
// This test was introduced in 4.4.4, but it was not the actual problem that lead to the patch release // This test was introduced in 4.4.4, but it was not the actual problem that lead to the patch release
// The test is deactivated, because in 3.x this template cases an exception and it also does not work in 4.4.3 // The test is deactivated, because in 3.x this template cases an exception and it also does not work in 4.4.3
// If anyone can make this template work without breaking everything else, then go for it, // If anyone can make this template work without breaking everything else, then go for it,
@@ -392,7 +392,7 @@ describe('helpers', function () {
return 'fail'; return 'fail';
}); });
handlebarsEnv.unregisterHelper('foo'); handlebarsEnv.unregisterHelper('foo');
expect(handlebarsEnv.helpers.foo).toBeUndefined(); equals(handlebarsEnv.helpers.foo, undefined);
}); });
it('allows multiple globals', function () { it('allows multiple globals', function () {
@@ -417,19 +417,23 @@ describe('helpers', function () {
}); });
it('fails with multiple and args', function () { it('fails with multiple and args', function () {
expect(function () { shouldThrow(
handlebarsEnv.registerHelper( function () {
{ handlebarsEnv.registerHelper(
world: function () { {
return 'world!'; world: function () {
return 'world!';
},
testHelper: function () {
return 'found it!';
},
}, },
testHelper: function () { {}
return 'found it!'; );
}, },
}, Error,
{} 'Arg not supported with multiple helpers'
); );
}).toThrow('Arg not supported with multiple helpers');
}); });
}); });
@@ -916,7 +920,7 @@ describe('helpers', function () {
expectTemplate('{{#goodbyes as |value|}}{{value}}{{/goodbyes}}{{value}}') expectTemplate('{{#goodbyes as |value|}}{{value}}{{/goodbyes}}{{value}}')
.withInput({ value: 'foo' }) .withInput({ value: 'foo' })
.withHelper('goodbyes', function (options) { .withHelper('goodbyes', function (options) {
expect(options.fn.blockParams).toBe(1); equals(options.fn.blockParams, 1);
return options.fn({ value: 'bar' }, { blockParams: [1, 2] }); return options.fn({ value: 'bar' }, { blockParams: [1, 2] });
}) })
.toCompileTo('1foo'); .toCompileTo('1foo');
@@ -928,7 +932,7 @@ describe('helpers', function () {
return 'foo'; return 'foo';
}) })
.withHelper('goodbyes', function (options) { .withHelper('goodbyes', function (options) {
expect(options.fn.blockParams).toBe(1); equals(options.fn.blockParams, 1);
return options.fn({}, { blockParams: [1, 2] }); return options.fn({}, { blockParams: [1, 2] });
}) })
.toCompileTo('1foo'); .toCompileTo('1foo');
@@ -943,7 +947,7 @@ describe('helpers', function () {
return 'foo'; return 'foo';
}) })
.withHelper('goodbyes', function (options) { .withHelper('goodbyes', function (options) {
expect(options.fn.blockParams).toBe(1); equals(options.fn.blockParams, 1);
return options.fn(this, { blockParams: [1, 2] }); return options.fn(this, { blockParams: [1, 2] });
}) })
.toCompileTo('barfoo'); .toCompileTo('barfoo');
@@ -973,7 +977,7 @@ describe('helpers', function () {
) )
.withInput({ value: 'foo' }) .withInput({ value: 'foo' })
.withHelper('goodbyes', function (options) { .withHelper('goodbyes', function (options) {
expect(options.fn.blockParams).toBe(1); equals(options.fn.blockParams, 1);
return options.fn({ value: 'bar' }, { blockParams: [1, 2] }); return options.fn({ value: 'bar' }, { blockParams: [1, 2] });
}) })
.toCompileTo('1foo'); .toCompileTo('1foo');
+89 -46
View File
@@ -1,55 +1,98 @@
<!doctype html> <!doctype html>
<html> <html>
<head> <head>
<title>Handlebars UMD Smoke Test</title> <title>Mocha</title>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" /> <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0">
</head>
<body>
<h1>Handlebars UMD Smoke Test</h1>
<pre id="results"></pre>
<script src="/dist/handlebars.js"></script>
<script>
var results = document.getElementById('results');
var failures = 0;
var tests = 0;
function assert(condition, message) { <link rel="stylesheet" href="/node_modules/mocha/mocha.css" />
tests++; <style>
if (!condition) { .headless .suite > h1,
failures++; .headless .test.pass {
results.textContent += 'FAIL: ' + message + '\n'; display: none;
} else { }
results.textContent += 'PASS: ' + message + '\n'; </style>
<script>
// Show only errors in "headless", non-interactive mode.
if (/headless=true/.test(location.href)) {
document.documentElement.className = 'headless';
}
</script>
<script src="/node_modules/sinon/pkg/sinon.js"></script>
<script src="/node_modules/chai/chai.js"></script>
<script src="/node_modules/dirty-chai/lib/dirty-chai.js"></script>
<script src="/node_modules/mocha/mocha.js"></script>
<script>
window.expect = chai.expect;
mocha.setup('bdd');
</script>
<script src="/dist/handlebars.js"></script>
<script src="/spec/env/common.js"></script>
<script>
var CompilerContext = {
compile: function(template, options) {
var templateSpec = handlebarsEnv.precompile(template, options);
return handlebarsEnv.template(safeEval(templateSpec));
},
compileWithPartial: function(template, options) {
return handlebarsEnv.compile(template, options);
}
};
function safeEval(templateSpec) {
try {
var ret;
eval('ret = ' + templateSpec);
return ret;
} catch (err) {
console.error(templateSpec);
throw err;
} }
} }
try {
assert(typeof Handlebars !== 'undefined', 'Handlebars is defined');
assert(
typeof Handlebars.compile === 'function',
'Handlebars.compile exists'
);
assert(
typeof Handlebars.template === 'function',
'Handlebars.template exists'
);
assert(
typeof Handlebars.VERSION === 'string',
'Handlebars.VERSION exists'
);
var template = Handlebars.compile('Hello {{name}}!');
var output = template({ name: 'World' });
assert(output === 'Hello World!', 'Basic compilation works: ' + output);
} catch (e) {
failures++;
results.textContent += 'ERROR: ' + e.message + '\n';
}
results.textContent +=
'\n' + tests + ' tests, ' + failures + ' failures\n';
window.mochaResults = { passes: tests - failures, failures: failures };
</script> </script>
<script src="/tmp/tests.js"></script>
<script>
onload = function(){
mocha.globals(['mochaResults'])
// The test harness leaks under FF. We should have decent global leak coverage from other tests
if (!navigator.userAgent.match(/Firefox\/([\d.]+)/)) {
mocha.checkLeaks();
}
var runner = mocha.run();
// Reporting to test-runner
var failedTests = [];
runner.on('end', function(){
window.mochaResults = runner.stats;
window.mochaResults.reports = failedTests;
});
runner.on('fail', logFailure);
function logFailure(test, err){
var flattenTitles = function(test){
var titles = [];
while (test.parent.title){
titles.push(test.parent.title);
test = test.parent;
}
return titles.reverse();
};
failedTests.push({
name: test.title,
result: false,
message: err.message,
stack: err.stack,
titles: flattenTitles(test)
});
};
};
</script>
</head>
<body>
<div id="mocha"></div>
</body> </body>
</html> </html>
+28 -31
View File
@@ -19,9 +19,11 @@ describe('javascript-compiler api', function () {
) { ) {
return parent + '.bar_' + name; return parent + '.bar_' + name;
}; };
/* eslint-disable camelcase */
expectTemplate('{{foo}}') expectTemplate('{{foo}}')
.withInput({ bar_foo: 'food' }) .withInput({ bar_foo: 'food' })
.toCompileTo('food'); .toCompileTo('food');
/* eslint-enable camelcase */
}); });
// Tests nameLookup dot vs. bracket behavior. Bracket is required in certain cases // Tests nameLookup dot vs. bracket behavior. Bracket is required in certain cases
@@ -32,35 +34,30 @@ describe('javascript-compiler api', function () {
.toCompileTo('food'); .toCompileTo('food');
}); });
}); });
// Monkey-patching VM.checkRevision is not possible when VM is an ESM describe('#compilerInfo', function () {
// namespace object (browser mode), so skip these tests in that context. var $superCheck, $superInfo;
(CompilerContext.browser ? describe.skip : describe)( beforeEach(function () {
'#compilerInfo', $superCheck = handlebarsEnv.VM.checkRevision;
function () { $superInfo = handlebarsEnv.JavaScriptCompiler.prototype.compilerInfo;
var $superCheck, $superInfo; });
beforeEach(function () { afterEach(function () {
$superCheck = handlebarsEnv.VM.checkRevision; handlebarsEnv.VM.checkRevision = $superCheck;
$superInfo = handlebarsEnv.JavaScriptCompiler.prototype.compilerInfo; handlebarsEnv.JavaScriptCompiler.prototype.compilerInfo = $superInfo;
}); });
afterEach(function () { it('should allow compilerInfo override', function () {
handlebarsEnv.VM.checkRevision = $superCheck; handlebarsEnv.JavaScriptCompiler.prototype.compilerInfo = function () {
handlebarsEnv.JavaScriptCompiler.prototype.compilerInfo = $superInfo; return 'crazy';
}); };
it('should allow compilerInfo override', function () { handlebarsEnv.VM.checkRevision = function (compilerInfo) {
handlebarsEnv.JavaScriptCompiler.prototype.compilerInfo = function () { if (compilerInfo !== 'crazy') {
return 'crazy'; throw new Error("It didn't work");
}; }
handlebarsEnv.VM.checkRevision = function (compilerInfo) { };
if (compilerInfo !== 'crazy') { expectTemplate('{{foo}} ')
throw new Error("It didn't work"); .withInput({ foo: 'food' })
} .toCompileTo('food ');
}; });
expectTemplate('{{foo}} ') });
.withInput({ foo: 'food' })
.toCompileTo('food ');
});
}
);
describe('buffer', function () { describe('buffer', function () {
var $superAppend, $superCreate; var $superAppend, $superCreate;
beforeEach(function () { beforeEach(function () {
@@ -108,7 +105,7 @@ describe('javascript-compiler api', function () {
handlebarsEnv.JavaScriptCompiler.isValidJavaScriptVariableName( handlebarsEnv.JavaScriptCompiler.isValidJavaScriptVariableName(
validVariableName validVariableName
) )
).toBe(true); ).to.be.true();
}); });
}); });
[('123test', 'abc()', 'abc.cde')].forEach(function (invalidVariableName) { [('123test', 'abc()', 'abc.cde')].forEach(function (invalidVariableName) {
@@ -117,7 +114,7 @@ describe('javascript-compiler api', function () {
handlebarsEnv.JavaScriptCompiler.isValidJavaScriptVariableName( handlebarsEnv.JavaScriptCompiler.isValidJavaScriptVariableName(
invalidVariableName invalidVariableName
) )
).toBe(false); ).to.be.false();
}); });
}); });
}); });
+8 -5
View File
@@ -164,9 +164,12 @@ describe('partials', function () {
}); });
it('registering undefined partial throws an exception', function () { it('registering undefined partial throws an exception', function () {
expect(function () { shouldThrow(
handlebarsEnv.registerPartial('undefined_test', undefined); function () {
}).toThrow( var undef;
handlebarsEnv.registerPartial('undefined_test', undef);
},
Handlebars.Exception,
'Attempting to register a partial called "undefined_test" as undefined' 'Attempting to register a partial called "undefined_test" as undefined'
); );
}); });
@@ -228,7 +231,7 @@ describe('partials', function () {
.toCompileTo('Dudes: Jeepers Creepers'); .toCompileTo('Dudes: Jeepers Creepers');
handlebarsEnv.unregisterPartial('globalTest'); handlebarsEnv.unregisterPartial('globalTest');
expect(handlebarsEnv.partials.globalTest).toBeUndefined(); equals(handlebarsEnv.partials.globalTest, undefined);
}); });
it('Multiple partial registration', function () { it('Multiple partial registration', function () {
@@ -553,7 +556,7 @@ describe('partials', function () {
var env = Handlebars.create(); var env = Handlebars.create();
env.registerPartial('partial', '{{foo}}'); env.registerPartial('partial', '{{foo}}');
var template = env.compile('{{foo}} {{> partial}}', { noEscape: true }); var template = env.compile('{{foo}} {{> partial}}', { noEscape: true });
expect(template({ foo: '<' })).toBe('< <'); equal(template({ foo: '<' }), '< <');
} }
}); });
+204 -183
View File
@@ -33,7 +33,7 @@ describe('precompiler', function () {
* @param {Error} loadError the error that should be thrown if uglify is loaded * @param {Error} loadError the error that should be thrown if uglify is loaded
* @param {function} callback a callback-function to run when the mock is active. * @param {function} callback a callback-function to run when the mock is active.
*/ */
async function mockRequireUglify(loadError, callback) { function mockRequireUglify(loadError, callback) {
var Module = require('module'); var Module = require('module');
var _resolveFilename = Module._resolveFilename; var _resolveFilename = Module._resolveFilename;
delete require.cache[require.resolve('uglify-js')]; delete require.cache[require.resolve('uglify-js')];
@@ -45,7 +45,7 @@ describe('precompiler', function () {
return _resolveFilename.call(this, request, mod); return _resolveFilename.call(this, request, mod);
}; };
try { try {
await callback(); callback();
} finally { } finally {
Module._resolveFilename = _resolveFilename; Module._resolveFilename = _resolveFilename;
delete require.cache[require.resolve('uglify-js')]; delete require.cache[require.resolve('uglify-js')];
@@ -83,312 +83,333 @@ describe('precompiler', function () {
console.error = errorLogFunction; console.error = errorLogFunction;
}); });
it('should output version', async function () { it('should output version', function () {
await Precompiler.cli({ templates: [], version: true }); Precompiler.cli({ templates: [], version: true });
expect(log).toBe(Handlebars.VERSION); equals(log, Handlebars.VERSION);
}); });
it('should throw if lacking templates', async function () { it('should throw if lacking templates', function () {
await expect(Precompiler.cli({ templates: [] })).rejects.toThrow( shouldThrow(
function () {
Precompiler.cli({ templates: [] });
},
Handlebars.Exception,
'Must define at least one template or directory.' 'Must define at least one template or directory.'
); );
}); });
it('should handle empty/filtered directories', async function () { it('should handle empty/filtered directories', function () {
Handlebars.precompile = function () { Precompiler.cli({ hasDirectory: true, templates: [] });
return 'simple';
};
await Precompiler.cli({ hasDirectory: true, templates: [] });
// Success is not throwing // Success is not throwing
}); });
it('should throw when combining simple and minimized', async function () { it('should throw when combining simple and minimized', function () {
await expect( shouldThrow(
Precompiler.cli({ templates: [__dirname], simple: true, min: true }) function () {
).rejects.toThrow('Unable to minimize simple output'); Precompiler.cli({ templates: [__dirname], simple: true, min: true });
},
Handlebars.Exception,
'Unable to minimize simple output'
);
}); });
it('should throw when combining simple and multiple templates', async function () { it('should throw when combining simple and multiple templates', function () {
await expect( shouldThrow(
Precompiler.cli({ function () {
templates: [ Precompiler.cli({
__dirname + '/artifacts/empty.handlebars', templates: [
__dirname + '/artifacts/empty.handlebars', __dirname + '/artifacts/empty.handlebars',
], __dirname + '/artifacts/empty.handlebars',
simple: true, ],
}) simple: true,
).rejects.toThrow('Unable to output multiple templates in simple mode'); });
},
Handlebars.Exception,
'Unable to output multiple templates in simple mode'
);
}); });
it('should throw when missing name', async function () { it('should throw when missing name', function () {
await expect( shouldThrow(
Precompiler.cli({ templates: [{ source: '' }], amd: true }) function () {
).rejects.toThrow('Name missing for template'); Precompiler.cli({ templates: [{ source: '' }], amd: true });
},
Handlebars.Exception,
'Name missing for template'
);
}); });
it('should throw when combining simple and directories', async function () { it('should throw when combining simple and directories', function () {
await expect( shouldThrow(
Precompiler.cli({ hasDirectory: true, templates: [1], simple: true }) function () {
).rejects.toThrow('Unable to output multiple templates in simple mode'); Precompiler.cli({ hasDirectory: true, templates: [1], simple: true });
},
Handlebars.Exception,
'Unable to output multiple templates in simple mode'
);
}); });
it('should output simple templates', async function () { it('should output simple templates', function () {
Handlebars.precompile = function () { Handlebars.precompile = function () {
return 'simple'; return 'simple';
}; };
await Precompiler.cli({ templates: [emptyTemplate], simple: true }); Precompiler.cli({ templates: [emptyTemplate], simple: true });
expect(log).toBe('simple\n'); equal(log, 'simple\n');
}); });
it('should default to simple templates', async function () { it('should default to simple templates', function () {
Handlebars.precompile = function () { Handlebars.precompile = function () {
return 'simple'; return 'simple';
}; };
await Precompiler.cli({ templates: [{ source: '' }] }); Precompiler.cli({ templates: [{ source: '' }] });
expect(log).toBe('simple\n'); equal(log, 'simple\n');
}); });
it('should output amd templates', async function () { it('should output amd templates', function () {
Handlebars.precompile = function () { Handlebars.precompile = function () {
return 'amd'; return 'amd';
}; };
await Precompiler.cli({ templates: [emptyTemplate], amd: true }); Precompiler.cli({ templates: [emptyTemplate], amd: true });
expect(log).toMatch(/template\(amd\)/); equal(/template\(amd\)/.test(log), true);
}); });
it('should output multiple amd', async function () { it('should output multiple amd', function () {
Handlebars.precompile = function () { Handlebars.precompile = function () {
return 'amd'; return 'amd';
}; };
await Precompiler.cli({ Precompiler.cli({
templates: [emptyTemplate, emptyTemplate], templates: [emptyTemplate, emptyTemplate],
amd: true, amd: true,
namespace: 'foo', namespace: 'foo',
}); });
expect(log).toMatch(/templates = foo = foo \|\|/); equal(/templates = foo = foo \|\|/.test(log), true);
expect(log).toMatch(/return templates/); equal(/return templates/.test(log), true);
expect(log).toMatch(/template\(amd\)/); equal(/template\(amd\)/.test(log), true);
}); });
it('should output amd partials', async function () { it('should output amd partials', function () {
Handlebars.precompile = function () { Handlebars.precompile = function () {
return 'amd'; return 'amd';
}; };
await Precompiler.cli({ Precompiler.cli({ templates: [emptyTemplate], amd: true, partial: true });
templates: [emptyTemplate], equal(/return Handlebars\.partials\['empty'\]/.test(log), true);
amd: true, equal(/template\(amd\)/.test(log), true);
partial: true,
});
expect(log).toMatch(/return Handlebars\.partials\['empty'\]/);
expect(log).toMatch(/template\(amd\)/);
}); });
it('should output multiple amd partials', async function () { it('should output multiple amd partials', function () {
Handlebars.precompile = function () { Handlebars.precompile = function () {
return 'amd'; return 'amd';
}; };
await Precompiler.cli({ Precompiler.cli({
templates: [emptyTemplate, emptyTemplate], templates: [emptyTemplate, emptyTemplate],
amd: true, amd: true,
partial: true, partial: true,
}); });
expect(log).not.toMatch(/return Handlebars\.partials\[/); equal(/return Handlebars\.partials\[/.test(log), false);
expect(log).toMatch(/template\(amd\)/); equal(/template\(amd\)/.test(log), true);
}); });
it('should output commonjs templates', async function () { it('should output es module templates', function () {
Handlebars.precompile = function () {
return 'esm';
};
Precompiler.cli({ templates: [emptyTemplate], esm: true });
equal(/template\(esm\)/.test(log), true);
});
it('should output commonjs templates', function () {
Handlebars.precompile = function () { Handlebars.precompile = function () {
return 'commonjs'; return 'commonjs';
}; };
await Precompiler.cli({ templates: [emptyTemplate], commonjs: true }); Precompiler.cli({ templates: [emptyTemplate], commonjs: true });
expect(log).toMatch(/template\(commonjs\)/); equal(/template\(commonjs\)/.test(log), true);
}); });
it('should set data flag', async function () { it('should set data flag', function () {
Handlebars.precompile = function (data, options) { Handlebars.precompile = function (data, options) {
expect(options.data).toBe(true); equal(options.data, true);
return 'simple'; return 'simple';
}; };
await Precompiler.cli({ Precompiler.cli({ templates: [emptyTemplate], simple: true, data: true });
templates: [emptyTemplate], equal(log, 'simple\n');
simple: true,
data: true,
});
expect(log).toBe('simple\n');
}); });
it('should set known helpers', async function () { it('should set known helpers', function () {
Handlebars.precompile = function (data, options) { Handlebars.precompile = function (data, options) {
expect(options.knownHelpers.foo).toBe(true); equal(options.knownHelpers.foo, true);
return 'simple'; return 'simple';
}; };
await Precompiler.cli({ Precompiler.cli({ templates: [emptyTemplate], simple: true, known: 'foo' });
templates: [emptyTemplate], equal(log, 'simple\n');
simple: true,
known: 'foo',
});
expect(log).toBe('simple\n');
}); });
it('should output to file system', async function () { it('should output to file system', function () {
Handlebars.precompile = function () { Handlebars.precompile = function () {
return 'simple'; return 'simple';
}; };
await Precompiler.cli({ Precompiler.cli({
templates: [emptyTemplate], templates: [emptyTemplate],
simple: true, simple: true,
output: 'file!', output: 'file!',
}); });
expect(file).toBe('file!'); equal(file, 'file!');
expect(content).toBe('simple\n'); equal(content, 'simple\n');
expect(log).toBe(''); equal(log, '');
}); });
it('should output minimized templates', async function () { it('should output minimized templates', function () {
Handlebars.precompile = function () { Handlebars.precompile = function () {
return 'amd'; return 'amd';
}; };
uglify.minify = function () { uglify.minify = function () {
return { code: 'min' }; return { code: 'min' };
}; };
await Precompiler.cli({ templates: [emptyTemplate], min: true }); Precompiler.cli({ templates: [emptyTemplate], min: true });
expect(log).toBe('min'); equal(log, 'min');
}); });
it('should omit minimization gracefully, if uglify-js is missing', async function () { it('should omit minimization gracefully, if uglify-js is missing', function () {
var error = new Error("Cannot find module 'uglify-js'"); var error = new Error("Cannot find module 'uglify-js'");
error.code = 'MODULE_NOT_FOUND'; error.code = 'MODULE_NOT_FOUND';
await mockRequireUglify(error, async function () { mockRequireUglify(error, function () {
var Precompiler = require('../dist/cjs/precompiler'); var Precompiler = require('../dist/cjs/precompiler');
Handlebars.precompile = function () { Handlebars.precompile = function () {
return 'amd'; return 'amd';
}; };
await Precompiler.cli({ templates: [emptyTemplate], min: true }); Precompiler.cli({ templates: [emptyTemplate], min: true });
expect(log).toMatch(/template\(amd\)/); equal(/template\(amd\)/.test(log), true);
expect(log).toMatch(/\n/); equal(/\n/.test(log), true);
expect(errorLog).toMatch(/Code minimization is disabled/); equal(/Code minimization is disabled/.test(errorLog), true);
}); });
}); });
it('should fail on errors (other than missing module) while loading uglify-js', async function () { it('should fail on errors (other than missing module) while loading uglify-js', function () {
await mockRequireUglify(new Error('Mock Error'), async function () { mockRequireUglify(new Error('Mock Error'), function () {
var Precompiler = require('../dist/cjs/precompiler'); shouldThrow(
Handlebars.precompile = function () { function () {
return 'amd'; var Precompiler = require('../dist/cjs/precompiler');
}; Handlebars.precompile = function () {
await expect( return 'amd';
Precompiler.cli({ templates: [emptyTemplate], min: true }) };
).rejects.toThrow('Mock Error'); Precompiler.cli({ templates: [emptyTemplate], min: true });
},
Error,
'Mock Error'
);
}); });
}); });
it('should output map', async function () { it('should output map', function () {
await Precompiler.cli({ templates: [emptyTemplate], map: 'foo.js.map' }); Precompiler.cli({ templates: [emptyTemplate], map: 'foo.js.map' });
expect(file).toBe('foo.js.map'); equal(file, 'foo.js.map');
expect(log.match(/sourceMappingURL=/g).length).toBe(1); equal(log.match(/sourceMappingURL=/g).length, 1);
}); });
it('should output map with minification', async function () { it('should output map', function () {
await Precompiler.cli({ Precompiler.cli({
templates: [emptyTemplate], templates: [emptyTemplate],
min: true, min: true,
map: 'foo.js.map', map: 'foo.js.map',
}); });
expect(file).toBe('foo.js.map'); equal(file, 'foo.js.map');
expect(log.match(/sourceMappingURL=/g).length).toBe(1); equal(log.match(/sourceMappingURL=/g).length, 1);
}); });
describe('#loadTemplates', function () { describe('#loadTemplates', function () {
function loadTemplatesAsync(inputOpts) { it('should throw on missing template', function (done) {
return new Promise(function (resolve, reject) { Precompiler.loadTemplates({ files: ['foo'] }, function (err) {
Precompiler.loadTemplates(inputOpts, function (err, opts) { equal(err.message, 'Unable to open template file "foo"');
if (err) { done();
reject(err);
} else {
resolve(opts);
}
});
}); });
} });
it('should enumerate directories by extension', function (done) {
Precompiler.loadTemplates(
{ files: [__dirname + '/artifacts'], extension: 'hbs' },
function (err, opts) {
equal(opts.templates.length, 2);
equal(opts.templates[0].name, 'example_2');
it('should throw on missing template', async function () { done(err);
try { }
await loadTemplatesAsync({ files: ['foo'] }); );
throw new Error('should have thrown');
} catch (err) {
expect(err.message).toBe('Unable to open template file "foo"');
}
}); });
it('should enumerate directories by extension', async function () { it('should enumerate all templates by extension', function (done) {
var opts = await loadTemplatesAsync({ Precompiler.loadTemplates(
files: [__dirname + '/artifacts'], { files: [__dirname + '/artifacts'], extension: 'handlebars' },
extension: 'hbs', function (err, opts) {
}); equal(opts.templates.length, 5);
expect(opts.templates.length).toBe(2); equal(opts.templates[0].name, 'bom');
expect(opts.templates[0].name).toBe('example_2'); equal(opts.templates[1].name, 'empty');
equal(opts.templates[2].name, 'example_1');
done(err);
}
);
}); });
it('should enumerate all templates by extension', async function () { it('should handle regular expression characters in extensions', function (done) {
var opts = await loadTemplatesAsync({ Precompiler.loadTemplates(
files: [__dirname + '/artifacts'], { files: [__dirname + '/artifacts'], extension: 'hb(s' },
extension: 'handlebars', function (err) {
}); // Success is not throwing
expect(opts.templates.length).toBe(5); done(err);
expect(opts.templates[0].name).toBe('bom'); }
expect(opts.templates[1].name).toBe('empty'); );
expect(opts.templates[2].name).toBe('example_1');
}); });
it('should handle regular expression characters in extensions', async function () { it('should handle BOM', function (done) {
await loadTemplatesAsync({ var opts = {
files: [__dirname + '/artifacts'],
extension: 'hb(s',
});
// Success is not throwing
});
it('should handle BOM', async function () {
var opts = await loadTemplatesAsync({
files: [__dirname + '/artifacts/bom.handlebars'], files: [__dirname + '/artifacts/bom.handlebars'],
extension: 'handlebars', extension: 'handlebars',
bom: true, bom: true,
};
Precompiler.loadTemplates(opts, function (err, opts) {
equal(opts.templates[0].source, 'a');
done(err);
}); });
expect(opts.templates[0].source).toBe('a');
}); });
it('should handle different root', async function () { it('should handle different root', function (done) {
var opts = await loadTemplatesAsync({ var opts = {
files: [__dirname + '/artifacts/empty.handlebars'], files: [__dirname + '/artifacts/empty.handlebars'],
simple: true, simple: true,
root: 'foo/', root: 'foo/',
};
Precompiler.loadTemplates(opts, function (err, opts) {
equal(opts.templates[0].name, __dirname + '/artifacts/empty');
done(err);
}); });
expect(opts.templates[0].name).toBe(__dirname + '/artifacts/empty');
}); });
it('should accept string inputs', async function () { it('should accept string inputs', function (done) {
var opts = await loadTemplatesAsync({ string: '' }); var opts = { string: '' };
expect(opts.templates[0].name).toBeUndefined(); Precompiler.loadTemplates(opts, function (err, opts) {
expect(opts.templates[0].source).toBe(''); equal(opts.templates[0].name, undefined);
}); equal(opts.templates[0].source, '');
it('should accept string array inputs', async function () { done(err);
var opts = await loadTemplatesAsync({
string: ['', 'bar'],
name: ['beep', 'boop'],
}); });
expect(opts.templates[0].name).toBe('beep');
expect(opts.templates[0].source).toBe('');
expect(opts.templates[1].name).toBe('boop');
expect(opts.templates[1].source).toBe('bar');
}); });
it('should accept stdin input', async function () { it('should accept string array inputs', function (done) {
var opts = { string: ['', 'bar'], name: ['beep', 'boop'] };
Precompiler.loadTemplates(opts, function (err, opts) {
equal(opts.templates[0].name, 'beep');
equal(opts.templates[0].source, '');
equal(opts.templates[1].name, 'boop');
equal(opts.templates[1].source, 'bar');
done(err);
});
});
it('should accept stdin input', function (done) {
var stdin = require('mock-stdin').stdin(); var stdin = require('mock-stdin').stdin();
var promise = loadTemplatesAsync({ string: '-' }); Precompiler.loadTemplates({ string: '-' }, function (err, opts) {
equal(opts.templates[0].source, 'foo');
done(err);
});
stdin.send('fo'); stdin.send('fo');
stdin.send('o'); stdin.send('o');
stdin.end(); stdin.end();
var opts = await promise;
expect(opts.templates[0].source).toBe('foo');
}); });
it('error on name missing', async function () { it('error on name missing', function (done) {
try { var opts = { string: ['', 'bar'] };
await loadTemplatesAsync({ string: ['', 'bar'] }); Precompiler.loadTemplates(opts, function (err) {
throw new Error('should have thrown'); equal(
} catch (err) { err.message,
expect(err.message).toBe(
'Number of names did not match the number of string inputs' 'Number of names did not match the number of string inputs'
); );
} done();
});
}); });
it('should complete when no args are passed', async function () { it('should complete when no args are passed', function (done) {
var opts = await loadTemplatesAsync({}); Precompiler.loadTemplates({}, function (err, opts) {
expect(opts.templates.length).toBe(0); equal(opts.templates.length, 0);
done(err);
});
}); });
}); });
}); });
+15 -14
View File
@@ -337,10 +337,7 @@ describe('Regressions', function () {
}, },
}; };
expectTemplate('{{helpa length="foo"}}') shouldCompileTo('{{helpa length="foo"}}', [obj, helpers], 'foo');
.withInput(obj)
.withHelpers(helpers)
.toCompileTo('foo');
}); });
it('GH-1319: "unless" breaks when "each" value equals "null"', function () { it('GH-1319: "unless" breaks when "each" value equals "null"', function () {
@@ -376,16 +373,20 @@ describe('Regressions', function () {
var result = newHandlebarsInstance.templates['test.hbs']({ var result = newHandlebarsInstance.templates['test.hbs']({
name: 'yehuda', name: 'yehuda',
}); });
expect(result.trim()).toBe('YEHUDA'); equals(result.trim(), 'YEHUDA');
}); });
it('should call "helperMissing" if a helper is missing', function () { it('should call "helperMissing" if a helper is missing', function () {
var newHandlebarsInstance = Handlebars.create(); var newHandlebarsInstance = Handlebars.create();
expect(function () { shouldThrow(
registerTemplate(newHandlebarsInstance, compiledTemplateVersion7()); function () {
newHandlebarsInstance.templates['test.hbs']({}); registerTemplate(newHandlebarsInstance, compiledTemplateVersion7());
}).toThrow('Missing helper: "loud"'); newHandlebarsInstance.templates['test.hbs']({});
},
Handlebars.Exception,
'Missing helper: "loud"'
);
}); });
it('should pass "options.lookupProperty" to "lookup"-helper, even with old templates', function () { it('should pass "options.lookupProperty" to "lookup"-helper, even with old templates', function () {
@@ -402,7 +403,7 @@ describe('Regressions', function () {
property: 'a', property: 'a',
test: { a: 'b' }, test: { a: 'b' },
}) })
).toBe('b'); ).to.equal('b');
}); });
function registerTemplate(Handlebars, compileTemplate) { function registerTemplate(Handlebars, compileTemplate) {
@@ -478,19 +479,19 @@ describe('Regressions', function () {
newHandlebarsInstance = Handlebars.create(); newHandlebarsInstance = Handlebars.create();
}); });
afterEach(function () { afterEach(function () {
vi.restoreAllMocks(); sinon.restore();
}); });
it('should only compile global partials once', function () { it('should only compile global partials once', function () {
var templateSpy = vi.spyOn(newHandlebarsInstance, 'template'); var templateSpy = sinon.spy(newHandlebarsInstance, 'template');
newHandlebarsInstance.registerPartial({ newHandlebarsInstance.registerPartial({
dude: 'I am a partial', dude: 'I am a partial',
}); });
var string = 'Dudes: {{> dude}} {{> dude}}'; var string = 'Dudes: {{> dude}} {{> dude}}';
newHandlebarsInstance.compile(string)(); // This should compile template + partial once newHandlebarsInstance.compile(string)(); // This should compile template + partial once
newHandlebarsInstance.compile(string)(); // This should only compile template newHandlebarsInstance.compile(string)(); // This should only compile template
expect(templateSpy).toHaveBeenCalledTimes(3); equal(templateSpy.callCount, 3);
vi.restoreAllMocks(); sinon.restore();
}); });
}); });
+4 -4
View File
@@ -2,22 +2,22 @@ if (typeof require !== 'undefined' && require.extensions['.handlebars']) {
describe('Require', function () { describe('Require', function () {
it('Load .handlebars files with require()', function () { it('Load .handlebars files with require()', function () {
var template = require('./artifacts/example_1'); var template = require('./artifacts/example_1');
expect(template).toBe(require('./artifacts/example_1.handlebars')); equal(template, require('./artifacts/example_1.handlebars'));
var expected = 'foo\n'; var expected = 'foo\n';
var result = template({ foo: 'foo' }); var result = template({ foo: 'foo' });
expect(result).toBe(expected); equal(result, expected);
}); });
it('Load .hbs files with require()', function () { it('Load .hbs files with require()', function () {
var template = require('./artifacts/example_2'); var template = require('./artifacts/example_2');
expect(template).toBe(require('./artifacts/example_2.hbs')); equal(template, require('./artifacts/example_2.hbs'));
var expected = 'Hello, World!\n'; var expected = 'Hello, World!\n';
var result = template({ name: 'World' }); var result = template({ name: 'World' });
expect(result).toBe(expected); equal(result, expected);
}); });
}); });
} }
+50 -31
View File
@@ -1,55 +1,74 @@
describe('runtime', function () { describe('runtime', function () {
describe('#template', function () { describe('#template', function () {
it('should throw on invalid templates', function () { it('should throw on invalid templates', function () {
expect(function () { shouldThrow(
Handlebars.template({}); function () {
}).toThrow('Unknown template object: object'); Handlebars.template({});
expect(function () { },
Handlebars.template(); Error,
}).toThrow('Unknown template object: undefined'); 'Unknown template object: object'
expect(function () { );
Handlebars.template(''); shouldThrow(
}).toThrow('Unknown template object: string'); function () {
Handlebars.template();
},
Error,
'Unknown template object: undefined'
);
shouldThrow(
function () {
Handlebars.template('');
},
Error,
'Unknown template object: string'
);
}); });
it('should throw on version mismatch', function () { it('should throw on version mismatch', function () {
expect(function () { shouldThrow(
Handlebars.template({ function () {
main: {}, Handlebars.template({
compiler: [Handlebars.COMPILER_REVISION + 1], main: {},
}); compiler: [Handlebars.COMPILER_REVISION + 1],
}).toThrow( });
},
Error,
/Template was precompiled with a newer version of Handlebars than the current runtime/ /Template was precompiled with a newer version of Handlebars than the current runtime/
); );
expect(function () { shouldThrow(
Handlebars.template({ function () {
main: {}, Handlebars.template({
compiler: [Handlebars.LAST_COMPATIBLE_COMPILER_REVISION - 1], main: {},
}); compiler: [Handlebars.LAST_COMPATIBLE_COMPILER_REVISION - 1],
}).toThrow( });
},
Error,
/Template was precompiled with an older version of Handlebars than the current runtime/ /Template was precompiled with an older version of Handlebars than the current runtime/
); );
expect(function () { shouldThrow(
Handlebars.template({ function () {
main: {}, Handlebars.template({
}); main: {},
}).toThrow( });
},
Error,
/Template was precompiled with an older version of Handlebars than the current runtime/ /Template was precompiled with an older version of Handlebars than the current runtime/
); );
}); });
}); });
describe('#noConflict', function () { describe('#noConflict', function () {
if (!CompilerContext.browser) {
return;
}
it('should reset on no conflict', function () { it('should reset on no conflict', function () {
if (!CompilerContext.browser) {
return;
}
var reset = Handlebars; var reset = Handlebars;
Handlebars.noConflict(); Handlebars.noConflict();
expect(Handlebars).toBe('no-conflict'); equal(Handlebars, 'no-conflict');
Handlebars = 'really, none'; Handlebars = 'really, none';
reset.noConflict(); reset.noConflict();
expect(Handlebars).toBe('really, none'); equal(Handlebars, 'really, none');
Handlebars = reset; Handlebars = reset;
}); });
+43 -72
View File
@@ -57,8 +57,8 @@ describe('security issues', function () {
functionCalls.push('called'); functionCalls.push('called');
}, },
}); });
}).toThrow(); }).to.throw(Error);
expect(functionCalls.length).toBe(0); expect(functionCalls.length).to.equal(0);
}); });
it('should throw an exception when calling "{{#blockHelperMissing .}}{{/blockHelperMissing}}"', function () { it('should throw an exception when calling "{{#blockHelperMissing .}}{{/blockHelperMissing}}"', function () {
@@ -96,7 +96,7 @@ describe('security issues', function () {
}, },
{ allowCallsToHelperMissing: true } { allowCallsToHelperMissing: true }
); );
expect(functionCalls.length).toBe(1); equals(functionCalls.length, 1);
}); });
it('should not throw an exception when calling "{{#blockHelperMissing .}}{{/blockHelperMissing}}"', function () { it('should not throw an exception when calling "{{#blockHelperMissing .}}{{/blockHelperMissing}}"', function () {
@@ -109,23 +109,20 @@ describe('security issues', function () {
}); });
describe('GH-1563', function () { describe('GH-1563', function () {
var browserSupportsExploit = it('should not allow to access constructor after overriding via __defineGetter__', function () {
{}.__defineGetter__ != null && {}.__lookupGetter__ != null; if ({}.__defineGetter__ == null || {}.__lookupGetter__ == null) {
return this.skip(); // Browser does not support this exploit anyway
it.skipIf(!browserSupportsExploit)(
'should not allow to access constructor after overriding via __defineGetter__',
function () {
expectTemplate(
'{{__defineGetter__ "undefined" valueOf }}' +
'{{#with __lookupGetter__ }}' +
'{{__defineGetter__ "propertyIsEnumerable" (this.bind (this.bind 1)) }}' +
'{{constructor.name}}' +
'{{/with}}'
)
.withInput({})
.toThrow(/Missing helper: "__defineGetter__"/);
} }
); expectTemplate(
'{{__defineGetter__ "undefined" valueOf }}' +
'{{#with __lookupGetter__ }}' +
'{{__defineGetter__ "propertyIsEnumerable" (this.bind (this.bind 1)) }}' +
'{{constructor.name}}' +
'{{/with}}'
)
.withInput({})
.toThrow(/Missing helper: "__defineGetter__"/);
});
}); });
describe('GH-1595: dangerous properties', function () { describe('GH-1595: dangerous properties', function () {
@@ -172,7 +169,7 @@ describe('security issues', function () {
}); });
afterEach(function () { afterEach(function () {
vi.restoreAllMocks(); sinon.restore();
}); });
describe('control access to prototype methods via "allowedProtoMethods"', function () { describe('control access to prototype methods via "allowedProtoMethods"', function () {
@@ -184,25 +181,19 @@ describe('security issues', function () {
function checkProtoMethodAccess(compileOptions) { function checkProtoMethodAccess(compileOptions) {
it('should be prohibited by default and log a warning', function () { it('should be prohibited by default and log a warning', function () {
var spy = vi var spy = sinon.spy(console, 'error');
.spyOn(console, 'error')
.mockImplementation(function () {});
expectTemplate('{{aMethod}}') expectTemplate('{{aMethod}}')
.withInput(new TestClass()) .withInput(new TestClass())
.withCompileOptions(compileOptions) .withCompileOptions(compileOptions)
.toCompileTo(''); .toCompileTo('');
expect(spy).toHaveBeenCalledTimes(1); expect(spy.calledOnce).to.be.true();
expect(spy.mock.calls[0][0]).toMatch( expect(spy.args[0][0]).to.match(/Handlebars: Access has been denied/);
/Handlebars: Access has been denied/
);
}); });
it('should only log the warning once', function () { it('should only log the warning once', function () {
var spy = vi var spy = sinon.spy(console, 'error');
.spyOn(console, 'error')
.mockImplementation(function () {});
expectTemplate('{{aMethod}}') expectTemplate('{{aMethod}}')
.withInput(new TestClass()) .withInput(new TestClass())
@@ -214,16 +205,12 @@ describe('security issues', function () {
.withCompileOptions(compileOptions) .withCompileOptions(compileOptions)
.toCompileTo(''); .toCompileTo('');
expect(spy).toHaveBeenCalledTimes(1); expect(spy.calledOnce).to.be.true();
expect(spy.mock.calls[0][0]).toMatch( expect(spy.args[0][0]).to.match(/Handlebars: Access has been denied/);
/Handlebars: Access has been denied/
);
}); });
it('can be allowed, which disables the warning', function () { it('can be allowed, which disables the warning', function () {
var spy = vi var spy = sinon.spy(console, 'error');
.spyOn(console, 'error')
.mockImplementation(function () {});
expectTemplate('{{aMethod}}') expectTemplate('{{aMethod}}')
.withInput(new TestClass()) .withInput(new TestClass())
@@ -235,13 +222,11 @@ describe('security issues', function () {
}) })
.toCompileTo('returnValue'); .toCompileTo('returnValue');
expect(spy).not.toHaveBeenCalled(); expect(spy.callCount).to.equal(0);
}); });
it('can be turned on by default, which disables the warning', function () { it('can be turned on by default, which disables the warning', function () {
var spy = vi var spy = sinon.spy(console, 'error');
.spyOn(console, 'error')
.mockImplementation(function () {});
expectTemplate('{{aMethod}}') expectTemplate('{{aMethod}}')
.withInput(new TestClass()) .withInput(new TestClass())
@@ -251,13 +236,11 @@ describe('security issues', function () {
}) })
.toCompileTo('returnValue'); .toCompileTo('returnValue');
expect(spy).not.toHaveBeenCalled(); expect(spy.callCount).to.equal(0);
}); });
it('can be turned off by default, which disables the warning', function () { it('can be turned off by default, which disables the warning', function () {
var spy = vi var spy = sinon.spy(console, 'error');
.spyOn(console, 'error')
.mockImplementation(function () {});
expectTemplate('{{aMethod}}') expectTemplate('{{aMethod}}')
.withInput(new TestClass()) .withInput(new TestClass())
@@ -267,7 +250,7 @@ describe('security issues', function () {
}) })
.toCompileTo(''); .toCompileTo('');
expect(spy).not.toHaveBeenCalled(); expect(spy.callCount).to.equal(0);
}); });
it('can be turned off, if turned on by default', function () { it('can be turned off, if turned on by default', function () {
@@ -317,25 +300,19 @@ describe('security issues', function () {
function checkProtoPropertyAccess(compileOptions) { function checkProtoPropertyAccess(compileOptions) {
it('should be prohibited by default and log a warning', function () { it('should be prohibited by default and log a warning', function () {
var spy = vi var spy = sinon.spy(console, 'error');
.spyOn(console, 'error')
.mockImplementation(function () {});
expectTemplate('{{aProperty}}') expectTemplate('{{aProperty}}')
.withInput(new TestClass()) .withInput(new TestClass())
.withCompileOptions(compileOptions) .withCompileOptions(compileOptions)
.toCompileTo(''); .toCompileTo('');
expect(spy).toHaveBeenCalledTimes(1); expect(spy.calledOnce).to.be.true();
expect(spy.mock.calls[0][0]).toMatch( expect(spy.args[0][0]).to.match(/Handlebars: Access has been denied/);
/Handlebars: Access has been denied/
);
}); });
it('can be explicitly prohibited by default, which disables the warning', function () { it('can be explicitly prohibited by default, which disables the warning', function () {
var spy = vi var spy = sinon.spy(console, 'error');
.spyOn(console, 'error')
.mockImplementation(function () {});
expectTemplate('{{aProperty}}') expectTemplate('{{aProperty}}')
.withInput(new TestClass()) .withInput(new TestClass())
@@ -345,13 +322,11 @@ describe('security issues', function () {
}) })
.toCompileTo(''); .toCompileTo('');
expect(spy).not.toHaveBeenCalled(); expect(spy.callCount).to.equal(0);
}); });
it('can be turned on, which disables the warning', function () { it('can be turned on, which disables the warning', function () {
var spy = vi var spy = sinon.spy(console, 'error');
.spyOn(console, 'error')
.mockImplementation(function () {});
expectTemplate('{{aProperty}}') expectTemplate('{{aProperty}}')
.withInput(new TestClass()) .withInput(new TestClass())
@@ -363,13 +338,11 @@ describe('security issues', function () {
}) })
.toCompileTo('propertyValue'); .toCompileTo('propertyValue');
expect(spy).not.toHaveBeenCalled(); expect(spy.callCount).to.equal(0);
}); });
it('can be turned on by default, which disables the warning', function () { it('can be turned on by default, which disables the warning', function () {
var spy = vi var spy = sinon.spy(console, 'error');
.spyOn(console, 'error')
.mockImplementation(function () {});
expectTemplate('{{aProperty}}') expectTemplate('{{aProperty}}')
.withInput(new TestClass()) .withInput(new TestClass())
@@ -379,7 +352,7 @@ describe('security issues', function () {
}) })
.toCompileTo('propertyValue'); .toCompileTo('propertyValue');
expect(spy).not.toHaveBeenCalled(); expect(spy.callCount).to.equal(0);
}); });
it('can be turned off, if turned on by default', function () { it('can be turned off, if turned on by default', function () {
@@ -400,16 +373,14 @@ describe('security issues', function () {
describe('compatibility with old runtimes, that do not provide the function "container.lookupProperty"', function () { describe('compatibility with old runtimes, that do not provide the function "container.lookupProperty"', function () {
beforeEach(function simulateRuntimeWithoutLookupProperty() { beforeEach(function simulateRuntimeWithoutLookupProperty() {
var oldTemplateMethod = handlebarsEnv.template; var oldTemplateMethod = handlebarsEnv.template;
vi.spyOn(handlebarsEnv, 'template').mockImplementation( sinon.replace(handlebarsEnv, 'template', function (templateSpec) {
function (templateSpec) { templateSpec.main = wrapToAdjustContainer(templateSpec.main);
templateSpec.main = wrapToAdjustContainer(templateSpec.main); return oldTemplateMethod.call(this, templateSpec);
return oldTemplateMethod.call(this, templateSpec); });
}
);
}); });
afterEach(function () { afterEach(function () {
vi.restoreAllMocks(); sinon.restore();
}); });
it('should work with simple properties', function () { it('should work with simple properties', function () {
+7 -12
View File
@@ -3,7 +3,7 @@ try {
var SourceMap = require('source-map'), var SourceMap = require('source-map'),
SourceMapConsumer = SourceMap.SourceMapConsumer; SourceMapConsumer = SourceMap.SourceMapConsumer;
} }
} catch { } catch (err) {
/* NOP for in browser */ /* NOP for in browser */
} }
@@ -18,14 +18,10 @@ describe('source-map', function () {
srcName: 'src.hbs', srcName: 'src.hbs',
}); });
expect(template.code).toBeTruthy(); equal(!!template.code, true);
if (CompilerContext.browser) { equal(!!template.map, !CompilerContext.browser);
expect(template.map).toBeFalsy();
} else {
expect(template.map).toBeTruthy();
}
}); });
it('should map source properly', async function () { it('should map source properly', function () {
var templateSource = var templateSource =
' b{{hello}} \n {{bar}}a {{#block arg hash=(subex 1 subval)}}{{/block}}', ' b{{hello}} \n {{bar}}a {{#block arg hash=(subex 1 subval)}}{{/block}}',
template = Handlebars.precompile(templateSource, { template = Handlebars.precompile(templateSource, {
@@ -34,16 +30,15 @@ describe('source-map', function () {
}); });
if (template.map) { if (template.map) {
var consumer = await new SourceMapConsumer(template.map), var consumer = new SourceMapConsumer(template.map),
lines = template.code.split('\n'), lines = template.code.split('\n'),
srcLines = templateSource.split('\n'), srcLines = templateSource.split('\n'),
generated = grepLine('" b"', lines), generated = grepLine('" b"', lines),
source = grepLine(' b', srcLines); source = grepLine(' b', srcLines);
var mapped = consumer.originalPositionFor(generated); var mapped = consumer.originalPositionFor(generated);
expect(mapped.line).toBe(source.line); equal(mapped.line, source.line);
expect(mapped.column).toBe(source.column); equal(mapped.column, source.column);
consumer.destroy();
} }
}); });
}); });
+6 -6
View File
@@ -92,8 +92,8 @@ describe('strict', function () {
}) })
.withHelpers({ .withHelpers({
helper: function (options) { helper: function (options) {
expect(options.hash).toHaveProperty('value'); equals('value' in options.hash, true);
expect(options.hash.value).toBeUndefined(); equals(options.hash.value, undefined);
return 'success'; return 'success';
}, },
}) })
@@ -113,10 +113,10 @@ describe('strict', function () {
}); });
template({}); template({});
} catch (error) { } catch (error) {
expect(error.lineNumber).toBe(4); equals(error.lineNumber, 4);
expect(error.endLineNumber).toBe(4); equals(error.endLineNumber, 4);
expect(error.column).toBe(5); equals(error.column, 5);
expect(error.endColumn).toBe(10); equals(error.endColumn, 10);
} }
}); });
}); });
+3 -3
View File
@@ -1,11 +1,11 @@
function shouldMatchTokens(result, tokens) { function shouldMatchTokens(result, tokens) {
for (var index = 0; index < result.length; index++) { for (var index = 0; index < result.length; index++) {
expect(result[index].name).toBe(tokens[index]); equals(result[index].name, tokens[index]);
} }
} }
function shouldBeToken(result, name, text) { function shouldBeToken(result, name, text) {
expect(result.name).toBe(name); equals(result.name, name);
expect(result.text).toBe(text); equals(result.text, text);
} }
describe('Tokenizer', function () { describe('Tokenizer', function () {
+79 -46
View File
@@ -1,58 +1,91 @@
<!doctype html>
<html> <html>
<head> <head>
<title>Handlebars Runtime UMD Smoke Test</title> <title>Mocha</title>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" /> <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0">
</head>
<body>
<h1>Handlebars Runtime UMD Smoke Test</h1>
<pre id="results"></pre>
<script src="/spec/vendor/require.js"></script>
<script>
var results = document.getElementById('results');
var failures = 0;
var tests = 0;
function assert(condition, message) { <link rel="stylesheet" href="/node_modules/mocha/mocha.css" />
tests++; <style>
if (!condition) { .headless .suite > h1,
failures++; .headless .test.pass {
results.textContent += 'FAIL: ' + message + '\n'; display: none;
} else {
results.textContent += 'PASS: ' + message + '\n';
}
} }
</style>
<script>
// Show only errors in "headless", non-interactive mode.
if (/headless=true/.test(location.href)) {
document.documentElement.className = 'headless';
}
</script>
<script src="/node_modules/sinon/pkg/sinon.js"></script>
<script src="/node_modules/chai/chai.js"></script>
<script src="/node_modules/dirty-chai/lib/dirty-chai.js"></script>
<script src="/node_modules/mocha/mocha.js"></script>
<script>
window.expect = chai.expect;
mocha.setup('bdd');
</script>
<script src="/spec/vendor/require.js"></script>
<script src="/spec/env/common.js"></script>
<script>
requirejs.config({ requirejs.config({
paths: { paths: {
'handlebars.runtime': '/dist/handlebars.runtime', 'handlebars.runtime': '/dist/handlebars.runtime'
},
});
require(['handlebars.runtime'], function (Handlebars) {
try {
assert(
typeof Handlebars !== 'undefined',
'Handlebars runtime loaded via RequireJS'
);
assert(
typeof Handlebars.template === 'function',
'Handlebars.template exists'
);
assert(
typeof Handlebars.VERSION === 'string',
'Handlebars.VERSION exists'
);
} catch (e) {
failures++;
results.textContent += 'ERROR: ' + e.message + '\n';
} }
results.textContent +=
'\n' + tests + ' tests, ' + failures + ' failures\n';
window.mochaResults = { passes: tests - failures, failures: failures };
}); });
</script> </script>
<script>
onload = function(){
require(['handlebars.runtime'], function(Handlebars) {
describe('runtime', function() {
it('should load', function() {
equal(!!Handlebars.template, true);
equal(!!Handlebars.VERSION, true);
});
});
mocha.globals(['mochaResults'])
// The test harness leaks under FF. We should have decent global leak coverage from other tests
if (!navigator.userAgent.match(/Firefox\/([\d.]+)/)) {
mocha.checkLeaks();
}
var runner = mocha.run();
// Reporting to test-runner
var failedTests = [];
runner.on('end', function(){
window.mochaResults = runner.stats;
window.mochaResults.reports = failedTests;
});
runner.on('fail', logFailure);
function logFailure(test, err){
var flattenTitles = function(test){
var titles = [];
while (test.parent.title){
titles.push(test.parent.title);
test = test.parent;
}
return titles.reverse();
};
failedTests.push({
name: test.title,
result: false,
message: err.message,
stack: err.stack,
titles: flattenTitles(test)
});
}
});
};
</script>
</head>
<body>
<div id="mocha"></div>
</body> </body>
</html> </html>
+99 -53
View File
@@ -1,66 +1,112 @@
<!doctype html>
<html> <html>
<head> <head>
<title>Handlebars UMD Module Smoke Test</title> <title>Mocha</title>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" /> <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0">
</head>
<body>
<h1>Handlebars UMD Module Smoke Test</h1>
<pre id="results"></pre>
<script src="/spec/vendor/require.js"></script>
<script>
var results = document.getElementById('results');
var failures = 0;
var tests = 0;
function assert(condition, message) { <link rel="stylesheet" href="/node_modules/mocha/mocha.css" />
tests++; <style>
if (!condition) { .headless .suite > h1,
failures++; .headless .test.pass {
results.textContent += 'FAIL: ' + message + '\n'; display: none;
} else {
results.textContent += 'PASS: ' + message + '\n';
}
} }
</style>
<script>
// Show only errors in "headless", non-interactive mode.
if (/headless=true/.test(location.href)) {
document.documentElement.className = 'headless';
}
</script>
<script src="/node_modules/sinon/pkg/sinon.js"></script>
<script src="/node_modules/chai/chai.js"></script>
<script src="/node_modules/dirty-chai/lib/dirty-chai.js"></script>
<script src="/node_modules/mocha/mocha.js"></script>
<script>
window.expect = chai.expect;
mocha.setup('bdd');
</script>
<script src="/spec/vendor/require.js"></script>
<script src="/spec/env/common.js"></script>
<script>
requirejs.config({ requirejs.config({
paths: { paths: {
handlebars: '/dist/handlebars', handlebars: '/dist/handlebars',
}, tests: '/tmp/tests'
});
require(['handlebars'], function (Handlebars) {
try {
assert(
typeof Handlebars !== 'undefined',
'Handlebars loaded via RequireJS'
);
assert(
typeof Handlebars.compile === 'function',
'Handlebars.compile exists'
);
assert(
typeof Handlebars.template === 'function',
'Handlebars.template exists'
);
assert(
typeof Handlebars.VERSION === 'string',
'Handlebars.VERSION exists'
);
var template = Handlebars.compile('Hello {{name}}!');
var output = template({ name: 'UMD' });
assert(output === 'Hello UMD!', 'Basic compilation works: ' + output);
} catch (e) {
failures++;
results.textContent += 'ERROR: ' + e.message + '\n';
} }
results.textContent +=
'\n' + tests + ' tests, ' + failures + ' failures\n';
window.mochaResults = { passes: tests - failures, failures: failures };
}); });
var CompilerContext = {
compile: function(template, options) {
var templateSpec = handlebarsEnv.precompile(template, options);
return handlebarsEnv.template(safeEval(templateSpec));
},
compileWithPartial: function(template, options) {
return handlebarsEnv.compile(template, options);
}
};
function safeEval(templateSpec) {
try {
var ret;
eval('ret = ' + templateSpec);
return ret;
} catch (err) {
console.error(templateSpec);
throw err;
}
}
</script> </script>
<script>
onload = function(){
require(['handlebars'], function(Handlebars) {
window.Handlebars = Handlebars;
require(['tests'], function() {
mocha.globals(['mochaResults'])
// The test harness leaks under FF. We should have decent global leak coverage from other tests
if (!navigator.userAgent.match(/Firefox\/([\d.]+)/)) {
mocha.checkLeaks();
}
var runner = mocha.run();
// Reporting to test-runner
var failedTests = [];
runner.on('end', function(){
window.mochaResults = runner.stats;
window.mochaResults.reports = failedTests;
});
runner.on('fail', logFailure);
function logFailure(test, err){
var flattenTitles = function(test){
var titles = [];
while (test.parent.title){
titles.push(test.parent.title);
test = test.parent;
}
return titles.reverse();
};
failedTests.push({
name: test.title,
result: false,
message: err.message,
stack: err.stack,
titles: flattenTitles(test)
});
};
});
});
};
</script>
</head>
<body>
<div id="mocha"></div>
</body> </body>
</html> </html>
+28 -40
View File
@@ -5,7 +5,11 @@ describe('utils', function () {
if (!(safe instanceof Handlebars.SafeString)) { if (!(safe instanceof Handlebars.SafeString)) {
throw new Error('Must be instance of SafeString'); throw new Error('Must be instance of SafeString');
} }
expect(safe.toString()).toBe('testing 1, 2, 3'); equals(
safe.toString(),
'testing 1, 2, 3',
'SafeString is equivalent to its underlying string'
);
}); });
it('it should not escape SafeString properties', function () { it('it should not escape SafeString properties', function () {
@@ -19,50 +23,51 @@ describe('utils', function () {
describe('#escapeExpression', function () { describe('#escapeExpression', function () {
it('should escape html', function () { it('should escape html', function () {
expect(Handlebars.Utils.escapeExpression('foo<&"\'>')).toBe( equals(
Handlebars.Utils.escapeExpression('foo<&"\'>'),
'foo&lt;&amp;&quot;&#x27;&gt;' 'foo&lt;&amp;&quot;&#x27;&gt;'
); );
expect(Handlebars.Utils.escapeExpression('foo=')).toBe('foo&#x3D;'); equals(Handlebars.Utils.escapeExpression('foo='), 'foo&#x3D;');
}); });
it('should not escape SafeString', function () { it('should not escape SafeString', function () {
var string = new Handlebars.SafeString('foo<&"\'>'); var string = new Handlebars.SafeString('foo<&"\'>');
expect(Handlebars.Utils.escapeExpression(string)).toBe('foo<&"\'>'); equals(Handlebars.Utils.escapeExpression(string), 'foo<&"\'>');
var obj = { var obj = {
toHTML: function () { toHTML: function () {
return 'foo<&"\'>'; return 'foo<&"\'>';
}, },
}; };
expect(Handlebars.Utils.escapeExpression(obj)).toBe('foo<&"\'>'); equals(Handlebars.Utils.escapeExpression(obj), 'foo<&"\'>');
}); });
it('should handle falsy', function () { it('should handle falsy', function () {
expect(Handlebars.Utils.escapeExpression('')).toBe(''); equals(Handlebars.Utils.escapeExpression(''), '');
expect(Handlebars.Utils.escapeExpression(undefined)).toBe(''); equals(Handlebars.Utils.escapeExpression(undefined), '');
expect(Handlebars.Utils.escapeExpression(null)).toBe(''); equals(Handlebars.Utils.escapeExpression(null), '');
expect(Handlebars.Utils.escapeExpression(false)).toBe('false'); equals(Handlebars.Utils.escapeExpression(false), 'false');
expect(Handlebars.Utils.escapeExpression(0)).toBe('0'); equals(Handlebars.Utils.escapeExpression(0), '0');
}); });
it('should handle empty objects', function () { it('should handle empty objects', function () {
expect(Handlebars.Utils.escapeExpression({})).toBe({}.toString()); equals(Handlebars.Utils.escapeExpression({}), {}.toString());
expect(Handlebars.Utils.escapeExpression([])).toBe([].toString()); equals(Handlebars.Utils.escapeExpression([]), [].toString());
}); });
}); });
describe('#isEmpty', function () { describe('#isEmpty', function () {
it('should not be empty', function () { it('should not be empty', function () {
expect(Handlebars.Utils.isEmpty(undefined)).toBe(true); equals(Handlebars.Utils.isEmpty(undefined), true);
expect(Handlebars.Utils.isEmpty(null)).toBe(true); equals(Handlebars.Utils.isEmpty(null), true);
expect(Handlebars.Utils.isEmpty(false)).toBe(true); equals(Handlebars.Utils.isEmpty(false), true);
expect(Handlebars.Utils.isEmpty('')).toBe(true); equals(Handlebars.Utils.isEmpty(''), true);
expect(Handlebars.Utils.isEmpty([])).toBe(true); equals(Handlebars.Utils.isEmpty([]), true);
}); });
it('should be empty', function () { it('should be empty', function () {
expect(Handlebars.Utils.isEmpty(0)).toBe(false); equals(Handlebars.Utils.isEmpty(0), false);
expect(Handlebars.Utils.isEmpty([1])).toBe(false); equals(Handlebars.Utils.isEmpty([1]), false);
expect(Handlebars.Utils.isEmpty('foo')).toBe(false); equals(Handlebars.Utils.isEmpty('foo'), false);
expect(Handlebars.Utils.isEmpty({ bar: 1 })).toBe(false); equals(Handlebars.Utils.isEmpty({ bar: 1 }), false);
}); });
}); });
@@ -77,25 +82,8 @@ describe('utils', function () {
Handlebars.Utils.extend(b, new A()); Handlebars.Utils.extend(b, new A());
expect(b.a).toBe(1); equals(b.a, 1);
expect(b.b).toBe(2); equals(b.b, 2);
});
});
describe('#isType', function () {
it('should check if variable is type Array', function () {
expect(Handlebars.Utils.isArray('string')).toBe(false);
expect(Handlebars.Utils.isArray([])).toBe(true);
});
it('should check if variable is type Map', function () {
expect(Handlebars.Utils.isMap('string')).toBe(false);
expect(Handlebars.Utils.isMap(new Map())).toBe(true);
});
it('should check if variable is type Set', function () {
expect(Handlebars.Utils.isSet('string')).toBe(false);
expect(Handlebars.Utils.isSet(new Set())).toBe(true);
}); });
}); });
}); });
+8
View File
@@ -0,0 +1,8 @@
module.exports = {
rules: {
'no-process-env': 'off',
'prefer-const': 'warn',
'compat/compat': 'off',
'dot-notation': ['error', { allowKeywords: true }],
},
};
+29
View File
@@ -0,0 +1,29 @@
const metrics = require('../tests/bench');
const { createRegisterAsyncTaskFn } = require('./util/async-grunt-task');
module.exports = function (grunt) {
const registerAsyncTask = createRegisterAsyncTaskFn(grunt);
registerAsyncTask('metrics', function () {
const onlyExecuteName = grunt.option('name');
const events = {};
const promises = Object.keys(metrics).map(async (name) => {
if (/^_/.test(name)) {
return;
}
if (onlyExecuteName != null && name !== onlyExecuteName) {
return;
}
return new Promise((resolve) => {
metrics[name](grunt, function (data) {
events[name] = data;
resolve();
});
});
});
return Promise.all(promises);
});
};
+78 -99
View File
@@ -1,101 +1,96 @@
/* eslint-disable no-console */ const AWS = require('aws-sdk');
const fs = require('fs');
const { S3, PutObjectCommand } = require('@aws-sdk/client-s3');
const git = require('./util/git'); const git = require('./util/git');
const { createRegisterAsyncTaskFn } = require('./util/async-grunt-task');
const semver = require('semver'); const semver = require('semver');
const PUBLISHED_FILES = [ module.exports = function (grunt) {
'handlebars.js', const registerAsyncTask = createRegisterAsyncTaskFn(grunt);
'handlebars.min.js',
'handlebars.runtime.js',
'handlebars.runtime.min.js',
];
let s3Client; registerAsyncTask('publish-to-aws', async () => {
grunt.log.writeln('remotes: ' + (await git.remotes()));
grunt.log.writeln('branches: ' + (await git.branches()));
async function main() { const commitInfo = await git.commitInfo();
console.log('remotes: ' + (await git.remotes())); grunt.log.writeln('tag: ', commitInfo.tagName);
console.log('branches: ' + (await git.branches()));
const commitInfo = await git.commitInfo(); const suffixes = [];
console.log('tag: ', commitInfo.tagName);
const suffixes = buildSuffixes(commitInfo); // Publish the master as "latest" and with the commit-id
if (commitInfo.isMaster) {
suffixes.push('-latest');
suffixes.push('-' + commitInfo.headSha);
}
if (suffixes.length > 0) { // Publish tags by their tag-name
validateS3Env(); if (commitInfo.tagName != null && semver.valid(commitInfo.tagName)) {
console.log('publishing file-suffixes: ' + JSON.stringify(suffixes)); suffixes.push('-' + commitInfo.tagName);
await publish(suffixes); }
}
}
function buildSuffixes(commitInfo) { if (suffixes.length > 0) {
const suffixes = []; initSDK();
grunt.log.writeln(
if (commitInfo.isMaster) { 'publishing file-suffixes: ' + JSON.stringify(suffixes)
suffixes.push('-latest'); );
suffixes.push('-' + commitInfo.headSha); await publish(suffixes);
} }
if (commitInfo.tagName != null && semver.valid(commitInfo.tagName)) {
suffixes.push('-' + commitInfo.tagName);
}
return suffixes;
}
function validateS3Env() {
const bucket = process.env.S3_BUCKET_NAME,
region = process.env.S3_REGION,
key = process.env.S3_ACCESS_KEY_ID,
secret = process.env.S3_SECRET_ACCESS_KEY;
if (!bucket || !region || !key || !secret) {
throw new Error('Missing S3 config values');
}
}
async function publish(suffixes, overrides) {
const publishPromises = suffixes.map((suffix) =>
publishSuffix(suffix, overrides)
);
return Promise.all(publishPromises);
}
async function publishSuffix(suffix, overrides) {
const publishPromises = PUBLISHED_FILES.map(async (filename) => {
const nameInBucket = getNameInBucket(filename, suffix);
const localFile = getLocalFile(filename);
await uploadToBucket(localFile, nameInBucket, overrides);
console.log(`Published ${localFile} to build server (${nameInBucket})`);
}); });
return Promise.all(publishPromises);
}
async function uploadToBucket(localFile, nameInBucket, overrides) { function initSDK() {
const s3 = overrides?.s3Client ?? getS3Client(); const bucket = process.env.S3_BUCKET_NAME,
const bucket = overrides?.bucket ?? process.env.S3_BUCKET_NAME; key = process.env.S3_ACCESS_KEY_ID,
secret = process.env.S3_SECRET_ACCESS_KEY;
return s3.send( if (!bucket || !key || !secret) {
new PutObjectCommand({ throw new Error('Missing S3 config values');
}
AWS.config.update({ accessKeyId: key, secretAccessKey: secret });
}
async function publish(suffixes) {
const publishPromises = suffixes.map((suffix) => publishSuffix(suffix));
return Promise.all(publishPromises);
}
async function publishSuffix(suffix) {
const filenames = [
'handlebars.js',
'handlebars.min.js',
'handlebars.runtime.js',
'handlebars.runtime.min.js',
];
const publishPromises = filenames.map(async (filename) => {
const nameInBucket = getNameInBucket(filename, suffix);
const localFile = getLocalFile(filename);
await uploadToBucket(localFile, nameInBucket);
grunt.log.writeln(
`Published ${localFile} to build server (${nameInBucket})`
);
});
return Promise.all(publishPromises);
}
async function uploadToBucket(localFile, nameInBucket) {
const bucket = process.env.S3_BUCKET_NAME;
const uploadParams = {
Bucket: bucket, Bucket: bucket,
Key: nameInBucket, Key: nameInBucket,
Body: fs.readFileSync(localFile, 'utf8'), Body: grunt.file.read(localFile),
}) };
); return s3PutObject(uploadParams);
}
function getS3Client() {
if (!s3Client) {
s3Client = new S3({
region: process.env.S3_REGION,
credentials: {
accessKeyId: process.env.S3_ACCESS_KEY_ID,
secretAccessKey: process.env.S3_SECRET_ACCESS_KEY,
},
});
} }
return s3Client; };
function s3PutObject(uploadParams) {
const s3 = new AWS.S3();
return new Promise((resolve, reject) => {
s3.putObject(uploadParams, (err) => {
if (err != null) {
return reject(err);
}
resolve();
});
});
} }
function getNameInBucket(filename, suffix) { function getNameInBucket(filename, suffix) {
@@ -105,19 +100,3 @@ function getNameInBucket(filename, suffix) {
function getLocalFile(filename) { function getLocalFile(filename) {
return 'dist/' + filename; return 'dist/' + filename;
} }
module.exports = {
PUBLISHED_FILES,
buildSuffixes,
validateS3Env,
publish,
getNameInBucket,
getLocalFile,
};
if (require.main === module) {
main().catch((err) => {
console.error(err);
process.exit(1);
});
}
+242
View File
@@ -0,0 +1,242 @@
const childProcess = require('child_process');
const fs = require('fs');
const os = require('os');
const path = require('path');
const chai = require('chai');
chai.use(require('chai-diff'));
const expect = chai.expect;
const testCases = [
{
binInputParameters: ['-a', 'spec/artifacts/empty.handlebars'],
outputLocation: 'stdout',
expectedOutputSpec: './spec/expected/empty.amd.js',
},
{
binInputParameters: [
'-a',
'-f',
'TEST_OUTPUT',
'spec/artifacts/empty.handlebars',
],
outputLocation: 'TEST_OUTPUT',
expectedOutputSpec: './spec/expected/empty.amd.js',
},
{
binInputParameters: [
'-a',
'-n',
'CustomNamespace.templates',
'spec/artifacts/empty.handlebars',
],
outputLocation: 'stdout',
expectedOutputSpec: './spec/expected/empty.amd.namespace.js',
},
{
binInputParameters: [
'-a',
'--namespace',
'CustomNamespace.templates',
'spec/artifacts/empty.handlebars',
],
outputLocation: 'stdout',
expectedOutputSpec: './spec/expected/empty.amd.namespace.js',
},
{
binInputParameters: ['-a', '-s', 'spec/artifacts/empty.handlebars'],
outputLocation: 'stdout',
expectedOutputSpec: './spec/expected/empty.amd.simple.js',
},
{
binInputParameters: ['-a', '-m', 'spec/artifacts/empty.handlebars'],
outputLocation: 'stdout',
expectedOutputSpec: './spec/expected/empty.amd.min.js',
},
{
binInputParameters: [
'spec/artifacts/known.helpers.handlebars',
'-a',
'-k',
'someHelper',
'-k',
'anotherHelper',
'-o',
],
outputLocation: 'stdout',
expectedOutputSpec: './spec/expected/non.empty.amd.known.helper.js',
},
{
binInputParameters: ['--help'],
outputLocation: 'stdout',
expectedOutputSpec: './spec/expected/help.menu.txt',
},
{
binInputParameters: ['-v'],
outputLocation: 'stdout',
expectedOutput: require('../package.json').version,
},
{
binInputParameters: [
'-a',
'-e',
'hbs',
'./spec/artifacts/non.default.extension.hbs',
],
outputLocation: 'stdout',
expectedOutputSpec: './spec/expected/non.default.extension.amd.js',
},
{
binInputParameters: [
'-a',
'-p',
'./spec/artifacts/partial.template.handlebars',
],
outputLocation: 'stdout',
expectedOutputSpec: './spec/expected/partial.template.js',
},
{
binInputParameters: ['spec/artifacts/empty.handlebars', '-c'],
outputLocation: 'stdout',
expectedOutputSpec: './spec/expected/empty.common.js',
},
{
binInputParameters: [
'spec/artifacts/empty.handlebars',
'spec/artifacts/empty.handlebars',
'-a',
'-n',
'someNameSpace',
],
outputLocation: 'stdout',
expectedOutputSpec: './spec/expected/namespace.amd.js',
},
{
binInputParameters: [
'spec/artifacts/empty.handlebars',
'-h',
'some-path/',
'-a',
],
outputLocation: 'stdout',
expectedOutputSpec: './spec/expected/handlebar.path.amd.js',
},
{
binInputParameters: [
'spec/artifacts/partial.template.handlebars',
'-r',
'spec',
'-a',
],
outputLocation: 'stdout',
expectedOutputSpec: './spec/expected/empty.root.amd.js',
},
{
binInputParameters: [
'-i',
'<div>1</div>',
'-i',
'<div>2</div>',
'-N',
'firstTemplate',
'-N',
'secondTemplate',
'-a',
],
outputLocation: 'stdout',
expectedOutputSpec: './spec/expected/empty.name.amd.js',
},
{
binInputParameters: [
'-i',
'<div>1</div>',
'-a',
'-m',
'-N',
'test',
'--map',
'./spec/tmp/source.map.amd.txt',
],
outputLocation: 'stdout',
expectedOutputSpec: './spec/expected/source.map.amd.js',
},
{
binInputParameters: ['./spec/artifacts/bom.handlebars', '-b', '-a'],
outputLocation: 'stdout',
expectedOutputSpec: './spec/expected/bom.amd.js',
},
// Issue #1673
{
binInputParameters: [
'--amd',
'--no-amd',
'spec/artifacts/empty.handlebars',
],
outputLocation: 'stdout',
expectedOutputSpec: './spec/expected/empty.common.js',
},
];
module.exports = function (grunt) {
grunt.registerTask('test:bin', function () {
testCases.forEach(
({
binInputParameters,
outputLocation,
expectedOutputSpec,
expectedOutput,
}) => {
const stdout = executeBinHandlebars(...binInputParameters);
if (!expectedOutput && expectedOutputSpec) {
expectedOutput = fs.readFileSync(expectedOutputSpec, 'utf-8');
}
const useStdout = outputLocation === 'stdout';
const normalizedOutput = normalizeCrlf(
useStdout ? stdout : fs.readFileSync(outputLocation, 'utf-8')
);
const normalizedExpectedOutput = normalizeCrlf(expectedOutput);
if (!useStdout) {
fs.unlinkSync(outputLocation);
}
expect(normalizedOutput).not.to.be.differentFrom(
normalizedExpectedOutput,
{
relaxedSpace: true,
}
);
}
);
});
};
// helper functions
function executeBinHandlebars(...args) {
if (os.platform() === 'win32') {
// On Windows, the executable handlebars.js file cannot be run directly
const nodeJs = process.argv[0];
return execFilesSyncUtf8(nodeJs, ['./bin/handlebars.js'].concat(args));
}
return execFilesSyncUtf8('./bin/handlebars.js', args);
}
function execFilesSyncUtf8(command, args) {
const env = process.env;
env.PATH = addPathToNodeJs(env.PATH);
return childProcess.execFileSync(command, args, { encoding: 'utf-8', env });
}
function addPathToNodeJs(pathEnvironment) {
return path.dirname(process.argv0) + path.delimiter + pathEnvironment;
}
function normalizeCrlf(string) {
if (typeof string === 'string') {
return string.replace(/\r\n/g, '\n');
}
return string;
}
+22
View File
@@ -0,0 +1,22 @@
const { execNodeJsScriptWithInheritedOutput } = require('./util/exec-file');
const { createRegisterAsyncTaskFn } = require('./util/async-grunt-task');
const nodeJs = process.argv0;
module.exports = function (grunt) {
const registerAsyncTask = createRegisterAsyncTaskFn(grunt);
registerAsyncTask('test:mocha', async () =>
execNodeJsScriptWithInheritedOutput('./spec/env/runner')
);
registerAsyncTask('test:cov', async () =>
execNodeJsScriptWithInheritedOutput('node_modules/nyc/bin/nyc', [
nodeJs,
'./spec/env/runner.js',
])
);
registerAsyncTask('test:min', async () =>
execNodeJsScriptWithInheritedOutput('./spec/env/runner', ['--min'])
);
};
+5
View File
@@ -0,0 +1,5 @@
module.exports = {
env: {
mocha: true,
},
};
-274
View File
@@ -1,274 +0,0 @@
const fs = require('fs');
const { exec } = require('child_process');
const { execCommand, FileTestHelper } = require('cli-testlab');
const Handlebars = require('../../lib');
const cli = 'node ./bin/handlebars.mjs';
expect.extend({
toEqualWithRelaxedSpace(received, expected) {
const normalize = (str) =>
typeof str === 'string'
? str
.replace(/\r\n/g, '\n')
.split('\n')
.map((line) => line.replace(/\s+/g, ' ').trim())
.filter((line) => line.length > 0)
.join('\n')
.trim()
: str;
const normalizedReceived = normalize(received);
const normalizedExpected = normalize(expected);
const pass = normalizedReceived === normalizedExpected;
return {
pass,
message: () =>
`Expected output to match with relaxed whitespace.\n\n` +
`Expected:\n${normalizedExpected}\n\nReceived:\n${normalizedReceived}`,
};
},
});
function expectedFile(specPath) {
return fs.readFileSync(specPath, 'utf-8');
}
describe('bin/handlebars', function () {
describe('help and version', function () {
it('--help displays help menu', async function () {
const result = await execCommand(`${cli} --help`);
expect(result.stdout).toEqualWithRelaxedSpace(
expectedFile('./spec/expected/help.menu.txt')
);
});
it('no arguments displays help menu', async function () {
const result = await execCommand(`${cli}`);
expect(result.stdout).toEqualWithRelaxedSpace(
expectedFile('./spec/expected/help.menu.txt')
);
});
it('-v prints the compiler version', async function () {
await execCommand(`${cli} -v`, {
expectedOutput: Handlebars.VERSION,
});
});
});
describe('AMD output', function () {
it('-a produces AMD output', async function () {
const result = await execCommand(
`${cli} -a spec/artifacts/empty.handlebars`
);
expect(result.stdout).toEqualWithRelaxedSpace(
expectedFile('./spec/expected/empty.amd.js')
);
});
it('-a -s produces simple AMD output', async function () {
const result = await execCommand(
`${cli} -a -s spec/artifacts/empty.handlebars`
);
expect(result.stdout).toEqualWithRelaxedSpace(
expectedFile('./spec/expected/empty.amd.simple.js')
);
});
it('-a -m produces minified AMD output', async function () {
const result = await execCommand(
`${cli} -a -m spec/artifacts/empty.handlebars`
);
expect(result.stdout).toEqualWithRelaxedSpace(
expectedFile('./spec/expected/empty.amd.min.js')
);
});
});
describe('CommonJS output', function () {
it('-c produces CommonJS output', async function () {
const result = await execCommand(
`${cli} spec/artifacts/empty.handlebars -c`
);
expect(result.stdout).toEqualWithRelaxedSpace(
expectedFile('./spec/expected/empty.common.js')
);
});
});
describe('namespace', function () {
it('-n sets custom namespace', async function () {
const result = await execCommand(
`${cli} -a -n CustomNamespace.templates spec/artifacts/empty.handlebars`
);
expect(result.stdout).toEqualWithRelaxedSpace(
expectedFile('./spec/expected/empty.amd.namespace.js')
);
});
it('--namespace sets custom namespace', async function () {
const result = await execCommand(
`${cli} -a --namespace CustomNamespace.templates spec/artifacts/empty.handlebars`
);
expect(result.stdout).toEqualWithRelaxedSpace(
expectedFile('./spec/expected/empty.amd.namespace.js')
);
});
it('multiple files share a namespace', async function () {
const result = await execCommand(
`${cli} spec/artifacts/empty.handlebars spec/artifacts/empty.handlebars -a -n someNameSpace`
);
expect(result.stdout).toEqualWithRelaxedSpace(
expectedFile('./spec/expected/namespace.amd.js')
);
});
});
describe('file output', function () {
let files;
beforeEach(function () {
files = new FileTestHelper({ basePath: '.' });
files.createDir('tmp');
});
afterEach(function () {
files.cleanup();
});
it('-f writes output to a file', async function () {
const outputFile = 'tmp/cli-test-output.js';
files.registerForCleanup(outputFile);
await execCommand(
`${cli} -a -f ${outputFile} spec/artifacts/empty.handlebars`
);
expect(files.fileExists(outputFile)).toBe(true);
const content = files.getFileTextContent(outputFile);
expect(content).toEqualWithRelaxedSpace(
expectedFile('./spec/expected/empty.amd.js')
);
});
it('--map writes source map and appends sourceMappingURL', async function () {
const mapFile = 'tmp/cli-test-source.map';
files.registerForCleanup(mapFile);
const result = await execCommand(
`${cli} -i "<div>1</div>" -a -m -N test --map ${mapFile}`
);
expect(result.stdout).toContain('sourceMappingURL=');
expect(files.fileExists(mapFile)).toBe(true);
const parsed = JSON.parse(files.getFileTextContent(mapFile));
expect(parsed).toHaveProperty('version', 3);
expect(parsed).toHaveProperty('sources');
expect(parsed).toHaveProperty('mappings');
});
});
describe('template options', function () {
it('-e sets custom extension', async function () {
const result = await execCommand(
`${cli} -a -e hbs ./spec/artifacts/non.default.extension.hbs`
);
expect(result.stdout).toEqualWithRelaxedSpace(
expectedFile('./spec/expected/non.default.extension.amd.js')
);
});
it('-p compiles as partial', async function () {
const result = await execCommand(
`${cli} -a -p ./spec/artifacts/partial.template.handlebars`
);
expect(result.stdout).toEqualWithRelaxedSpace(
expectedFile('./spec/expected/partial.template.js')
);
});
it('-r strips root from template names', async function () {
const result = await execCommand(
`${cli} spec/artifacts/partial.template.handlebars -r spec -a`
);
expect(result.stdout).toEqualWithRelaxedSpace(
expectedFile('./spec/expected/empty.root.amd.js')
);
});
it('-b strips BOM', async function () {
const result = await execCommand(
`${cli} ./spec/artifacts/bom.handlebars -b -a`
);
expect(result.stdout).toEqualWithRelaxedSpace(
expectedFile('./spec/expected/bom.amd.js')
);
});
it('-h sets custom handlebar path', async function () {
const result = await execCommand(
`${cli} spec/artifacts/empty.handlebars -h some-path/ -a`
);
expect(result.stdout).toEqualWithRelaxedSpace(
expectedFile('./spec/expected/handlebar.path.amd.js')
);
});
it('-k -o sets known helpers only', async function () {
const result = await execCommand(
`${cli} spec/artifacts/known.helpers.handlebars -a -k someHelper -k anotherHelper -o`
);
expect(result.stdout).toEqualWithRelaxedSpace(
expectedFile('./spec/expected/non.empty.amd.known.helper.js')
);
});
});
describe('inline templates', function () {
it('-i compiles inline template', async function () {
const result = await execCommand(
`${cli} -i "<div>hello</div>" -a -N myTemplate`
);
expect(result.stdout).toContain("define(['handlebars.runtime']");
expect(result.stdout).toContain("templates['myTemplate']");
});
it('-i compiles multiple inline templates with -N', async function () {
const result = await execCommand(
`${cli} -i "<div>1</div>" -i "<div>2</div>" -N firstTemplate -N secondTemplate -a`
);
expect(result.stdout).toEqualWithRelaxedSpace(
expectedFile('./spec/expected/empty.name.amd.js')
);
});
});
describe('error handling', function () {
it('should not produce unhandled promise rejections on async errors', async function () {
const { stderr } = await new Promise((resolve) => {
exec(
`node --unhandled-rejections=warn ./bin/handlebars.mjs -i "<div>test</div>" -N test --map /nonexistent/dir/test.map`,
(error, stdout, stderr) => {
resolve({ exitCode: error ? error.code : 0, stderr });
}
);
});
expect(stderr).not.toMatch(/unhandled/i);
});
});
describe('negated boolean flags', function () {
it('--no-amd negates --amd (issue #1673)', async function () {
const result = await execCommand(
`${cli} --amd --no-amd spec/artifacts/empty.handlebars`
);
expect(result.stdout).toEqualWithRelaxedSpace(
expectedFile('./spec/expected/empty.common.js')
);
});
});
});
-81
View File
@@ -1,81 +0,0 @@
const http = require('http');
/**
* Minimal in-memory S3-compatible server for testing.
* Supports PutObject and GetObject via path-style requests.
*/
function createFakeS3() {
const buckets = new Map();
const server = http.createServer((req, res) => {
const { bucket, key } = parsePath(req.url);
if (!bucket || !buckets.has(bucket)) {
res.writeHead(404, { 'Content-Type': 'application/xml' });
res.end('<Error><Code>NoSuchBucket</Code></Error>');
return;
}
const store = buckets.get(bucket);
if (req.method === 'PUT' && key) {
const chunks = [];
req.on('data', (chunk) => chunks.push(chunk));
req.on('end', () => {
store.set(key, Buffer.concat(chunks));
res.writeHead(200);
res.end();
});
} else if ((req.method === 'GET' || req.method === 'HEAD') && key) {
if (!store.has(key)) {
res.writeHead(404, { 'Content-Type': 'application/xml' });
res.end('<Error><Code>NoSuchKey</Code></Error>');
return;
}
const body = store.get(key);
res.writeHead(200, {
'Content-Length': body.length,
'Content-Type': 'application/octet-stream',
});
res.end(req.method === 'HEAD' ? undefined : body);
} else {
res.writeHead(405);
res.end();
}
});
return {
start() {
return new Promise((resolve) => {
server.listen(0, '127.0.0.1', () => {
const { port } = server.address();
resolve({
address: `http://127.0.0.1:${port}`,
createBucket(name) {
if (!buckets.has(name)) buckets.set(name, new Map());
},
reset() {
for (const store of buckets.values()) store.clear();
},
stop() {
return new Promise((r) => server.close(r));
},
});
});
});
},
};
}
function parsePath(url) {
// Path-style: /<bucket>/<key>
const parts = new URL(url, 'http://localhost').pathname
.replace(/^\//, '')
.split('/');
return {
bucket: parts[0] || null,
key: parts.slice(1).join('/') || null,
};
}
module.exports = { createFakeS3 };
+16 -18
View File
@@ -1,26 +1,26 @@
const os = require('os'); const os = require('os');
const path = require('path'); const path = require('path');
const fs = require('fs-extra'); const fs = require('fs-extra');
const { spawnSync } = require('child_process'); const chai = require('chai');
chai.use(require('dirty-chai'));
const git = require('../util/git'); const git = require('../util/git');
const expect = chai.expect;
const tmpBaseDir = path.join(os.tmpdir(), 'handlebars-task-tests'); const tmpBaseDir = path.join(os.tmpdir(), 'handlebars-task-tests');
const tmpDir = path.join(tmpBaseDir, Date.now().toString(36)); const tmpDir = path.join(tmpBaseDir, Date.now().toString(36));
const remoteDir = path.join(tmpDir, 'remote-repo'); const remoteDir = path.join(tmpDir, 'remote-repo');
const cloneDir = path.join(tmpDir, 'clone-repo'); const cloneDir = path.join(tmpDir, 'clone-repo');
const oldCwd = process.cwd(); const oldCwd = process.cwd();
const gitInstalled = spawnSync('git', ['-v'], { stdio: 'ignore' }).status === 0;
describe.runIf(gitInstalled)('utils/git', function () { describe('utils/git', function () {
beforeEach(async function () { beforeEach(async function () {
await fs.remove(tmpDir); await fs.remove(tmpDir);
await createRepositoryThatActsAsRemote(); await createRepositoryThatActsAsRemote();
process.chdir(tmpDir); process.chdir(tmpDir);
await git.git('clone', 'remote-repo', 'clone-repo'); await git.git('clone', 'remote-repo', 'clone-repo');
process.chdir(cloneDir); process.chdir(cloneDir);
await git.git('config', 'user.email', 'test@test.com');
await git.git('config', 'user.name', 'Test');
}); });
async function createRepositoryThatActsAsRemote() { async function createRepositoryThatActsAsRemote() {
@@ -28,8 +28,6 @@ describe.runIf(gitInstalled)('utils/git', function () {
process.chdir(remoteDir); process.chdir(remoteDir);
await git.git('init'); await git.git('init');
await git.git('config', 'user.email', 'test@test.com');
await git.git('config', 'user.name', 'Test');
await fs.writeFile('testfile.txt', 'Testfile'); await fs.writeFile('testfile.txt', 'Testfile');
await git.add('testfile.txt'); await git.add('testfile.txt');
await git.commit('commit message'); await git.commit('commit message');
@@ -46,7 +44,7 @@ describe.runIf(gitInstalled)('utils/git', function () {
const result = await git.remotes(); const result = await git.remotes();
expect(result.trim().split('\n')).toEqual([ expect(result.trim().split('\n')).to.deep.equal([
'origin\thttps://test.org/test (fetch)', 'origin\thttps://test.org/test (fetch)',
'origin\thttps://test.org/test (push)', 'origin\thttps://test.org/test (push)',
'second-remote\thttps://test.org/test2 (fetch)', 'second-remote\thttps://test.org/test2 (fetch)',
@@ -61,7 +59,7 @@ describe.runIf(gitInstalled)('utils/git', function () {
await git.git('branch', 'test2'); await git.git('branch', 'test2');
const result = await git.branches(); const result = await git.branches();
expect(result.trim().split('\n')).toEqual([ expect(result.trim().split('\n')).to.deep.equal([
'* master', '* master',
' test', ' test',
' test2', ' test2',
@@ -74,21 +72,21 @@ describe.runIf(gitInstalled)('utils/git', function () {
describe('the "commitInfo"-function', function () { describe('the "commitInfo"-function', function () {
it('should list head and master sha', async function () { it('should list head and master sha', async function () {
const result = await git.commitInfo(); const result = await git.commitInfo();
expect(result.masterSha).toBe(result.headSha); expect(result.masterSha).to.equal(result.headSha);
expect(result.masterSha).toMatch(/^[0-9a-f]+$/); expect(result.masterSha).to.match(/^[0-9a-f]+$/);
expect(result.headSha).toMatch(/^[0-9a-f]+$/); expect(result.headSha).to.match(/^[0-9a-f]+$/);
}); });
it('should have "isMaster=true" if the master branch is checked out', async function () { it('should have "isMaster=true" if the master branch is checked out', async function () {
const result = await git.commitInfo(); const result = await git.commitInfo();
expect(result.isMaster).toBe(true); expect(result.isMaster).to.be.true();
}); });
it('should have "isMaster=true" if the current commit is the last commit of the master branch', async function () { it('should have "isMaster=true" if the current commit is the last commit of the master branch', async function () {
await git.git('checkout', '-b', 'new-branch'); await git.git('checkout', '-b', 'new-branch');
const result = await git.commitInfo(); const result = await git.commitInfo();
expect(result.isMaster).toBe(true); expect(result.isMaster).to.be.true();
}); });
it('should have "isMaster=false" if the current commit is NOT the last commit of the master branch', async function () { it('should have "isMaster=false" if the current commit is NOT the last commit of the master branch', async function () {
@@ -98,13 +96,13 @@ describe.runIf(gitInstalled)('utils/git', function () {
await git.commit('added new file'); await git.commit('added new file');
const result = await git.commitInfo(); const result = await git.commitInfo();
expect(result.isMaster).toBe(false); expect(result.isMaster).to.be.false();
}); });
it('should show the current tag', async function () { it('should show the current tag', async function () {
await git.git('tag', 'test-tag'); await git.git('tag', 'test-tag');
const result = await git.commitInfo(); const result = await git.commitInfo();
expect(result.tagName).toBe('test-tag'); expect(result.tagName).to.be.equal('test-tag');
}); });
it('should show a version tag rather than standard tags', async function () { it('should show a version tag rather than standard tags', async function () {
@@ -112,12 +110,12 @@ describe.runIf(gitInstalled)('utils/git', function () {
await git.git('tag', 'v1.2'); await git.git('tag', 'v1.2');
await git.git('tag', 'test-tag2'); await git.git('tag', 'test-tag2');
const result = await git.commitInfo(); const result = await git.commitInfo();
expect(result.tagName).toBe('v1.2'); expect(result.tagName).to.be.equal('v1.2');
}); });
it('should show no tag if there is no tag', async function () { it('should show no tag if there is no tag', async function () {
const result = await git.commitInfo(); const result = await git.commitInfo();
expect(result.tagName).toBeNull(); expect(result.tagName).to.be.null();
}); });
}); });
}); });
View File
-221
View File
@@ -1,221 +0,0 @@
const fs = require('fs');
const { S3, GetObjectCommand } = require('@aws-sdk/client-s3');
const { createFakeS3 } = require('./fake-s3');
const BUCKET = 'test-bucket';
const {
PUBLISHED_FILES,
buildSuffixes,
validateS3Env,
publish,
getNameInBucket,
getLocalFile,
} = require('../publish-to-aws');
let server;
let s3Client;
beforeAll(async () => {
server = await createFakeS3().start();
server.createBucket(BUCKET);
s3Client = new S3({
endpoint: server.address,
region: 'us-east-1',
credentials: { accessKeyId: 'test', secretAccessKey: 'test' },
forcePathStyle: true,
});
});
afterAll(async () => {
await server?.stop();
});
beforeEach(() => {
server.reset();
});
async function getObjectBody(key) {
const response = await s3Client.send(
new GetObjectCommand({ Bucket: BUCKET, Key: key })
);
return response.Body.transformToString();
}
describe('buildSuffixes', () => {
it('should add -latest and commit sha for master', () => {
const result = buildSuffixes({
isMaster: true,
headSha: 'abc123',
tagName: null,
});
expect(result).toEqual(['-latest', '-abc123']);
});
it('should add tag suffix for valid semver tag', () => {
const result = buildSuffixes({
isMaster: false,
headSha: 'abc123',
tagName: '4.7.0',
});
expect(result).toEqual(['-4.7.0']);
});
it('should add both master and tag suffixes when on master with a tag', () => {
const result = buildSuffixes({
isMaster: true,
headSha: 'abc123',
tagName: '4.7.0',
});
expect(result).toEqual(['-latest', '-abc123', '-4.7.0']);
});
it('should return empty array for non-master with no valid tag', () => {
const result = buildSuffixes({
isMaster: false,
headSha: 'abc123',
tagName: null,
});
expect(result).toEqual([]);
});
it('should ignore invalid semver tags', () => {
const result = buildSuffixes({
isMaster: false,
headSha: 'abc123',
tagName: 'not-semver',
});
expect(result).toEqual([]);
});
});
describe('getNameInBucket', () => {
it('should insert suffix before .js extension', () => {
expect(getNameInBucket('handlebars.js', '-latest')).toBe(
'handlebars-latest.js'
);
});
it('should handle .min.js files', () => {
expect(getNameInBucket('handlebars.min.js', '-4.7.0')).toBe(
'handlebars.min-4.7.0.js'
);
});
it('should handle runtime files', () => {
expect(getNameInBucket('handlebars.runtime.js', '-abc123')).toBe(
'handlebars.runtime-abc123.js'
);
});
});
describe('getLocalFile', () => {
it('should prefix with dist/', () => {
expect(getLocalFile('handlebars.js')).toBe('dist/handlebars.js');
});
});
describe('validateS3Env', () => {
const S3_KEYS = [
'S3_BUCKET_NAME',
'S3_REGION',
'S3_ACCESS_KEY_ID',
'S3_SECRET_ACCESS_KEY',
];
const saved = {};
beforeEach(() => {
for (const key of S3_KEYS) {
saved[key] = process.env[key];
}
});
afterEach(() => {
for (const key of S3_KEYS) {
if (saved[key] === undefined) {
delete process.env[key];
} else {
process.env[key] = saved[key];
}
}
});
it('should throw when S3 env vars are missing', () => {
for (const key of S3_KEYS) {
delete process.env[key];
}
expect(() => validateS3Env()).toThrow('Missing S3 config values');
});
it('should not throw when all S3 env vars are set', () => {
process.env.S3_BUCKET_NAME = 'test-bucket';
process.env.S3_REGION = 'us-east-1';
process.env.S3_ACCESS_KEY_ID = 'key';
process.env.S3_SECRET_ACCESS_KEY = 'secret';
expect(() => validateS3Env()).not.toThrow();
});
});
describe('publish to S3', () => {
const overrides = {};
beforeAll(() => {
overrides.s3Client = s3Client;
overrides.bucket = BUCKET;
});
it('should upload all 4 files for a single suffix', async () => {
await publish(['-latest'], overrides);
for (const filename of PUBLISHED_FILES) {
const key = getNameInBucket(filename, '-latest');
const body = await getObjectBody(key);
const localContent = fs.readFileSync(getLocalFile(filename), 'utf8');
expect(body).toBe(localContent);
}
});
it('should upload files for multiple suffixes', async () => {
await publish(['-latest', '-abc123'], overrides);
for (const suffix of ['-latest', '-abc123']) {
for (const filename of PUBLISHED_FILES) {
const key = getNameInBucket(filename, suffix);
const body = await getObjectBody(key);
expect(body).toBeTruthy();
}
}
});
it('should upload correct content from dist/', async () => {
await publish(['-v1.0.0'], overrides);
const key = getNameInBucket('handlebars.js', '-v1.0.0');
const uploaded = await getObjectBody(key);
const local = fs.readFileSync('dist/handlebars.js', 'utf8');
expect(uploaded).toBe(local);
});
it('should produce correct keys for a version tag', async () => {
await publish(['-4.7.0'], overrides);
const expectedKeys = PUBLISHED_FILES.map((f) =>
getNameInBucket(f, '-4.7.0')
);
for (const key of expectedKeys) {
const body = await getObjectBody(key);
expect(body).toBeTruthy();
}
expect(expectedKeys).toEqual([
'handlebars-4.7.0.js',
'handlebars.min-4.7.0.js',
'handlebars.runtime-4.7.0.js',
'handlebars.runtime.min-4.7.0.js',
]);
});
});
+13
View File
@@ -0,0 +1,13 @@
module.exports = { createRegisterAsyncTaskFn };
function createRegisterAsyncTaskFn(grunt) {
return function registerAsyncTask(name, asyncFunction) {
grunt.registerTask(name, function () {
asyncFunction()
.catch((error) => {
grunt.fatal(error);
})
.finally(this.async());
});
};
}
+1 -1
View File
@@ -52,7 +52,7 @@ async function getTagName() {
} }
const tags = trimmedStdout.split(/\n|\r\n/); const tags = trimmedStdout.split(/\n|\r\n/);
const versionTags = tags.filter((tag) => tag.startsWith('v')); const versionTags = tags.filter((tag) => /^v/.test(tag));
if (versionTags[0] != null) { if (versionTags[0] != null) {
return versionTags[0]; return versionTags[0];
} }
+57 -63
View File
@@ -1,68 +1,62 @@
const fs = require('fs');
const { execSync } = require('child_process');
const git = require('./util/git'); const git = require('./util/git');
const semver = require('semver'); const semver = require('semver');
const { createRegisterAsyncTaskFn } = require('./util/async-grunt-task');
async function main() { module.exports = function (grunt) {
const version = process.argv[2]; const registerAsyncTask = createRegisterAsyncTaskFn(grunt);
if (!semver.valid(version)) {
throw new Error( registerAsyncTask('version', async () => {
'Must provide a valid semver version as first argument (e.g.: node tasks/version.js 1.0.0):\n\t' + const pkg = grunt.config('pkg');
version + const version = grunt.option('ver');
'\n\n' if (!semver.valid(version)) {
throw new Error(
'Must provide a version number (Ex: --ver=1.0.0):\n\t' +
version +
'\n\n'
);
}
pkg.version = version;
grunt.config('pkg', pkg);
const replaceSpec = [
{
path: 'lib/handlebars/base.js',
regex: /const VERSION = ['"](.*)['"];/,
replacement: `const VERSION = '${version}';`,
},
{
path: 'components/bower.json',
regex: /"version":.*/,
replacement: `"version": "${version}",`,
},
{
path: 'components/package.json',
regex: /"version":.*/,
replacement: `"version": "${version}",`,
},
{
path: 'components/handlebars.js.nuspec',
regex: /<version>.*<\/version>/,
replacement: `<version>${version}</version>`,
},
];
await Promise.all(
replaceSpec.map((replaceSpec) =>
replaceAndAdd(
replaceSpec.path,
replaceSpec.regex,
replaceSpec.replacement
)
)
); );
} grunt.task.run(['default']);
const pkg = JSON.parse(fs.readFileSync('package.json', 'utf8'));
pkg.version = version;
fs.writeFileSync('package.json', JSON.stringify(pkg, null, 2) + '\n');
await git.add('package.json');
const replaceSpec = [
{
path: 'lib/handlebars/base.js',
regex: /const VERSION = ['"](.*)['"];/,
replacement: `const VERSION = '${version}';`,
},
{
path: 'components/bower.json',
regex: /"version":.*/,
replacement: `"version": "${version}",`,
},
{
path: 'components/package.json',
regex: /"version":.*/,
replacement: `"version": "${version}",`,
},
{
path: 'components/handlebars.js.nuspec',
regex: /<version>.*<\/version>/,
replacement: `<version>${version}</version>`,
},
];
await Promise.all(
replaceSpec.map((spec) =>
replaceAndAdd(spec.path, spec.regex, spec.replacement)
)
);
execSync('npm run build', { stdio: 'inherit' });
}
async function replaceAndAdd(filePath, regex, value) {
let content = fs.readFileSync(filePath, 'utf8');
content = content.replace(regex, value);
fs.writeFileSync(filePath, content);
await git.add(filePath);
}
module.exports = { replaceAndAdd };
if (require.main === module) {
main().catch((err) => {
// eslint-disable-next-line no-console
console.error(err);
process.exit(1);
}); });
}
async function replaceAndAdd(path, regex, value) {
let content = grunt.file.read(path);
content = content.replace(regex, value);
grunt.file.write(path, content);
await git.add(path);
}
};
+6
View File
@@ -0,0 +1,6 @@
module.exports = {
rules: {
'no-console': 'off',
'no-var': 'off',
},
};
-250
View File
@@ -1,250 +0,0 @@
import {
mkdirSync,
readFileSync,
readdirSync,
writeFileSync,
statSync,
} from 'node:fs';
import { basename, dirname, join } from 'node:path';
import { fileURLToPath } from 'node:url';
const __dirname = dirname(fileURLToPath(import.meta.url));
import { formatOps } from './report.mjs';
// ─── Resolve files ───────────────────────────────────────────────────────────
const args = process.argv.slice(2);
let baselinePath, currentPath;
if (args.length >= 2) {
[baselinePath, currentPath] = args;
} else {
const resultsDir = join(__dirname, 'results');
let files;
try {
files = readdirSync(resultsDir)
.filter((f) => f.endsWith('.md') && f.startsWith('bench-'))
.map((f) => join(resultsDir, f));
} catch {
files = [];
}
if (files.length < 2) {
console.error(
files.length === 0
? 'No benchmark results found in bench/results/.'
: 'Only one benchmark result found. Run benchmarks on another branch first.'
);
console.error('');
console.error('Usage: node bench/compare.mjs [<baseline.md> <current.md>]');
console.error('');
console.error(
'With no arguments, auto-selects the two most recent results.'
);
console.error('A result labelled "main" is always used as the baseline.');
process.exit(1);
}
// Find the "main" labelled file if any
const mainFile = files.find((f) => {
const content = readFileSync(f, 'utf8');
const match = content.match(/^- \*\*Label:\*\* (.+)$/m);
return match && match[1] === 'main';
});
if (mainFile) {
// Baseline is main, current is the most recent non-main file
baselinePath = mainFile;
const others = files
.filter((f) => f !== mainFile)
.sort((a, b) => statSync(b).mtimeMs - statSync(a).mtimeMs);
currentPath = others[0];
} else {
// Sort by mtime, older = baseline, newer = current
files.sort((a, b) => statSync(a).mtimeMs - statSync(b).mtimeMs);
baselinePath = files[files.length - 2];
currentPath = files[files.length - 1];
}
console.log(`Auto-selected files:`);
console.log(` baseline: ${basename(baselinePath)}`);
console.log(` current: ${basename(currentPath)}`);
console.log();
}
// ─── Parse ───────────────────────────────────────────────────────────────────
function parseOps(str) {
str = str.trim();
if (str.endsWith('M')) return parseFloat(str) * 1_000_000;
if (str.endsWith('K')) return parseFloat(str) * 1_000;
return parseFloat(str);
}
function parseNs(str) {
str = str.trim();
if (str.endsWith('ms')) return parseFloat(str) * 1_000_000;
if (str.endsWith('µs')) return parseFloat(str) * 1_000;
if (str.endsWith('ns')) return parseFloat(str);
return parseFloat(str);
}
function parseReport(filepath) {
const content = readFileSync(filepath, 'utf8');
const lines = content.split('\n');
let label = null;
const labelMatch = content.match(/^- \*\*Label:\*\* (.+)$/m);
if (labelMatch) label = labelMatch[1];
const sections = {};
let currentSection = null;
for (const line of lines) {
if (line.startsWith('## ')) {
currentSection = line.slice(3).trim();
sections[currentSection] = {};
continue;
}
if (
!currentSection ||
!line.startsWith('|') ||
line.startsWith('|---') ||
line.startsWith('| Benchmark')
) {
continue;
}
const cells = line
.split('|')
.map((c) => c.trim())
.filter(Boolean);
if (cells.length < 8) continue;
const [name, opsSec, avg, p50, p75, p99, rme, samples] = cells;
sections[currentSection][name] = {
opsSec: parseOps(opsSec),
avg: parseNs(avg),
p50: parseNs(p50),
p75: parseNs(p75),
p99: parseNs(p99),
rme: parseFloat(rme),
samples: parseInt(samples, 10),
};
}
return { label, sections };
}
// ─── Compare ─────────────────────────────────────────────────────────────────
function pctChange(baseline, current) {
if (baseline === 0) return 0;
return ((current - baseline) / baseline) * 100;
}
function formatPct(pct) {
const sign = pct > 0 ? '+' : '';
return `${sign}${pct.toFixed(1)}%`;
}
function indicator(pct, higherIsBetter) {
const effective = higherIsBetter ? pct : -pct;
if (effective > 5) return ' !!';
if (effective > 2) return ' !';
if (effective < -5) return ' !!';
if (effective < -2) return ' !';
return '';
}
// ─── Output ──────────────────────────────────────────────────────────────────
const baseline = parseReport(baselinePath);
const current = parseReport(currentPath);
const baseLabel = baseline.label || 'baseline';
const curLabel = current.label || 'current';
console.log(`Benchmark Comparison: ${baseLabel} vs ${curLabel}`);
console.log('='.repeat(50));
console.log(`Baseline: ${baselinePath}`);
console.log(`Current: ${currentPath}`);
console.log();
console.log('Legend: ! = >2% change, !! = >5% change');
console.log();
const mdLines = [];
mdLines.push(`# Benchmark Comparison: ${baseLabel} vs ${curLabel}`);
mdLines.push('');
mdLines.push(`- **Baseline:** ${baseLabel} (${baselinePath})`);
mdLines.push(`- **Current:** ${curLabel} (${currentPath})`);
mdLines.push(`- **Legend:** ! = >2% change, !! = >5% change`);
mdLines.push('');
const allSections = [
...new Set([
...Object.keys(baseline.sections),
...Object.keys(current.sections),
]),
].sort();
for (const section of allSections) {
const baseBenches = baseline.sections[section] || {};
const curBenches = current.sections[section] || {};
const allNames = [
...new Set([...Object.keys(baseBenches), ...Object.keys(curBenches)]),
].sort();
if (allNames.size === 0) continue;
console.log(`## ${section}`);
console.log();
const header = `| Benchmark | ${baseLabel} ops/sec | ${curLabel} ops/sec | ops/sec | p75 latency |`;
const sep = '|---|---|---|---|---|';
console.log(header);
console.log(sep);
mdLines.push(`## ${section}`);
mdLines.push('');
mdLines.push(header);
mdLines.push(sep);
for (const name of allNames) {
const b = baseBenches[name];
const c = curBenches[name];
if (!b || !c) {
const row = `| ${name} | ${b ? formatOps(b.opsSec) : 'n/a'} | ${c ? formatOps(c.opsSec) : 'n/a'} | - | - |`;
console.log(row);
mdLines.push(row);
continue;
}
const opsPct = pctChange(b.opsSec, c.opsSec);
const p75Pct = pctChange(b.p75, c.p75);
const opsStr = `${formatPct(opsPct)}${indicator(opsPct, true)}`;
const p75Str = `${formatPct(p75Pct)}${indicator(p75Pct, false)}`;
const row = `| ${name} | ${formatOps(b.opsSec)} | ${formatOps(c.opsSec)} | ${opsStr} | ${p75Str} |`;
console.log(row);
mdLines.push(row);
}
console.log();
mdLines.push('');
}
const resultsDir = join(__dirname, 'results');
mkdirSync(resultsDir, { recursive: true });
const curLabelSlug = curLabel.replace(/[^a-zA-Z0-9-_]/g, '_');
const baseLabelSlug = baseLabel.replace(/[^a-zA-Z0-9-_]/g, '_');
const outPath = join(
resultsDir,
`compare-${baseLabelSlug}-vs-${curLabelSlug}.md`
);
writeFileSync(outPath, mdLines.join('\n'));
console.log(`Comparison saved to: ${outPath}`);
+43
View File
@@ -0,0 +1,43 @@
var async = require('neo-async'),
fs = require('fs'),
zlib = require('zlib');
module.exports = function (grunt, callback) {
var distFiles = fs.readdirSync('dist'),
distSizes = {};
async.each(
distFiles,
function (file, callback) {
var content;
try {
content = fs.readFileSync('dist/' + file);
} catch (err) {
if (err.code === 'EISDIR') {
callback();
return;
} else {
throw err;
}
}
file = file.replace(/\.js/, '').replace(/\./g, '_');
distSizes[file] = content.length;
zlib.gzip(content, function (err, data) {
if (err) {
throw err;
}
distSizes[file + '_gz'] = data.length;
callback();
});
},
function () {
grunt.log.writeln(
'Distribution sizes: ' + JSON.stringify(distSizes, undefined, 2)
);
callback([distSizes]);
}
);
};
+14
View File
@@ -0,0 +1,14 @@
var fs = require('fs');
var metrics = fs.readdirSync(__dirname);
metrics.forEach(function (metric) {
if (metric === 'index.js' || !/(.*)\.js$/.test(metric)) {
return;
}
var name = RegExp.$1;
metric = require('./' + name);
if (metric instanceof Function) {
module.exports[name] = metric;
}
});
-288
View File
@@ -1,288 +0,0 @@
import { execSync } from 'node:child_process';
import { Bench } from 'tinybench';
import Handlebars from '../../lib/index.js';
import { templates as allTemplates } from './templates.mjs';
import {
printResults,
printSectionHeader,
saveMarkdownReport,
} from './report.mjs';
// ─── Configuration ───────────────────────────────────────────────────────────
// Compilation happens once at app startup — low warmup reflects real-world conditions.
// Execution happens thousands of times — higher warmup lets V8 optimize hot paths.
const COMPILE_BENCH_CONFIG = {
warmupIterations: 10,
iterations: 1000,
time: 3000,
};
const EXEC_BENCH_CONFIG = {
warmupIterations: 500,
iterations: 5000,
time: 3000,
};
// ─── CLI Args ────────────────────────────────────────────────────────────────
function getGitBranch() {
try {
return execSync('git rev-parse --abbrev-ref HEAD', {
encoding: 'utf8',
}).trim();
} catch {
return null;
}
}
const args = process.argv.slice(2);
const labelIdx = args.indexOf('--label');
const label = labelIdx !== -1 ? args[labelIdx + 1] : getGitBranch();
const grepIdx = args.indexOf('--grep');
let grepPattern = null;
if (grepIdx !== -1 && args[grepIdx + 1]) {
try {
grepPattern = new RegExp(args[grepIdx + 1], 'i');
} catch (e) {
console.error(`Invalid --grep pattern: ${e.message}`);
process.exit(1);
}
}
const sectionIdx = args.indexOf('--section');
let sectionPattern = null;
if (sectionIdx !== -1 && args[sectionIdx + 1]) {
try {
sectionPattern = new RegExp(args[sectionIdx + 1], 'i');
} catch (e) {
console.error(`Invalid --section pattern: ${e.message}`);
process.exit(1);
}
}
// ─── Filter templates ───────────────────────────────────────────────────────
const templates = Object.fromEntries(
Object.entries(allTemplates).filter(
([name]) => !grepPattern || grepPattern.test(name)
)
);
if (Object.keys(templates).length === 0) {
console.error(
`No templates match --grep ${grepPattern}. Available: ${Object.keys(allTemplates).join(', ')}`
);
process.exit(1);
}
if (grepPattern) {
console.log(
`Filtering templates: ${Object.keys(templates).length}/${Object.keys(allTemplates).length} match /${grepPattern.source}/i`
);
console.log();
}
// ─── Bench helpers ───────────────────────────────────────────────────────────
function newBench(config) {
return new Bench(config);
}
function createEnv(def) {
const hb = Handlebars.create();
if (def.helpers) {
hb.registerHelper(def.helpers);
}
if (def.partials) {
for (const [name, tpl] of Object.entries(def.partials)) {
hb.registerPartial(name, tpl);
}
}
return hb;
}
function shouldRunSection(title) {
if (!sectionPattern) return true;
return sectionPattern.test(title);
}
async function runSection(title, config, setup) {
if (!shouldRunSection(title)) return null;
printSectionHeader(title);
const bench = newBench(config);
setup(bench);
await bench.run();
printResults(bench);
console.log();
return { title, bench };
}
// ─── Main ────────────────────────────────────────────────────────────────────
async function run() {
const now = new Date();
const headerLabel = label ? ` [${label}]` : '';
console.log(`Handlebars Performance Benchmark${headerLabel}`);
console.log('================================');
console.log(
`tinybench | compile: warmup ${COMPILE_BENCH_CONFIG.warmupIterations}, min ${COMPILE_BENCH_CONFIG.iterations} | exec: warmup ${EXEC_BENCH_CONFIG.warmupIterations}, min ${EXEC_BENCH_CONFIG.iterations} | time: ${EXEC_BENCH_CONFIG.time}ms per bench`
);
console.log(`Node ${process.version} | ${process.platform} ${process.arch}`);
console.log();
const allSections = [];
if (sectionPattern) {
console.log(`Filtering sections: /${sectionPattern.source}/i`);
console.log();
}
// ─── COMPILATION ────────────────────────────────────────────────────────────
allSections.push(
await runSection(
'COMPILATION (Handlebars.compile + first render)',
COMPILE_BENCH_CONFIG,
(bench) => {
for (const [name, def] of Object.entries(templates)) {
const hb = createEnv(def);
// Handlebars.compile() uses lazy evaluation — actual compilation
// (parsing, codegen, new Function()) only happens on first invocation.
bench.add(`compile: ${name}`, () => {
hb.compile(def.template)({});
});
}
}
)
);
// ─── EXECUTION + output verification ────────────────────────────────────────
const expectedOutputs = {};
const execResult = await runSection(
'EXECUTION (template rendering)',
EXEC_BENCH_CONFIG,
(bench) => {
for (const [name, def] of Object.entries(templates)) {
const compiled = createEnv(def).compile(def.template);
expectedOutputs[name] = compiled(def.context);
bench.add(`exec: ${name}`, () => {
compiled(def.context);
});
}
}
);
allSections.push(execResult);
// Verify outputs haven't changed during benchmarking
if (execResult) {
let verifyFails = 0;
for (const [name, def] of Object.entries(templates)) {
const compiled = createEnv(def).compile(def.template);
const actual = compiled(def.context);
if (actual !== expectedOutputs[name]) {
console.warn(
` WARNING: output mismatch for "${name}"\n expected: ${JSON.stringify(expectedOutputs[name])}\n actual: ${JSON.stringify(actual)}`
);
verifyFails++;
}
}
if (verifyFails === 0) {
console.log('Output verification: all templates OK');
} else {
console.warn(`Output verification: ${verifyFails} mismatch(es)`);
}
console.log();
}
// ─── PRECOMPILATION ─────────────────────────────────────────────────────────
allSections.push(
await runSection(
'PRECOMPILATION (Handlebars.precompile)',
COMPILE_BENCH_CONFIG,
(bench) => {
for (const [name, def] of Object.entries(templates)) {
bench.add(`precompile: ${name}`, () => {
Handlebars.precompile(def.template);
});
}
}
)
);
// ─── END-TO-END ─────────────────────────────────────────────────────────────
allSections.push(
await runSection(
'END-TO-END (compile + render)',
COMPILE_BENCH_CONFIG,
(bench) => {
for (const [name, def] of Object.entries(templates)) {
const hb = createEnv(def);
bench.add(`e2e: ${name}`, () => {
const fn = hb.compile(def.template);
fn(def.context);
});
}
}
)
);
// ─── COMPILE OPTIONS ───────────────────────────────────────────────────────
allSections.push(
await runSection(
'COMPILE OPTIONS COMPARISON',
EXEC_BENCH_CONFIG,
(bench) => {
const src = allTemplates['complex (if/each/helpers)'].template;
const ctx = allTemplates['complex (if/each/helpers)'].context;
const defaultFn = Handlebars.compile(src);
bench.add('exec: default options', () => defaultFn(ctx));
const noEscapeFn = Handlebars.compile(src, { noEscape: true });
bench.add('exec: noEscape=true', () => noEscapeFn(ctx));
const strictFn = Handlebars.compile(src, {
strict: true,
assumeObjects: true,
});
bench.add('exec: strict + assumeObjects', () => strictFn(ctx));
const knownFn = Handlebars.compile(src, {
knownHelpers: { if: true, each: true },
knownHelpersOnly: false,
});
bench.add('exec: knownHelpers', () => knownFn(ctx));
const compatFn = Handlebars.compile(src, { compat: true });
bench.add('exec: compat=true', () => compatFn(ctx));
const noDataFn = Handlebars.compile(src, { data: false });
bench.add('exec: data=false', () => noDataFn(ctx));
}
)
);
const filepath = saveMarkdownReport(allSections.filter(Boolean), {
label,
compileConfig: COMPILE_BENCH_CONFIG,
execConfig: EXEC_BENCH_CONFIG,
date: now,
});
console.log(`Results saved to: ${filepath}`);
}
run().catch((err) => {
console.error(err);
process.exit(1);
});
+24
View File
@@ -0,0 +1,24 @@
var _ = require('underscore'),
templates = require('./templates');
module.exports = function (grunt, callback) {
// Deferring to here in case we have a build for parser, etc as part of this grunt exec
var Handlebars = require('../../lib');
var templateSizes = {};
_.each(templates, function (info, template) {
var src = info.handlebars,
compiled = Handlebars.precompile(src, {}),
knownHelpers = Handlebars.precompile(src, {
knownHelpersOnly: true,
knownHelpers: info.helpers,
});
templateSizes[template] = compiled.length;
templateSizes['knownOnly_' + template] = knownHelpers.length;
});
grunt.log.writeln(
'Precompiled sizes: ' + JSON.stringify(templateSizes, undefined, 2)
);
callback([templateSizes]);
};
-126
View File
@@ -1,126 +0,0 @@
import { writeFileSync, mkdirSync } from 'node:fs';
import { dirname, join } from 'node:path';
import { fileURLToPath } from 'node:url';
const __dirname = dirname(fileURLToPath(import.meta.url));
// ─── Formatting ──────────────────────────────────────────────────────────────
export function formatNs(ns) {
if (ns < 1000) return `${ns.toFixed(0)}ns`;
if (ns < 1_000_000) return `${(ns / 1000).toFixed(2)}µs`;
return `${(ns / 1_000_000).toFixed(2)}ms`;
}
export function formatOps(ops) {
if (ops >= 1_000_000) return `${(ops / 1_000_000).toFixed(2)}M`;
if (ops >= 1000) return `${(ops / 1000).toFixed(2)}K`;
return ops.toFixed(0);
}
function taskToRow(task) {
const { latency, throughput } = task.result;
const toNs = (ms) => ms * 1_000_000;
return {
name: task.name,
opsSec: formatOps(throughput.mean),
avg: formatNs(toNs(latency.mean)),
p50: formatNs(toNs(latency.p50)),
p75: formatNs(toNs(latency.p75)),
p99: formatNs(toNs(latency.p99)),
rme: latency.rme.toFixed(2),
samples: latency.samplesCount,
};
}
function completedTasks(bench) {
return bench.tasks.filter((t) => t.result?.state === 'completed');
}
// ─── Console output ──────────────────────────────────────────────────────────
export function printResults(bench) {
for (const task of bench.tasks) {
if (!task.result || task.result.state !== 'completed') {
console.error(
` FAILED: ${task.name}`,
task.result?.error || 'no result'
);
}
}
const results = completedTasks(bench).map((task) => {
const row = taskToRow(task);
return {
Name: row.name,
'ops/sec': row.opsSec,
avg: row.avg,
p50: row.p50,
p75: row.p75,
p99: row.p99,
'±%': row.rme,
samples: row.samples,
};
});
console.table(results);
}
export function printSectionHeader(title) {
const padded = ` ${title} `;
const width = Math.max(padded.length + 4, 59);
const inner = padded.padEnd(width - 2);
console.log(`${'─'.repeat(width - 2)}`);
console.log(`${inner}`);
console.log(`${'─'.repeat(width - 2)}`);
console.log();
}
// ─── Markdown report ─────────────────────────────────────────────────────────
export function saveMarkdownReport(
sections,
{ label, compileConfig, execConfig, date }
) {
const resultsDir = join(__dirname, 'results');
mkdirSync(resultsDir, { recursive: true });
const timestamp = date.toISOString().replace(/[:.]/g, '-').slice(0, 19);
const labelSlug = label ? `-${label.replace(/[^a-zA-Z0-9-_]/g, '_')}` : '';
const filename = `bench-${timestamp}${labelSlug}.md`;
const filepath = join(resultsDir, filename);
const lines = [];
const labelStr = label ? ` [${label}]` : '';
lines.push(`# Handlebars Benchmark Results${labelStr}`);
lines.push('');
lines.push(`- **Date:** ${date.toISOString()}`);
if (label) lines.push(`- **Label:** ${label}`);
lines.push(`- **Node:** ${process.version}`);
lines.push(`- **Platform:** ${process.platform} ${process.arch}`);
lines.push(
`- **Compile config:** warmup=${compileConfig.warmupIterations}, minIterations=${compileConfig.iterations}, time=${compileConfig.time}ms`
);
lines.push(
`- **Exec config:** warmup=${execConfig.warmupIterations}, minIterations=${execConfig.iterations}, time=${execConfig.time}ms`
);
lines.push('');
for (const { title, bench } of sections) {
lines.push(`## ${title}`);
lines.push('');
lines.push(
'| Benchmark | ops/sec | avg | p50 | p75 | p99 | ±% | samples |'
);
lines.push('|---|---|---|---|---|---|---|---|');
for (const row of completedTasks(bench).map(taskToRow)) {
lines.push(
`| ${row.name} | ${row.opsSec} | ${row.avg} | ${row.p50} | ${row.p75} | ${row.p99} | ${row.rme} | ${row.samples} |`
);
}
lines.push('');
}
writeFileSync(filepath, lines.join('\n'));
return filepath;
}
-94
View File
@@ -1,94 +0,0 @@
import { readFileSync, readdirSync, statSync } from 'node:fs';
import { dirname, join } from 'node:path';
import { fileURLToPath } from 'node:url';
const __dirname = dirname(fileURLToPath(import.meta.url));
import { gzip } from 'node:zlib';
import { promisify } from 'node:util';
import Handlebars from '../../lib/index.js';
import { templates } from './templates.mjs';
const gzipAsync = promisify(gzip);
// ─── Dist sizes ──────────────────────────────────────────────────────────────
async function measureDistSizes() {
const distDir = join(__dirname, '..', 'dist');
let files;
try {
files = readdirSync(distDir).filter((f) => {
try {
return statSync(join(distDir, f)).isFile();
} catch {
return false;
}
});
} catch {
console.warn('dist/ directory not found — run `npm run build` first.');
return [];
}
const results = [];
for (const file of files) {
const content = readFileSync(join(distDir, file));
const gzipped = await gzipAsync(content);
results.push({
File: file,
'Raw (bytes)': content.length,
'Gzip (bytes)': gzipped.length,
});
}
return results;
}
// ─── Precompile sizes ────────────────────────────────────────────────────────
function measurePrecompileSizes() {
const results = [];
for (const [name, def] of Object.entries(templates)) {
const compiled = Handlebars.precompile(def.template, {});
const helpers = {};
if (def.helpers) {
for (const k of Object.keys(def.helpers)) {
helpers[k] = true;
}
}
const knownOnly = Handlebars.precompile(def.template, {
knownHelpersOnly: true,
knownHelpers: helpers,
});
results.push({
Template: name,
'Default (chars)': compiled.length,
'knownHelpersOnly (chars)': knownOnly.length,
'Saved (chars)': compiled.length - knownOnly.length,
});
}
return results;
}
// ─── Main ────────────────────────────────────────────────────────────────────
async function run() {
console.log('Handlebars Size Report');
console.log('=====================');
console.log();
const distSizes = await measureDistSizes();
if (distSizes.length > 0) {
console.log('## Distribution files');
console.table(distSizes);
console.log();
}
console.log('## Precompiled template sizes');
const precompileSizes = measurePrecompileSizes();
console.table(precompileSizes);
}
run().catch((err) => {
console.error(err);
process.exit(1);
});
-265
View File
@@ -1,265 +0,0 @@
export const templates = {
// --- Small templates ---
'static string (no expressions)': {
template: 'Hello world',
context: {},
},
'simple variables': {
template: 'Hello {{name}}! You have {{count}} new messages.',
context: { name: 'Mick', count: 30 },
},
'dot paths': {
template:
'{{person.name.bar.baz}}{{person.age}}{{person.foo}}{{animal.age}}',
context: { person: { name: { bar: { baz: 'Larry' } }, age: 45 } },
},
'with helper': {
template: '{{#with person}}{{name}}{{age}}{{/with}}',
context: { person: { name: 'Larry', age: 45 } },
},
// --- Arguments & mustache-style ---
'arguments (positional + hash)': {
template:
'{{foo person "person" 1 true foo=bar foo="person" foo=1 foo=true}}',
context: { bar: true },
helpers: { foo: () => '' },
},
'depth-1 (../)': {
template: '{{#each names}}{{../foo}}{{/each}}',
context: {
names: [
{ name: 'Moe' },
{ name: 'Larry' },
{ name: 'Curly' },
{ name: 'Shemp' },
],
foo: 'bar',
},
},
'mustache-style section (array)': {
template: '{{#names}}{{name}}{{/names}}',
context: {
names: [
{ name: 'Moe' },
{ name: 'Larry' },
{ name: 'Curly' },
{ name: 'Shemp' },
],
},
},
'mustache-style section (object)': {
template: '{{#person}}{{name}}{{age}}{{/person}}',
context: { person: { name: 'Larry', age: 45 } },
},
// --- Medium templates ---
'each (small array, 4 items)': {
template: '{{#each names}}{{name}}{{/each}}',
context: {
names: [
{ name: 'Moe' },
{ name: 'Larry' },
{ name: 'Curly' },
{ name: 'Shemp' },
],
},
},
'each with @index/@key': {
template: '{{#each names}}{{@index}}:{{name}} {{/each}}',
context: {
names: [
{ name: 'Moe' },
{ name: 'Larry' },
{ name: 'Curly' },
{ name: 'Shemp' },
],
},
},
'if/else conditional': {
template:
'{{#if active}}<span class="active">{{name}}</span>{{else}}<span>{{name}}</span>{{/if}}',
context: { active: true, name: 'Widget' },
},
'partial (each + partial)': {
template: '{{#each peeps}}{{>variables}}{{/each}}',
context: {
peeps: [
{ name: 'Moe', count: 15 },
{ name: 'Larry', count: 5 },
{ name: 'Curly', count: 1 },
],
},
partials: {
variables: 'Hello {{name}}! You have {{count}} new messages.',
},
},
'nested depth (../../)': {
template:
'{{#each names}}{{#each name}}{{../bat}}{{../../foo}}{{/each}}{{/each}}',
context: {
names: [
{ bat: 'foo', name: ['Moe'] },
{ bat: 'foo', name: ['Larry'] },
{ bat: 'foo', name: ['Curly'] },
{ bat: 'foo', name: ['Shemp'] },
],
foo: 'bar',
},
},
subexpressions: {
template: '{{echo (header)}}',
context: { echo: () => {}, header: () => {} },
helpers: {
echo: (value) => 'foo ' + value,
header: () => 'Colors',
},
},
// --- Complex template ---
'complex (if/each/helpers)': {
template: `<h1>{{header}}</h1>
{{#if items}}
<ul>
{{#each items}}
{{#if current}}
<li><strong>{{name}}</strong></li>
{{^}}
<li><a href="{{url}}">{{name}}</a></li>
{{/if}}
{{/each}}
</ul>
{{^}}
<p>The list is empty.</p>
{{/if}}`,
context: {
header() {
return 'Colors';
},
hasItems: true,
items: [
{ name: 'red', current: true, url: '#Red' },
{ name: 'green', current: false, url: '#Green' },
{ name: 'blue', current: false, url: '#Blue' },
],
},
},
'recursive partials': {
template: '{{name}}{{#each kids}}{{>recursion}}{{/each}}',
context: {
name: '1',
kids: [{ name: '1.1', kids: [{ name: '1.1.1', kids: [] }] }],
},
partials: {
recursion: '{{name}}{{#each kids}}{{>recursion}}{{/each}}',
},
},
// --- Large/stress templates ---
'each (large array, 100 items)': {
template:
'{{#each items}}<div class="item"><h3>{{title}}</h3><p>{{description}}</p><span>{{price}}</span></div>{{/each}}',
context: {
items: Array.from({ length: 100 }, (_, i) => ({
title: `Item ${i}`,
description: `Description for item ${i} with some longer text to simulate real content`,
price: `$${(i * 9.99).toFixed(2)}`,
})),
},
},
'each (large array, 1000 items)': {
template: '{{#each items}}{{name}} {{/each}}',
context: {
items: Array.from({ length: 1000 }, (_, i) => ({ name: `item-${i}` })),
},
},
'deeply nested context (4 levels)': {
template: `{{#with level1}}
{{#with level2}}
{{#each items}}
{{#if active}}
{{../../title}}: {{name}} ({{../label}})
{{/if}}
{{/each}}
{{/with}}
{{/with}}`,
context: {
level1: {
title: 'Root',
level2: {
label: 'Section',
items: Array.from({ length: 20 }, (_, i) => ({
name: `item-${i}`,
active: i % 2 === 0,
})),
},
},
},
},
'many partials (10 partials)': {
template: Array.from({ length: 10 }, (_, i) => `{{>partial${i}}}`).join(
'\n'
),
context: { name: 'World', count: 42 },
partials: Object.fromEntries(
Array.from({ length: 10 }, (_, i) => [
`partial${i}`,
`<section><h2>Section {{name}} #${i}</h2><p>Count: {{count}}</p></section>`,
])
),
},
'page template (mixed features)': {
template: `<!DOCTYPE html>
<html>
<head><title>{{title}}</title></head>
<body>
<header>{{>header}}</header>
<nav>
<ul>
{{#each nav}}
<li{{#if active}} class="active"{{/if}}><a href="{{url}}">{{label}}</a></li>
{{/each}}
</ul>
</nav>
<main>
{{#if showBanner}}<div class="banner">{{bannerText}}</div>{{/if}}
{{#each sections}}
<section>
<h2>{{title}}</h2>
{{#each items}}
<div class="card">
<h3>{{name}}</h3>
<p>{{description}}</p>
{{#if featured}}<span class="badge">Featured</span>{{/if}}
</div>
{{/each}}
</section>
{{/each}}
</main>
<footer>{{>footer}}</footer>
</body>
</html>`,
context: {
title: 'My Page',
showBanner: true,
bannerText: 'Welcome!',
nav: [
{ label: 'Home', url: '/', active: true },
{ label: 'About', url: '/about', active: false },
{ label: 'Contact', url: '/contact', active: false },
],
sections: Array.from({ length: 5 }, (_, si) => ({
title: `Section ${si + 1}`,
items: Array.from({ length: 8 }, (_, ii) => ({
name: `Card ${si * 8 + ii + 1}`,
description: `Description for card ${si * 8 + ii + 1}`,
featured: ii === 0,
})),
})),
},
partials: {
header: '<h1>{{title}}</h1>',
footer: '<p>&copy; 2026 {{title}}</p>',
},
},
};
+13
View File
@@ -0,0 +1,13 @@
module.exports = {
helpers: {
foo: function () {
return '';
},
},
context: {
bar: true,
},
handlebars:
'{{foo person "person" 1 true foo=bar foo="person" foo=1 foo=true}}',
};
+13
View File
@@ -0,0 +1,13 @@
module.exports = {
context: {
names: [
{ name: 'Moe' },
{ name: 'Larry' },
{ name: 'Curly' },
{ name: 'Shemp' },
],
},
handlebars: '{{#each names}}{{name}}{{/each}}',
dust: '{#names}{name}{/names}',
mustache: '{{#names}}{{name}}{{/names}}',
};
+11
View File
@@ -0,0 +1,11 @@
module.exports = {
context: {
names: [
{ name: 'Moe' },
{ name: 'Larry' },
{ name: 'Curly' },
{ name: 'Shemp' },
],
},
handlebars: '{{#names}}{{name}}{{/names}}',
};
+14
View File
@@ -0,0 +1,14 @@
<h1>{header}</h1>
{?items}
<ul>
{#items}
{#current}
<li><strong>{name}</strong></li>
{:else}
<li><a href="{url}">{name}</a></li>
{/current}
{/items}
</ul>
{:else}
<p>The list is empty.</p>
{/items}
+14
View File
@@ -0,0 +1,14 @@
<h1>{{header}}</h1>
{{#if items}}
<ul>
{{#each items}}
{{#if current}}
<li><strong>{{name}}</strong></li>
{{^}}
<li><a href="{{url}}">{{name}}</a></li>
{{/if}}
{{/each}}
</ul>
{{^}}
<p>The list is empty.</p>
{{/if}}
+19
View File
@@ -0,0 +1,19 @@
var fs = require('fs');
module.exports = {
context: {
header: function () {
return 'Colors';
},
hasItems: true, // To make things fairer in mustache land due to no `{{if}}` construct on arrays
items: [
{ name: 'red', current: true, url: '#Red' },
{ name: 'green', current: false, url: '#Green' },
{ name: 'blue', current: false, url: '#Blue' },
],
},
handlebars: fs.readFileSync(__dirname + '/complex.handlebars').toString(),
dust: fs.readFileSync(__dirname + '/complex.dust').toString(),
mustache: fs.readFileSync(__dirname + '/complex.mustache').toString(),
};
+13
View File
@@ -0,0 +1,13 @@
<h1>{{header}}</h1>
{{#hasItems}}
<ul>
{{#items}}
{{#current}}
<li><strong>{{name}}</strong></li>
{{/current}}
{{^current}}
<li><a href="{{url}}">{{name}}</a></li>
{{/current}}
{{/items}}
</ul>
{{/hasItems}}
+11
View File
@@ -0,0 +1,11 @@
module.exports = {
context: {
names: [
{ name: 'Moe' },
{ name: 'Larry' },
{ name: 'Curly' },
{ name: 'Shemp' },
],
},
handlebars: '{{#each names}}{{@index}}{{name}}{{/each}}',
};

Some files were not shown because too many files have changed in this diff Show More