Compare commits

...

19 Commits

Author SHA1 Message Date
Jakob Linskeseder dbe04946ce Improve security mitigations
Instead of validating the AST in the parser, fix the compiler instead by
handling the types in a safe way.
2026-03-30 00:31:02 +02:00
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
40 changed files with 6417 additions and 14016 deletions
+9 -9
View File
@@ -12,10 +12,10 @@ jobs:
runs-on: 'ubuntu-latest'
steps:
- name: Checkout
uses: actions/checkout@v2
uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v2
uses: actions/setup-node@v4
with:
node-version: '16'
@@ -33,16 +33,16 @@ jobs:
matrix:
operating-system: ['ubuntu-latest', 'windows-latest']
# https://nodejs.org/en/about/releases/
node-version: ['10', '12', '14', '16', '18', '20']
node-version: ['16', '18', '20', '22']
steps:
- name: Checkout
uses: actions/checkout@v2
uses: actions/checkout@v4
with:
submodules: true
- name: Setup Node.js
uses: actions/setup-node@v2
uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node-version }}
@@ -54,7 +54,7 @@ jobs:
- name: Test (Integration)
# https://github.com/webpack/webpack/issues/14532
if: ${{ matrix.node-version != '18' && matrix.node-version != '20' }}
if: ${{ matrix.node-version == '16' }}
run: |
cd ./tests/integration/rollup-test && ./test.sh && cd -
cd ./tests/integration/webpack-babel-test && ./test.sh && cd -
@@ -62,15 +62,15 @@ jobs:
browser:
name: Test (Browser)
runs-on: 'ubuntu-20.04'
runs-on: 'ubuntu-22.04'
steps:
- name: Checkout
uses: actions/checkout@v2
uses: actions/checkout@v4
with:
submodules: true
- name: Setup Node.js
uses: actions/setup-node@v2
uses: actions/setup-node@v4
with:
node-version: '16'
+1
View File
@@ -15,5 +15,6 @@ node_modules
lib/handlebars/compiler/parser.js
/coverage/
/dist/
/test-results/
/tests/integration/*/dist/
/spec/tmp/*
+8 -4
View File
@@ -132,9 +132,11 @@ A full release via Docker may be completed with the following:
# Generate config for yo generator-release:
# https://github.com/kpdecker/generator-release#example
# You have to add a valid GitHub OAuth token!
RUN echo "module.exports = {\n auth: 'oauth',\n token: 'GitHub OAuth token'\n};" > /home/node/.config/generator-release
# 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"
@@ -155,9 +157,12 @@ A full release via Docker may be completed with the following:
* Add GitHub API token: `vi /home/node/.config/generator-release`
* Execute the following steps:
```bash
npm ci
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
@@ -168,7 +173,6 @@ A full release via Docker may be completed with the following:
docker run --rm --interactive --tty \
--volume $PWD:/app \
--workdir /app \
--user $(id -u):$(id -g) \
ruby:3.2-slim bash
```
* Execute the following steps:
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "handlebars",
"version": "4.7.8",
"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
@@ -2,7 +2,7 @@
<package>
<metadata>
<id>handlebars.js</id>
<version>4.7.8</version>
<version>4.7.9</version>
<authors>handlebars.js Authors</authors>
<licenseUrl>https://github.com/handlebars-lang/handlebars.js/blob/master/LICENSE</licenseUrl>
<projectUrl>https://github.com/handlebars-lang/handlebars.js/</projectUrl>
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "handlebars",
"version": "4.7.8",
"version": "4.7.9",
"license": "MIT",
"jspm": {
"main": "handlebars",
+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.8';
export const VERSION = '4.7.9';
export const COMPILER_REVISION = 8;
export const LAST_COMPATIBLE_COMPILER_REVISION = 7;
+30 -21
View File
@@ -1,7 +1,13 @@
/* eslint-disable new-cap */
import Exception from '../exception';
import { isArray, indexOf, extend } from '../utils';
import {
isArray,
indexOf,
extend,
sanitizeDepth,
sanitizeParts
} from '../utils';
import AST from './ast';
const slice = [].slice;
@@ -243,7 +249,7 @@ Compiler.prototype = {
name = path.parts[0],
isBlock = program != null || inverse != null;
this.opcode('getContext', path.depth);
this.opcode('getContext', sanitizeDepth(path.depth));
this.opcode('pushProgram', program);
this.opcode('pushProgram', inverse);
@@ -288,29 +294,32 @@ Compiler.prototype = {
},
PathExpression: function(path) {
this.addDepth(path.depth);
this.opcode('getContext', path.depth);
// Sanitize untrusted AST values at the compiler boundary.
// javascript-compiler.js trusts all opcode arguments to be safe.
const depth = sanitizeDepth(path.depth);
const parts = sanitizeParts(path.parts);
let name = path.parts[0],
this.addDepth(depth);
this.opcode('getContext', depth);
let name = parts[0],
scoped = AST.helpers.scopedId(path),
blockParamId = !path.depth && !scoped && this.blockParamIndex(name);
blockParamId = !depth && !scoped && this.blockParamIndex(name);
if (blockParamId) {
this.opcode('lookupBlockParam', blockParamId, path.parts);
this.opcode(
'lookupBlockParam',
[Number(blockParamId[0]), Number(blockParamId[1])],
parts
);
} else if (!name) {
// Context reference, i.e. `{{foo .}}` or `{{foo ..}}`
this.opcode('pushContext');
} else if (path.data) {
this.options.data = true;
this.opcode('lookupData', path.depth, path.parts, path.strict);
this.opcode('lookupData', depth, parts, path.strict);
} else {
this.opcode(
'lookupOnContext',
path.parts,
path.falsy,
path.strict,
scoped
);
this.opcode('lookupOnContext', parts, path.falsy, path.strict, scoped);
}
},
@@ -319,11 +328,11 @@ Compiler.prototype = {
},
NumberLiteral: function(number) {
this.opcode('pushLiteral', number.value);
this.opcode('pushLiteral', Number(number.value));
},
BooleanLiteral: function(bool) {
this.opcode('pushLiteral', bool.value);
this.opcode('pushLiteral', bool.value === true ? 'true' : 'false');
},
UndefinedLiteral: function() {
@@ -410,16 +419,16 @@ Compiler.prototype = {
pushParam: function(val) {
let value = val.value != null ? val.value : val.original || '';
let depth = sanitizeDepth(val.depth);
if (this.stringParams) {
if (value.replace) {
value = value.replace(/^(\.?\.\/)*/g, '').replace(/\//g, '.');
}
if (val.depth) {
this.addDepth(val.depth);
if (depth) {
this.addDepth(depth);
}
this.opcode('getContext', val.depth || 0);
this.opcode('getContext', depth);
this.opcode('pushStringParam', value, val.type);
if (val.type === 'SubExpression') {
+28 -15
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,16 +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, i, type)
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);
@@ -682,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',
@@ -908,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(
@@ -1263,14 +1276,14 @@ JavaScriptCompiler.isValidJavaScriptVariableName = function(name) {
);
};
function strictLookup(requireTerminal, compiler, parts, i, type) {
function strictLookup(requireTerminal, compiler, parts, startPartIndex, type) {
let stack = compiler.popStack(),
len = parts.length;
if (requireTerminal) {
len--;
}
for (; i < len; i++) {
for (let i = startPartIndex; i < len; i++) {
stack = compiler.nameLookup(stack, parts[i], type);
}
@@ -1280,7 +1293,7 @@ function strictLookup(requireTerminal, compiler, parts, i, 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;
}
}
}
@@ -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);
}
+16 -17
View File
@@ -1,32 +1,31 @@
import { createNewLookupObject } from './create-new-lookup-object';
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
}
};
+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;
});
}
+27
View File
@@ -114,3 +114,30 @@ export function blockParams(params, ids) {
export function appendContextPath(contextPath, id) {
return (contextPath ? contextPath + '.' : '') + id;
}
/**
* Coerce an untrusted depth value to a safe non-negative integer.
* Returns `0` for any value that is not a finite, non-negative number.
*
* @param {unknown} depth - The depth value to sanitize.
* @returns {number} A non-negative integer.
*/
export function sanitizeDepth(depth) {
let number = Number(depth);
if (!Number.isFinite(number) || number < 0) {
return 0;
}
return Math.floor(number);
}
/**
* Return a sanitized copy of a PathExpression AST node's parts array.
* Coerces each element to a string, or returns an empty array if parts
* is not an array.
*
* @param {unknown} parts - The parts value to sanitize.
* @returns {string[]} A safe string array.
*/
export function sanitizeParts(parts) {
return Array.isArray(parts) ? parts.map(String) : [];
}
+45 -8
View File
@@ -196,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');
}
@@ -255,9 +263,9 @@ module.exports.cli = function(opts) {
}
output.add([
objectName,
"['",
template.name,
"'] = template(",
'[',
quoteForJavaScript(template.name),
'] = template(',
precompiled,
');\n'
]);
@@ -277,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();
@@ -307,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.
*
+5739 -13846
View File
File diff suppressed because it is too large Load Diff
+4 -5
View File
@@ -1,7 +1,7 @@
{
"name": "handlebars",
"barename": "handlebars",
"version": "4.7.8",
"version": "4.7.9",
"description": "Handlebars provides the power necessary to let you build semantic templates effectively with no frustration",
"homepage": "https://handlebarsjs.com/",
"keywords": [
@@ -30,7 +30,7 @@
"uglify-js": "^3.1.4"
},
"devDependencies": {
"@playwright/test": "^1.17.1",
"@playwright/test": "1.44.1",
"aws-sdk": "^2.1.49",
"babel-loader": "^5.0.0",
"babel-runtime": "^5.1.10",
@@ -39,7 +39,6 @@
"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",
@@ -47,7 +46,7 @@
"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-cli": "^1",
"grunt-contrib-clean": "^1",
@@ -87,7 +86,7 @@
"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",
"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",
+11 -1
View File
@@ -2,7 +2,17 @@
## Development
[Commits](https://github.com/handlebars-lang/handlebars.js/compare/v4.7.8...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
+109
View File
@@ -128,6 +128,115 @@ describe('compiler', function() {
);
});
function createPathExpressionAST(depth, parts) {
return {
type: 'Program',
body: [
{
type: 'MustacheStatement',
escaped: true,
strip: { open: false, close: false },
path: {
type: 'PathExpression',
data: false,
depth: depth,
parts: parts,
original: 'this'
},
params: []
}
]
};
}
it('should safely handle AST with non-integer PathExpression depth', function() {
// depth '0' is coerced to 0 via Number(), compiles safely
var result = Handlebars.compile(createPathExpressionAST('0', ['this']))();
expect(result).to.be.a('string');
});
it('should safely handle AST with negative PathExpression depth', function() {
// Negative depth is clamped to 0
var result = Handlebars.compile(createPathExpressionAST(-1, ['this']))();
expect(result).to.be.a('string');
});
it('should safely handle AST with fractional PathExpression depth', function() {
// Fractional depth is floored to an integer
var result = Handlebars.compile(createPathExpressionAST(0.5, ['this']))();
expect(result).to.be.a('string');
});
it('should safely handle AST with non-array PathExpression parts', function() {
// Non-array parts are coerced to empty array, compiles safely
var result = Handlebars.compile(createPathExpressionAST(0, 'this'))();
expect(result).to.be.a('string');
});
it('should safely handle AST with non-string PathExpression part', function() {
// Non-string parts are coerced to strings via String()
var result = Handlebars.compile(createPathExpressionAST(0, [1]))();
expect(result).to.be.a('string');
});
it('should safely handle AST with non-boolean BooleanLiteral value type', function() {
// The compiler coerces BooleanLiteral.value via === true before
// emitting a pushLiteral opcode, so a non-boolean value like the
// string 'true' becomes the literal 'false'.
var loc = {
source: null,
start: { line: 1, column: 0 },
end: { line: 1, column: 10 }
};
var result = Handlebars.compile({
type: 'Program',
body: [
{
type: 'MustacheStatement',
escaped: true,
strip: { open: false, close: false },
loc: loc,
path: {
type: 'BooleanLiteral',
value: 'true',
original: true,
loc: loc
},
params: []
}
]
})();
// 'true' !== true, so the compiler emits pushLiteral('false').
// Handlebars does not render falsy values, so the output is empty.
expect(result).to.equal('');
});
it('should ignore loc metadata in 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});
});
+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');
});
});
+220
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,224 @@ 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() {
var loc = {
source: null,
start: { line: 1, column: 0 },
end: { line: 1, column: 20 }
};
return {
type: 'Program',
body: [
{
type: 'MustacheStatement',
escaped: true,
strip: {
open: false,
close: false
},
loc: loc,
path: {
type: 'PathExpression',
data: false,
depth: 0,
parts: ['lookup'],
original: 'lookup',
loc: loc
},
params: [
{
type: 'PathExpression',
data: false,
depth: 0,
parts: [],
original: 'this',
loc: loc
},
{
type: 'NumberLiteral',
value: '{},{})) + (Function) + (({}',
original: 1,
loc: loc
}
]
}
]
};
}
it('should neutralize AST NumberLiteral type confusion in compile()', function() {
// The compiler coerces NumberLiteral.value via Number() before
// emitting a pushLiteral opcode, so a type-confused string value
// becomes NaN, preventing code injection.
var template = Handlebars.compile(createInjectedProgram());
var result = template({});
expect(result).to.not.contain('Function');
});
it('should reject AST objects passed via dynamic partial lookup', function() {
expect(function() {
var template = Handlebars.compile('{{> (lookup . "payload")}}');
template({
payload: createInjectedProgram()
});
}).to.throw(/could not be found/);
});
it('should sanitize param depth in stringParams mode', function() {
// pushParam passes val.depth directly to the getContext opcode.
// In stringParams mode, getContext stores the depth in lastContext,
// which contextName interpolates into generated code as
// 'depths[' + depth + ']'. A malicious depth string can escape the
// bracket expression and inject arbitrary code at template runtime.
//
// With sanitization the depth becomes 0, producing 'depth0' (safe).
// Without sanitization the injected expression executes and throws.
var loc = {
source: null,
start: { line: 1, column: 0 },
end: { line: 1, column: 20 }
};
var maliciousAST = {
type: 'Program',
body: [
{
type: 'MustacheStatement',
escaped: true,
strip: { open: false, close: false },
loc: loc,
path: {
type: 'PathExpression',
data: false,
depth: 0,
parts: ['lookup'],
original: 'lookup',
loc: loc
},
params: [
{
type: 'PathExpression',
data: false,
depth: 'function(){throw new Error("INJECTION")}()',
parts: [],
original: '',
loc: loc
}
]
}
]
};
var template = Handlebars.compile(maliciousAST, {
stringParams: true
});
// After sanitization the depth is 0, so the template runs without
// executing the injected throw expression.
expect(function() {
template({});
}).to.not.throw();
});
});
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) {
+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) {
+1 -2
View File
@@ -9,6 +9,5 @@ Execute the following commands in the project root:
```bash
npm install
npx grunt prepare
docker pull mcr.microsoft.com/playwright:focal
docker run -it --rm --volume $(pwd):/srv/app --workdir /srv/app --ipc=host mcr.microsoft.com/playwright:focal npm run test:browser
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
```
+1
View File
@@ -2,6 +2,7 @@ const { devices } = require('@playwright/test');
/** @type {import('@playwright/test').PlaywrightTestConfig} */
const config = {
testMatch: ['spec.js'],
projects: [
{
name: 'chromium',
+10 -10
View File
@@ -7,27 +7,27 @@ async function waitForMochaAndAssertResult(page) {
expect(mochaResults.failures).toBe(0);
}
test('Spec handlebars.js', async ({ page, baseURL }) => {
await page.goto(`${baseURL}/spec/?headless=true`);
test('Spec handlebars.js', async ({ page }) => {
await page.goto(`/spec/?headless=true`);
await waitForMochaAndAssertResult(page);
});
test('Spec handlebars.amd.js (AMD)', async ({ page, baseURL }) => {
await page.goto(`${baseURL}/spec/amd.html?headless=true`);
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, baseURL }) => {
await page.goto(`${baseURL}/spec/amd-runtime.html?headless=true`);
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, baseURL }) => {
await page.goto(`${baseURL}/spec/umd.html?headless=true`);
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, baseURL }) => {
await page.goto(`${baseURL}/spec/umd-runtime.html?headless=true`);
test('Spec handlebars.runtime.js (UMD)', async ({ page }) => {
await page.goto(`/spec/umd-runtime.html?headless=true`);
await waitForMochaAndAssertResult(page);
});
+3 -3
View File
@@ -25,7 +25,7 @@ declare namespace Handlebars {
partial?: boolean;
depths?: any[];
helpers?: { [name: string]: Function };
partials?: { [name: string]: HandlebarsTemplateDelegate };
partials?: { [name: string]: Template };
decorators?: { [name: string]: Function };
data?: any;
blockParams?: any[];
@@ -39,7 +39,7 @@ declare namespace Handlebars {
export interface HelperOptions {
fn: TemplateDelegate;
inverse: TemplateDelegate;
hash: any;
hash: Record<string, any>;
data?: any;
}
@@ -60,7 +60,7 @@ declare namespace Handlebars {
export function unregisterHelper(name: string): void;
export function registerPartial(name: string, fn: Template): void;
export function registerPartial(spec: { [name: string]: HandlebarsTemplateDelegate }): void;
export function registerPartial(spec: { [name: string]: Template }): void;
export function unregisterPartial(name: string): void;
// TODO: replace Function with actual signature
+3
View File
@@ -249,6 +249,9 @@ function testProtoAccessControlControlOptions() {
allowedProtoProperties: { allowedProperty: true, forbiddenProperty: false },
allowProtoMethodsByDefault: true,
allowProtoPropertiesByDefault: false,
partials: {
link: '<a href="/people/{{id}}">{{name}}</a>'
}
}
);
}