diff --git a/.prettierignore b/.prettierignore index df7a6372..2a386cf1 100644 --- a/.prettierignore +++ b/.prettierignore @@ -1,2 +1,3 @@ # Ignore all HTML files in test directory: test/cases/**/*.html +test/cases/**/*.hbs diff --git a/CHANGELOG.md b/CHANGELOG.md index 86bc0b07..a6da2a1c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # Change log +## 2.10.0 (2023-09-01) + +- feat: add Handlebars helpers `assign`, `partial` and `block` to extend a template layout with blocks +- chore: add `handlebars-layout` example +- docs: update README + ## 2.9.0 (2023-08-27) - feat(experimental): add support the Webpack `cache.type` as `filesystem`. This is yet an alpha version of the feature. diff --git a/README.md b/README.md index 85a658ef..9f05bcda 100644 --- a/README.md +++ b/README.md @@ -20,12 +20,8 @@ > _HTML as an entry point works in both Vite and Parcel, and now also in Webpack._ -The plugin allows to specify template files as [entry points](#option-entry). -You can use various templates such as HTML, PHTML, EJS, Eta, Handlebars, Nunjucks and others without additional loaders and plugins. - -The plugin resolves references in the HTML template and adds them to the Webpack compilation. -Webpack will automatically process the source files and the plugin replace the references with their URLs to output files in the generated HTML. -Using the [`js.inline`](#option-js) and [`css.inline`](#option-css) options, you can inline JS and CSS into HTML. +The plugin allows to use any [template](#template-engine) file as [entry point](#option-entry). +In an HTML template can be referenced any source files, similar to how it works in [Vite](https://vitejs.dev/guide/#index-html-and-project-root) or [Parcel](https://parceljs.org/). @@ -38,33 +34,38 @@ Using the [`js.inline`](#option-js) and [`css.inline`](#option-css) options, you
Entry point is HTML
-In a HTML template can be referenced any source files, similar to how it works in [Vite](https://vitejs.dev/guide/#index-html-and-project-root) or [Parcel](https://parceljs.org/). - -- `` -- `` -- `` -- `` - -### βš™οΈ How works the plugin - - +See the [simple example](#example), [install](#install) and [quick start](#contents). ### πŸ’‘ Highlights - An [entry point](#option-entry) is any template. -- Reference to source **scripts** and **styles** in HTML using `` +- **Inlines** **JS** and **CSS** into HTML using the [`js.inline`](#option-js) and [`css.inline`](#option-css) options. +- **Resolves** [source asset files](#loader-option-sources) in attributes `href` `src` `srcset` etc. using **relative path** or **alias**: + - `` + - `` +- **Inlines images**, e.g., [SVG](#recipe-inline-image), [PNG](#recipe-inline-image) without additional plugins and loaders. +- Support for [template engines](#template-engine) such as [Eta](#using-template-eta), [EJS](#using-template-ejs), [Handlebars](#using-template-handlebars), [Nunjucks](#using-template-nunjucks), [LiquidJS](#using-template-liquidjs) and others. +- **Auto processing many HTML templates** using the [entry path](#option-entry-path), add/delete/rename w/o restarting. - Auto generation of `` to [preload](#option-preload) fonts, images, video, scripts, styles, etc. -- Automatically processing many HTML templates using the [entry path](#option-entry-path), add/delete/rename w/o restarting. -- Dynamically loading template variables using the [data](#loader-option-data) option, change data w/o restarting. -- Support for React. + +See the [full list of features](#features). + +### βš™οΈ How works the plugin + +The plugin resolves references in the HTML template and adds them to the Webpack compilation. +Webpack will automatically process the source files and the plugin replace the references with their output filenames in the generated HTML. +See the [example](#example). + + ### βœ… Profit -You can specify the script and style source files directly in an HTML template, -no longer need to define them in the Webpack entry or import styles in JavaScript. +You can specify script and style source files directly in an HTML template, +and you no longer need to define them in Webpack entry or import styles in JavaScript. +Use one powerful plugin instead of [many different plugins](#list-of-plugins). ### ❓Question / Feature Request / Bug @@ -77,10 +78,7 @@ If you have discovered a bug or have a feature suggestion, feel free to create a ## πŸ”† What's New in v2 - **NEW:** you can add/delete/rename a template file in the [entry path](#option-entry-path) without restarting Webpack -- **NEW:** added support for importing style files in JavaScript.\ - **Note:** this feature was added for compatibility with `React` projects.\ - The importing styles in JavaScript is the `bad practice`. This is the `wrong way`.\ - In new projects you should specify style source files directly in HTML. +- **NEW:** added importing style files in JavaScript. - **POTENTIAL BREAKING CHANGE:** Upgrade the default [Eta](https://eta.js.org) templating engine from `v2` to `v3`.\ If you use the `Eta` syntax, may be you need to update templates. @@ -94,6 +92,8 @@ For full release notes see the [changelog](https://github.com/webdiscus/html-bun --- + + ## Simple usage example Start with an HTML template. Add the `` and ` +{{/partial}} + + +{{#partial 'styles'}} + +{{/partial}} + + +{{#partial 'content'}} + {{> about/content}} +{{/partial}} + +{{#partial 'custom_block'}} +

About custom block

+{{/partial}} + + +{{> layout}} diff --git a/examples/handlebars-layout/src/views/pages/home/content.hbs b/examples/handlebars-layout/src/views/pages/home/content.hbs new file mode 100644 index 00000000..3e1b5594 --- /dev/null +++ b/examples/handlebars-layout/src/views/pages/home/content.hbs @@ -0,0 +1 @@ +

Home content

diff --git a/examples/handlebars-layout/src/views/pages/home/home.js b/examples/handlebars-layout/src/views/pages/home/home.js new file mode 100644 index 00000000..c482e2d8 --- /dev/null +++ b/examples/handlebars-layout/src/views/pages/home/home.js @@ -0,0 +1 @@ +console.log('>> Home page'); diff --git a/examples/handlebars-layout/src/views/pages/home/home.scss b/examples/handlebars-layout/src/views/pages/home/home.scss new file mode 100644 index 00000000..36998cdf --- /dev/null +++ b/examples/handlebars-layout/src/views/pages/home/home.scss @@ -0,0 +1,3 @@ +.container { + color: #118851; +} diff --git a/examples/handlebars-layout/src/views/pages/home/index.hbs b/examples/handlebars-layout/src/views/pages/home/index.hbs new file mode 100644 index 00000000..97b182c6 --- /dev/null +++ b/examples/handlebars-layout/src/views/pages/home/index.hbs @@ -0,0 +1,19 @@ +{{assign title='Homepage' header='Home'}} + + +{{#partial 'scripts'}} + +{{/partial}} + + +{{#partial 'styles'}} + +{{/partial}} + + +{{#partial 'content'}} + {{> home/content}} +{{/partial}} + + +{{> layout}} diff --git a/examples/handlebars-layout/src/views/partials/footer.hbs b/examples/handlebars-layout/src/views/partials/footer.hbs new file mode 100644 index 00000000..5191aac4 --- /dev/null +++ b/examples/handlebars-layout/src/views/partials/footer.hbs @@ -0,0 +1 @@ + diff --git a/examples/handlebars-layout/src/views/partials/header.hbs b/examples/handlebars-layout/src/views/partials/header.hbs new file mode 100644 index 00000000..74eaf8c8 --- /dev/null +++ b/examples/handlebars-layout/src/views/partials/header.hbs @@ -0,0 +1 @@ +

{{header}}

diff --git a/examples/handlebars-layout/src/views/partials/layout.hbs b/examples/handlebars-layout/src/views/partials/layout.hbs new file mode 100644 index 00000000..edeea833 --- /dev/null +++ b/examples/handlebars-layout/src/views/partials/layout.hbs @@ -0,0 +1,34 @@ + + + + {{ title }} + + + + + + + + + {{#block 'styles'}}{{/block}} + {{#block 'scripts'}}{{/block}} + + + {{> header }} + + + +
+ + {{#block 'content'}}{{/block}} +
+ + + {{#block 'custom_block'}}

default block content

{{/block}} + + {{> footer }} + + diff --git a/examples/handlebars-layout/webpack.config.js b/examples/handlebars-layout/webpack.config.js new file mode 100644 index 00000000..55658b3e --- /dev/null +++ b/examples/handlebars-layout/webpack.config.js @@ -0,0 +1,77 @@ +const path = require('path'); +//const HtmlBundlerPlugin = require('html-bundler-webpack-plugin'); +const HtmlBundlerPlugin = require('../../'); + +module.exports = { + //mode: 'development', + mode: 'production', + + output: { + path: path.resolve(__dirname, 'dist'), + clean: true, + }, + + resolve: { + alias: { + '@scripts': path.join(__dirname, 'src/js'), + '@styles': path.join(__dirname, 'src/scss'), + '@images': path.join(__dirname, 'src/images'), + }, + }, + + plugins: [ + new HtmlBundlerPlugin({ + entry: { + // define templates here + index: 'src/views/pages/home/index.hbs', // => dist/index.html + // index: { + // import: 'src/views/pages/home/index.hbs', + // data: { title: 'Home' }, + // }, + about: 'src/views/pages/about/index.hbs', // => dist/about.html + }, + // specify the `handlebars` template engine + preprocessor: 'handlebars', + // define handlebars options + preprocessorOptions: { + //helpers: [path.join(__dirname, 'src/views/helpers')], + partials: ['src/views/pages/', 'src/views/partials/'], + }, + js: { + // output filename of compiled JavaScript + filename: 'js/[name].[contenthash:8].js', + }, + css: { + // output filename of extracted CSS + filename: 'css/[name].[contenthash:8].css', + }, + }), + ], + + module: { + rules: [ + { + test: /\.(scss)$/, + use: ['css-loader', 'sass-loader'], + }, + { + test: /\.(png|svg|jpe?g|webp)$/i, + type: 'asset/resource', + generator: { + filename: 'img/[name].[hash:8][ext]', + }, + }, + ], + }, + + // enable live reload + devServer: { + static: path.resolve(__dirname, 'dist'), + watchFiles: { + paths: ['src/**/*.*'], + options: { + usePolling: true, + }, + }, + }, +}; diff --git a/examples/handlebars/webpack.config.js b/examples/handlebars/webpack.config.js index e7d008ae..80a38fd4 100644 --- a/examples/handlebars/webpack.config.js +++ b/examples/handlebars/webpack.config.js @@ -67,7 +67,7 @@ module.exports = { ], }, - // enable HMR with live reload + // enable live reload devServer: { static: path.resolve(__dirname, 'dist'), watchFiles: { diff --git a/examples/hello-world/webpack.config.js b/examples/hello-world/webpack.config.js index f14395d8..d192e716 100644 --- a/examples/hello-world/webpack.config.js +++ b/examples/hello-world/webpack.config.js @@ -42,7 +42,7 @@ module.exports = { ], }, - // enable HMR with live reload + // enable live reload devServer: { static: path.resolve(__dirname, 'dist'), watchFiles: { diff --git a/examples/simple-site/webpack.config.js b/examples/simple-site/webpack.config.js index aea7d96f..bd068c8a 100644 --- a/examples/simple-site/webpack.config.js +++ b/examples/simple-site/webpack.config.js @@ -53,7 +53,7 @@ module.exports = { ], }, - // enable HMR with live reload + // enable live reload devServer: { static: path.resolve(__dirname, 'dist'), watchFiles: { diff --git a/examples/tailwindcss/webpack.config.js b/examples/tailwindcss/webpack.config.js index 3944157a..c64ef7ac 100644 --- a/examples/tailwindcss/webpack.config.js +++ b/examples/tailwindcss/webpack.config.js @@ -48,7 +48,7 @@ module.exports = { ], }, - // enable HMR with live reload + // enable live reload devServer: { static: path.resolve(__dirname, 'dist'), watchFiles: { diff --git a/package-lock.json b/package-lock.json index a169512d..2f202634 100644 --- a/package-lock.json +++ b/package-lock.json @@ -27,6 +27,7 @@ "css-minimizer-webpack-plugin": "^5.0.1", "ejs": "^3.1.9", "handlebars": "^4.7.8", + "handlebars-loader": "^1.7.3", "jest": "^29.6.4", "liquidjs": "^10.9.1", "mustache": "^4.2.0", @@ -36,6 +37,7 @@ "sass": "^1.65.1", "sass-loader": "^13.3.2", "sharp": "^0.32.5", + "ts-loader": "9.4.4", "webpack": "^5.88.2", "webpack-cli": "5.1.4", "webpack-dev-server": "^4.15.1" @@ -3551,6 +3553,15 @@ "integrity": "sha512-x+VAiMRL6UPkx+kudNvxTl6hB2XNNCG2r+7wixVfIYwu/2HKRXimwQyaumLjMveWvT2Hkd/cAJw+QBMfJ/EKVw==", "dev": true }, + "node_modules/big.js": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/big.js/-/big.js-5.2.2.tgz", + "integrity": "sha512-vyL2OymJxmarO8gxMr0mhChsO9QGwhynfuu4+MHTAW6czfq9humCB7rKpUjDd9YUiDPU4mzpyupFSvOClAwbmQ==", + "dev": true, + "engines": { + "node": "*" + } + }, "node_modules/binary-extensions": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz", @@ -4815,6 +4826,15 @@ "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", "dev": true }, + "node_modules/emojis-list": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/emojis-list/-/emojis-list-3.0.0.tgz", + "integrity": "sha512-/kyM18EfinwXZbno9FyUGeFh87KC8HRQBQGildHZbEuRyWFOmv1U10o9BBp8XVZDVNNuQKyIGIu5ZYAAXJ0V2Q==", + "dev": true, + "engines": { + "node": ">= 4" + } + }, "node_modules/encodeurl": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", @@ -5182,6 +5202,12 @@ "node": ">= 4.9.1" } }, + "node_modules/fastparse": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/fastparse/-/fastparse-1.1.2.tgz", + "integrity": "sha512-483XLLxTVIwWK3QTrMGRqUfUpoOs/0hbQrl2oz4J0pAcm3A3bu84wxTFqGqkJzewCLdME38xJLJAxBABfQT8sQ==", + "dev": true + }, "node_modules/fastq": { "version": "1.15.0", "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.15.0.tgz", @@ -5567,6 +5593,47 @@ "uglify-js": "^3.1.4" } }, + "node_modules/handlebars-loader": { + "version": "1.7.3", + "resolved": "https://registry.npmjs.org/handlebars-loader/-/handlebars-loader-1.7.3.tgz", + "integrity": "sha512-dDb+8D51vE3OTSE2wuGPWRAegtsEuw8Mk8hCjtRu/pNcBfN5q+M8ZG3kVJxBuOeBrVElpFStipGmaxSBTRR1mQ==", + "dev": true, + "dependencies": { + "async": "^3.2.2", + "fastparse": "^1.0.0", + "loader-utils": "1.4.x", + "object-assign": "^4.1.0" + }, + "peerDependencies": { + "handlebars": ">= 1.3.0 < 5" + } + }, + "node_modules/handlebars-loader/node_modules/json5": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.2.tgz", + "integrity": "sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==", + "dev": true, + "dependencies": { + "minimist": "^1.2.0" + }, + "bin": { + "json5": "lib/cli.js" + } + }, + "node_modules/handlebars-loader/node_modules/loader-utils": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-1.4.2.tgz", + "integrity": "sha512-I5d00Pd/jwMD2QCduo657+YM/6L3KZu++pmX9VFncxaxvHcru9jx1lBaFft+r4Mt2jK0Yhp41XlRAihzPxHNCg==", + "dev": true, + "dependencies": { + "big.js": "^5.2.2", + "emojis-list": "^3.0.0", + "json5": "^1.0.1" + }, + "engines": { + "node": ">=4.0.0" + } + }, "node_modules/has": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", @@ -8473,6 +8540,15 @@ "node": ">= 6" } }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/object-inspect": { "version": "1.12.3", "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.12.3.tgz", @@ -10815,6 +10891,128 @@ "node": ">=0.6" } }, + "node_modules/ts-loader": { + "version": "9.4.4", + "resolved": "https://registry.npmjs.org/ts-loader/-/ts-loader-9.4.4.tgz", + "integrity": "sha512-MLukxDHBl8OJ5Dk3y69IsKVFRA/6MwzEqBgh+OXMPB/OD01KQuWPFd1WAQP8a5PeSCAxfnkhiuWqfmFJzJQt9w==", + "dev": true, + "dependencies": { + "chalk": "^4.1.0", + "enhanced-resolve": "^5.0.0", + "micromatch": "^4.0.0", + "semver": "^7.3.4" + }, + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "typescript": "*", + "webpack": "^5.0.0" + } + }, + "node_modules/ts-loader/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/ts-loader/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/ts-loader/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/ts-loader/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "node_modules/ts-loader/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/ts-loader/node_modules/lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "dev": true, + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/ts-loader/node_modules/semver": { + "version": "7.5.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", + "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", + "dev": true, + "dependencies": { + "lru-cache": "^6.0.0" + }, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/ts-loader/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/ts-loader/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true + }, "node_modules/tslib": { "version": "2.6.2", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", @@ -10866,6 +11064,20 @@ "node": ">= 0.6" } }, + "node_modules/typescript": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.2.2.tgz", + "integrity": "sha512-mI4WrpHsbCIcwT9cF4FZvr80QUeKvsUsUvKDoR+X/7XHQH98xYD8YHZg7ANtz2GtZt/CBq2QJ0thkGJMHfqc1w==", + "dev": true, + "peer": true, + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, "node_modules/uglify-js": { "version": "3.17.4", "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.17.4.tgz", @@ -11542,6 +11754,7 @@ } }, "test/fixtures/modules/import-css": { + "name": "@test/import-css", "dev": true } }, @@ -14167,6 +14380,12 @@ "integrity": "sha512-x+VAiMRL6UPkx+kudNvxTl6hB2XNNCG2r+7wixVfIYwu/2HKRXimwQyaumLjMveWvT2Hkd/cAJw+QBMfJ/EKVw==", "dev": true }, + "big.js": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/big.js/-/big.js-5.2.2.tgz", + "integrity": "sha512-vyL2OymJxmarO8gxMr0mhChsO9QGwhynfuu4+MHTAW6czfq9humCB7rKpUjDd9YUiDPU4mzpyupFSvOClAwbmQ==", + "dev": true + }, "binary-extensions": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz", @@ -15083,6 +15302,12 @@ "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", "dev": true }, + "emojis-list": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/emojis-list/-/emojis-list-3.0.0.tgz", + "integrity": "sha512-/kyM18EfinwXZbno9FyUGeFh87KC8HRQBQGildHZbEuRyWFOmv1U10o9BBp8XVZDVNNuQKyIGIu5ZYAAXJ0V2Q==", + "dev": true + }, "encodeurl": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", @@ -15371,6 +15596,12 @@ "integrity": "sha512-eRnCtTTtGZFpQCwhJiUOuxPQWRXVKYDn0b2PeHfXL6/Zi53SLAzAHfVhVWK2AryC/WH05kGfxhFIPvTF0SXQzg==", "dev": true }, + "fastparse": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/fastparse/-/fastparse-1.1.2.tgz", + "integrity": "sha512-483XLLxTVIwWK3QTrMGRqUfUpoOs/0hbQrl2oz4J0pAcm3A3bu84wxTFqGqkJzewCLdME38xJLJAxBABfQT8sQ==", + "dev": true + }, "fastq": { "version": "1.15.0", "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.15.0.tgz", @@ -15664,6 +15895,40 @@ "wordwrap": "^1.0.0" } }, + "handlebars-loader": { + "version": "1.7.3", + "resolved": "https://registry.npmjs.org/handlebars-loader/-/handlebars-loader-1.7.3.tgz", + "integrity": "sha512-dDb+8D51vE3OTSE2wuGPWRAegtsEuw8Mk8hCjtRu/pNcBfN5q+M8ZG3kVJxBuOeBrVElpFStipGmaxSBTRR1mQ==", + "dev": true, + "requires": { + "async": "^3.2.2", + "fastparse": "^1.0.0", + "loader-utils": "1.4.x", + "object-assign": "^4.1.0" + }, + "dependencies": { + "json5": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.2.tgz", + "integrity": "sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==", + "dev": true, + "requires": { + "minimist": "^1.2.0" + } + }, + "loader-utils": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-1.4.2.tgz", + "integrity": "sha512-I5d00Pd/jwMD2QCduo657+YM/6L3KZu++pmX9VFncxaxvHcru9jx1lBaFft+r4Mt2jK0Yhp41XlRAihzPxHNCg==", + "dev": true, + "requires": { + "big.js": "^5.2.2", + "emojis-list": "^3.0.0", + "json5": "^1.0.1" + } + } + } + }, "has": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", @@ -17804,6 +18069,12 @@ } } }, + "object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "dev": true + }, "object-inspect": { "version": "1.12.3", "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.12.3.tgz", @@ -19453,6 +19724,93 @@ "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", "dev": true }, + "ts-loader": { + "version": "9.4.4", + "resolved": "https://registry.npmjs.org/ts-loader/-/ts-loader-9.4.4.tgz", + "integrity": "sha512-MLukxDHBl8OJ5Dk3y69IsKVFRA/6MwzEqBgh+OXMPB/OD01KQuWPFd1WAQP8a5PeSCAxfnkhiuWqfmFJzJQt9w==", + "dev": true, + "requires": { + "chalk": "^4.1.0", + "enhanced-resolve": "^5.0.0", + "micromatch": "^4.0.0", + "semver": "^7.3.4" + }, + "dependencies": { + "ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "requires": { + "color-convert": "^2.0.1" + } + }, + "chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "requires": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + } + }, + "color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "requires": { + "color-name": "~1.1.4" + } + }, + "color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true + }, + "lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "dev": true, + "requires": { + "yallist": "^4.0.0" + } + }, + "semver": { + "version": "7.5.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", + "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", + "dev": true, + "requires": { + "lru-cache": "^6.0.0" + } + }, + "supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "requires": { + "has-flag": "^4.0.0" + } + }, + "yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true + } + } + }, "tslib": { "version": "2.6.2", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", @@ -19489,6 +19847,13 @@ "mime-types": "~2.1.24" } }, + "typescript": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.2.2.tgz", + "integrity": "sha512-mI4WrpHsbCIcwT9cF4FZvr80QUeKvsUsUvKDoR+X/7XHQH98xYD8YHZg7ANtz2GtZt/CBq2QJ0thkGJMHfqc1w==", + "dev": true, + "peer": true + }, "uglify-js": { "version": "3.17.4", "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.17.4.tgz", diff --git a/package.json b/package.json index aeb8fa38..00b54555 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "html-bundler-webpack-plugin", - "version": "2.9.0", + "version": "2.10.0", "description": "HTML bundler plugin for webpack handles a template as an entry point, extracts CSS and JS from their sources referenced in HTML, supports template engines like Eta, EJS, Handlebars, Nunjucks.", "keywords": [ "html", @@ -120,6 +120,7 @@ "sass": "^1.65.1", "sass-loader": "^13.3.2", "sharp": "^0.32.5", + "ts-loader": "9.4.4", "webpack": "^5.88.2", "webpack-cli": "5.1.4", "webpack-dev-server": "^4.15.1" diff --git a/src/Loader/Options.js b/src/Loader/Options.js index 67b16ab6..95316242 100644 --- a/src/Loader/Options.js +++ b/src/Loader/Options.js @@ -69,9 +69,12 @@ class Options { const data = { ...loaderData, ...entryData, ...queryData }; if (Object.keys(data).length > 0) loaderObject.data = data; - Preprocessor.init({ fileSystem: this.fileSystem, rootContext, watch: this.#watch }); if (!Preprocessor.isReady(options.preprocessor)) { - options.preprocessor = Preprocessor.factory(options.preprocessor, options.preprocessorOptions); + options.preprocessor = Preprocessor.factory(loaderContext, { + preprocessor: options.preprocessor, + watch: this.#watch, + options: options.preprocessorOptions, + }); } // clean loaderContext of artifacts diff --git a/src/Loader/Preprocessor.js b/src/Loader/Preprocessor.js index d1ac6baf..dfe17869 100644 --- a/src/Loader/Preprocessor.js +++ b/src/Loader/Preprocessor.js @@ -1,12 +1,8 @@ const { unsupportedPreprocessorException } = require('./Messages/Exeptions'); -class Preprocessor { - static init({ fileSystem, rootContext, watch }) { - this.fileSystem = fileSystem; - this.rootContext = rootContext; - this.watch = watch; - } +/** @typedef {import('webpack').LoaderContext} LoaderContext */ +class Preprocessor { /** * Whether the preprocessor is ready to use. * @@ -39,40 +35,28 @@ class Preprocessor { * Factory preprocessor as a function. * The default preprocessor uses the Eta template engine. * + * @param {LoaderContext} loaderContext The loader context of Webpack. * @param {string|null|*} preprocessor The preprocessor value, should be a string * @param {Object} options The preprocessor options. + * @param {Function} watch The function called by watching. * @return {Function|Promise|Object} * @throws */ - static factory(preprocessor, options = {}) { + static factory(loaderContext, { preprocessor, options = {}, watch }) { if (preprocessor == null) preprocessor = 'eta'; switch (preprocessor) { case 'eta': - return require('./Preprocessors/Eta/index')({ - rootContext: this.rootContext, - options, - }); + return require('./Preprocessors/Eta/index')(loaderContext, options); case 'ejs': - return require('./Preprocessors/Ejs/index')({ - rootContext: this.rootContext, - options, - }); + return require('./Preprocessors/Ejs/index')(loaderContext, options); case 'handlebars': - return require('./Preprocessors/Handlebars/index')({ - fs: this.fileSystem, - rootContext: this.rootContext, - options, - }); + return require('./Preprocessors/Handlebars/index')(loaderContext, options); case 'nunjucks': - return require('./Preprocessors/Nunjucks/index')({ - watch: this.watch, - rootContext: this.rootContext, - options, - }); + return require('./Preprocessors/Nunjucks/index')(loaderContext, options, watch); default: unsupportedPreprocessorException(preprocessor); diff --git a/src/Loader/Preprocessors/Ejs/index.js b/src/Loader/Preprocessors/Ejs/index.js index 9d4aa5e8..2f82ea80 100644 --- a/src/Loader/Preprocessors/Ejs/index.js +++ b/src/Loader/Preprocessors/Ejs/index.js @@ -1,7 +1,8 @@ const { loadModule } = require('../../../Common/FileUtils'); -const preprocessor = ({ rootContext, options }) => { +const preprocessor = (loaderContext, options) => { const Ejs = loadModule('ejs'); + const { rootContext } = loaderContext; return (template, { resourcePath, data = {} }) => Ejs.render(template, data, { diff --git a/src/Loader/Preprocessors/Eta/index.js b/src/Loader/Preprocessors/Eta/index.js index 4da0b849..01c9b5db 100644 --- a/src/Loader/Preprocessors/Eta/index.js +++ b/src/Loader/Preprocessors/Eta/index.js @@ -1,8 +1,9 @@ const path = require('path'); const { loadModule } = require('../../../Common/FileUtils'); -const preprocessor = ({ rootContext, options }) => { +const preprocessor = (loaderContext, options) => { const Eta = loadModule('eta', () => require('eta').Eta); + const { rootContext } = loaderContext; let views = options.views; if (!views) { diff --git a/src/Loader/Preprocessors/Handlebars/helpers/assign.js b/src/Loader/Preprocessors/Handlebars/helpers/assign.js new file mode 100644 index 00000000..ffe9f8b6 --- /dev/null +++ b/src/Loader/Preprocessors/Handlebars/helpers/assign.js @@ -0,0 +1,30 @@ +'use strict'; + +/** @typedef {import('handlebars')} Handlebars */ +/** @typedef {import('handlebars').HelperOptions} HelperOptions */ + +/** + * Assign a value to data variable. + * + * Usage: + * {{assign title='About'}} - define `title` variable + * {{title}} - output variable + * + * @param {Handlebars} Handlebars + * @return {function(string, HelperOptions, *): *} + */ +module.exports = (Handlebars) => { + /** + * @param {HelperOptions} options The options passed via tag attributes into a template. + * @return {void} + */ + return function (options) { + // don't modify `this` in code directly, because it will be compiled in `exports` as an immutable object + const context = this; + const attrs = options.hash; + + for (const key in attrs) { + context[key] = attrs[key]; + } + }; +}; diff --git a/src/Loader/Preprocessors/Handlebars/helpers/block/block.js b/src/Loader/Preprocessors/Handlebars/helpers/block/block.js new file mode 100644 index 00000000..fe0b08f0 --- /dev/null +++ b/src/Loader/Preprocessors/Handlebars/helpers/block/block.js @@ -0,0 +1,34 @@ +'use strict'; + +/** @typedef {import('handlebars')} Handlebars */ +/** @typedef {import('handlebars').HelperOptions} HelperOptions */ + +/** + * Insert the partial content as a block. + * Note: `partial` and `block` are paar helpers. + * + * Usage: + * {{#partial 'BLOCK_NAME'}}BLOCK_CONTENT{{/partial}} - define block content + * {{#block 'BLOCK_NAME'}}{{/block}} - output block content + * + * @param {Handlebars} Handlebars + * @return {function(string, HelperOptions, *): *} + */ +module.exports = (Handlebars) => { + /** + * @param {string} name The block name. + * @param {HelperOptions} options The options passed via tag attributes into a template. + * @return {string} + */ + return function (name, options) { + const context = this; + let partial = context._blocks[name] || options.fn; + + if (typeof partial === 'string') { + partial = Handlebars.compile(partial); + context._blocks[name] = partial; + } + + return partial(context, { data: options.hash }); + }; +}; diff --git a/src/Loader/Preprocessors/Handlebars/helpers/block/partial.js b/src/Loader/Preprocessors/Handlebars/helpers/block/partial.js new file mode 100644 index 00000000..36ef8c10 --- /dev/null +++ b/src/Loader/Preprocessors/Handlebars/helpers/block/partial.js @@ -0,0 +1,33 @@ +'use strict'; + +/** @typedef {import('handlebars')} Handlebars */ +/** @typedef {import('handlebars').HelperOptions} HelperOptions */ + +/** + * Save the partial content for a block. + * Note: `partial` and `block` are paar helpers. + * + * Usage: + * {{#partial 'BLOCK_NAME'}}BLOCK_CONTENT{{/partial}} - define block content + * {{#block 'BLOCK_NAME'}}{{/block}} - output block content + * + * @param {Handlebars} Handlebars + * @return {function(string, HelperOptions, *): *} + */ +module.exports = (Handlebars) => { + /** + * @param {string} name The block name. + * @param {HelperOptions} options The options passed via tag attributes into a template. + * @return {void} + */ + return function (name, options) { + // don't modify `this` in code directly, because it will be compiled in `exports` as an immutable object + const context = this; + + if (!context._blocks) { + context._blocks = {}; + } + + context._blocks[name] = options.fn; + }; +}; diff --git a/src/Loader/Preprocessors/Handlebars/helpers/include.js b/src/Loader/Preprocessors/Handlebars/helpers/include.js index d15fde0d..4caa1433 100644 --- a/src/Loader/Preprocessors/Handlebars/helpers/include.js +++ b/src/Loader/Preprocessors/Handlebars/helpers/include.js @@ -1,21 +1,24 @@ const { resolveFile } = require('../../../../Common/FileUtils'); +/** @typedef {import('handlebars')} Handlebars */ +/** @typedef {import('handlebars').HelperOptions} HelperOptions */ + /** * Return the include helper function. * - * @param {FileSystem} fs The file system. * @param {Handlebars} Handlebars The instance of Handlebars module. + * @param {FileSystem} fs The file system. * @param {string} root The root path to template partials. * @param {Array} views The paths of including partials. * @param {Array} extensions The default extensions of including partials. * @return {function(filename: string, options: Object, args: Object): Handlebars.SafeString} */ -module.exports = ({ fs, Handlebars, root, views = [], extensions = [] }) => { +module.exports = ({ Handlebars, fs, root, views = [], extensions = [] }) => { /** * Include the partial file in a template. * * @param {string} filename The partial file name. - * @param {Object} options The options passed via tag attributes into a template. + * @param {HelperOptions} options The options passed via tag attributes into a template. * @param {Object} args The parent options passed using the `this` attribute. * @return {Handlebars.SafeString} */ diff --git a/src/Loader/Preprocessors/Handlebars/index.js b/src/Loader/Preprocessors/Handlebars/index.js index 405078a6..e7906de0 100644 --- a/src/Loader/Preprocessors/Handlebars/index.js +++ b/src/Loader/Preprocessors/Handlebars/index.js @@ -3,8 +3,10 @@ const Dependency = require('../../Dependency'); const { loadModule, readDirRecursiveSync } = require('../../../Common/FileUtils'); const { isWin, pathToPosix } = require('../../../Common/Helpers'); -const preprocessor = ({ fs, rootContext, options }) => { +const preprocessor = (loaderContext, options) => { const Handlebars = loadModule('handlebars'); + const fs = loaderContext.fs.fileSystem; + const { rootContext } = loaderContext; const extensions = ['.html', '.hbs', '.handlebars']; const includeFiles = [/\.(html|hbs|handlebars)$/i]; const root = options?.root || rootContext; @@ -46,7 +48,7 @@ const preprocessor = ({ fs, rootContext, options }) => { /** * Get actual partials. * - * @param {Array|{}} options The partials option. + * @param {Array|{}} options The partial's option. * @return {{}} */ const getPartials = (options) => { @@ -60,7 +62,7 @@ const preprocessor = ({ fs, rootContext, options }) => { /** * Get actual helpers. * - * @param {Array|{}} options The helpers option. + * @param {Array|{}} options The helper's option. * @return {{}} */ const getHelpers = (options) => { @@ -91,8 +93,8 @@ const preprocessor = ({ fs, rootContext, options }) => { partials = actualPartials; // update content of actual partials - for (const partial in partials) { - const partialFile = partials[partial]; + for (const name in partials) { + const partialFile = partials[name]; // watch changes in a file (change/rename) Dependency.addFile(partialFile); @@ -102,7 +104,7 @@ const preprocessor = ({ fs, rootContext, options }) => { } const template = fs.readFileSync(partialFile, 'utf8'); - Handlebars.registerPartial(partial, template); + Handlebars.registerPartial(name, template); } }; @@ -125,8 +127,8 @@ const preprocessor = ({ fs, rootContext, options }) => { helpers = actualHelpers; - for (const helperName in helpers) { - const helperFile = helpers[helperName]; + for (const name in helpers) { + const helperFile = helpers[name]; // watch changes in a file (change/rename) Dependency.addFile(helperFile); @@ -136,32 +138,37 @@ const preprocessor = ({ fs, rootContext, options }) => { } const helper = require(helperFile); - Handlebars.registerHelper(helperName, helper); + Handlebars.registerHelper(name, helper); } }; - // build-in helpers + // first, register build-in helpers const buildInHelpers = { + assign: require('./helpers/assign')(Handlebars), + block: require('./helpers/block/block')(Handlebars), + partial: require('./helpers/block/partial')(Handlebars), + include: require('./helpers/include')({ - fs, Handlebars, + fs, root, views: Array.isArray(views) ? views : [views], extensions, }), }; - for (const helper in buildInHelpers) { - Handlebars.registerHelper(helper, buildInHelpers[helper]); + for (const name in buildInHelpers) { + Handlebars.registerHelper(name, buildInHelpers[name]); } + // seconds, register user helpers, build-in helpers can be overridden with custom helpers if (options.helpers) { if (Array.isArray(options.helpers)) { updateHelpers(); } else { // object of helper name => absolute path to helper file helpers = options.helpers; - for (const helper in helpers) { - Handlebars.registerHelper(helper, helpers[helper]); + for (const name in helpers) { + Handlebars.registerHelper(name, helpers[name]); } } } @@ -173,12 +180,16 @@ const preprocessor = ({ fs, rootContext, options }) => { return { /** * Called to render each template page + * * @param {string} template The template content. * @param {string} resourcePath The request of template. * @param {object} data The data passed into template. + * Note: + * call compiled function with the argument as new object + * to allow defining properties in `this` of some helpers. * @return {string} */ - render: (template, { resourcePath, data = {} }) => Handlebars.compile(template, options)(data), + render: (template, { resourcePath, data = {} }) => Handlebars.compile(template, options)({ ...data }), /** * Called before each new compilation after changes, in the serve/watch mode. diff --git a/src/Loader/Preprocessors/Nunjucks/index.js b/src/Loader/Preprocessors/Nunjucks/index.js index f8d9c5e9..1e29a373 100644 --- a/src/Loader/Preprocessors/Nunjucks/index.js +++ b/src/Loader/Preprocessors/Nunjucks/index.js @@ -1,7 +1,8 @@ const { loadModule } = require('../../../Common/FileUtils'); -const preprocessor = ({ watch, rootContext, options = {} }) => { +const preprocessor = (loaderContext, options = {}, watch) => { const Nunjucks = loadModule('nunjucks'); + const { rootContext } = loaderContext; const templatesPath = options.views || rootContext; if (watch === true) { diff --git a/test/cases/loader-option-preprocessor-handlebars-helpers-path/expected/index.html b/test/cases/loader-option-preprocessor-handlebars-helpers-path/expected/index.html index 87383034..265d7e47 100644 --- a/test/cases/loader-option-preprocessor-handlebars-helpers-path/expected/index.html +++ b/test/cases/loader-option-preprocessor-handlebars-helpers-path/expected/index.html @@ -6,7 +6,7 @@

Hello World

italic - + span
footer
diff --git a/test/cases/loader-option-preprocessor-handlebars-helpers-path/src/views/pages/home.hbs b/test/cases/loader-option-preprocessor-handlebars-helpers-path/src/views/pages/home.hbs index ee23f2cc..3b242d8d 100644 --- a/test/cases/loader-option-preprocessor-handlebars-helpers-path/src/views/pages/home.hbs +++ b/test/cases/loader-option-preprocessor-handlebars-helpers-path/src/views/pages/home.hbs @@ -6,8 +6,8 @@

{{#bold}}Hello World{{/bold}}

{{#italic}}italic{{/italic}} - + {{#[wrapper/span]}}span{{/[wrapper/span]}} - {{ include '/src/views/includes/footer.html' }} + {{include '/src/views/includes/footer.html'}} diff --git a/test/cases/preprocessor-handlebars-helper-block-buildIn/.editorconfig b/test/cases/preprocessor-handlebars-helper-block-buildIn/.editorconfig new file mode 100644 index 00000000..3280e6bd --- /dev/null +++ b/test/cases/preprocessor-handlebars-helper-block-buildIn/.editorconfig @@ -0,0 +1,7 @@ +root = true + +[*.html] +insert_final_newline = true + +[*.hbs] +insert_final_newline = true diff --git a/test/cases/preprocessor-handlebars-helper-block-buildIn/expected/about.html b/test/cases/preprocessor-handlebars-helper-block-buildIn/expected/about.html new file mode 100644 index 00000000..b1cf8462 --- /dev/null +++ b/test/cases/preprocessor-handlebars-helper-block-buildIn/expected/about.html @@ -0,0 +1,24 @@ + + + + + + + About + + +

ABOUT header

+ + + + +
ABOUT content
+ + +
one
+
two
+
ABOUT block content
+ +
LAYOUT footer
+ + diff --git a/test/cases/preprocessor-handlebars-helper-block-buildIn/expected/index.html b/test/cases/preprocessor-handlebars-helper-block-buildIn/expected/index.html new file mode 100644 index 00000000..91151309 --- /dev/null +++ b/test/cases/preprocessor-handlebars-helper-block-buildIn/expected/index.html @@ -0,0 +1,24 @@ + + + + + + + Homepage + + +

HOME header

+ + + + +
HOME content
+ + +
βœ…one
+
βœ…two
+
LAYOUT default block content
+ +
LAYOUT footer
+ + diff --git a/test/cases/preprocessor-handlebars-helper-block-buildIn/src/views/pages/about/content.hbs b/test/cases/preprocessor-handlebars-helper-block-buildIn/src/views/pages/about/content.hbs new file mode 100644 index 00000000..a50e4d66 --- /dev/null +++ b/test/cases/preprocessor-handlebars-helper-block-buildIn/src/views/pages/about/content.hbs @@ -0,0 +1 @@ +
ABOUT content
diff --git a/test/cases/preprocessor-handlebars-helper-block-buildIn/src/views/pages/about/index.hbs b/test/cases/preprocessor-handlebars-helper-block-buildIn/src/views/pages/about/index.hbs new file mode 100644 index 00000000..61843b0c --- /dev/null +++ b/test/cases/preprocessor-handlebars-helper-block-buildIn/src/views/pages/about/index.hbs @@ -0,0 +1,11 @@ +{{#partial 'header'}} +

ABOUT header

+{{/partial}} + +{{#partial 'content'}} + {{> about/content}} +{{/partial}} + +{{#partial 'default_block'}}ABOUT block content{{/partial}} + +{{> layout}} diff --git a/test/cases/preprocessor-handlebars-helper-block-buildIn/src/views/pages/home/content.hbs b/test/cases/preprocessor-handlebars-helper-block-buildIn/src/views/pages/home/content.hbs new file mode 100644 index 00000000..3afcd9eb --- /dev/null +++ b/test/cases/preprocessor-handlebars-helper-block-buildIn/src/views/pages/home/content.hbs @@ -0,0 +1 @@ +
HOME content
diff --git a/test/cases/preprocessor-handlebars-helper-block-buildIn/src/views/pages/home/index.hbs b/test/cases/preprocessor-handlebars-helper-block-buildIn/src/views/pages/home/index.hbs new file mode 100644 index 00000000..33d0df6f --- /dev/null +++ b/test/cases/preprocessor-handlebars-helper-block-buildIn/src/views/pages/home/index.hbs @@ -0,0 +1,11 @@ +{{#partial 'header'}} +

HOME header

+{{/partial}} + +{{#partial 'content'}} + {{> home/content}} +{{/partial}} + +{{#partial 'icon_check'}}βœ…{{/partial}} + +{{> layout}} diff --git a/test/cases/preprocessor-handlebars-helper-block-buildIn/src/views/partials/layout.hbs b/test/cases/preprocessor-handlebars-helper-block-buildIn/src/views/partials/layout.hbs new file mode 100644 index 00000000..47f96687 --- /dev/null +++ b/test/cases/preprocessor-handlebars-helper-block-buildIn/src/views/partials/layout.hbs @@ -0,0 +1,18 @@ + + + {{title}} + + + {{#block 'header'}}{{/block}} + + + + {{#block 'content'}}{{/block}} + +
{{#block 'icon_check'}}{{/block}}one
+
{{#block 'icon_check'}}{{/block}}two
+
{{#block 'default_block'}}LAYOUT default block content{{/block}}
+ +
LAYOUT footer
+ + diff --git a/test/cases/preprocessor-handlebars-helper-block-buildIn/tsconfig.json b/test/cases/preprocessor-handlebars-helper-block-buildIn/tsconfig.json new file mode 100644 index 00000000..fb784617 --- /dev/null +++ b/test/cases/preprocessor-handlebars-helper-block-buildIn/tsconfig.json @@ -0,0 +1,37 @@ +{ + "include": ["*env.d.ts", "src/**/*", "./typings/**/*"], + "exclude": ["node_modules"], + "compilerOptions": { + "outDir": "lib", + "module": "CommonJS", + "moduleResolution": "Node", + "target": "ES6", + "lib": ["ES6", "DOM"], + "jsx": "preserve", + "allowJs": false, + "checkJs": false, + "importHelpers": false, + "esModuleInterop": true, + "allowSyntheticDefaultImports": true, + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noImplicitReturns": true, + "noFallthroughCasesInSwitch": true, + "skipLibCheck": true, + "resolveJsonModule": true, + "declaration": true, + "declarationMap": true, + "sourceMap": true, + "noEmit": false, + "forceConsistentCasingInFileNames": true, + "isolatedModules": true, + "incremental": false, + "strictNullChecks": true, + "removeComments": true, + "preserveConstEnums": true, + "alwaysStrict": true, + "noImplicitAny": true, + "noImplicitThis": true + } +} diff --git a/test/cases/preprocessor-handlebars-helper-block-buildIn/webpack.config.js b/test/cases/preprocessor-handlebars-helper-block-buildIn/webpack.config.js new file mode 100644 index 00000000..20999dbe --- /dev/null +++ b/test/cases/preprocessor-handlebars-helper-block-buildIn/webpack.config.js @@ -0,0 +1,29 @@ +const path = require('path'); +const HtmlBundlerPlugin = require('../../../'); + +const config = { + mode: 'production', + output: { + path: path.resolve(__dirname, './dist'), + }, + plugins: [ + new HtmlBundlerPlugin({ + entry: { + index: { + import: './src/views/pages/home/index.hbs', + data: { title: 'Homepage' }, + }, + about: { + import: './src/views/pages/about/index.hbs', + data: { title: 'About' }, + }, + }, + preprocessor: 'handlebars', + preprocessorOptions: { + partials: [path.join(__dirname, 'src/views/pages/'), path.join(__dirname, 'src/views/partials/')], + }, + }), + ], +}; + +module.exports = config; diff --git a/test/cases/preprocessor-handlebars-helper-block/.editorconfig b/test/cases/preprocessor-handlebars-helper-block/.editorconfig new file mode 100644 index 00000000..3280e6bd --- /dev/null +++ b/test/cases/preprocessor-handlebars-helper-block/.editorconfig @@ -0,0 +1,7 @@ +root = true + +[*.html] +insert_final_newline = true + +[*.hbs] +insert_final_newline = true diff --git a/test/cases/preprocessor-handlebars-helper-block/expected/about.html b/test/cases/preprocessor-handlebars-helper-block/expected/about.html new file mode 100644 index 00000000..b1cf8462 --- /dev/null +++ b/test/cases/preprocessor-handlebars-helper-block/expected/about.html @@ -0,0 +1,24 @@ + + + + + + + About + + +

ABOUT header

+ + + + +
ABOUT content
+ + +
one
+
two
+
ABOUT block content
+ +
LAYOUT footer
+ + diff --git a/test/cases/preprocessor-handlebars-helper-block/expected/index.html b/test/cases/preprocessor-handlebars-helper-block/expected/index.html new file mode 100644 index 00000000..91151309 --- /dev/null +++ b/test/cases/preprocessor-handlebars-helper-block/expected/index.html @@ -0,0 +1,24 @@ + + + + + + + Homepage + + +

HOME header

+ + + + +
HOME content
+ + +
βœ…one
+
βœ…two
+
LAYOUT default block content
+ +
LAYOUT footer
+ + diff --git a/test/cases/preprocessor-handlebars-helper-block/src/views/helpers/block.js b/test/cases/preprocessor-handlebars-helper-block/src/views/helpers/block.js new file mode 100644 index 00000000..1feb52bd --- /dev/null +++ b/test/cases/preprocessor-handlebars-helper-block/src/views/helpers/block.js @@ -0,0 +1,29 @@ +'use strict'; + +const Handlebars = require('handlebars'); + +/** @typedef {import('handlebars').HelperOptions} HelperOptions */ + +/** + * Insert the partial content as a block. + * Note: `partial` and `block` are paar helpers. + * + * Usage: + * {{#partial 'BLOCK_NAME'}}BLOCK_CONTENT{{/partial}} - define block content + * {{#block 'BLOCK_NAME'}}{{/block}} - output block content + * + * @param {string} name The block name. + * @param {HelperOptions} options The options passed via tag attributes into a template. + * @return {string} + */ +module.exports = function (name, options) { + const context = this; + let partial = context._blocks[name] || options.fn; + + if (typeof partial === 'string') { + partial = Handlebars.compile(partial); + context._blocks[name] = partial; + } + + return partial(context, { data: options.hash }); +}; diff --git a/test/cases/preprocessor-handlebars-helper-block/src/views/helpers/partial.js b/test/cases/preprocessor-handlebars-helper-block/src/views/helpers/partial.js new file mode 100644 index 00000000..7a37149a --- /dev/null +++ b/test/cases/preprocessor-handlebars-helper-block/src/views/helpers/partial.js @@ -0,0 +1,26 @@ +'use strict'; + +/** @typedef {import('handlebars').HelperOptions} HelperOptions */ + +/** + * Save the partial content for a block. + * Note: `partial` and `block` are paar helpers. + * + * Usage: + * {{#partial 'BLOCK_NAME'}}BLOCK_CONTENT{{/partial}} - define block content + * {{#block 'BLOCK_NAME'}}{{/block}} - output block content + * + * @param {string} name The block name. + * @param {HelperOptions} options The options passed via tag attributes into a template. + * @return {void} + */ +module.exports = function (name, options) { + // don't modify `this` in code directly, because it will be compiled in `exports` as an immutable object + const context = this; + + if (!context._blocks) { + context._blocks = {}; + } + + context._blocks[name] = options.fn; +}; diff --git a/test/cases/preprocessor-handlebars-helper-block/src/views/pages/about/content.hbs b/test/cases/preprocessor-handlebars-helper-block/src/views/pages/about/content.hbs new file mode 100644 index 00000000..a50e4d66 --- /dev/null +++ b/test/cases/preprocessor-handlebars-helper-block/src/views/pages/about/content.hbs @@ -0,0 +1 @@ +
ABOUT content
diff --git a/test/cases/preprocessor-handlebars-helper-block/src/views/pages/about/index.hbs b/test/cases/preprocessor-handlebars-helper-block/src/views/pages/about/index.hbs new file mode 100644 index 00000000..61843b0c --- /dev/null +++ b/test/cases/preprocessor-handlebars-helper-block/src/views/pages/about/index.hbs @@ -0,0 +1,11 @@ +{{#partial 'header'}} +

ABOUT header

+{{/partial}} + +{{#partial 'content'}} + {{> about/content}} +{{/partial}} + +{{#partial 'default_block'}}ABOUT block content{{/partial}} + +{{> layout}} diff --git a/test/cases/preprocessor-handlebars-helper-block/src/views/pages/home/content.hbs b/test/cases/preprocessor-handlebars-helper-block/src/views/pages/home/content.hbs new file mode 100644 index 00000000..3afcd9eb --- /dev/null +++ b/test/cases/preprocessor-handlebars-helper-block/src/views/pages/home/content.hbs @@ -0,0 +1 @@ +
HOME content
diff --git a/test/cases/preprocessor-handlebars-helper-block/src/views/pages/home/index.hbs b/test/cases/preprocessor-handlebars-helper-block/src/views/pages/home/index.hbs new file mode 100644 index 00000000..33d0df6f --- /dev/null +++ b/test/cases/preprocessor-handlebars-helper-block/src/views/pages/home/index.hbs @@ -0,0 +1,11 @@ +{{#partial 'header'}} +

HOME header

+{{/partial}} + +{{#partial 'content'}} + {{> home/content}} +{{/partial}} + +{{#partial 'icon_check'}}βœ…{{/partial}} + +{{> layout}} diff --git a/test/cases/preprocessor-handlebars-helper-block/src/views/partials/layout.hbs b/test/cases/preprocessor-handlebars-helper-block/src/views/partials/layout.hbs new file mode 100644 index 00000000..47f96687 --- /dev/null +++ b/test/cases/preprocessor-handlebars-helper-block/src/views/partials/layout.hbs @@ -0,0 +1,18 @@ + + + {{title}} + + + {{#block 'header'}}{{/block}} + + + + {{#block 'content'}}{{/block}} + +
{{#block 'icon_check'}}{{/block}}one
+
{{#block 'icon_check'}}{{/block}}two
+
{{#block 'default_block'}}LAYOUT default block content{{/block}}
+ +
LAYOUT footer
+ + diff --git a/test/cases/preprocessor-handlebars-helper-block/tsconfig.json b/test/cases/preprocessor-handlebars-helper-block/tsconfig.json new file mode 100644 index 00000000..fb784617 --- /dev/null +++ b/test/cases/preprocessor-handlebars-helper-block/tsconfig.json @@ -0,0 +1,37 @@ +{ + "include": ["*env.d.ts", "src/**/*", "./typings/**/*"], + "exclude": ["node_modules"], + "compilerOptions": { + "outDir": "lib", + "module": "CommonJS", + "moduleResolution": "Node", + "target": "ES6", + "lib": ["ES6", "DOM"], + "jsx": "preserve", + "allowJs": false, + "checkJs": false, + "importHelpers": false, + "esModuleInterop": true, + "allowSyntheticDefaultImports": true, + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noImplicitReturns": true, + "noFallthroughCasesInSwitch": true, + "skipLibCheck": true, + "resolveJsonModule": true, + "declaration": true, + "declarationMap": true, + "sourceMap": true, + "noEmit": false, + "forceConsistentCasingInFileNames": true, + "isolatedModules": true, + "incremental": false, + "strictNullChecks": true, + "removeComments": true, + "preserveConstEnums": true, + "alwaysStrict": true, + "noImplicitAny": true, + "noImplicitThis": true + } +} diff --git a/test/cases/preprocessor-handlebars-helper-block/webpack.config.js b/test/cases/preprocessor-handlebars-helper-block/webpack.config.js new file mode 100644 index 00000000..f48f23c5 --- /dev/null +++ b/test/cases/preprocessor-handlebars-helper-block/webpack.config.js @@ -0,0 +1,32 @@ +const path = require('path'); +const HtmlBundlerPlugin = require('../../../'); + +const config = { + mode: 'production', + output: { + path: path.resolve(__dirname, './dist'), + }, + plugins: [ + new HtmlBundlerPlugin({ + entry: { + index: { + import: './src/views/pages/home/index.hbs', + data: { title: 'Homepage' }, + }, + about: { + import: './src/views/pages/about/index.hbs', + data: { title: 'About' }, + }, + }, + + preprocessor: 'handlebars', + preprocessorOptions: { + // test override build-in plugin helpers with own helper + helpers: [path.join(__dirname, 'src/views/helpers')], + partials: [path.join(__dirname, 'src/views/pages/'), path.join(__dirname, 'src/views/partials/')], + }, + }), + ], +}; + +module.exports = config; diff --git a/test/integration.test.js b/test/integration.test.js index daccc8a5..cfcdea95 100644 --- a/test/integration.test.js +++ b/test/integration.test.js @@ -1,5 +1,5 @@ import { compareFiles } from './utils/helpers'; -import { removeDirsSync } from './utils/file'; +//import { removeDirsSync } from './utils/file'; // Remove all 'dist/' directories from tests, use it only for some local tests. //removeDirsSync(__dirname, /dist$/); @@ -159,6 +159,12 @@ describe('loader preprocessor options', () => { test('multiple templating engines', () => compareFiles('loader-option-preprocessor-many-ejs-hbs')); }); +describe('handlebars', () => { + // test helpers + test('build-in `block` helper', () => compareFiles('preprocessor-handlebars-helper-block-buildIn')); + test('user `block` helper overrides build-in', () => compareFiles('preprocessor-handlebars-helper-block')); +}); + describe('inline images', () => { test('inline-asset-bypass-data-url', () => compareFiles('inline-asset-bypass-data-url')); test('inline-asset-decide-size', () => compareFiles('inline-asset-decide-size')); diff --git a/test/jest.config.js b/test/jest.config.js index c1caff2f..2137c806 100644 --- a/test/jest.config.js +++ b/test/jest.config.js @@ -206,7 +206,7 @@ module.exports = { // testSequencer: '@jest/test-sequencer', // Default timeout of a test in milliseconds. - testTimeout: isLocalEnv ? 2000 : 5000, + testTimeout: isLocalEnv ? 2000 : 8000, // This option sets the URL for the jsdom environment. It is reflected in properties such as location.href // testURL: "http://localhost", diff --git a/test/utils/helpers.js b/test/utils/helpers.js index 9b77bd0c..ba94f018 100644 --- a/test/utils/helpers.js +++ b/test/utils/helpers.js @@ -33,9 +33,10 @@ export const getCompareFileContents = function (receivedFile, expectedFile, filt * Compare the file list and content of files. * * @param {string} relTestCasePath The relative path to the test directory. + * @param {boolean} compareContent Whether the content of files should be compared too. * @return {Promise} */ -export const compareFiles = (relTestCasePath) => { +export const compareFiles = (relTestCasePath, compareContent = true) => { const absTestPath = path.join(PATHS.testSource, relTestCasePath), webRootPath = path.join(absTestPath, PATHS.webRoot), expectedPath = path.join(absTestPath, PATHS.expected); @@ -46,13 +47,16 @@ export const compareFiles = (relTestCasePath) => { const { received: receivedFiles, expected: expectedFiles } = getCompareFileList(webRootPath, expectedPath); expect(receivedFiles).toEqual(expectedFiles); - expectedFiles.forEach((file) => { - const { received, expected } = getCompareFileContents( - path.join(webRootPath, file), - path.join(expectedPath, file) - ); - expect(received).toEqual(expected); - }); + if (compareContent) { + expectedFiles.forEach((file) => { + const { received, expected } = getCompareFileContents( + path.join(webRootPath, file), + path.join(expectedPath, file) + ); + expect(received).toEqual(expected); + }); + } + return Promise.resolve(true); }) .catch((error) => { diff --git a/test/utils/webpack.js b/test/utils/webpack.js index d03f8e9f..60346578 100644 --- a/test/utils/webpack.js +++ b/test/utils/webpack.js @@ -23,11 +23,27 @@ const prepareWebpackConfig = (PATHS, relTestCasePath, webpackOpts = {}) => { context: testPath, }; + if (Array.isArray(testConfig)) { + const finalConfig = []; + + testConfig.forEach((config) => { + const commonConfig = require(commonConfigFile); + + // remove module rules in common config when custom rules are defined by test config or options + if ((webpackOpts.module && webpackOpts.module.rules) || (config.module && config.module.rules)) { + commonConfig.module.rules = []; + } + + finalConfig.push(merge(baseConfig, commonConfig, webpackOpts, config)); + }); + + return finalConfig; + } + // remove module rules in common config when custom rules are defined by test config or options if ((webpackOpts.module && webpackOpts.module.rules) || (testConfig.module && testConfig.module.rules)) { commonConfig.module.rules = []; } - return merge(baseConfig, commonConfig, webpackOpts, testConfig); };