Skip to content

Commit d278fce

Browse files
committed
feat: add support for the template function on the client-side for eta
1 parent de779b8 commit d278fce

22 files changed

Lines changed: 163 additions & 154 deletions

File tree

CHANGELOG.md

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

3+
## 3.3.0 (2023-11-29)
4+
5+
- feat: add support for the template function on the client-side for `eta`
6+
37
## 3.2.0 (2023-11-28)
48

59
- feat: add Twig preprocessor. Now you can use "best of the best" template engine. Enjoy ;-)

README.md

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4159,7 +4159,9 @@ _./partials/people.ejs_
41594159
41604160
#### Template engines that do support the `template function` on client-side
41614161

4162-
- [ejs](#loader-option-preprocessor-options-ejs) - generates a fast small pure template function w/o runtime (**recommended**)\
4162+
- [eta](#loader-option-preprocessor-options-eta) - generates a fast small template function with runtime (~3KB) (**recommended**)\
4163+
`include` is supported
4164+
- [ejs](#loader-option-preprocessor-options-ejs) - generates a fast small pure template function w/o runtime\
41634165
`include` is NOT supported (yet)
41644166
- [handlebars](#loader-option-preprocessor-options-handlebars) - generates a precompiled template with runtime (~28KB)\
41654167
`include` is NOT supported (yet)
@@ -4171,7 +4173,6 @@ _./partials/people.ejs_
41714173

41724174
#### Template engines that do NOT support the `template function` on client-side
41734175

4174-
- [eta](#loader-option-preprocessor-options-eta) - use the `ejs` instead
41754176
- LiquidJS
41764177

41774178
---

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.2.0",
3+
"version": "3.3.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: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -9,13 +9,13 @@ class Loader {
99
static compiler = null;
1010

1111
/**
12-
* @param {Object} loaderContext
12+
* @param {BundlerPluginLoaderContext} loaderContext
1313
*/
1414
static init(loaderContext) {
1515
const { rootContext, hot } = loaderContext;
1616
const { preprocessor, preprocessorMode, data, esModule, self: useSelf } = Option.get();
1717

18-
this.data = data;
18+
//this.data = data;
1919

2020
// prevent double initialization with same options, it occurs when many entry files used in one webpack config
2121
if (!PluginService.isCached(rootContext)) {
@@ -57,11 +57,11 @@ class Loader {
5757
* Export generated result.
5858
*
5959
* @param {string} source
60-
* @param {string} issuer
60+
* @param {BundlerPluginLoaderContext} loaderContext
6161
* @return {string}
6262
*/
63-
static export(source, issuer) {
64-
return this.compiler.export(source, this.data, issuer);
63+
static export(source, loaderContext) {
64+
return this.compiler.export(source, loaderContext);
6565
}
6666

6767
/**

src/Loader/Modes/Compile.js

Lines changed: 6 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -7,14 +7,11 @@ const { decodeReservedChars, escapeSequences } = require('../Utils');
77
*/
88
class Compile extends PreprocessorMode {
99
enclosingQuotes = `'`;
10+
isExport = false;
1011

1112
constructor({ preprocessor, esModule, hot }) {
1213
super({ preprocessor, esModule, hot });
13-
14-
this.preprocessorExport =
15-
typeof preprocessor.export === 'function'
16-
? preprocessor.export
17-
: (content) => `const templateFn = () => '` + escapeSequences(content) + `';${this.exportCode}templateFn;`;
14+
this.isExport = typeof preprocessor.export === 'function';
1815
}
1916

2017
/**
@@ -35,15 +32,14 @@ class Compile extends PreprocessorMode {
3532
}
3633

3734
/**
38-
* Export template function.
35+
* Export a template function depending on the code generated by the preprocessor.
3936
*
4037
* @param {string} content The source of template function.
41-
* @param {{}} data The object with variables passed in template.
42-
* @param {string} issuer
38+
* @param {BundlerPluginLoaderContext} loaderContext
4339
* @return {string}
4440
*/
45-
export(content, data, issuer) {
46-
return this.preprocessorExport(content);
41+
export(content, loaderContext) {
42+
return this.isExport ? this.preprocessor.export(content, loaderContext) : content;
4743
}
4844

4945
/**

src/Loader/Modes/PreprocessorMode.js

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -23,20 +23,19 @@ class PreprocessorMode {
2323
* @abstract
2424
*/
2525
requireExpression(file) {
26-
return '';
26+
return file;
2727
}
2828

2929
/**
3030
* Export template code with rendered HTML.
3131
*
3232
* @param {string} content The template content.
33-
* @param {{}} data The object with variables passed in template.
34-
* @param {string} issuer The issuer of the template file.
33+
* @param {BundlerPluginLoaderContext} loaderContext
3534
* @return {string}
3635
* @abstract
3736
*/
38-
export(content, data, issuer) {
39-
return '';
37+
export(content, loaderContext) {
38+
return content;
4039
}
4140

4241
/**
@@ -48,7 +47,7 @@ class PreprocessorMode {
4847
* @abstract
4948
*/
5049
exportError(error, issuer) {
51-
return '';
50+
return error;
5251
}
5352

5453
/**

src/Loader/Modes/Render.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -44,10 +44,10 @@ class Render extends PreprocessorMode {
4444
*
4545
* @param {string} content The template content.
4646
* @param {{}} data The object with variables passed in template.
47-
* @param {string} issuer
47+
* @param {string} issuer The issuer of the template file.
4848
* @return {string}
4949
*/
50-
export(content, data, issuer) {
50+
export(content, { data, resource: issuer }) {
5151
/* istanbul ignore next: Webpack API no provide `loaderContext.hot` for testing */
5252
if (this.hot && PluginService.useHotUpdate()) {
5353
content = this.injectHotScript(content);

src/Loader/Preprocessors/Ejs/index.js

Lines changed: 14 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -13,19 +13,17 @@ const preprocessor = (loaderContext, options) => {
1313
const { rootContext } = loaderContext;
1414

1515
return {
16-
externalData: '{}',
17-
1816
/**
1917
* Render template into HTML.
2018
* Called for rendering of template defined as entry point.
2119
*
22-
* @param {string} template
20+
* @param {string} source The template source code.
2321
* @param {string} resourcePath
2422
* @param {{}} data
2523
* @return {string}
2624
*/
27-
render: (template, { resourcePath, data = {} }) =>
28-
Ejs.render(template, data, {
25+
render: (source, { resourcePath, data = {} }) =>
26+
Ejs.render(source, data, {
2927
async: false,
3028
root: rootContext, // root path for includes with an absolute path (e.g., /file.html)
3129
...options,
@@ -38,13 +36,13 @@ const preprocessor = (loaderContext, options) => {
3836
*
3937
* TODO: add support for the `include`
4038
*
41-
* @param {string} template
39+
* @param {string} source The template source code.
4240
* @param {string} resourcePath
4341
* @param {{}} data
4442
* @return {string}
4543
*/
46-
compile: (template, { resourcePath, data = {} }) => {
47-
let source = Ejs.compile(template, {
44+
compile: (source, { resourcePath, data = {} }) => {
45+
let templateFunction = Ejs.compile(source, {
4846
client: true,
4947
compileDebug: false,
5048
root: rootContext, // root path for includes with an absolute path (e.g., /file.html)
@@ -53,26 +51,25 @@ const preprocessor = (loaderContext, options) => {
5351
filename: resourcePath, // allow including a partial relative to the template
5452
}).toString();
5553

56-
this.externalData = stringifyData(data);
57-
58-
return source.replace(`var __output = "";`, 'locals = Object.assign(__data__, locals); var __output = "";');
54+
return templateFunction
55+
.replace(`var __output = "";`, 'locals = Object.assign(__data__, locals); var __output = "";')
56+
.replaceAll('include(', 'require(');
5957
},
6058

6159
/**
6260
* Export the compiled template function contained resolved source asset files.
6361
* Note: this method is required for `compile` mode.
6462
*
65-
* @param {string} content The source code of the template function.
63+
* @param {string} templateFunction The source code of the template function.
64+
* @param {{}} data The object with variables passed in template.
6665
* @return {string} The exported template function.
6766
*/
68-
export: (content) => {
67+
export: (templateFunction, { data }) => {
6968
// the name of template function in generated code
70-
const fnName = 'anonymous';
69+
const exportFunction = 'anonymous';
7170
const exportCode = 'module.exports=';
7271

73-
content = content.replaceAll('include(', 'require(');
74-
75-
return `var __data__ = ${this.externalData};` + content + `;${exportCode}${fnName};`;
72+
return `var __data__ = ${stringifyData(data)};` + templateFunction + `;${exportCode}${exportFunction};`;
7673
},
7774
};
7875
};

src/Loader/Preprocessors/Eta/index.js

Lines changed: 39 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,8 @@
11
const path = require('path');
2-
const { escapeSequences } = require('../../Utils');
2+
const { stringifyData } = require('../../Utils');
33
const { loadModule } = require('../../../Common/FileUtils');
4-
const { yellow } = require('ansis');
54

6-
const compileModeWarning = () => {
7-
// TODO: warning
8-
console.log(
9-
yellow`[html-bundler-webpack-plugin] WARNING: Eta supports only rendering to an html string and cannot compile into a JS template function.${'\n'}You can use ESJ for using the JS template function with variables.`
10-
);
11-
};
5+
const includeRegexp = /=include\((?<file>.+?)(?:\)|,\s*{(?<data>.+?)}\))/g;
126

137
const preprocessor = (loaderContext, options) => {
148
const Eta = loadModule('eta', () => require('eta').Eta);
@@ -22,7 +16,7 @@ const preprocessor = (loaderContext, options) => {
2216
}
2317

2418
const eta = new Eta({
25-
useWith: true, // allow using variables in template without `it.` scope
19+
useWith: true, // allow using variables in template without `it.` namespace
2620
...options,
2721
views, // directory that contains templates
2822
});
@@ -36,54 +30,67 @@ const preprocessor = (loaderContext, options) => {
3630
* Render template into HTML.
3731
* Called for rendering of template defined as entry point.
3832
*
39-
* @param {string} template
33+
* @param {string} source The template source code.
4034
* @param {string} resourcePath
4135
* @param {{}} data
4236
* @return {string}
4337
*/
4438
render: async
45-
? (template, { resourcePath, data = {} }) => {
46-
return eta.renderStringAsync(template, data);
39+
? (source, { resourcePath, data = {} }) => {
40+
return eta.renderStringAsync(source, data);
4741
}
48-
: (template, { resourcePath, data = {} }) => {
49-
return eta.renderString(template, data);
42+
: (source, { resourcePath, data = {} }) => {
43+
return eta.renderString(source, data);
5044
},
5145

5246
/**
5347
* Compile template into template function.
5448
* Called when a template is loaded in JS in `compile` mode.
5549
*
56-
* Note:
57-
* Eta does not compile the template into template function source code,
58-
* so we compile the template into an HTML string that will be wrapped in a function for client-side compatibility.
59-
*
60-
* @param {string} template
50+
* @param {string} source The template source code.
6151
* @param {string} resourcePath
6252
* @param {{}} data
6353
* @return {string}
6454
*/
65-
compile: async
66-
? (template, { resourcePath, data = {} }) => {
67-
compileModeWarning();
68-
return eta.renderStringAsync(template, data);
69-
}
70-
: (template, { resourcePath, data = {} }) => {
71-
compileModeWarning();
72-
return eta.renderString(template, data);
73-
},
55+
compile: (source, { resourcePath, data = {} }) => {
56+
const varName = options.varName || 'it';
57+
const eta = new Eta({
58+
useWith: true, // allow using variables in template without `it.` namespace
59+
...options,
60+
views,
61+
});
62+
63+
// parse and replace the partial file and data
64+
// include("./file.eta") => require("./file.eta")({...it, ...{}})
65+
// include('./file.eta', { name: 'eta' }) => require('./file.eta')({...it, ...{name: 'eta'}})
66+
const templateFunctionBody = eta
67+
.compileToString(source)
68+
.replaceAll(includeRegexp, `=require($<file>)({...${varName}, ...{$<data>}})`);
69+
70+
return `function(${varName}){${templateFunctionBody}}`;
71+
},
7472

7573
/**
7674
* Export the compiled template function contained resolved source asset files.
7775
* Note: this method is required for `compile` mode.
7876
*
79-
* @param {string} content The source code of the template function.
77+
* @param {string} templateFunction The source code of the template function.
78+
* @param {{}} data The object with variables passed in template.
8079
* @return {string} The exported template function.
8180
*/
82-
export: (content) => {
83-
const fnName = 'templateFn';
81+
export: (templateFunction, { data }) => {
82+
// note: resolved the file is for node, therefore, we need to get the module path plus file for browser
83+
const runtimeFile = path.join(path.dirname(require.resolve('eta')), 'browser.module.mjs');
84+
const exportFunction = 'templateFn';
8485
const exportCode = 'module.exports=';
8586

86-
return `const ${fnName} = () => '` + escapeSequences(content) + `';${exportCode}${fnName};`;
87+
return `
88+
var { Eta } = require('${runtimeFile}');
89+
var eta = new Eta(${stringifyData(options)});
90+
var __data__ = ${stringifyData(data)};
91+
var etaFn = ${templateFunction};
92+
var ${exportFunction} = (context) => etaFn.bind(eta)(Object.assign(__data__, context));
93+
${exportCode}${exportFunction};`;
8794
},
8895
};
8996
};

0 commit comments

Comments
 (0)