Skip to content

Commit 896f382

Browse files
committed
feat: add support for the template function on the client-side for ejs
1 parent d278fce commit 896f382

53 files changed

Lines changed: 453 additions & 194 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

CHANGELOG.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,11 @@
11
# Change log
22

3+
## 3.4.0 (2023-12-03)
4+
5+
- feat: add support for the template function on the client-side for `ejs`
6+
- docs: update readme
7+
- test: add tests for compile mode
8+
39
## 3.3.0 (2023-11-29)
410

511
- feat: add support for the template function on the client-side for `eta`

README.md

Lines changed: 19 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -4096,14 +4096,21 @@ The [preprocessor](#loader-option-preprocessor) works in two modes: `render` and
40964096

40974097
### Render mode
40984098

4099-
The `render` is default mode for a template defined in the [entry](#option-entry) option.
4100-
The processed template in `render` mode is HTML string, which is saved as HTML file.
4101-
You can import a template file as generated HTML string in JS with the `?render` query.
4099+
The `render` is the default mode for the template defined in the [entry](#option-entry) option.
4100+
The rendered template is an HTML string, which is saved as an HTML file.
4101+
4102+
You can import the template file as a generated HTML string in JS using the `?render` query.
4103+
To pass simple variables into the imported template you can use query parameters, e.g.: `?render&name=Arnold&age=25`.
4104+
To pass complex variables such as an array or an object use the global [data](#option-data) option.
4105+
4106+
> **Note**
4107+
>
4108+
> At runtime in JavaScript will be used the already rendered HTML from the template.
41024109

41034110
For example:
41044111

41054112
```js
4106-
import html from './partials/star-button.html?render';
4113+
import html from './partials/star-button.html?render&buttonText=Star';
41074114
41084115
document.getElementById('star-button').innerHTML = html;
41094116
```
@@ -4114,15 +4121,16 @@ _./partials/star-button.html_
41144121
<!-- you can use a source image file with webpack alias,
41154122
in the bundle it will be auto replaced with the output asset filename -->
41164123
<img src="@images/star.svg">
4117-
<span>Star</span>
4124+
<!-- the `buttonText` variable is passed via query -->
4125+
<span><%= buttonText %></span>
41184126
</button>
41194127
```
41204128

41214129
### Compile mode
41224130

4123-
The `compile` is default mode for a template imported in JavaScript file.
4124-
The processed template in `compile` mode is a template function,
4125-
which can be executed with passed variables in the runtime on client-side.
4131+
The `compile` is the default mode for the template imported in JavaScript file.
4132+
The compiled template is a template function,
4133+
which can be executed with passed variables in the runtime on the client-side in the browser.
41264134

41274135
For example:
41284136

@@ -4159,10 +4167,10 @@ _./partials/people.ejs_
41594167
41604168
#### Template engines that do support the `template function` on client-side
41614169

4162-
- [eta](#loader-option-preprocessor-options-eta) - generates a fast small template function with runtime (~3KB) (**recommended**)\
4170+
- [eta](#loader-option-preprocessor-options-eta) - generates a template function with runtime (~3KB)\
4171+
`include` is supported
4172+
- [ejs](#loader-option-preprocessor-options-ejs) - generates a fast smallest pure template function w/o runtime (**recommended** for use on client-side)\
41634173
`include` is supported
4164-
- [ejs](#loader-option-preprocessor-options-ejs) - generates a fast small pure template function w/o runtime\
4165-
`include` is NOT supported (yet)
41664174
- [handlebars](#loader-option-preprocessor-options-handlebars) - generates a precompiled template with runtime (~28KB)\
41674175
`include` is NOT supported (yet)
41684176
- [nunjucks](#loader-option-preprocessor-options-nunjucks) - generates a precompiled template with runtime (~41KB)\

images/plugin-logo.pxd

169 KB
Binary file not shown.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "html-bundler-webpack-plugin",
3-
"version": "3.3.0",
3+
"version": "3.4.0",
44
"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.",
55
"keywords": [
66
"html",

src/Loader/Loader.js

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -13,9 +13,7 @@ class Loader {
1313
*/
1414
static init(loaderContext) {
1515
const { rootContext, hot } = loaderContext;
16-
const { preprocessor, preprocessorMode, data, esModule, self: useSelf } = Option.get();
17-
18-
//this.data = data;
16+
const { preprocessor, preprocessorMode, esModule, self: useSelf } = Option.get();
1917

2018
// prevent double initialization with same options, it occurs when many entry files used in one webpack config
2119
if (!PluginService.isCached(rootContext)) {
@@ -56,12 +54,12 @@ class Loader {
5654
/**
5755
* Export generated result.
5856
*
59-
* @param {string} source
57+
* @param {string} content
6058
* @param {BundlerPluginLoaderContext} loaderContext
6159
* @return {string}
6260
*/
63-
static export(source, loaderContext) {
64-
return this.compiler.export(source, loaderContext);
61+
static export(content, loaderContext) {
62+
return this.compiler.export(content, loaderContext);
6563
}
6664

6765
/**

src/Loader/Modes/Render.js

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -43,11 +43,12 @@ class Render extends PreprocessorMode {
4343
* Export template code with rendered HTML.
4444
*
4545
* @param {string} content The template content.
46-
* @param {{}} data The object with variables passed in template.
47-
* @param {string} issuer The issuer of the template file.
46+
* @param {BundlerPluginLoaderContext} loaderContext
4847
* @return {string}
4948
*/
50-
export(content, { data, resource: issuer }) {
49+
export(content, loaderContext) {
50+
const { resource: issuer } = loaderContext;
51+
5152
/* istanbul ignore next: Webpack API no provide `loaderContext.hot` for testing */
5253
if (this.hot && PluginService.useHotUpdate()) {
5354
content = this.injectHotScript(content);

src/Loader/Option.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -119,7 +119,7 @@ class Option {
119119

120120
// watch only files matched to RegExps,
121121
// if empty then watch all files, except ignored
122-
files: [PluginService.getOptions().getFilterRegexp()],
122+
files: PluginService.getOptions().getEntryTest(),
123123

124124
// ignore paths and files matched to RegExps
125125
ignore: [

src/Loader/Preprocessor.js

Lines changed: 58 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@ const { unsupportedPreprocessorException } = require('./Messages/Exeptions');
33
/** @typedef {import('webpack').LoaderContext} LoaderContext */
44

55
class Preprocessor {
6+
static cache = new Map();
7+
68
/**
79
* Whether the preprocessor is used.
810
*
@@ -16,11 +18,60 @@ class Preprocessor {
1618
}
1719

1820
/**
19-
* Returns preprocessor to render/compile a template.
20-
* The default preprocessor uses the Eta templating engine.
21+
* Load preprocessor module.
22+
* The default preprocessor uses the Eta template engine.
23+
*
24+
* @param {string|function|boolean|undefined} preprocessor
25+
* @return {*} TODO: add jsdoc for the preprocessor module type
26+
* @throws
27+
*/
28+
static load(preprocessor) {
29+
const preprocessorDirs = {
30+
eta: 'Eta',
31+
ejs: 'Ejs',
32+
handlebars: 'Handlebars',
33+
nunjucks: 'Nunjucks',
34+
twig: 'Twig',
35+
};
36+
37+
// disabled preprocessor
38+
if (preprocessor === false) return null;
39+
40+
// default preprocessor is Eta
41+
if (preprocessor == null) preprocessor = 'eta';
42+
43+
if (this.cache.has(preprocessor)) {
44+
return this.cache.get(preprocessor);
45+
}
46+
47+
let dirname = preprocessorDirs[preprocessor];
48+
49+
if (dirname) {
50+
// we are sure the file exists
51+
const module = require(`./Preprocessors/${dirname}/index.js`);
52+
this.cache.set(preprocessor, module);
53+
54+
return module;
55+
}
56+
57+
unsupportedPreprocessorException(preprocessor);
58+
}
59+
60+
/**
61+
* @param {*} preprocessor
62+
* @return {RegExp|null}
63+
*/
64+
static getTest(preprocessor) {
65+
const module = typeof preprocessor === 'function' ? preprocessor : this.load(preprocessor);
66+
67+
return module?.test;
68+
}
69+
70+
/**
71+
* Returns a preprocessor method to render/compile a template.
2172
*
2273
* @param {Object} options The loader options.
23-
* @return {null|(function(string, {data?: {}}): Promise|null)}
74+
* @return {null|(function(string, loaderContext: BundlerPluginLoaderContext): Promise|null)}
2475
*/
2576
static getPreprocessor(options) {
2677
const { preprocessor, preprocessorMode } = options;
@@ -36,37 +87,17 @@ class Preprocessor {
3687

3788
/**
3889
* Factory preprocessor as a function.
39-
* The default preprocessor uses the Eta template engine.
4090
*
41-
* @param {LoaderContext} loaderContext The loader context of Webpack.
91+
* @param {BundlerPluginLoaderContext} loaderContext The loader context of Webpack.
4292
* @param {string|null|*} preprocessor The preprocessor value, should be a string
4393
* @param {Object} options The preprocessor options.
44-
* @param {Function} watch The function called by watching.
94+
* @param {boolean} watch Whether is serve/watch mode.
4595
* @return {Function|Promise|Object}
46-
* @throws
4796
*/
4897
static factory(loaderContext, { preprocessor, options = {}, watch }) {
49-
if (preprocessor == null) preprocessor = 'eta';
50-
51-
switch (preprocessor) {
52-
case 'eta':
53-
return require('./Preprocessors/Eta/index.js')(loaderContext, options);
54-
55-
case 'ejs':
56-
return require('./Preprocessors/Ejs/index.js')(loaderContext, options);
98+
const module = this.load(preprocessor);
5799

58-
case 'handlebars':
59-
return require('./Preprocessors/Handlebars/index.js')(loaderContext, options);
60-
61-
case 'nunjucks':
62-
return require('./Preprocessors/Nunjucks/index.js')(loaderContext, options, watch);
63-
64-
case 'twig':
65-
return require('./Preprocessors/Twig/index.js')(loaderContext, options);
66-
67-
default:
68-
unsupportedPreprocessorException(preprocessor);
69-
}
100+
return module(loaderContext, options, watch);
70101
}
71102

72103
static watchRun({ preprocessor }) {
Lines changed: 27 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,17 @@
11
const { loadModule } = require('../../../Common/FileUtils');
22
const { stringifyData } = require('../../Utils');
33

4+
// replace the partial file and data to load nested included template via the Webpack loader
5+
// include("./file.html") => require("./file.eta")({...locals, ...{}})
6+
// include('./file.html', { name: 'Siri' }) => require('./file.eta')({...locals, ...{name: 'Siri'}})
7+
const includeRegexp = /include\((.+?)(?:\)|,\s*{(.+?)}\))/g;
8+
49
/**
5-
* Transform the raw template to template function or HTML.
10+
* Transform the raw template source to a template function or HTML.
611
*
7-
* @param {{}} loaderContext
12+
* @param {BundlerPluginLoaderContext} loaderContext
813
* @param {{}} options
9-
* @return {function(template: string, {resourcePath: string, data?: {}}, {preprocessorMode: string}): string}
14+
* @return {{compile: (function(string, {resourcePath: string, data?: {}}): *), render: (function(string, {resourcePath: string, data?: {}}): *), export: (function(string, {data: {}}): string)}}
1015
*/
1116
const preprocessor = (loaderContext, options) => {
1217
const Ejs = loadModule('ejs');
@@ -22,38 +27,36 @@ const preprocessor = (loaderContext, options) => {
2227
* @param {{}} data
2328
* @return {string}
2429
*/
25-
render: (source, { resourcePath, data = {} }) =>
26-
Ejs.render(source, data, {
30+
render(source, { resourcePath, data = {} }) {
31+
return Ejs.render(source, data, {
2732
async: false,
2833
root: rootContext, // root path for includes with an absolute path (e.g., /file.html)
2934
...options,
3035
filename: resourcePath, // allow including a partial relative to the template
31-
}),
36+
});
37+
},
3238

3339
/**
3440
* Compile template into template function.
3541
* Called when a template is loaded in JS in `compile` mode.
3642
*
37-
* TODO: add support for the `include`
38-
*
3943
* @param {string} source The template source code.
4044
* @param {string} resourcePath
4145
* @param {{}} data
4246
* @return {string}
4347
*/
44-
compile: (source, { resourcePath, data = {} }) => {
45-
let templateFunction = Ejs.compile(source, {
46-
client: true,
48+
compile(source, { resourcePath, data = {} }) {
49+
return Ejs.compile(source, {
4750
compileDebug: false,
48-
root: rootContext, // root path for includes with an absolute path (e.g., /file.html)
51+
root: rootContext,
4952
...options,
50-
async: false, // for client is used the sync function
53+
client: true,
54+
async: false,
5155
filename: resourcePath, // allow including a partial relative to the template
52-
}).toString();
53-
54-
return templateFunction
55-
.replace(`var __output = "";`, 'locals = Object.assign(__data__, locals); var __output = "";')
56-
.replaceAll('include(', 'require(');
56+
context: data,
57+
})
58+
.toString()
59+
.replaceAll(includeRegexp, `require($1)({...locals, ...{$2}})`);
5760
},
5861

5962
/**
@@ -64,14 +67,18 @@ const preprocessor = (loaderContext, options) => {
6467
* @param {{}} data The object with variables passed in template.
6568
* @return {string} The exported template function.
6669
*/
67-
export: (templateFunction, { data }) => {
70+
export(templateFunction, { data }) {
6871
// the name of template function in generated code
6972
const exportFunction = 'anonymous';
7073
const exportCode = 'module.exports=';
7174

72-
return `var __data__ = ${stringifyData(data)};` + templateFunction + `;${exportCode}${exportFunction};`;
75+
return `${templateFunction};
76+
var __data__ = ${stringifyData(data)};
77+
var template = (context) => ${exportFunction}(Object.assign(__data__, context));
78+
${exportCode}template;`;
7379
},
7480
};
7581
};
7682

7783
module.exports = preprocessor;
84+
module.exports.test = /\.(html|ejs)$/;

0 commit comments

Comments
 (0)