Compare commits

...

50 Commits

Author SHA1 Message Date
Jakob Linskeseder d069c1caf1 Fix Ruby component publishing documentation
Docker images has to use root because it tries
to write to `/.local` which would fail otherwise.
2026-03-26 22:26:07 +01:00
Jakob Linskeseder 6714e07a6a Fix Composer component definition
This aligns the Composer component with
https://github.com/components/handlebars.js/blob/master/composer.json
2026-03-26 22:15:07 +01:00
handlebars-lang dce542c9a6 v4.7.9 2026-03-26 20:44:05 +00:00
handlebars-lang 8a41389ba5 Update release notes 2026-03-26 20:43:42 +00:00
Jakob Linskeseder 68d8df5a88 Fix security issues
Fixes GHSA-2w6w-674q-4c4q, GHSA-xhpv-hc6g-r9c6, GHSA-3mfm-83xf-c92r, GHSA-2qvq-rjwj-gvw9, GHSA-9cx6-37pm-9jff, GHSA-7rx3-28cr-v5wh, GHSA-442j-39wm-28r2, GHSA-xjpj-3mr7-gcpf
2026-03-26 21:29:35 +01:00
Jakob Linskeseder b2a083136b Fix browser tests
Pinned the browser tests to the latest Playwright version that still works with Node 16.
We need Node 16 for a successful build.
2026-03-25 22:08:13 +01:00
Jakob Linskeseder 9f98c16298 Fix release script
We have to pin Grunt to 1.5.3, since this
is the latest version to support Node 10,
which is needed for the release process.

Also improved the Dockerfile and added
a warning related to "yo release".
2026-03-25 21:44:59 +01:00
Mohamed Akram 45443b4290 Revert "Improve partial indenting performance"
This reverts commit 08fddee033.

The change caused performance regressions in some cases and performance
was not consistent across engines.
2026-03-25 20:31:53 +01:00
Jakob Linskeseder 8841a5f6d3 Fix CI errors with linting 2026-02-07 22:22:17 +01:00
Tyler Blackham e0137c26f2 fix: enable shell mode for spawn to resolve Windows EINVAL issue 2026-02-07 22:12:25 +01:00
Mohamed Akram e914d6037f Improve rendering performance
Avoid unnecessary copies via Utils.extend in hot paths.
2024-09-03 22:08:38 +02:00
Christian Clauss 7de4b41c34 Upgrade GitHub Actions checkout and setup-node on 4.x branch
Like #2024 but on another branch.
2023-12-14 00:07:16 +01:00
Benoit Vallée eab1d141cb Fix type "RuntimeOptions" also accepting string partials 2023-09-21 22:30:08 +02:00
Jonas Thelemann de4414d7fc feat(types): set hash to be a Record<string, any>
Resolves #2001
2023-09-12 23:23:59 +02:00
Mohamed Akram 08fddee033 Improve partial indenting performance 2023-09-07 22:42:30 +02:00
Mohamed Akram 4512766919 Fix non-contiguous program indices 2023-09-07 22:24:57 +02:00
Nils Knappmeier e497a35d7f refactor: rename i to startPartIndex 2023-08-05 19:47:17 +02:00
Nils Knappmeier 8c9f866655 update package-lock.json to correct version 2023-08-05 00:14:57 +02:00
Jakob Linskeseder 520e1d5f08 Fix links in README
This commit backports changes from `master`.
2023-08-02 23:37:05 +02:00
Jakob Linskeseder 02423780a9 Fix link to homepage
When visiting the www-variant, a `SSL_ERROR_INTERNAL_ERROR_ALERT`
error is displayed by the browser.
2023-08-02 23:35:42 +02:00
Jakob Linskeseder be92d2f254 Backport: publish latest and tags to AWS S3
In order to automate publishing 4.x releases to AWS S3,
we also need this workflow on the 4.x branch.

See #1972
2023-08-02 23:09:34 +02:00
Jakob Linskeseder 443a613b3a Add guideline for pushing to RubyGems 2023-08-02 22:56:15 +02:00
Nils Knappmeier 83ee5908f2 add links to publish requirements 2023-08-01 23:24:13 +02:00
Jakob Linskeseder 8dc3d2517b v4.7.8 2023-08-01 23:06:18 +02:00
Jakob Linskeseder 668c4fb878 Fix browser tests in CI pipeline 2023-07-23 20:46:46 +02:00
Jakob Linskeseder c65c6cce3f Test on Node 18
* Replaced outdated `grunt-bg-shell`-package to get rid of
  coffee-script warnings

Backports parts of 785a63e0a8.
2022-10-21 16:48:14 +02:00
Marnix Kok 3d3796c1e9 Make library compatible with workers
When using Handlebars in a Cloudflare Worker, an environment
in which the `window` and `global` objects both don't exist,
an error is thrown about `window` being undefined.

According to the ECMA specification, only `self` is available
in a worker. Since we support old runtimes in our 4.x branch,
we can't use `globalThis` but have to use a polyfill.
2022-10-16 22:17:50 +02:00
Jakob Linskeseder 075b354a3b Fix sync issue with npm lock-file
```
npm ERR! `npm ci` can only install packages when your package.json and package-lock.json or npm-shrinkwrap.json are in sync. Please update your lock file with `npm install` before continuing.
npm ERR!
npm ERR! Invalid: lock file's acorn@5.7.4 does not satisfy acorn@7.4.1
npm ERR! Missing: acorn@5.7.4 from lock file
```
2022-10-16 21:55:05 +02:00
arturmakaranka 30dbf04781 Fix compiling of each block params in strict mode
Fixes #1820
Closes #1821
2022-05-25 22:18:36 +02:00
Jakob Linskeseder e3a54485db Fix bundler issue with webpack 5
As explained in issue #1844 and in issue
https://github.com/webpack/webpack/issues/15007#issuecomment-996615250,
the way we used the `browser`-field was wrong.

The main reason for using the `browser`-field is the requirement of
`require('fs')` in the main-entry-file.
The workaround for this was using `require('handlebars/lib/handlebars')`,
but now it will also work via `require('handlebars')` for bundlers that
respect the `browser`-field.

The `"./runtime"`-config was removed, because it didn't have any effect.
In order to detect regressions, the webpack-integration test was
extended to test with different webpack versions.

Fixes #1174
Closes #1844
2022-05-17 21:19:48 +02:00
Jakob Linskeseder 8e23642ea2 Fix integration-tests issue with npm >= 7
Fixes the following error when running integration-tests:
```
npm ERR! code ERESOLVE
npm ERR! ERESOLVE unable to resolve dependency tree
npm ERR!
npm ERR! While resolving: webpack-test@1.0.0
npm ERR! Found: handlebars@5.0.0-alpha.1
npm ERR! node_modules/handlebars
npm ERR!   dev handlebars@"file:../../.." from the root project
npm ERR!
npm ERR! Could not resolve dependency:
npm ERR! peer handlebars@">= 1.3.0 < 5" from handlebars-loader@1.7.1
npm ERR! node_modules/handlebars-loader
npm ERR!   dev handlebars-loader@"^1.7.1" from the root project
npm ERR!
```
2022-05-17 21:19:48 +02:00
James Wilson 88ac06875f use https instead of git for mustache submodule 2022-03-19 19:19:27 +01:00
Jakob Linskeseder c68bc08a0d Fix typo 2022-01-08 00:39:29 +01:00
Nils Knappmeier 6cfbc2653a Use eslint-env "shared-node-browser" for shared handlebars code 2022-01-07 18:15:48 +01:00
Jakob Linskeseder b65135acef Replace shields.io with faster badgen.net
Badgen is a faster successor of shields.io:
https://github.com/badgen/badgen.net

Backport from `master`-branch.
2021-12-30 00:58:48 +01:00
Jakob Linskeseder e2f63da5c0 Don't create security alerts for dev-dependencies 2021-12-29 22:48:01 +01:00
Jakob Linskeseder 78e7e28ff9 Replace custom test-server with Grunt connect 2021-12-29 21:35:16 +01:00
Jakob Linskeseder 03d387bf8e Fix rollup warning when importing Handlebars as ESM
Closes #1696
2021-12-29 21:35:16 +01:00
Jakob Linskeseder e0f50b4eec Add integration-tests to CI workflow 2021-12-29 21:35:16 +01:00
Jakob Linskeseder 9ed9418488 Replace Saucelabs with Playwright
Also reorganized npm scripts.
2021-12-29 21:35:16 +01:00
Jakob Linskeseder ef0fc290b9 Move print-script.js-file in tests-folder
Also removed it from published files, since the script doesn't seem to be used.

Fourth part of reorganizing and cleaning up test-folders.
2021-12-29 21:35:16 +01:00
Jakob Linskeseder edc65b5c19 Move integration-testing-folder in tests-folder
Third part of reorganizing and cleaning up test-folders.
2021-12-29 21:35:16 +01:00
Jakob Linskeseder 715f4af179 Remove redundant task-part in task-tests-folder
Second part of reorganizing and cleaning up test-folders.
2021-12-29 21:35:16 +01:00
Jakob Linskeseder 3bd0fa8b32 Move bench-folder in tests-folder
First part of reorganizing and cleaning up test-folders.
2021-12-29 21:35:16 +01:00
Jakob Linskeseder c295ef085f Update repository URL 2021-12-03 23:12:20 +01:00
Jakob Linskeseder c1ad3c8057 Update contributing guidelines 2021-12-03 23:12:20 +01:00
Jakob Linskeseder af92e32822 Run CI tests on more versions 2021-12-03 23:12:20 +01:00
Jakob Linskeseder 2954e7ea66 Don't rely on Node.js global object
If `global` is used and handlebars is compiled for browser
usage without a Node.js `global` polyfill, handlebars
fails with a `global is undefined` error.

