Compare commits

...

25 Commits

Author SHA1 Message Date
Nils Knappmeier f637dc76bd remove old aws-sdk 2023-08-06 01:20:58 +02:00
Nils Knappmeier e8299e0485 generate file list after publish 2023-08-06 01:17:42 +02:00
Nils Knappmeier 7989a5e9f9 function to upload a file list 2023-08-05 18:26:51 +02:00
Nils Knappmeier bde5506b10 more functions that help uploading a file listing. 2023-08-05 18:09:54 +02:00
Nils Knappmeier 6118a3c829 fixup: revert accidental webpack-config changes 2023-08-05 15:42:32 +02:00
Nils Knappmeier c5e5cad579 implement publish workflow 2023-08-05 15:11:00 +02:00
Nils Knappmeier 1c75903eaf add delete and upload functions 2023-08-05 13:57:00 +02:00
Nils Knappmeier ab920cc1ce wip: refactor s3 access and generate file listing on build 2023-08-05 00:11:49 +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
38 changed files with 4583 additions and 378 deletions
+3 -3
View File
@@ -33,7 +33,7 @@ jobs:
matrix:
operating-system: ['ubuntu-latest', 'windows-latest']
# https://nodejs.org/en/about/releases/
node-version: ['10', '12', '14', '16', '17']
node-version: ['10', '12', '14', '16', '18', '20']
steps:
- name: Checkout
@@ -54,7 +54,7 @@ jobs:
- name: Test (Integration)
# https://github.com/webpack/webpack/issues/14532
if: ${{ matrix.node-version != '17' }}
if: ${{ matrix.node-version != '18' && matrix.node-version != '20' }}
run: |
cd ./tests/integration/rollup-test && ./test.sh && cd -
cd ./tests/integration/webpack-babel-test && ./test.sh && cd -
@@ -62,7 +62,7 @@ jobs:
browser:
name: Test (Browser)
runs-on: 'ubuntu-latest'
runs-on: 'ubuntu-20.04'
steps:
- name: Checkout
uses: actions/checkout@v2
+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 }}
+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
+76 -9
View File
@@ -86,33 +86,100 @@ 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 our CI job 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 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 `handlebars-lang/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._
A full release may be completed with the following:
A full release via Docker 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 OAuth token!
RUN echo "module.exports = {\n auth: 'oauth',\n token: 'GitHub OAuth token'\n};" > /home/node/.config/generator-release
RUN chown -R node:node /home/node/.config
# 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 ci
npx grunt
npm install -g yo@1 grunt@1 generator-release
npm run release
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 \
--user $(id -u):$(id -g) \
ruby:3.2-slim bash
```
* Execute the following steps:
```bash
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
+5 -7
View File
@@ -166,11 +166,9 @@ module.exports = function(grunt) {
}
},
bgShell: {
shell: {
integrationTests: {
cmd: './tests/integration/run-integration-tests.sh',
bg: false,
fail: true
command: './tests/integration/run-integration-tests.sh'
}
},
@@ -195,7 +193,7 @@ 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('grunt-shell');
grunt.loadNpmTasks('grunt-webpack');
grunt.task.loadTasks('tasks');
@@ -214,7 +212,7 @@ module.exports = function(grunt) {
// Requires secret properties from .travis.yaml
grunt.registerTask('extensive-tests-and-publish-to-aws', [
'default',
'bgShell:integrationTests',
'shell:integrationTests',
'metrics',
'publish-to-aws'
]);
@@ -234,6 +232,6 @@ module.exports = function(grunt) {
);
grunt.registerTask('integration-tests', [
'default',
'bgShell:integrationTests'
'shell:integrationTests'
]);
};
+7 -7
View File
@@ -1,18 +1,18 @@
[![CI Build Status](https://img.shields.io/github/workflow/status/handlebars-lang/handlebars.js/ci/master)](https://github.com/handlebars-lang/handlebars.js/actions)
[![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)
[![Bundle size](https://img.shields.io/bundlephobia/minzip/handlebars?label=minified%20%2B%20gzipped)](https://bundlephobia.com/package/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](https://mustache.github.io/) 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
----------
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "handlebars",
"version": "4.7.7",
"version": "4.7.8",
"main": "handlebars.js",
"license": "MIT",
"dependencies": {}
+1 -1
View File
@@ -2,7 +2,7 @@
<package>
<metadata>
<id>handlebars.js</id>
<version>4.7.7</version>
<version>4.7.8</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.7",
"version": "4.7.8",
"license": "MIT",
"jspm": {
"main": "handlebars",
+4 -2
View File
@@ -1,6 +1,8 @@
module.exports = {
env: {
// Handlebars should run natively in the browser
node: false
// 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.8';
export const COMPILER_REVISION = 8;
export const LAST_COMPATIBLE_COMPILER_REVISION = 7;
+2 -2
View File
@@ -1,4 +1,4 @@
/* global define */
/* global define, require */
import { isArray } from '../utils';
let SourceNode;
@@ -8,7 +8,7 @@ try {
if (typeof define !== 'function' || !define.amd) {
// 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'); // eslint-disable-line no-undef
let SourceMap = require('source-map');
SourceNode = SourceMap.SourceNode;
}
} catch (err) {
@@ -537,7 +537,9 @@ JavaScriptCompiler.prototype = {
resolvePath: function(type, parts, i, 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, i, type)
);
return;
}
@@ -1261,9 +1263,8 @@ JavaScriptCompiler.isValidJavaScriptVariableName = function(name) {
);
};
function strictLookup(requireTerminal, compiler, parts, type) {
function strictLookup(requireTerminal, compiler, parts, i, type) {
let stack = compiler.popStack(),
i = 0,
len = parts.length;
if (requireTerminal) {
len--;
+15 -4
View File
@@ -1,11 +1,22 @@
/* global globalThis */
export default function(Handlebars) {
/* istanbul ignore next */
let root = typeof global !== 'undefined' ? global : window, // eslint-disable-line no-undef
$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;
};
+3772 -229
View File
File diff suppressed because it is too large Load Diff
+6 -8
View File
@@ -1,9 +1,9 @@
{
"name": "handlebars",
"barename": "handlebars",
"version": "4.7.7",
"version": "4.7.8",
"description": "Handlebars provides the power necessary to let you build semantic templates effectively with no frustration",
"homepage": "https://www.handlebarsjs.com/",
"homepage": "https://handlebarsjs.com/",
"keywords": [
"handlebars",
"mustache",
@@ -30,8 +30,8 @@
"uglify-js": "^3.1.4"
},
"devDependencies": {
"@aws-sdk/client-s3": "^3.385.0",
"@playwright/test": "^1.17.1",
"aws-sdk": "^2.1.49",
"babel-loader": "^5.0.0",
"babel-runtime": "^5.1.10",
"benchmark": "~1.0",
@@ -49,7 +49,6 @@
"fs-extra": "^8.1.0",
"grunt": "^1.0.4",
"grunt-babel": "^5.0.0",
"grunt-bg-shell": "^2.3.3",
"grunt-cli": "^1",
"grunt-contrib-clean": "^1",
"grunt-contrib-concat": "^1",
@@ -58,6 +57,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,15 +76,13 @@
},
"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 .",
"lint": "npm run lint:eslint && npm run lint:prettier && npm run lint:types",
"lint:eslint": "eslint --max-warnings 0 .",
+12 -1
View File
@@ -2,7 +2,18 @@
## 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.8...master)
## 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
+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}}!'
+127
View File
@@ -0,0 +1,127 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport"
content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<style>
table {
column-gap: 1rem;
}
thead {
position: sticky;
top: 0;
background: #efefef;
}
th:not(:last-child), td:not(:last-child) {
margin-right: 1rem;
}
th {
text-align: left;
}
</style>
<title>Handlebars.js Builds</title>
</head>
<body>
<h1>Handlebars.js builds</h1>
<p>See <a href="https://handlebarsjs.com">https://handlebarsjs.com</a> for documentation.</p>
<p>Machine-readable version: <a href="{{jsonListUrl}}">{{jsonListUrl}}</a></p>
<table>
<thead>
<tr>
<th data-col="key"><a href="#" onclick="return toggleSort('key')">Name</a></th>
<th data-col="size"><a href="#" onclick="return toggleSort('size')">Size</a></th>
<th data-col="lastModified"><a href="#" onclick="return toggleSort('lastModified')">Last-Modified</a></th>
</tr>
</thead>
<tbody id="files">
{{#each fileList as | file |}}
<tr>
<td data-col="key"><a href="{{file.key}}">{{key}}</a></td>
<td data-col="size">{{file.size}}</td>
<td data-col="lastModified">{{file.lastModified}}</td>
</tr>
{{/each}}
</tbody>
</table>
<script type="application/javascript">
const files = {{{json fileList}}};
const fileElements = Array.from(document.querySelectorAll("#files > tr"));
applyNewOrder()
function getSearchParams() {
return new URLSearchParams(window.location.hash.slice(1));
}
function toggleSort(newSortProperty) {
const params = getSearchParams()
const oldSortProperty = params.get("sort");
if (oldSortProperty === newSortProperty) {
const newDir = params.get("dir") === "asc" ? "desc" : "asc"
window.location.hash = "sort=" + newSortProperty + "&dir=" + newDir
} else {
window.location.hash = "sort=" + newSortProperty
}
setTimeout(() => applyNewOrder())
return false
}
function applyNewOrder() {
const params = getSearchParams()
const sortProperty = params.get("sort") ?? "lastModified"
const ascending = params.get("dir") === "asc"
sortFilesArray(sortProperty, ascending);
updateRows();
}
function sortFilesArray(propertyName, ascending) {
files.sort(compareByProp(propertyName))
if (!ascending) {
files.reverse()
}
}
function compareByProp(propertyName) {
return (file1, file2) => {
if (file1[propertyName] === file2[propertyName]) {
return 0
}
if (file1[propertyName] > file2[propertyName]) {
return 1
}
return -1
}
}
function updateRows() {
let index = 0;
for (const rowElement of fileElements) {
update(rowElement, files[index++])
}
}
function update(rowElement, file) {
const link = rowElement.querySelector('[data-col="key"] a')
link.setAttribute("href", file.key)
link.innerText = file.key
const size = rowElement.querySelector('[data-col="size"]')
size.innerText = file.size
const lastModified = rowElement.querySelector('[data-col="lastModified"]')
lastModified.innerText = file.lastModified
}
</script>
</body>
</html>
@@ -0,0 +1,31 @@
const crypto = require('crypto');
const { runTest } = require('./test-utils/runTest');
const { createS3Client } = require('./s3client');
const { generateFileList } = require('./generateFileList');
const assert = require('node:assert');
// This is a test file. It is intended to be run manually with the proper environment variables set
//
// Run it from the project root using "node tasks/aws-s3-builds-page/generateFileList-test.js"
const s3Client = createS3Client();
runTest(async ({ log }) => {
log('Generate file list');
const filename = `test-file-list-${crypto.randomUUID()}`;
await generateFileList(filename);
log(`Checking JSON at ${s3Client.fileUrl(`${filename}.json`)}`);
const jsonList = JSON.parse(await s3Client.fetchFile(`${filename}.json`));
assert(jsonList.find(s3obj => s3obj.key === 'handlebars-v4.7.7.js'));
log(`Checking HTML at ${s3Client.fileUrl(`${filename}.html`)}`);
const htmlList = await s3Client.fetchFile(`${filename}.html`);
assert(htmlList.includes('handlebars-v4.7.7.js'));
assert(htmlList.includes('handlebarsjs.com'));
assert(!htmlList.includes('index.html'));
log(`Deleting file ${filename}.json`);
await s3Client.deleteFile(`${filename}.json`);
});
@@ -0,0 +1,39 @@
/* eslint-disable no-console */
const { createS3Client } = require('./s3client');
const Handlebars = require('../..');
const fs = require('node:fs/promises');
const path = require('path');
async function generateFileList(nameWithoutExtension) {
const s3Client = createS3Client();
const fileList = await s3Client.listFiles();
const relevantFiles = fileList.filter(s3obj => s3obj.key.endsWith('.js'));
await uploadJson(s3Client, relevantFiles, nameWithoutExtension);
await uploadHtml(s3Client, relevantFiles, nameWithoutExtension);
}
async function uploadJson(s3Client, fileList, nameWithoutExtension) {
const fileListJson = JSON.stringify(fileList, null, 2);
await s3Client.uploadData(fileListJson, nameWithoutExtension + '.json', {
contentType: 'application/json'
});
}
async function uploadHtml(s3Client, fileList, nameWithoutExtension) {
const templateStr = await fs.readFile(
path.join(__dirname, 'fileList.hbs'),
'utf-8'
);
const template = Handlebars.compile(templateStr);
Handlebars.registerHelper('json', obj => JSON.stringify(obj));
const fileListHtml = template({
fileList,
jsonListUrl: nameWithoutExtension + '.json'
});
await s3Client.uploadData(fileListHtml, nameWithoutExtension + '.html', {
contentType: 'text/html'
});
}
module.exports = { generateFileList };
+47
View File
@@ -0,0 +1,47 @@
const crypto = require('crypto');
const { publishWithSuffixes } = require('./publish');
const { runTest } = require('./test-utils/runTest');
const { createS3Client } = require('./s3client');
const fs = require('node:fs/promises');
// This is a test file. It is intended to be run manually with the proper environment variables set
//
// Run it from the project root using "node tasks/aws-s3-builds-page/publish-test.js"
const s3Client = createS3Client();
runTest(async ({ log }) => {
const suffix1 = `-test-file-` + crypto.randomUUID();
const suffix2 = `-test-file-` + crypto.randomUUID();
log(`Publish ${suffix1} and ${suffix2}`);
await publishWithSuffixes([suffix1, suffix2]);
await compareAndDeleteFiles(suffix1, log);
await compareAndDeleteFiles(suffix2, log);
});
async function compareAndDeleteFiles(suffix, log) {
const pairs = [
['dist/handlebars.js', `handlebars${suffix}.js`],
['dist/handlebars.min.js', `handlebars.min${suffix}.js`],
['dist/handlebars.runtime.js', `handlebars.runtime${suffix}.js`],
['dist/handlebars.runtime.min.js', `handlebars.runtime.min${suffix}.js`]
];
for (const [localFile, remoteFile] of pairs) {
await expectSameContents(localFile, remoteFile, log);
log(`Deleting "${remoteFile}"`);
await s3Client.deleteFile(remoteFile);
}
}
async function expectSameContents(localFile, remoteFile, log) {
log(
`Checking file contents "${localFile}" vs "${s3Client.fileUrl(remoteFile)}"`
);
const remoteContents = await s3Client.fetchFile(remoteFile);
const localContents = await fs.readFile(localFile, 'utf-8');
if (remoteContents !== localContents) {
throw new Error(
`Files do not match: ${localFile}" vs "${s3Client.fileUrl(remoteFile)}"`
);
}
}
+37
View File
@@ -0,0 +1,37 @@
/* eslint-disable no-console */
const { createS3Client } = require('./s3client');
const filenames = [
'handlebars.js',
'handlebars.min.js',
'handlebars.runtime.js',
'handlebars.runtime.min.js'
];
async function publishWithSuffixes(suffixes) {
const s3Client = createS3Client();
const publishPromises = suffixes.map(suffix =>
publishSuffix(s3Client, suffix)
);
return Promise.all(publishPromises);
}
async function publishSuffix(s3client, suffix) {
const publishPromises = filenames.map(async filename => {
const nameInBucket = getNameInBucket(filename, suffix);
const localFile = getLocalFile(filename);
await s3client.uploadFile(localFile, nameInBucket);
console.log(`Published ${localFile} to build server (${nameInBucket})`);
});
return Promise.all(publishPromises);
}
function getNameInBucket(filename, suffix) {
return filename.replace(/\.js$/, suffix + '.js');
}
function getLocalFile(filename) {
return 'dist/' + filename;
}
module.exports = { publishWithSuffixes };
@@ -0,0 +1,11 @@
const { DeleteObjectCommand } = require('@aws-sdk/client-s3');
async function deleteFile(s3Client, bucket, remoteName) {
const command = new DeleteObjectCommand({
Bucket: bucket,
Key: remoteName
});
await s3Client.send(command);
}
module.exports = { deleteFile };
@@ -0,0 +1,10 @@
async function fetchFile(bucket, remoteName) {
return (await fetch(fileUrl(bucket, remoteName))).text();
}
function fileUrl(bucket, remoteName) {
const bucketUrl = `https://s3.amazonaws.com/${bucket}`;
return `${bucketUrl}/${remoteName}`;
}
module.exports = { fetchFile, fileUrl };
@@ -0,0 +1,42 @@
const { listFiles } = require('./listFiles');
const { uploadFile, uploadData } = require('./uploadFile');
const { deleteFile } = require('./deleteFile');
const { S3Client } = require('@aws-sdk/client-s3');
const { requireEnvVar } = require('./requireEnvVar');
const { fetchFile, fileUrl } = require('./fetchFile');
module.exports = { createS3Client };
function createS3Client() {
// https://docs.aws.amazon.com/sdk-for-javascript/v2/developer-guide/loading-node-credentials-environment.html
requireEnvVar('AWS_ACCESS_KEY_ID');
requireEnvVar('AWS_SECRET_ACCESS_KEY');
const bucket = requireEnvVar('S3_BUCKET_NAME');
const s3Client = new S3Client({
region: 'us-east-1'
});
return {
async listFiles() {
return listFiles(s3Client, bucket);
},
async uploadFile(localName, remoteName, { contentType } = {}) {
await uploadFile(s3Client, bucket, localName, remoteName, {
contentType
});
},
async uploadData(data, remoteName, { contentType } = {}) {
await uploadData(s3Client, bucket, data, remoteName, { contentType });
},
async deleteFile(remoteName) {
await deleteFile(s3Client, bucket, remoteName);
},
async fetchFile(remoteName) {
return fetchFile(bucket, remoteName);
},
fileUrl(remoteName) {
return fileUrl(bucket, remoteName);
}
};
}
@@ -0,0 +1,32 @@
const { ListObjectsV2Command } = require('@aws-sdk/client-s3');
async function listFiles(s3Client, bucket) {
const command = new ListObjectsV2Command({
Bucket: bucket
});
let isTruncated = true;
const files = [];
while (isTruncated) {
const {
Contents,
IsTruncated,
NextContinuationToken
} = await s3Client.send(command);
files.push(...Contents.map(dataFromS3Object));
isTruncated = IsTruncated;
command.input.ContinuationToken = NextContinuationToken;
}
return files;
}
function dataFromS3Object(s3obj) {
return {
key: s3obj.Key,
size: s3obj.Size,
lastModified: s3obj.LastModified.toISOString()
};
}
module.exports = { listFiles };
@@ -0,0 +1,8 @@
function requireEnvVar(name) {
if (!process.env[name]) {
throw new Error(`Environment variable "${name}" is required.`);
}
return process.env[name];
}
module.exports = { requireEnvVar };
@@ -0,0 +1,89 @@
/* eslint-disable no-console */
const { createS3Client } = require('./index');
const crypto = require('crypto');
const { runTest } = require('../test-utils/runTest');
const assert = require('node:assert');
// This is a test file. It is intended to be run manually
// with the proper environment variables set
// It tests whether the upload/list/delete methods in this directory
// work properly.
//
// Run it from the project root using "node tasks/aws-s3-builds-page/s3client/s3-test.js"
const client = createS3Client();
const ISO_DATE = /\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}.\d{3}Z/;
runTest(async ({ log }) => {
const uuid = crypto.randomUUID();
const filename = `test-file-${uuid}`;
log(`Starting test with target file "${filename}"`);
log(`Uploading "${filename}"`);
await client.uploadFile('package.json', filename);
log(`Check if uploaded "${filename}"`);
const listing = await client.listFiles();
if (!listing.find(s3obj => s3obj.key === filename)) {
throw new Error(`File "${filename}" has not been uploaded`);
}
log(`Check contents of "${filename}"`);
const uploadedContents = await client.fetchFile(filename);
expectStringContains('"name": "handlebars"', uploadedContents);
await expectContentType(filename, 'application/octet-stream');
log(`Uploading with content type "${filename}"`);
await client.uploadFile('package.json', filename, {
contentType: 'text/html'
});
log('Checking content-type');
await expectContentType(filename, 'text/html');
log('Upload data as text/plain');
await client.uploadData('Hello world', filename, {
contentType: 'text/plain'
});
log('Checking content-type');
await expectContentType(filename, 'text/plain');
log(`Check contents of "${filename}"`);
expectStringContains('Hello world', await client.fetchFile(filename));
const helloWorldObj = (await client.listFiles()).find(
s3obj => s3obj.key === filename
);
assert.equal(helloWorldObj.size, 11, 'Checking file size of hello world');
assert.match(
helloWorldObj.lastModified,
ISO_DATE,
'Last modified must be an iso-date'
);
log(`Delete "${filename}"`);
await client.deleteFile(filename);
log(`Check if deleted "${filename}"`);
const foundFile = (await client.listFiles()).find(
s3obj => s3obj.key === filename
);
if (foundFile != null) {
throw new Error(`File "${filename}" has not been deleted`);
}
});
function expectStringContains(needle, haystack) {
if (!haystack.includes(needle)) {
throw new Error(`Expecting to find "${needle}" in string "${haystack}"`);
}
}
async function expectContentType(remoteName, expectedContentType) {
const contentType = (await fetch(client.fileUrl(remoteName))).headers.get(
'Content-Type'
);
if (contentType !== expectedContentType) {
throw new Error(
`Expecting to find content-type "${expectedContentType}" but found "${contentType}"`
);
}
}
@@ -0,0 +1,31 @@
const { PutObjectCommand } = require('@aws-sdk/client-s3');
const fs = require('node:fs/promises');
async function uploadFile(
s3Client,
bucket,
localName,
remoteName,
{ contentType } = {}
) {
const fileContents = await fs.readFile(localName);
await uploadData(s3Client, bucket, fileContents, remoteName, { contentType });
}
async function uploadData(
s3Client,
bucket,
data,
remoteName,
{ contentType } = {}
) {
const command = new PutObjectCommand({
Bucket: bucket,
Key: remoteName,
Body: data,
ContentType: contentType
});
await s3Client.send(command);
}
module.exports = { uploadFile, uploadData };
@@ -0,0 +1,37 @@
/* eslint-disable no-console */
const { createS3Client } = require('../s3client/index');
const s3Client = createS3Client();
function runTest(asyncFn) {
asyncFn({ log: console.log.bind(console) })
.finally(detectSurplusFiles)
.then(() => {
console.log('DONE');
})
.catch(error => {
console.error(error);
process.exit(1);
});
}
async function detectSurplusFiles() {
const listing = await s3Client.listFiles();
let surplusFileDetected = false;
const testFilesInBucket = listing.filter(name =>
name.key.includes('test-file')
);
for (const { key: filename } of testFilesInBucket) {
if (process.argv[2] === '--delete-surplus') {
await s3Client.deleteFile(filename);
} else {
console.log(`Detected surplus file "${filename}"`);
surplusFileDetected = true;
}
}
if (surplusFileDetected) {
console.log(`run with --delete-surplus to delete surplus files`);
}
}
module.exports = { runTest };
+4 -71
View File
@@ -1,7 +1,8 @@
const AWS = require('aws-sdk');
const git = require('./util/git');
const { createRegisterAsyncTaskFn } = require('./util/async-grunt-task');
const semver = require('semver');
const { publishWithSuffixes } = require('./aws-s3-builds-page/publish');
const { generateFileList } = require('./aws-s3-builds-page/generateFileList');
module.exports = function(grunt) {
const registerAsyncTask = createRegisterAsyncTaskFn(grunt);
@@ -27,76 +28,8 @@ module.exports = function(grunt) {
}
if (suffixes.length > 0) {
initSDK();
grunt.log.writeln(
'publishing file-suffixes: ' + JSON.stringify(suffixes)
);
await publish(suffixes);
await publishWithSuffixes(suffixes);
await generateFileList('index');
}
});
function initSDK() {
const bucket = process.env.S3_BUCKET_NAME,
key = process.env.S3_ACCESS_KEY_ID,
secret = process.env.S3_SECRET_ACCESS_KEY;
if (!bucket || !key || !secret) {
throw new Error('Missing S3 config values');
}
AWS.config.update({ accessKeyId: key, secretAccessKey: secret });
}
async function publish(suffixes) {
const publishPromises = suffixes.map(suffix => publishSuffix(suffix));
return Promise.all(publishPromises);
}
async function publishSuffix(suffix) {
const filenames = [
'handlebars.js',
'handlebars.min.js',
'handlebars.runtime.js',
'handlebars.runtime.min.js'
];
const publishPromises = filenames.map(async filename => {
const nameInBucket = getNameInBucket(filename, suffix);
const localFile = getLocalFile(filename);
await uploadToBucket(localFile, nameInBucket);
grunt.log.writeln(
`Published ${localFile} to build server (${nameInBucket})`
);
});
return Promise.all(publishPromises);
}
async function uploadToBucket(localFile, nameInBucket) {
const bucket = process.env.S3_BUCKET_NAME;
const uploadParams = {
Bucket: bucket,
Key: nameInBucket,
Body: grunt.file.read(localFile)
};
return s3PutObject(uploadParams);
}
};
function s3PutObject(uploadParams) {
const s3 = new AWS.S3();
return new Promise((resolve, reject) => {
s3.putObject(uploadParams, err => {
if (err != null) {
return reject(err);
}
resolve();
});
});
}
function getNameInBucket(filename, suffix) {
return filename.replace(/\.js$/, suffix + '.js');
}
function getLocalFile(filename) {
return 'dist/' + filename;
}
+1 -1
View File
@@ -14,7 +14,7 @@ cd "$( dirname "$( readlink -f "$0" )" )" || exit 1
unset npm_config_prefix
echo "Handlebars should be able to run in various versions of NodeJS"
for node_version_to_test in 0.10 0.12 4 5 6 7 8 9 10 11 12 13 14 15 16 17; do
for node_version_to_test in 0.10 0.12 4 5 6 7 8 9 10 11 12 13 14 15 16 18; do
rm target node_modules package-lock.json -rf
mkdir target
+1 -1
View File
@@ -4,7 +4,7 @@ set -e
# Cleanup: package-lock and "npm ci" is not working with local dependencies
rm dist package-lock.json -rf
npm install
npm install --legacy-peer-deps
npm run build
for i in dist/*-test.js ; do
+3 -9
View File
@@ -1,6 +1,6 @@
{
"name": "webpack-test",
"description": "Various tests with Handlebars and Webpack",
"description": "Various tests with Handlebars and multiple webpack versions",
"version": "1.0.0",
"main": "index.js",
"scripts": {
@@ -8,14 +8,8 @@
"test": "node dist/main.js"
},
"private": true,
"keywords": [],
"author": "",
"license": "ISC",
"dependencies": {},
"devDependencies": {
"dependencies": {
"handlebars": "file:../../..",
"handlebars-loader": "^1.7.1",
"webpack": "^4.39.3",
"webpack-cli": "^3.3.7"
"handlebars-loader": "^1.7.1"
}
}
@@ -0,0 +1,5 @@
import Handlebars from 'handlebars/lib/handlebars';
import { assertEquals } from './lib/assert';
const template = Handlebars.compile('Author: {{author}}');
assertEquals(template({ author: 'Yehuda' }), 'Author: Yehuda');
@@ -0,0 +1,43 @@
import * as Handlebars from 'handlebars/runtime';
import { assertEquals } from './lib/assert';
const template = Handlebars.template({
compiler: [8, '>= 4.3.0'],
main: function(container, depth0, helpers, partials, data) {
var helper,
lookupProperty =
container.lookupProperty ||
function(parent, propertyName) {
if (Object.prototype.hasOwnProperty.call(parent, propertyName)) {
return parent[propertyName];
}
return undefined;
};
return (
'Author: ' +
container.escapeExpression(
((helper =
(helper =
lookupProperty(helpers, 'author') ||
(depth0 != null ? lookupProperty(depth0, 'author') : depth0)) !=
null
? helper
: container.hooks.helperMissing),
typeof helper === 'function'
? helper.call(depth0 != null ? depth0 : container.nullContext || {}, {
name: 'author',
hash: {},
data: data,
loc: {
start: { line: 1, column: 8 },
end: { line: 1, column: 18 }
}
})
: helper)
)
);
},
useData: true
});
assertEquals(template({ author: 'Yehuda' }), 'Author: Yehuda');
+16 -5
View File
@@ -2,11 +2,7 @@
set -e
# Cleanup: package-lock and "npm ci" is not working with local dependencies
rm dist package-lock.json -rf
npm install
npm run build
run_tests () {
for i in dist/*-test.js ; do
echo "----------------------"
echo "-- Running $i"
@@ -14,3 +10,18 @@ for i in dist/*-test.js ; do
node "$i"
echo "Success"
done
}
# Cleanup: package-lock and "npm ci" is not working with local dependencies
rm dist package-lock.json -rf
npm install --legacy-peer-deps
# Test with webpack 4
npm install --legacy-peer-deps --no-save webpack@^4 webpack-cli@^3
npm run build
run_tests
# Test with webpack 5
npm install --legacy-peer-deps --no-save webpack@^5 webpack-cli@^4
npm run build
run_tests