Fixes #1593
2021-12-03 21:57:55 +01:00
Jay Linski 8eefee56ff Replace Travis CI with GitHub action (#1772) 2021-12-03 19:04:01 +01:00
Shon Frazier fd93073146 Fix Mustache links
...to point to 'https://mustache.github.io/...'
2021-11-23 23:58:57 +01:00
137 changed files with 14985 additions and 5603 deletions
+11
View File
@@ -0,0 +1,11 @@
root = true
[*.js]
indent_size = 2
insert_final_newline = true
trim_trailing_whitespace = true
[*.yml]
indent_size = 2
insert_final_newline = true
trim_trailing_whitespace = true
+1 -2
View File
@@ -4,7 +4,6 @@
*.sublime-project
*.sublime-workspace
npm-debug.log
sauce_connect.log*
.idea
yarn-error.log
node_modules
@@ -15,7 +14,7 @@ node_modules
lib/handlebars/compiler/parser.js
/coverage/
/dist/
/integration-testing/*/dist/
/tests/integration/*/dist/
# Third-party or files that must remain unchanged
/spec/expected/
+9
View File
@@ -0,0 +1,9 @@
version: 2
updates:
- package-ecosystem: npm
directory: "/"
open-pull-requests-limit: 0
schedule:
interval: weekly
allow:
- dependency-type: production
+89
View File
@@ -0,0 +1,89 @@
name: CI
on:
push:
branches:
- master
pull_request: {}
jobs:
lint:
name: Lint
runs-on: 'ubuntu-latest'
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '16'
- name: Install dependencies
run: npm ci
- name: Lint
run: npm run lint
test:
name: Test (Node)
runs-on: ${{ matrix.operating-system }}
strategy:
fail-fast: false
matrix:
operating-system: ['ubuntu-latest', 'windows-latest']
# https://nodejs.org/en/about/releases/
node-version: ['16', '18', '20', '22']
steps:
- name: Checkout
uses: actions/checkout@v4
with:
submodules: true
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node-version }}
- name: Install dependencies
run: npm ci
- name: Test
run: npm run test
- name: Test (Integration)
# https://github.com/webpack/webpack/issues/14532
if: ${{ matrix.node-version == '16' }}
run: |
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:
name: Test (Browser)
runs-on: 'ubuntu-22.04'
steps:
- name: Checkout
uses: actions/checkout@v4
with:
submodules: true
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '16'
- name: Install dependencies
run: npm ci
- name: Install Playwright
run: |
npx playwright install-deps
npx playwright install
- name: Build
run: npx grunt prepare
- name: Test
run: npm run test:browser
+38
View File
@@ -0,0 +1,38 @@
name: Release
on:
workflow_dispatch:
push:
branches:
- master
tags:
- '*'
jobs:
publish-aws-s3:
name: Publish to AWS S3
runs-on: 'ubuntu-latest'
environment: 'builds.handlebarsjs.com.s3.amazonaws.com'
steps:
- name: Checkout
uses: actions/checkout@v2
with:
submodules: true
- name: Setup Node.js
uses: actions/setup-node@v2
with:
node-version: '16'
- name: Install dependencies
run: npm ci
- name: Publish
run: |
git config --global user.email "release@handlebarsjs.com"
git config --global user.name "handlebars-lang"
npm run publish:aws
env:
S3_BUCKET_NAME: "builds.handlebarsjs.com"
S3_ACCESS_KEY_ID: ${{ secrets.S3_ACCESS_KEY_ID }}
S3_SECRET_ACCESS_KEY: ${{ secrets.S3_SECRET_ACCESS_KEY }}
+3 -3
View File
@@ -4,7 +4,6 @@
*.sublime-project
*.sublime-workspace
npm-debug.log
sauce_connect.log*
.idea
/yarn-error.log
/yarn.lock
@@ -16,5 +15,6 @@ node_modules
lib/handlebars/compiler/parser.js
/coverage/
/dist/
/integration-testing/*/dist/
/spec/tmp/*
/test-results/
/tests/integration/*/dist/
/spec/tmp/*
+1 -1
View File
@@ -1,3 +1,3 @@
[submodule "spec/mustache"]
path = spec/mustache
url = git://github.com/mustache/spec.git
url = https://github.com/mustache/spec.git
+1 -1
View File
@@ -15,7 +15,7 @@ node_modules
lib/handlebars/compiler/parser.js
/coverage/
/dist/
/integration-testing/*/dist/
/tests/integration/*/dist/
# Third-party or files that must remain unchanged
/spec/expected/
-39
View File
@@ -1,39 +0,0 @@
language: node_js
jobs:
include:
- stage: test
name: check javascript (eslint)
node_js: lts/*
script: npm run lint
- stage: test
name: check formatting (prettier)
node_js: lts/*
script: npm run check-format
- stage: test
name: check typescript definitions (dtslint)
node_js: lts/*
script: npm run dtslint
- stage: test
name: extensive tests and publish to aws
script: npm run extensive-tests-and-publish-to-aws
env:
- S3_BUCKET_NAME=builds.handlebarsjs.com
- secure: ckyEe5dzjdFDjmZ6wIrhGm0CFBEnKq8c1dYptfgVV/Q5/nJFGzu8T0yTjouS/ERxzdT2H327/63VCxhFnLCRHrsh4rlW/rCy4XI3O/0TeMLgFPa4TXkO8359qZ4CB44TBb3NsJyQXNMYdJpPLTCVTMpuiqqkFFOr+6OeggR7ufA=
- secure: Nm4AgSfsgNB21kgKrF9Tl7qVZU8YYREhouQunFracTcZZh2NZ2XH5aHuSiXCj88B13Cr/jGbJKsZ4T3QS3wWYtz6lkyVOx3H3iI+TMtqhD9RM3a7A4O+4vVN8IioB2YjhEu0OKjwgX5gp+0uF+pLEi7Hpj6fupD3AbbL5uYcKg8=
- SAUCE_USERNAME=handlebars
- secure: 1VkLQhbsEug4ZMQ52tTOus/WLvW3Etqe7GbCzZfzsI8d2ygJPjFfzU8fNm4pVVwoTI21MaM5AQq7SVPu8DWN1YbDjJycMdY1zO3DsB9aZBxTal98fIB7ZIUce9r5z2EP6mETrsbYjZkeckzIBI0A4UVa+F2BO4KbRDXP1Db3u3I=
node_js: '10'
- stage: test
name: test with latest nodejs-lts
node_js: lts/*
script: npm run test
- stage: test
name: test with active nodejs
node_js: node
script: npm run test
cache: npm
email:
on_failure: change
on_success: never
git:
depth: 100
+96 -28
View File
@@ -2,13 +2,13 @@
## Reporting Issues
Please see our [FAQ](https://github.com/wycats/handlebars.js/blob/master/FAQ.md) for common issues that people run into.
Please see our [FAQ](https://github.com/handlebars-lang/handlebars.js/blob/master/FAQ.md) for common issues that people run into.
Should you run into other issues with the project, please don't hesitate to let us know by filing an [issue][issue]! In general we are going to ask for an example of the problem failing, which can be as simple as a jsfiddle/jsbin/etc. We've put together a jsfiddle [template][jsfiddle] to ease this. (We will keep this link up to date as new releases occur, so feel free to check back here)
Pull requests containing only failing tests demonstrating the issue are welcomed and this also helps ensure that your issue won't regress in the future once it's fixed.
Documentation issues on the handlebarsjs.com site should be reported on [handlebars-site](https://github.com/wycats/handlebars-site).
Documentation issues on the [handlebarsjs.com](https://handlebarsjs.com) site should be reported on [handlebars-lang/docs](https://github.com/handlebars-lang/docs).
## Branches
@@ -47,9 +47,9 @@ 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
[http://github.com/wycats/handlebars.js/issues](http://github.com/wycats/handlebars.js/issues).
[http://github.com/handlebars-lang/handlebars.js/issues](http://github.com/handlebars-lang/handlebars.js/issues).
##Running Tests
## Running Tests
To run tests locally, first install all dependencies.
@@ -78,43 +78,111 @@ 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 travis-ci job also lints all files and checks if they are formatted correctly. In this stage, warnings
- 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 travis-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 `eslint` and fail on warnings
- **npm run format** will run `prettier` on all files
- **npm run check-before-pull-request** will perform all most checks that travis does in its build-job, excluding the "integration-test".
- **npm run integration-test** will run integration tests (using old NodeJS versions and integrations with webpack, babel and so on)
- **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 (using old NodeJS versions and integrations with webpack, babel and so on)
These tests only work on a Linux-machine with `nvm` installed (for running tests in multiple versions of NodeJS).
## Releasing the latest version
Before attempting the release Handlebars, please make sure that you have the following authorizations:
- Push-access to `wycats/handlebars.js`
- Publishing rights on npmjs.com for the `handlebars` package
- Publishing rights on gemfury for the `handlebars-source` package
- Push-access to the repo for legacy package managers: `components/handlebars`
- Push-access to the production-repo of the handlebars site: `handlebars-lang/handlebarsjs.com-github-pages`
- Push-access to [handlebars-lang/handlebars.js](https://github.com/handlebars-lang/handlebars.js/)
- Publishing rights on npmjs.com for the [handlebars](https://www.npmjs.com/package/handlebars) package
- Publishing rights on rubygems for the [handlebars-source](https://rubygems.org/gems/handlebars-source) package
- Push-access to the repo for legacy package managers: [components/handlebars.js](https://github.com/components/handlebars.js)
- Push-access to the production-repo of the handlebars site: [handlebars-lang/docs](https://github.com/handlebars-lang/docs)
_When releasing a previous version of Handlebars, please look into the CONTRIBUNG.md in the corresponding branch._
Handlebars utilizes the [release yeoman generator][generator-release] to perform most release tasks.
A full release via Docker may be completed with the following:
A full release may be completed with the following:
1. Create a `Dockerfile` in this folder for releasing
```Dockerfile
FROM node:10-slim
ENV EDITOR=vim
# Update stretch repositories
RUN sed -i -e 's/deb.debian.org/archive.debian.org/g' \
-e 's|security.debian.org|archive.debian.org/|g' \
-e '/stretch-updates/d' /etc/apt/sources.list
# Install release dependencies
RUN apt-get update
RUN apt-get install -y git vim
# Work around deprecated npm dependency install via unauthenticated git-protocol:
# https://github.com/kpdecker/generator-release/blob/87aab9b84c9f083635c3fcc822f18acce1f48736/package.json#L31
RUN git config --system url."https://github.com/".insteadOf git://github.com/
# Configure git
RUN git config --system user.email "release@handlebarsjs.com"
RUN git config --system user.name "handlebars-lang"
RUN mkdir /home/node/.config
RUN mkdir /home/node/.ssh
RUN mkdir /home/node/tmp
# Generate config for yo generator-release:
# https://github.com/kpdecker/generator-release#example
# You have to add a valid GitHub access token! (Used for reading issues and pull requests.)
RUN echo "module.exports = {\n auth: 'oauth',\n token: 'GitHub personal access token'\n};" > /home/node/.config/generator-release
RUN chown -R node:node /home/node/.config
RUN chown -R node:node /home/node/.ssh
RUN chown -R node:node /home/node/tmp
# Add the generated key to GitHub: https://github.com/settings/keys
RUN ssh-keygen -q -t ed25519 -N '' -f /home/node/.ssh/id_ed25519 -C "release@handlebarsjs.com"
RUN chmod 0600 /home/node/.ssh/id_ed25519*
RUN chown node:node /home/node/.ssh/id_ed25519*
```
2. Build and run the Docker image
```bash
docker build --tag handlebars:release .
docker run --rm --interactive --tty \
--volume $PWD:/app \
--workdir /app \
--user $(id -u):$(id -g) \
--env NPM_CONFIG_PREFIX=/home/node/.npm-global \
handlebars:release bash -c 'export PATH=$PATH:/home/node/.npm-global/bin; bash'
```
* Add SSH key to GitHub: `cat /home/node/.ssh/id_ed25519.pub` (https://github.com/settings/keys)
* Add GitHub API token: `vi /home/node/.config/generator-release`
* Execute the following steps:
```bash
npm install
npm install -g yo@1 grunt@1 generator-release
npm run release
# Warning! This step will collect data from GitHub, bump the version,
# create a new commit, create a new tag and push it to GitHub.
# https://github.com/kpdecker/generator-release?tab=readme-ov-file#usage
yo release
npm login
npm publish
yo release:publish components handlebars.js dist/components/
```
6. Publish Ruby `handlebars-source` gem:
```bash
docker run --rm --interactive --tty \
--volume $PWD:/app \
--workdir /app \
ruby:3.2-slim bash
```
* Execute the following steps:
```bash
cd dist/components/
gem build handlebars-source.gemspec
gem push handlebars-source-*.gem
```
```
npm ci
yo release
npm publish
yo release:publish components handlebars.js dist/components/
cd dist/components/
gem build handlebars-source.gemspec
gem push handlebars-source-*.gem
```
### After the release
After the release, you should check that all places have really been updated. Especially verify that the `latest`-tags
in those places still point to the latest version
@@ -126,13 +194,13 @@ in those places still point to the latest version
When everything is OK, the **handlebars site** needs to be updated.
Go to the master branch of the repo [handlebars-lang/handlebarsjs.com-github-pages](https://github.com/handlebars-lang/handlebarsjs.com-github-pages/tree/master)
Go to the master branch of the repo [handlebars-lang/docs](https://github.com/handlebars-lang/docs/tree/master)
and make a minimal change to the README. This will invoke a github-action that redeploys
the site, fetching the latest version-number from the npm-registry.
(note that the default-branch of this repo is not the master and regular changes are done
in the `handlebars-lang/docs`-repo).
[generator-release]: https://github.com/walmartlabs/generator-release
[pull-request]: https://github.com/wycats/handlebars.js/pull/new/master
[issue]: https://github.com/wycats/handlebars.js/issues/new
[pull-request]: https://github.com/handlebars-lang/handlebars.js/pull/new/master
[issue]: https://github.com/handlebars-lang/handlebars.js/issues/new
[jsfiddle]: https://jsfiddle.net/9D88g/180/
+7 -5
View File
@@ -2,11 +2,11 @@
1. How can I file a bug report:
See our guidelines on [reporting issues](https://github.com/wycats/handlebars.js/blob/master/CONTRIBUTING.md#reporting-issues).
See our guidelines on [reporting issues](https://github.com/handlebars-lang/handlebars.js/blob/master/CONTRIBUTING.md#reporting-issues).
1. Why isn't my Mustache template working?
Handlebars deviates from Mustache slightly on a few behaviors. These variations are documented in our [readme](https://github.com/wycats/handlebars.js#differences-between-handlebarsjs-and-mustache).
Handlebars deviates from Mustache slightly on a few behaviors. These variations are documented in our [readme](https://github.com/handlebars-lang/handlebars.js#differences-between-handlebarsjs-and-mustache).
1. Why is it slower when compiling?
@@ -36,16 +36,18 @@
```sh
handlebars --version
```
If using the integrated precompiler and
```javascript
console.log(Handlebars.VERSION);
```
On the client side.
We include the built client libraries in the npm package for those who want to be certain that they are using the same client libraries as the compiler.
Should these match, please file an issue with us, per our [issue filing guidelines](https://github.com/wycats/handlebars.js/blob/master/CONTRIBUTING.md#reporting-issues).
Should these match, please file an issue with us, per our [issue filing guidelines](https://github.com/handlebars-lang/handlebars.js/blob/master/CONTRIBUTING.md#reporting-issues).
1. Why doesn't IE like the `default` name in the AMD module?
@@ -53,8 +55,8 @@
1. How do I load the runtime library when using AMD?
There are two options for loading under AMD environments. The first is to use the `handlebars.runtime.amd.js` file. This may require a [path mapping](https://github.com/wycats/handlebars.js/blob/master/spec/amd-runtime.html#L31) as well as access via the `default` field.
There are two options for loading under AMD environments. The first is to use the `handlebars.runtime.amd.js` file. This may require a [path mapping](https://github.com/handlebars-lang/handlebars.js/blob/master/spec/amd-runtime.html#L31) as well as access via the `default` field.
The other option is to load the `handlebars.runtime.js` UMD build, which might not require path configuration and exposes the library as both the module root and the `default` field for compatibility.
The other option is to load the `handlebars.runtime.js` UMD build, which might not require path configuration and exposes the library as both the module root and the `default` field for compatibility.
If not using ES6 transpilers or accessing submodules in the build the former option should be sufficient for most use cases.
+21 -82
View File
@@ -7,7 +7,7 @@ module.exports = function(grunt) {
'tmp',
'dist',
'lib/handlebars/compiler/parser.js',
'integration-testing/**/node_modules'
'/tests/integration/**/node_modules'
],
copy: {
@@ -165,54 +165,10 @@ module.exports = function(grunt) {
}
}
},
'saucelabs-mocha': {
all: {
options: {
build: process.env.TRAVIS_JOB_ID,
urls: [
'http://localhost:9999/spec/?headless=true',
'http://localhost:9999/spec/amd.html?headless=true'
],
detailedError: true,
concurrency: 4,
browsers: [
{ browserName: 'chrome' },
{ browserName: 'firefox', platform: 'Linux' }
// {browserName: 'safari', version: 9, platform: 'OS X 10.11'},
// {browserName: 'safari', version: 8, platform: 'OS X 10.10'},
// {
// browserName: 'internet explorer',
// version: 11,
// platform: 'Windows 8.1'
// },
// {
// browserName: 'internet explorer',
// version: 10,
// platform: 'Windows 8'
// }
]
}
},
sanity: {
options: {
build: process.env.TRAVIS_JOB_ID,
urls: [
'http://localhost:9999/spec/umd.html?headless=true',
'http://localhost:9999/spec/amd-runtime.html?headless=true',
'http://localhost:9999/spec/umd-runtime.html?headless=true'
],
detailedError: true,
concurrency: 2,
browsers: [{ browserName: 'chrome' }]
}
}
},
bgShell: {
shell: {
integrationTests: {
cmd: './integration-testing/run-integration-tests.sh',
bg: false,
fail: true
command: './tests/integration/run-integration-tests.sh'
}
},
@@ -237,23 +193,15 @@ module.exports = function(grunt) {
grunt.loadNpmTasks('grunt-contrib-uglify');
grunt.loadNpmTasks('grunt-contrib-watch');
grunt.loadNpmTasks('grunt-babel');
grunt.loadNpmTasks('grunt-bg-shell');
grunt.loadNpmTasks('@knappi/grunt-saucelabs');
grunt.loadNpmTasks('grunt-shell');
grunt.loadNpmTasks('grunt-webpack');
grunt.task.loadTasks('tasks');
this.registerTask(
'build',
'Builds a distributable version of the current project',
['parser', 'node', 'globals']
);
this.registerTask('node', ['babel:cjs']);
this.registerTask('globals', ['webpack']);
this.registerTask('release', 'Build final packages', [
'amd',
grunt.registerTask('node', ['babel:cjs']);
grunt.registerTask('amd', ['babel:amd', 'requirejs']);
grunt.registerTask('globals', ['webpack']);
grunt.registerTask('release', 'Build final packages', [
'uglify',
'test:min',
'copy:dist',
@@ -261,38 +209,29 @@ module.exports = function(grunt) {
'copy:cdnjs'
]);
this.registerTask('amd', ['babel:amd', 'requirejs']);
this.registerTask('test', ['test:bin', 'test:cov']);
grunt.registerTask('bench', ['metrics']);
if (process.env.SAUCE_ACCESS_KEY) {
grunt.registerTask('sauce', ['concat:tests', 'connect', 'saucelabs-mocha']);
} else {
grunt.registerTask('sauce', []);
}
// Requires secret properties (saucelabs-credentials etc.) from .travis.yaml
// Requires secret properties from .travis.yaml
grunt.registerTask('extensive-tests-and-publish-to-aws', [
'default',
'bgShell:integrationTests',
'sauce',
'shell:integrationTests',
'metrics',
'publish-to-aws'
]);
grunt.registerTask('on-file-change', [
'build',
'amd',
'concat:tests',
'test'
]);
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',
['parser', 'node', 'amd', 'globals']
);
grunt.registerTask('integration-tests', [
'default',
'bgShell:integrationTests'
'shell:integrationTests'
]);
};
+15 -16
View File
@@ -1,17 +1,18 @@
[![Travis Build Status](https://img.shields.io/travis/wycats/handlebars.js/master.svg)](https://travis-ci.org/wycats/handlebars.js)
[![Appveyor Build Status](https://ci.appveyor.com/api/projects/status/github/wycats/handlebars.js?branch=master&svg=true)](https://ci.appveyor.com/project/wycats/handlebars-js)
[![Selenium Test Status](https://saucelabs.com/buildstatus/handlebars)](https://saucelabs.com/u/handlebars)
[![CI Build Status](https://github.com/handlebars-lang/handlebars.js/actions/workflows/ci.yml/badge.svg)](https://github.com/handlebars-lang/handlebars.js/actions/workflows/ci.yml)
[![jsDelivr Hits](https://data.jsdelivr.com/v1/package/npm/handlebars/badge?style=rounded)](https://www.jsdelivr.com/package/npm/handlebars)
[![npm downloads](https://badgen.net/npm/dm/handlebars)](https://www.npmjs.com/package/handlebars)
[![npm version](https://badgen.net/npm/v/handlebars)](https://www.npmjs.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)
Handlebars.js
=============
Handlebars.js is an extension to the [Mustache templating
language](http://mustache.github.com/) created by Chris Wanstrath.
Handlebars.js and Mustache are both logicless templating languages that
keep the view and the code separated like we all know they should be.
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.
Checkout the official Handlebars docs site at
[https://handlebarsjs.com/](https://handlebarsjs.com) and the live demo at [http://tryhandlebarsjs.com/](http://tryhandlebarsjs.com/).
[handlebarsjs.com](https://handlebarsjs.com) and try our [live demo](https://handlebarsjs.com/playground.html).
Installing
----------
@@ -22,7 +23,7 @@ Usage
-----
In general, the syntax of Handlebars.js templates is a superset
of Mustache templates. For basic syntax, check out the [Mustache
manpage](http://mustache.github.com/mustache.5.html).
manpage](https://mustache.github.io/mustache.5.html).
Once you have a template, use the `Handlebars.compile` method to compile
the template into a function. The generated function takes a context
@@ -64,7 +65,7 @@ templates easier and also changes a tiny detail of how partials work.
- [Literal Values](https://handlebarsjs.com/guide/expressions.html#literal-segments)
- [Delimited Comments](https://handlebarsjs.com/guide/#template-comments)
Block expressions have the same syntax as mustache sections but should not be confused with one another. Sections are akin to an implicit `each` or `with` statement depending on the input data and helpers are explicit pieces of code that are free to implement whatever behavior they like. The [mustache spec](http://mustache.github.io/mustache.5.html) defines the exact behavior of sections. In the case of name conflicts, helpers are given priority.
Block expressions have the same syntax as mustache sections but should not be confused with one another. Sections are akin to an implicit `each` or `with` statement depending on the input data and helpers are explicit pieces of code that are free to implement whatever behavior they like. The [mustache spec](https://mustache.github.io/mustache.5.html) defines the exact behavior of sections. In the case of name conflicts, helpers are given priority.
### Compatibility
@@ -89,8 +90,6 @@ Handlebars has been designed to work in any ECMAScript 3 environment. This inclu
Older versions and other runtimes are likely to work but have not been formally
tested. The compiler requires `JSON.stringify` to be implemented natively or via a polyfill. If using the precompiler this is not necessary.
[![Selenium Test Status](https://saucelabs.com/browser-matrix/handlebars.svg)](https://saucelabs.com/u/handlebars)
Performance
-----------
@@ -102,18 +101,18 @@ does have some big performance advantages. Justin Marney, a.k.a.
[gotascii](http://github.com/gotascii), confirmed that with an
[independent test](http://sorescode.com/2010/09/12/benchmarks.html). The
rewritten Handlebars (current version) is faster than the old version,
with many [performance tests](https://travis-ci.org/wycats/handlebars.js/builds/33392182#L538) being 5 to 7 times faster than the Mustache equivalent.
with many performance tests being 5 to 7 times faster than the Mustache equivalent.
Upgrading
---------
See [release-notes.md](https://github.com/wycats/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.
Known Issues
------------
See [FAQ.md](https://github.com/wycats/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
@@ -165,4 +164,4 @@ License
-------
Handlebars.js is released under the MIT license.
[pull-request]: https://github.com/wycats/handlebars.js/pull/new/master
[pull-request]: https://github.com/handlebars-lang/handlebars.js/pull/new/master
-36
View File
@@ -1,36 +0,0 @@
# Test against these versions of Node.js
environment:
matrix:
- nodejs_version: "10"
platform:
- x64
# Install scripts (runs after repo cloning)
install:
# Get the latest stable version of Node.js
- ps: Install-Product node $env:nodejs_version $env:platform
# Clone submodules (mustache spec)
- cmd: git submodule update --init --recursive
# Install modules
- cmd: npm ci
# Post-install test scripts
test_script:
# Output useful info for debugging
- cmd: node --version
- cmd: npm --version
# Run tests
- cmd: npm run test
# Don't actually build
build: off
on_failure:
- cmd: 7z a coverage.zip coverage
- cmd: appveyor PushArtifact coverage.zip
# Set build version format here instead of in the admin panel
version: "{build}"
-15
View File
@@ -1,15 +0,0 @@
{
"extends": "prettier",
"globals": {
"require": true
},
"rules": {
// Disabling for tests, for now.
"no-path-concat": 0,
"no-var": 0,
"no-shadow": 0,
"handle-callback-err": 0,
"no-console": 0
}
}
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "handlebars",
"version": "4.7.7",
"version": "4.7.9",
"main": "handlebars.js",
"license": "MIT",
"dependencies": {}
+2 -6
View File
@@ -1,7 +1,7 @@
{
"name": "components/handlebars.js",
"description": "Handlebars.js and Mustache are both logicless templating languages that keep the view and the code separated like we all know they should be.",
"homepage": "http://handlebarsjs.com",
"homepage": "https://handlebarsjs.com",
"license": "MIT",
"type": "component",
"keywords": [
@@ -11,13 +11,9 @@
],
"authors": [
{
"name": "Chris Wanstrath",
"homepage": "http://chriswanstrath.com"
"name": "Chris Wanstrath"
}
],
"require": {
"robloach/component-installer": "*"
},
"extra": {
"component": {
"name": "handlebars",
+1 -1
View File
@@ -10,7 +10,7 @@ Gem::Specification.new do |gem|
gem.date = Time.now.strftime("%Y-%m-%d")
gem.description = %q{Handlebars.js source code wrapper for (pre)compilation gems.}
gem.summary = %q{Handlebars.js source code wrapper}
gem.homepage = "https://github.com/wycats/handlebars.js/"
gem.homepage = "https://github.com/handlebars-lang/handlebars.js/"
gem.version = package["version"].sub "-", "."
gem.license = "MIT"
+3 -3
View File
@@ -2,10 +2,10 @@
<package>
<metadata>
<id>handlebars.js</id>
<version>4.7.7</version>
<version>4.7.9</version>
<authors>handlebars.js Authors</authors>
<licenseUrl>https://github.com/wycats/handlebars.js/blob/master/LICENSE</licenseUrl>
<projectUrl>https://github.com/wycats/handlebars.js/</projectUrl>
<licenseUrl>https://github.com/handlebars-lang/handlebars.js/blob/master/LICENSE</licenseUrl>
<projectUrl>https://github.com/handlebars-lang/handlebars.js/</projectUrl>
<requireLicenseAcceptance>false</requireLicenseAcceptance>
<description>Extension of the Mustache logicless template language</description>
<releaseNotes></releaseNotes>
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "handlebars",
"version": "4.7.7",
"version": "4.7.9",
"license": "MIT",
"jspm": {
"main": "handlebars",
+1 -1
View File
@@ -16,4 +16,4 @@ Decorators are executed when the block program is instantiated and are passed `(
Decorators may set values on `props` or return a modified function that wraps `program` in particular behaviors. If the decorator returns nothing, then `program` is left unaltered.
The [inline partial](https://github.com/wycats/handlebars.js/blob/master/lib/handlebars/decorators/inline.js) implementation provides an example of decorators being used for both metadata and wrapping behaviors.
The [inline partial](https://github.com/handlebars-lang/handlebars.js/blob/master/lib/handlebars/decorators/inline.js) implementation provides an example of decorators being used for both metadata and wrapping behaviors.
@@ -1,21 +0,0 @@
{
"name": "webpack-test",
"description": "Various tests with Handlebars and Webpack",
"version": "1.0.0",
"main": "index.js",
"scripts": {
"build": "webpack --config webpack.config.js",
"test": "node dist/main.js"
},
"private": true,
"keywords": [],
"author": "",
"license": "ISC",
"dependencies": {},
"devDependencies": {
"handlebars": "file:../..",
"handlebars-loader": "^1.7.1",
"webpack": "^4.39.3",
"webpack-cli": "^3.3.7"
}
}
+8
View File
@@ -0,0 +1,8 @@
module.exports = {
env: {
// Handlebars should not use node or browser-specific APIs
'shared-node-browser': true,
node: false,
browser: false
}
};
+1 -1
View File
@@ -5,7 +5,7 @@ import { registerDefaultDecorators } from './decorators';
import logger from './logger';
import { resetLoggedProperties } from './internal/proto-access';
export const VERSION = '4.7.7';
export const VERSION = '4.7.9';
export const COMPILER_REVISION = 8;
export const LAST_COMPATIBLE_COMPILER_REVISION = 7;
+67
View File
@@ -1,6 +1,7 @@
import parser from './parser';
import WhitespaceControl from './whitespace-control';
import * as Helpers from './helpers';
import Exception from '../exception';
import { extend } from '../utils';
export { parser };
@@ -11,6 +12,9 @@ extend(yy, Helpers);
export function parseWithoutProcessing(input, options) {
// Just return if an already-compiled AST was passed in.
if (input.type === 'Program') {
// When a pre-parsed AST is passed in, validate all node values to prevent
// code injection via type-confused literals.
validateInputAst(input);
return input;
}
@@ -32,3 +36,66 @@ export function parse(input, options) {
return strip.accept(ast);
}
function validateInputAst(ast) {
validateAstNode(ast);
}
function validateAstNode(node) {
if (node == null) {
return;
}
if (Array.isArray(node)) {
node.forEach(validateAstNode);
return;
}
if (typeof node !== 'object') {
return;
}
if (node.type === 'PathExpression') {
if (!isValidDepth(node.depth)) {
throw new Exception(
'Invalid AST: PathExpression.depth must be an integer'
);
}
if (!Array.isArray(node.parts)) {
throw new Exception('Invalid AST: PathExpression.parts must be an array');
}
for (let i = 0; i < node.parts.length; i++) {
if (typeof node.parts[i] !== 'string') {
throw new Exception(
'Invalid AST: PathExpression.parts must only contain strings'
);
}
}
} else if (node.type === 'NumberLiteral') {
if (typeof node.value !== 'number' || !isFinite(node.value)) {
throw new Exception('Invalid AST: NumberLiteral.value must be a number');
}
} else if (node.type === 'BooleanLiteral') {
if (typeof node.value !== 'boolean') {
throw new Exception(
'Invalid AST: BooleanLiteral.value must be a boolean'
);
}
}
Object.keys(node).forEach(propertyName => {
if (propertyName === 'loc') {
return;
}
validateAstNode(node[propertyName]);
});
}
function isValidDepth(depth) {
return (
typeof depth === 'number' &&
isFinite(depth) &&
Math.floor(depth) === depth &&
depth >= 0
);
}
+2 -2
View File
@@ -1,4 +1,4 @@
/* global define */
/* global define, require */
import { isArray } from '../utils';
let SourceNode;
@@ -6,7 +6,7 @@ let SourceNode;
try {
/* istanbul ignore next */
if (typeof define !== 'function' || !define.amd) {
// We don't support this in AMD environments. For these environments, we asusme 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.
let SourceMap = require('source-map');
SourceNode = SourceMap.SourceNode;
+30 -16
View File
@@ -165,12 +165,10 @@ JavaScriptCompiler.prototype = {
let { programs, decorators } = this.context;
for (i = 0, l = programs.length; i < l; i++) {
if (programs[i]) {
ret[i] = programs[i];
if (decorators[i]) {
ret[i + '_d'] = decorators[i];
ret.useDecorators = true;
}
ret[i] = programs[i];
if (decorators[i]) {
ret[i + '_d'] = decorators[i];
ret.useDecorators = true;
}
}
@@ -535,14 +533,22 @@ JavaScriptCompiler.prototype = {
this.resolvePath('data', parts, 0, true, strict);
},
resolvePath: function(type, parts, i, falsy, strict) {
resolvePath: function(type, parts, startPartIndex, falsy, strict) {
if (this.options.strict || this.options.assumeObjects) {
this.push(strictLookup(this.options.strict && strict, this, parts, type));
this.push(
strictLookup(
this.options.strict && strict,
this,
parts,
startPartIndex,
type
)
);
return;
}
let len = parts.length;
for (; i < len; i++) {
for (let i = startPartIndex; i < len; i++) {
/* eslint-disable no-loop-func */
this.replaceStack(current => {
let lookup = this.nameLookup(current, parts[i], type);
@@ -680,9 +686,18 @@ JavaScriptCompiler.prototype = {
let foundDecorator = this.nameLookup('decorators', name, 'decorator'),
options = this.setupHelperArgs(name, paramSize);
// Store the resolved decorator in a variable and verify it is a function before
// calling it. Without this, unregistered decorators can cause an unhandled TypeError
// (calling undefined), which crashes the process — enabling Denial of Service.
this.decorators.push(['var decorator = ', foundDecorator, ';']);
this.decorators.push([
'if (typeof decorator !== "function") { throw new Error(',
this.quotedString('Missing decorator: "' + name + '"'),
'); }'
]);
this.decorators.push([
'fn = ',
this.decorators.functionCall(foundDecorator, '', [
this.decorators.functionCall('decorator', '', [
'fn',
'props',
'container',
@@ -906,8 +921,8 @@ JavaScriptCompiler.prototype = {
let existing = this.matchExistingProgram(child);
if (existing == null) {
this.context.programs.push(''); // Placeholder to prevent name conflicts for nested children
let index = this.context.programs.length;
// Placeholder to prevent name conflicts for nested children
let index = this.context.programs.push('') - 1;
child.index = index;
child.name = 'program' + index;
this.context.programs[index] = compiler.compile(
@@ -1261,15 +1276,14 @@ JavaScriptCompiler.isValidJavaScriptVariableName = function(name) {
);
};
function strictLookup(requireTerminal, compiler, parts, type) {
function strictLookup(requireTerminal, compiler, parts, startPartIndex, type) {
let stack = compiler.popStack(),
i = 0,
len = parts.length;
if (requireTerminal) {
len--;
}
for (; i < len; i++) {
for (let i = startPartIndex; i < len; i++) {
stack = compiler.nameLookup(stack, parts[i], type);
}
@@ -1279,7 +1293,7 @@ function strictLookup(requireTerminal, compiler, parts, type) {
'(',
stack,
', ',
compiler.quotedString(parts[i]),
compiler.quotedString(parts[len]),
', ',
JSON.stringify(compiler.source.currentLocation),
' )'
+2 -1
View File
@@ -20,7 +20,8 @@ export function moveHelperToHooks(instance, helperName, keepHelper) {
if (instance.helpers[helperName]) {
instance.hooks[helperName] = instance.helpers[helperName];
if (!keepHelper) {
delete instance.helpers[helperName];
// Using delete is slow
instance.helpers[helperName] = undefined;
}
}
}
+2 -2
View File
@@ -63,9 +63,9 @@ export default function(instance) {
execIteration(i, i, i === context.length - 1);
}
}
} else if (global.Symbol && context[global.Symbol.iterator]) {
} else if (typeof Symbol === 'function' && context[Symbol.iterator]) {
const newContext = [];
const iterator = context[global.Symbol.iterator]();
const iterator = context[Symbol.iterator]();
for (let it = iterator.next(); !it.done; it = iterator.next()) {
newContext.push(it.value);
}
@@ -1,11 +0,0 @@
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 -18
View File
@@ -1,32 +1,31 @@
import { createNewLookupObject } from './create-new-lookup-object';
import * as logger from '../logger';
import { extend } from '../utils';
import logger from '../logger';
const loggedProperties = Object.create(null);
export function createProtoAccessControl(runtimeOptions) {
let defaultMethodWhiteList = Object.create(null);
defaultMethodWhiteList['constructor'] = false;
defaultMethodWhiteList['__defineGetter__'] = false;
defaultMethodWhiteList['__defineSetter__'] = false;
defaultMethodWhiteList['__lookupGetter__'] = false;
let defaultPropertyWhiteList = Object.create(null);
// Create an object with "null"-prototype to avoid truthy results on
// prototype properties.
const propertyWhiteList = Object.create(null);
// eslint-disable-next-line no-proto
defaultPropertyWhiteList['__proto__'] = false;
propertyWhiteList['__proto__'] = false;
extend(propertyWhiteList, runtimeOptions.allowedProtoProperties);
const methodWhiteList = Object.create(null);
methodWhiteList['constructor'] = false;
methodWhiteList['__defineGetter__'] = false;
methodWhiteList['__defineSetter__'] = false;
methodWhiteList['__lookupGetter__'] = false;
methodWhiteList['__lookupSetter__'] = false;
extend(methodWhiteList, runtimeOptions.allowedProtoMethods);
return {
properties: {
whitelist: createNewLookupObject(
defaultPropertyWhiteList,
runtimeOptions.allowedProtoProperties
),
whitelist: propertyWhiteList,
defaultValue: runtimeOptions.allowProtoPropertiesByDefault
},
methods: {
whitelist: createNewLookupObject(
defaultMethodWhiteList,
runtimeOptions.allowedProtoMethods
),
whitelist: methodWhiteList,
defaultValue: runtimeOptions.allowProtoMethodsByDefault
}
};
+15 -4
View File
@@ -1,11 +1,22 @@
/* global globalThis */
export default function(Handlebars) {
/* istanbul ignore next */
let root = typeof global !== 'undefined' ? global : window,
$Handlebars = root.Handlebars;
// https://mathiasbynens.be/notes/globalthis
(function() {
if (typeof globalThis === 'object') return;
Object.prototype.__defineGetter__('__magic__', function() {
return this;
});
__magic__.globalThis = __magic__; // eslint-disable-line no-undef
delete Object.prototype.__magic__;
})();
const $Handlebars = globalThis.Handlebars;
/* istanbul ignore next */
Handlebars.noConflict = function() {
if (root.Handlebars === Handlebars) {
root.Handlebars = $Handlebars;
if (globalThis.Handlebars === Handlebars) {
globalThis.Handlebars = $Handlebars;
}
return Handlebars;
};
+24 -22
View File
@@ -74,17 +74,10 @@ export function template(templateSpec, env) {
}
partial = env.VM.resolvePartial.call(this, partial, context, options);
let extendedOptions = Utils.extend({}, options, {
hooks: this.hooks,
protoAccessControl: this.protoAccessControl
});
options.hooks = this.hooks;
options.protoAccessControl = this.protoAccessControl;
let result = env.VM.invokePartial.call(
this,
partial,
context,
extendedOptions
);
let result = env.VM.invokePartial.call(this, partial, context, options);
if (result == null && env.compile) {
options.partials[options.name] = env.compile(
@@ -92,7 +85,7 @@ export function template(templateSpec, env) {
templateSpec.compilerOptions,
env
);
result = options.partials[options.name](context, extendedOptions);
result = options.partials[options.name](context, options);
}
if (result != null) {
if (options.indent) {
@@ -145,7 +138,7 @@ export function template(templateSpec, env) {
for (let i = 0; i < len; i++) {
let result = depths[i] && container.lookupProperty(depths[i], name);
if (result != null) {
return depths[i][name];
return result;
}
}
},
@@ -254,8 +247,9 @@ export function template(templateSpec, env) {
ret._setup = function(options) {
if (!options.partial) {
let mergedHelpers = Utils.extend({}, env.helpers, options.helpers);
wrapHelpersToPassLookupProperty(mergedHelpers, container);
let mergedHelpers = {};
addHelpers(mergedHelpers, env.helpers, container);
addHelpers(mergedHelpers, options.helpers, container);
container.helpers = mergedHelpers;
if (templateSpec.usePartial) {
@@ -355,21 +349,21 @@ export function wrapProgram(
export function resolvePartial(partial, context, options) {
if (!partial) {
if (options.name === '@partial-block') {
partial = options.data['partial-block'];
partial = lookupOwnProperty(options.data, 'partial-block');
} else {
partial = options.partials[options.name];
partial = lookupOwnProperty(options.partials, options.name);
}
} else if (!partial.call && !options.name) {
// This is a dynamic partial that returned a string
options.name = partial;
partial = options.partials[partial];
partial = lookupOwnProperty(options.partials, partial);
}
return partial;
}
export function invokePartial(partial, context, options) {
// Use the current closure context to save the partial-block if this partial
const currentPartialBlock = options.data && options.data['partial-block'];
const currentPartialBlock = lookupOwnProperty(options.data, 'partial-block');
options.partial = true;
if (options.ids) {
options.data.contextPath = options.ids[0] || options.data.contextPath;
@@ -410,6 +404,12 @@ export function noop() {
return '';
}
function lookupOwnProperty(obj, name) {
if (obj && Object.prototype.hasOwnProperty.call(obj, name)) {
return obj[name];
}
}
function initData(context, data) {
if (!data || !('root' in data)) {
data = data ? createFrame(data) : {};
@@ -435,9 +435,10 @@ function executeDecorators(fn, prog, container, depths, data, blockParams) {
return prog;
}
function wrapHelpersToPassLookupProperty(mergedHelpers, container) {
Object.keys(mergedHelpers).forEach(helperName => {
let helper = mergedHelpers[helperName];
function addHelpers(mergedHelpers, helpers, container) {
if (!helpers) return;
Object.keys(helpers).forEach(helperName => {
let helper = helpers[helperName];
mergedHelpers[helperName] = passLookupPropertyOption(helper, container);
});
}
@@ -445,6 +446,7 @@ function wrapHelpersToPassLookupProperty(mergedHelpers, container) {
function passLookupPropertyOption(helper, container) {
const lookupProperty = container.lookupProperty;
return wrapHelper(helper, options => {
return Utils.extend({ lookupProperty }, options);
options.lookupProperty = lookupProperty;
return options;
});
}
+1
View File
@@ -1,5 +1,6 @@
// USAGE:
// var handlebars = require('handlebars');
/* eslint-env node */
/* eslint-disable no-var */
// var local = handlebars.create();
+46 -8
View File
@@ -1,3 +1,4 @@
/* eslint-env node */
/* eslint-disable no-console */
import Async from 'neo-async';
import fs from 'fs';
@@ -195,16 +196,24 @@ module.exports.cli = function(opts) {
const objectName = opts.partial ? 'Handlebars.partials' : 'templates';
if (opts.namespace && !isValidNamespace(opts.namespace)) {
throw new Handlebars.Exception('Invalid namespace format');
}
let output = new SourceNode();
if (!opts.simple) {
if (opts.amd) {
const runtimeModulePath =
(opts.handlebarPath || '') + 'handlebars.runtime';
output.add(
"define(['" +
opts.handlebarPath +
'handlebars.runtime\'], function(Handlebars) {\n Handlebars = Handlebars["default"];'
'define([' +
quoteForJavaScript(runtimeModulePath) +
'], function(Handlebars) {\n Handlebars = Handlebars["default"];'
);
} else if (opts.commonjs) {
output.add('var Handlebars = require("' + opts.commonjs + '");');
output.add(
'var Handlebars = require(' + quoteForJavaScript(opts.commonjs) + ');'
);
} else {
output.add('(function() {\n');
}
@@ -254,9 +263,9 @@ module.exports.cli = function(opts) {
}
output.add([
objectName,
"['",
template.name,
"'] = template(",
'[',
quoteForJavaScript(template.name),
'] = template(',
precompiled,
');\n'
]);
@@ -276,7 +285,9 @@ module.exports.cli = function(opts) {
}
if (opts.map) {
output.add('\n//# sourceMappingURL=' + opts.map + '\n');
output.add(
'\n//# sourceMappingURL=' + sanitizeSourceMapComment(opts.map) + '\n'
);
}
output = output.toStringWithSourceMap();
@@ -306,6 +317,33 @@ function arrayCast(value) {
return value;
}
/*
* Safely quotes a value for embedding in generated JavaScript strings
*
* Uses JSON.stringify which handles all special characters.
*/
function quoteForJavaScript(value) {
return JSON.stringify(String(value));
}
/**
* Validates that a namespace is a legitimate dotted JavaScript identifier
* (e.g. "App.templates") to prevent arbitrary code injection
*/
function isValidNamespace(namespace) {
return /^[A-Za-z_$][A-Za-z0-9_$]*(\.[A-Za-z_$][A-Za-z0-9_$]*)*$/.test(
namespace
);
}
/**
* Strips line terminators from source map URLs to prevent injection of new
* JavaScript lines via the sourceMappingURL comment
*/
function sanitizeSourceMapComment(value) {
return String(value).replace(/[\r\n\u2028\u2029]/g, '');
}
/**
* Run uglify to minify the compiled template, if uglify exists in the dependencies.
*
+13664 -5124
View File
File diff suppressed because it is too large Load Diff
+23 -22
View File
@@ -1,9 +1,9 @@
{
"name": "handlebars",
"barename": "handlebars",
"version": "4.7.7",
"version": "4.7.9",
"description": "Handlebars provides the power necessary to let you build semantic templates effectively with no frustration",
"homepage": "http://www.handlebarsjs.com/",
"homepage": "https://handlebarsjs.com/",
"keywords": [
"handlebars",
"mustache",
@@ -12,7 +12,7 @@
],
"repository": {
"type": "git",
"url": "https://github.com/wycats/handlebars.js.git"
"url": "https://github.com/handlebars-lang/handlebars.js.git"
},
"author": "Yehuda Katz",
"license": "MIT",
@@ -22,7 +22,7 @@
},
"dependencies": {
"minimist": "^1.2.5",
"neo-async": "^2.6.0",
"neo-async": "^2.6.2",
"source-map": "^0.6.1",
"wordwrap": "^1.0.0"
},
@@ -30,7 +30,7 @@
"uglify-js": "^3.1.4"
},
"devDependencies": {
"@knappi/grunt-saucelabs": "^9.0.2",
"@playwright/test": "1.44.1",
"aws-sdk": "^2.1.49",
"babel-loader": "^5.0.0",
"babel-runtime": "^5.1.10",
@@ -39,17 +39,15 @@
"chai-diff": "^1.0.1",
"concurrently": "^5.0.0",
"dirty-chai": "^2.0.1",
"dtslint": "^0.5.5",
"dustjs-linkedin": "^2.0.2",
"eco": "~1.1.0-rc-3",
"eslint": "^6.7.2",
"eslint-config-prettier": "^6.7.0",
"eslint-plugin-compat": "^3.3.0",
"eslint-plugin-compat": "^3.13.0",
"eslint-plugin-es5": "^1.4.1",
"fs-extra": "^8.1.0",
"grunt": "^1.0.4",
"grunt": "1.5.3",
"grunt-babel": "^5.0.0",
"grunt-bg-shell": "^2.3.3",
"grunt-cli": "^1",
"grunt-contrib-clean": "^1",
"grunt-contrib-concat": "^1",
@@ -58,6 +56,7 @@
"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",
"jison": "~0.3.0",
@@ -76,23 +75,26 @@
},
"main": "lib/index.js",
"types": "types/index.d.ts",
"browser": {
".": "./dist/cjs/handlebars.js",
"./runtime": "./dist/cjs/handlebars.runtime.js"
},
"browser": "./dist/cjs/handlebars.js",
"bin": {
"handlebars": "bin/handlebars"
},
"scripts": {
"build": "grunt build",
"release": "npm run build && grunt release",
"format": "prettier --write '**/*.js' && eslint --fix .",
"check-format": "prettier --check '**/*.js'",
"lint": "eslint --max-warnings 0 .",
"dtslint": "dtslint types",
"test": "grunt",
"extensive-tests-and-publish-to-aws": "npx mocha tasks/task-tests/ && grunt --stack extensive-tests-and-publish-to-aws",
"integration-test": "grunt integration-tests",
"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": "tsc --noEmit --project types",
"test": "npm run test:mocha",
"test:mocha": "grunt build && grunt test",
"test:browser": "playwright test --config tests/browser/playwright.config.js tests/browser/spec.js",
"test:integration": "grunt integration-tests",
"test:serve": "grunt connect:server:keepalive",
"extensive-tests-and-publish-to-aws": "npx mocha tasks/tests/ && grunt --stack extensive-tests-and-publish-to-aws",
"--- combined tasks ---": "",
"check-before-pull-request": "concurrently --kill-others-on-fail npm:lint npm:dtslint npm:check-format npm:test"
"check-before-pull-request": "concurrently --kill-others-on-fail npm:lint npm:test"
},
"jspm": {
"main": "handlebars",
@@ -109,7 +111,6 @@
"dist/amd/**/*.js",
"dist/cjs/**/*.js",
"lib",
"print-script",
"release-notes.md",
"runtime.js",
"types/*.d.ts",
@@ -121,7 +122,7 @@
}
},
"lint-staged": {
"*.{js,css,json,md}": [
"*.{js,css,json}": [
"prettier --write",
"git add"
],
+22 -1
View File
@@ -2,7 +2,28 @@
## Development
[Commits](https://github.com/handlebars-lang/handlebars.js/compare/v4.7.7...master)
[Commits](https://github.com/handlebars-lang/handlebars.js/compare/v4.7.9...master)
## v4.7.9 - March 26th, 2026
- fix: enable shell mode for spawn to resolve Windows EINVAL issue - e0137c2
- fix type "RuntimeOptions" also accepting string partials - eab1d14
- feat(types): set `hash` to be a `Record<string, any>` - de4414d
- fix non-contiguous program indices - 4512766
- refactor: rename i to startPartIndex - e497a35
- security: fix security issues - 68d8df5
[Commits](https://github.com/handlebars-lang/handlebars.js/compare/v4.7.8...v4.7.9)
## v4.7.8 - July 27th, 2023
- Make library compatible with workers (#1894) - 3d3796c
- Don't rely on Node.js global object (#1776) - 2954e7e
- Fix compiling of each block params in strict mode (#1855) - 30dbf04
- Fix rollup warning when importing Handlebars as ESM - 03d387b
- Fix bundler issue with webpack 5 (#1862) - c6c6bbb
- Use https instead of git for mustache submodule - 88ac068
[Commits](https://github.com/handlebars-lang/handlebars.js/compare/v4.7.7...v4.7.8)
## v4.7.7 - February 15th, 2021
+1 -1
View File
@@ -59,7 +59,7 @@
}
var runner = mocha.run();
//Reporting for saucelabs
// Reporting to test-runner
var failedTests = [];
runner.on('end', function(){
window.mochaResults = runner.stats;
+1 -1
View File
@@ -79,7 +79,7 @@
}
var runner = mocha.run();
//Reporting for saucelabs
// Reporting to test-runner
var failedTests = [];
runner.on('end', function(){
window.mochaResults = runner.stats;
+9
View File
@@ -324,6 +324,15 @@ describe('builtin helpers', function() {
);
});
it('each with block params and strict compilation', function() {
expectTemplate(
'{{#each goodbyes as |value index|}}{{index}}. {{value.text}}!{{/each}}'
)
.withCompileOptions({ strict: true })
.withInput({ goodbyes: [{ text: 'goodbye' }, { text: 'Goodbye' }] })
.toCompileTo('0. goodbye!1. Goodbye!');
});
it('each object with @index', function() {
expectTemplate(
'{{#each goodbyes}}{{@index}}. {{text}}! {{/each}}cruel {{world}}!'
+140
View File
@@ -128,6 +128,146 @@ describe('compiler', function() {
);
});
it('should reject AST with invalid PathExpression depth', function() {
shouldThrow(
function() {
Handlebars.compile({
type: 'Program',
body: [
{
type: 'MustacheStatement',
escaped: true,
strip: { open: false, close: false },
path: {
type: 'PathExpression',
data: false,
depth: '0',
parts: ['this'],
original: 'this'
},
params: []
}
]
})();
},
Error,
'Invalid AST: PathExpression.depth must be an integer'
);
});
it('should reject AST with non-array PathExpression parts', function() {
shouldThrow(
function() {
Handlebars.compile({
type: 'Program',
body: [
{
type: 'MustacheStatement',
escaped: true,
strip: { open: false, close: false },
path: {
type: 'PathExpression',
data: false,
depth: 0,
parts: 'this',
original: 'this'
},
params: []
}
]
})();
},
Error,
'Invalid AST: PathExpression.parts must be an array'
);
});
it('should reject AST with non-string PathExpression part', function() {
shouldThrow(
function() {
Handlebars.compile({
type: 'Program',
body: [
{
type: 'MustacheStatement',
escaped: true,
strip: { open: false, close: false },
path: {
type: 'PathExpression',
data: false,
depth: 0,
parts: [1],
original: 'this'
},
params: []
}
]
})();
},
Error,
'Invalid AST: PathExpression.parts must only contain strings'
);
});
it('should reject AST with invalid BooleanLiteral value type', function() {
shouldThrow(
function() {
Handlebars.compile({
type: 'Program',
body: [
{
type: 'MustacheStatement',
escaped: true,
strip: { open: false, close: false },
path: {
type: 'PathExpression',
data: false,
depth: 0,
parts: ['if'],
original: 'if'
},
params: [
{
type: 'BooleanLiteral',
value: 'true',
original: true
}
]
}
]
})();
},
Error,
'Invalid AST: BooleanLiteral.value must be a boolean'
);
});
it('should ignore loc metadata while validating AST nodes', function() {
equal(
Handlebars.compile({
type: 'Program',
meta: null,
loc: { source: 'fake', start: { line: 1, column: 0 } },
body: [{ type: 'ContentStatement', value: 'Hello' }]
})(),
'Hello'
);
});
it('should accept AST with valid NumberLiteral values', function() {
equal(
Handlebars.compile(Handlebars.parse('{{lookup this 1}}'))(['a', 'b']),
'b'
);
});
it('should accept AST with valid BooleanLiteral values', function() {
equal(
Handlebars.compile(Handlebars.parse('{{#if true}}ok{{/if}}'))({}),
'ok'
);
});
it('can pass through an empty string', function() {
equal(Handlebars.compile('')(), '');
});
+2 -2
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 || {};
return templates['bom'] = template({"compiler":[8,">= 4.3.0"],"main":function(container,depth0,helpers,partials,data) {
return templates["bom"] = template({"compiler":[8,">= 4.3.0"],"main":function(container,depth0,helpers,partials,data) {
return "a";
},"useData":true});
});
+2 -2
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 || {};
return templates['empty'] = template({"compiler":[8,">= 4.3.0"],"main":function(container,depth0,helpers,partials,data) {
return templates["empty"] = template({"compiler":[8,">= 4.3.0"],"main":function(container,depth0,helpers,partials,data) {
return "";
},"useData":true});
});
+2 -2
View File
@@ -1,6 +1,6 @@
define(['handlebars.runtime'], function(Handlebars) {
define(["handlebars.runtime"], function(Handlebars) {
Handlebars = Handlebars["default"]; var template = Handlebars.template, templates = CustomNamespace.templates = CustomNamespace.templates || {};
return templates['empty'] = template({"compiler":[8,">= 4.3.0"],"main":function(container,depth0,helpers,partials,data) {
return templates["empty"] = template({"compiler":[8,">= 4.3.0"],"main":function(container,depth0,helpers,partials,data) {
return "";
},"useData":true});
});
+1 -1
View File
@@ -1,6 +1,6 @@
(function() {
var template = Handlebars.template, templates = Handlebars.templates = Handlebars.templates || {};
templates['empty'] = template({"compiler":[8,">= 4.3.0"],"main":function(container,depth0,helpers,partials,data) {
templates["empty"] = template({"compiler":[8,">= 4.3.0"],"main":function(container,depth0,helpers,partials,data) {
return "";
},"useData":true});
})();
+3 -3
View File
@@ -1,9 +1,9 @@
define(['handlebars.runtime'], function(Handlebars) {
define(["handlebars.runtime"], function(Handlebars) {
Handlebars = Handlebars["default"]; var template = Handlebars.template, templates = Handlebars.templates = Handlebars.templates || {};
templates['firstTemplate'] = template({"compiler":[8,">= 4.3.0"],"main":function(container,depth0,helpers,partials,data) {
templates["firstTemplate"] = template({"compiler":[8,">= 4.3.0"],"main":function(container,depth0,helpers,partials,data) {
return "<div>1</div>";
},"useData":true});
templates['secondTemplate'] = template({"compiler":[8,">= 4.3.0"],"main":function(container,depth0,helpers,partials,data) {
templates["secondTemplate"] = template({"compiler":[8,">= 4.3.0"],"main":function(container,depth0,helpers,partials,data) {
return "<div>2</div>";
},"useData":true});
return templates;
+2 -2
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 || {};
return templates['artifacts/partial.template'] = template({"compiler":[8,">= 4.3.0"],"main":function(container,depth0,helpers,partials,data) {
return templates["artifacts/partial.template"] = template({"compiler":[8,">= 4.3.0"],"main":function(container,depth0,helpers,partials,data) {
return "<div>Test Partial</div>";
},"useData":true});
});
+2 -2
View File
@@ -1,6 +1,6 @@
define(['some-path/handlebars.runtime'], function(Handlebars) {
define(["some-path/handlebars.runtime"], function(Handlebars) {
Handlebars = Handlebars["default"]; var template = Handlebars.template, templates = Handlebars.templates = Handlebars.templates || {};
return templates['empty'] = template({"compiler":[8,">= 4.3.0"],"main":function(container,depth0,helpers,partials,data) {
return templates["empty"] = template({"compiler":[8,">= 4.3.0"],"main":function(container,depth0,helpers,partials,data) {
return "";
},"useData":true});
});
+3 -3
View File
@@ -1,9 +1,9 @@
define(['handlebars.runtime'], function(Handlebars) {
define(["handlebars.runtime"], function(Handlebars) {
Handlebars = Handlebars["default"]; var template = Handlebars.template, templates = someNameSpace = someNameSpace || {};
templates['empty'] = template({"compiler":[8,">= 4.3.0"],"main":function(container,depth0,helpers,partials,data) {
templates["empty"] = template({"compiler":[8,">= 4.3.0"],"main":function(container,depth0,helpers,partials,data) {
return "";
},"useData":true});
templates['empty'] = template({"compiler":[8,">= 4.3.0"],"main":function(container,depth0,helpers,partials,data) {
templates["empty"] = template({"compiler":[8,">= 4.3.0"],"main":function(container,depth0,helpers,partials,data) {
return "";
},"useData":true});
return templates;
+2 -2
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 || {};
return templates['non.default.extension'] = template({"compiler":[8,">= 4.3.0"],"main":function(container,depth0,helpers,partials,data) {
return templates["non.default.extension"] = template({"compiler":[8,">= 4.3.0"],"main":function(container,depth0,helpers,partials,data) {
return "<div>This is a test</div>";
},"useData":true});
});
+5 -5
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 || {};
return templates['known.helpers'] = template({"1":function(container,depth0,helpers,partials,data) {
return templates["known.helpers"] = template({"0":function(container,depth0,helpers,partials,data) {
var stack1, lookupProperty = container.lookupProperty || function(parent, propertyName) {
if (Object.prototype.hasOwnProperty.call(parent, propertyName)) {
return parent[propertyName];
@@ -8,8 +8,8 @@ return parent[propertyName];
return undefined
};
return " <div>Some known helper</div>\n"
+ ((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 : "");
},"2":function(container,depth0,helpers,partials,data) {
+ ((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 : "");
},"1":function(container,depth0,helpers,partials,data) {
return " <div>Another known helper</div>\n";
},"compiler":[8,">= 4.3.0"],"main":function(container,depth0,helpers,partials,data) {
var stack1, lookupProperty = container.lookupProperty || function(parent, propertyName) {
@@ -18,7 +18,7 @@ return parent[propertyName];
}
return undefined
};
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 : "");
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 : "");
},"useData":true});
});
+2 -2
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 || {};
return Handlebars.partials['partial.template'] = template({"compiler":[8,">= 4.3.0"],"main":function(container,depth0,helpers,partials,data) {
return Handlebars.partials["partial.template"] = template({"compiler":[8,">= 4.3.0"],"main":function(container,depth0,helpers,partials,data) {
return "<div>Test Partial</div>";
},"useData":true});
});
+1 -1
View File
@@ -62,7 +62,7 @@
}
var runner = mocha.run();
//Reporting for saucelabs
// Reporting to test-runner
var failedTests = [];
runner.on('end', function(){
window.mochaResults = runner.stats;
+82 -1
View File
@@ -182,7 +182,7 @@ describe('precompiler', function() {
return 'amd';
};
Precompiler.cli({ templates: [emptyTemplate], amd: true, partial: true });
equal(/return Handlebars\.partials\['empty'\]/.test(log), true);
equal(/return Handlebars\.partials\["empty"\]/.test(log), true);
equal(/template\(amd\)/.test(log), true);
});
it('should output multiple amd partials', function() {
@@ -405,4 +405,85 @@ describe('precompiler', function() {
});
});
});
describe('GHSA-xjpj-3mr7-gcpf: precompiler output escaping', function() {
var FullHandlebars = require('../dist/cjs/handlebars')['default'];
function runCliAndCaptureOutput(options) {
var output = '';
var oldLog = console.log;
console.log = function() {
output += Array.prototype.join.call(arguments, '');
};
try {
Precompiler.cli(options);
} finally {
console.log = oldLog;
}
return output;
}
it('should not inject raw template names into generated code', function() {
var output = runCliAndCaptureOutput({
templates: [
{
name: "evil'];global.__xjpjName=1;//",
source: ''
}
],
amd: true
});
expect(output).to.not.match(/\['evil'\];global\.__xjpjName=1/);
});
it('should not inject raw commonjs option values into generated code', function() {
var output = runCliAndCaptureOutput({
templates: [{ name: 'safe', source: '' }],
commonjs: 'handlebars");global.__xjpjCommon=1;//'
});
expect(output).to.not.match(
/require\("handlebars"\);global\.__xjpjCommon=1/
);
});
it('should reject invalid namespace expressions', function() {
expect(function() {
runCliAndCaptureOutput({
templates: [{ name: 'safe', source: '' }],
namespace: 'App.ns;global.__xjpjNamespace=1;//'
});
}).to.throw(/Invalid namespace/);
});
it('should sanitize sourceMappingURL comment values', function() {
var oldPrecompile = FullHandlebars.precompile;
var oldWriteFileSync = fs.writeFileSync;
FullHandlebars.precompile = function() {
return {
code: '""',
map: '{"version":3,"sources":[],"names":[],"mappings":""}'
};
};
fs.writeFileSync = function() {};
var output;
try {
output = runCliAndCaptureOutput({
templates: [{ name: 'safe', source: '' }],
map: 'good.js.map\n;global.__xjpjMap=1;//'
});
} finally {
FullHandlebars.precompile = oldPrecompile;
fs.writeFileSync = oldWriteFileSync;
}
expect(output).to.not.match(
/sourceMappingURL=[^\n]*\n;global\.__xjpjMap=1/
);
});
});
});
+10 -3
View File
@@ -54,6 +54,13 @@ describe('runtime', function() {
/Template was precompiled with an older version of Handlebars than the current runtime/
);
});
it('should safely resolve missing partial map entries', function() {
equal(
Handlebars.VM.resolvePartial(undefined, {}, { name: 'missing' }),
undefined
);
});
});
describe('#child', function() {
@@ -91,13 +98,13 @@ describe('runtime', function() {
it('should expose child template', function() {
var template = Handlebars.compile('{{#foo}}bar{{/foo}}');
// Calling twice to hit the non-compiled case.
equal(template._child(1)(), 'bar');
equal(template._child(1)(), 'bar');
equal(template._child(0)(), 'bar');
equal(template._child(0)(), 'bar');
});
it('should render depthed content', function() {
var template = Handlebars.compile('{{#foo}}{{../bar}}{{/foo}}');
// Calling twice to hit the non-compiled case.
equal(template._child(1, undefined, [], [{ bar: 'baz' }])(), 'baz');
equal(template._child(0, undefined, [], [{ bar: 'baz' }])(), 'baz');
});
});
+155
View File
@@ -133,11 +133,13 @@ describe('security issues', function() {
'{{__defineGetter__}}',
'{{__defineSetter__}}',
'{{__lookupGetter__}}',
'{{__lookupSetter__}}',
'{{__proto__}}',
'{{lookup this "constructor"}}',
'{{lookup this "__defineGetter__"}}',
'{{lookup this "__defineSetter__"}}',
'{{lookup this "__lookupGetter__"}}',
'{{lookup this "__lookupSetter__"}}',
'{{lookup this "__proto__"}}'
];
@@ -422,6 +424,159 @@ describe('security issues', function() {
.toCompileTo('c');
});
});
describe('GHSA-2qvq-rjwj-gvw9: partial resolution must not use polluted prototypes', function() {
if (!Handlebars.compile) {
return;
}
afterEach(function() {
delete Object.prototype.widget;
});
it('should not resolve partial names from Object.prototype', function() {
// eslint-disable-next-line no-extend-native
Object.prototype.widget = '<img src=x onerror="alert(1)">';
expect(function() {
Handlebars.compile('<div>{{> widget}}</div>')({});
}).to.throw(/could not be found/);
});
});
describe('GHSA-2w6w-674q-4c4q, GHSA-xhpv-hc6g-r9c6, GHSA-3mfm-83xf-c92r: untrusted AST inputs', function() {
if (!Handlebars.compile) {
return;
}
function createInjectedProgram() {
return {
type: 'Program',
body: [
{
type: 'MustacheStatement',
escaped: true,
strip: {
open: false,
close: false
},
path: {
type: 'PathExpression',
data: false,
depth: 0,
parts: ['lookup'],
original: 'lookup'
},
params: [
{
type: 'PathExpression',
data: false,
depth: 0,
parts: [],
original: 'this'
},
{
type: 'NumberLiteral',
value: '{},{})) + (Function) + (({}',
original: 1
}
]
}
]
};
}
it('should reject AST NumberLiteral type confusion in compile()', function() {
expect(function() {
var template = Handlebars.compile(createInjectedProgram());
template({});
}).to.throw(/Invalid AST/);
});
it('should reject AST objects passed via dynamic partial lookup', function() {
expect(function() {
var template = Handlebars.compile('{{> (lookup . "payload")}}');
template({
payload: createInjectedProgram()
});
}).to.throw(/Invalid AST|could not be found/);
});
});
describe('GHSA-442j-39wm-28r2: lookup must return checked value', function() {
it('should use the validated value from lookupProperty() in compat mode', function() {
var input = { child: {} };
var readCount = 0;
Object.defineProperty(input, 'unstable', {
enumerable: true,
get: function() {
readCount++;
return readCount === 1 ? 'first-read' : 'second-read';
}
});
expectTemplate('{{#with child}}{{unstable}}{{/with}}')
.withInput(input)
.withCompileOptions({ compat: true })
.toCompileTo('first-read');
});
});
describe('GHSA-9cx6-37pm-9jff: malformed decorators should fail safely', function() {
if (!Handlebars.compile) {
return;
}
it('should throw a controlled error for unknown decorators', function() {
var template = Handlebars.compile('{{*notRegistered}}');
expect(function() {
template({});
}).to.throw(/Missing decorator|not registered/);
});
});
describe('GHSA-new: @partial-block must not resolve from polluted prototype', function() {
if (!Handlebars.compile) {
return;
}
afterEach(function() {
delete Object.prototype['partial-block'];
});
it('should not resolve @partial-block from Object.prototype', function() {
// eslint-disable-next-line no-extend-native
Object.prototype['partial-block'] = '<img src=x onerror="alert(1)">';
expect(function() {
Handlebars.compile('{{> @partial-block}}')({});
}).to.throw(/could not be found/);
});
it('should not resolve @partial-block from Object.prototype inside a partial', function() {
// eslint-disable-next-line no-extend-native
Object.prototype['partial-block'] = '<img src=x onerror="alert(1)">';
Handlebars.registerPartial('testPartial', '{{> @partial-block}}');
try {
expect(function() {
Handlebars.compile('{{> testPartial}}')({});
}).to.throw(/could not be found/);
} finally {
Handlebars.unregisterPartial('testPartial');
}
});
it('should still render legitimate @partial-block content', function() {
Handlebars.registerPartial('wrapper', '<div>{{> @partial-block}}</div>');
try {
var result = Handlebars.compile('{{#> wrapper}}hello{{/wrapper}}')({});
expect(result).to.equal('<div>hello</div>');
} finally {
Handlebars.unregisterPartial('wrapper');
}
});
});
});
function wrapToAdjustContainer(precompiledTemplateFunction) {
+1 -1
View File
@@ -54,7 +54,7 @@
}
var runner = mocha.run();
//Reporting for saucelabs
// Reporting to test-runner
var failedTests = [];
runner.on('end', function(){
window.mochaResults = runner.stats;
+1 -1
View File
@@ -74,7 +74,7 @@
}
var runner = mocha.run();
//Reporting for saucelabs
// Reporting to test-runner
var failedTests = [];
runner.on('end', function(){
window.mochaResults = runner.stats;
+1 -1
View File
@@ -1,4 +1,4 @@
const metrics = require('../bench');
const metrics = require('../tests/bench');
const { createRegisterAsyncTaskFn } = require('./util/async-grunt-task');
module.exports = function(grunt) {
-1
View File
@@ -1 +0,0 @@
Use `mocha tasks/task-tests` to run these tests
+1
View File
@@ -0,0 +1 @@
Use `mocha tasks/tests` to run these tests
+2 -1
View File
@@ -23,7 +23,8 @@ async function execFileWithInheritedOutput(command, args) {
return new Promise((resolve, reject) => {
const resolvedCommand = preferLocalDependencies(command);
const child = childProcess.spawn(resolvedCommand, args, {
stdio: 'inherit'
stdio: 'inherit',
shell: process.platform === 'win32' // Workaround for CVE-2024-27980
});
child.on('exit', code => {
if (code !== 0) {
+10
View File
@@ -0,0 +1,10 @@
module.exports = {
root: true,
extends: ['eslint:recommended', 'prettier'],
env: {
node: true
},
parserOptions: {
ecmaVersion: 6
}
};
@@ -3,7 +3,7 @@ var _ = require('underscore'),
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 Handlebars = require('../../lib');
var templateSizes = {};
_.each(templates, function(info, template) {
@@ -145,7 +145,7 @@ function makeSuite(bench, name, template, handlebarsOnly) {
module.exports = function(grunt, callback) {
// Deferring load incase we are being run inline with the grunt build
Handlebars = require('../lib');
Handlebars = require('../../lib');
console.log('Execution Throughput');
runner(grunt, makeSuite, function(times, scaled) {
@@ -11,8 +11,6 @@ function BenchWarmer() {
this.errors = {};
}
var print = require('util').print;
BenchWarmer.prototype = {
winners: function(benches) {
return Benchmark.filter(benches, 'fastest');
@@ -69,7 +67,7 @@ BenchWarmer.prototype = {
self.startLine('');
print('\n');
console.log('\n');
self.printHeader('scaled');
_.each(self.scaled, function(value, name) {
self.startLine(name);
@@ -78,7 +76,7 @@ BenchWarmer.prototype = {
self.writeValue(value[lang] || '');
});
});
print('\n');
console.log('\n');
var errors = false,
prop,
@@ -94,16 +92,16 @@ BenchWarmer.prototype = {
}
if (errors) {
print('\n\nErrors:\n');
console.log('\n\nErrors:\n');
Object.keys(self.errors).forEach(function(prop) {
if (self.errors[prop].error.message !== 'EWOT') {
bench = self.errors[prop];
print('\n' + bench.name + ':\n');
print(bench.error.message);
console.log('\n' + bench.name + ':\n');
console.log(bench.error.message);
if (bench.error.stack) {
print(bench.error.stack.join('\n'));
console.log(bench.error.stack.join('\n'));
}
print('\n');
console.log('\n');
}
});
}
@@ -112,7 +110,7 @@ BenchWarmer.prototype = {
}
});
print('\n');
console.log('\n');
},
scaleTimes: function() {
@@ -163,11 +161,11 @@ BenchWarmer.prototype = {
}
if (winners) {
print('WINNER(S)');
console.log('WINNER(S)');
horSize = horSize + 'WINNER(S)'.length;
}
print('\n' + new Array(horSize + 1).join('-'));
console.log('\n' + new Array(horSize + 1).join('-'));
},
startLine: function(name) {
@@ -179,8 +177,8 @@ BenchWarmer.prototype = {
this.currentBenches = [];
print(winners.join(', '));
print('\n');
console.log(winners.join(', '));
console.log('\n');
if (name) {
this.writeValue(name);
@@ -216,7 +214,7 @@ BenchWarmer.prototype = {
writeValue: function(out) {
var padding = this.benchSize - out.length + 1;
out = out + new Array(padding).join(' ');
print(out);
console.log(out);
}
};
+5
View File
@@ -0,0 +1,5 @@
module.exports = {
parserOptions: {
ecmaVersion: 2020
}
};
+13
View File
@@ -0,0 +1,13 @@
# Browser Tests with Playwright
These tests execute Mocha tests from the `spec`-folder in multiple browsers.
## Using Docker
Execute the following commands in the project root:
```bash
npm install
npx grunt prepare
docker run -it --rm --volume $(pwd):/srv/app --workdir /srv/app --ipc=host mcr.microsoft.com/playwright:v1.44.1-jammy npm run test:browser
```
+28
View File
@@ -0,0 +1,28 @@
const { devices } = require('@playwright/test');
/** @type {import('@playwright/test').PlaywrightTestConfig} */
const config = {
testMatch: ['spec.js'],
projects: [
{
name: 'chromium',
use: { ...devices['Desktop Chrome'] }
},
{
name: 'firefox',
use: { ...devices['Desktop Firefox'] }
},
{
name: 'webkit',
use: { ...devices['Desktop Safari'] }
}
],
reporter: 'list',
webServer: {
command: 'npm run test:serve',
port: 9999,
reuseExistingServer: false
}
};
module.exports = config;
+33
View File
@@ -0,0 +1,33 @@
const { test, expect } = require('@playwright/test');
async function waitForMochaAndAssertResult(page) {
await page.waitForFunction(() => window.mochaResults); // eslint-disable-line no-undef
const mochaResults = await page.evaluate('window.mochaResults');
expect(mochaResults.failures).toBe(0);
}
test('Spec handlebars.js', async ({ page }) => {
await page.goto(`/spec/?headless=true`);
await waitForMochaAndAssertResult(page);
});
test('Spec handlebars.amd.js (AMD)', async ({ page }) => {
await page.goto(`/spec/amd.html?headless=true`);
await waitForMochaAndAssertResult(page);
});
test('Spec handlebars.runtime.amd.js (AMD)', async ({ page }) => {
await page.goto(`/spec/amd-runtime.html?headless=true`);
await waitForMochaAndAssertResult(page);
});
test('Spec handlebars.js (UMD)', async ({ page }) => {
await page.goto(`/spec/umd.html?headless=true`);
await waitForMochaAndAssertResult(page);
});
test('Spec handlebars.runtime.js (UMD)', async ({ page }) => {
await page.goto(`/spec/umd-runtime.html?headless=true`);
await waitForMochaAndAssertResult(page);
});
@@ -1,12 +1,10 @@
Add a new integration test by creating a new subfolder
Add a file "test.sh" to that runs the test. "test.sh" should exit with a non-zero exit code
Add a file "test.sh" to that runs the test. "test.sh" should exit with a non-zero exit code
and display an error message, if something goes wrong.
* An integration test should reflect real-world setups that use handlebars.
* It should compile a minimal template and compare the output to an expected output.
* It should use "../.." as dependency for Handlebars so that the currently built library is used.
- An integration test should reflect real-world setups that use handlebars.
- It should compile a minimal template and compare the output to an expected output.
- It should use "../.." as dependency for Handlebars so that the currently built library is used.
Currently, integration tests are only running on Linux, especially in travis-ci.
Currently, integration tests are only running on Linux, especially in our CI GitHub action.

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