Skip to content

Commit

Permalink
feat: add support for the template function on the client-side for eta
Browse files Browse the repository at this point in the history
  • Loading branch information
webdiscus committed Nov 29, 2023
1 parent de779b8 commit d278fce
Show file tree
Hide file tree
Showing 22 changed files with 163 additions and 154 deletions.
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
# Change log

## 3.3.0 (2023-11-29)

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

## 3.2.0 (2023-11-28)

- feat: add Twig preprocessor. Now you can use "best of the best" template engine. Enjoy ;-)
Expand Down
5 changes: 3 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4159,7 +4159,9 @@ _./partials/people.ejs_
#### Template engines that do support the `template function` on client-side

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

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

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

---
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "html-bundler-webpack-plugin",
"version": "3.2.0",
"version": "3.3.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",
Expand Down
10 changes: 5 additions & 5 deletions src/Loader/Loader.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,13 @@ class Loader {
static compiler = null;

/**
* @param {Object} loaderContext
* @param {BundlerPluginLoaderContext} loaderContext
*/
static init(loaderContext) {
const { rootContext, hot } = loaderContext;
const { preprocessor, preprocessorMode, data, esModule, self: useSelf } = Option.get();

this.data = data;
//this.data = data;

// prevent double initialization with same options, it occurs when many entry files used in one webpack config
if (!PluginService.isCached(rootContext)) {
Expand Down Expand Up @@ -57,11 +57,11 @@ class Loader {
* Export generated result.
*
* @param {string} source
* @param {string} issuer
* @param {BundlerPluginLoaderContext} loaderContext
* @return {string}
*/
static export(source, issuer) {
return this.compiler.export(source, this.data, issuer);
static export(source, loaderContext) {
return this.compiler.export(source, loaderContext);
}

/**
Expand Down
16 changes: 6 additions & 10 deletions src/Loader/Modes/Compile.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,11 @@ const { decodeReservedChars, escapeSequences } = require('../Utils');
*/
class Compile extends PreprocessorMode {
enclosingQuotes = `'`;
isExport = false;

constructor({ preprocessor, esModule, hot }) {
super({ preprocessor, esModule, hot });

this.preprocessorExport =
typeof preprocessor.export === 'function'
? preprocessor.export
: (content) => `const templateFn = () => '` + escapeSequences(content) + `';${this.exportCode}templateFn;`;
this.isExport = typeof preprocessor.export === 'function';
}

/**
Expand All @@ -35,15 +32,14 @@ class Compile extends PreprocessorMode {
}

/**
* Export template function.
* Export a template function depending on the code generated by the preprocessor.
*
* @param {string} content The source of template function.
* @param {{}} data The object with variables passed in template.
* @param {string} issuer
* @param {BundlerPluginLoaderContext} loaderContext
* @return {string}
*/
export(content, data, issuer) {
return this.preprocessorExport(content);
export(content, loaderContext) {
return this.isExport ? this.preprocessor.export(content, loaderContext) : content;
}

/**
Expand Down
11 changes: 5 additions & 6 deletions src/Loader/Modes/PreprocessorMode.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,20 +23,19 @@ class PreprocessorMode {
* @abstract
*/
requireExpression(file) {
return '';
return file;
}

/**
* Export template code with rendered HTML.
*
* @param {string} content The template content.
* @param {{}} data The object with variables passed in template.
* @param {string} issuer The issuer of the template file.
* @param {BundlerPluginLoaderContext} loaderContext
* @return {string}
* @abstract
*/
export(content, data, issuer) {
return '';
export(content, loaderContext) {
return content;
}

/**
Expand All @@ -48,7 +47,7 @@ class PreprocessorMode {
* @abstract
*/
exportError(error, issuer) {
return '';
return error;
}

/**
Expand Down
4 changes: 2 additions & 2 deletions src/Loader/Modes/Render.js
Original file line number Diff line number Diff line change
Expand Up @@ -44,10 +44,10 @@ class Render extends PreprocessorMode {
*
* @param {string} content The template content.
* @param {{}} data The object with variables passed in template.
* @param {string} issuer
* @param {string} issuer The issuer of the template file.
* @return {string}
*/
export(content, data, issuer) {
export(content, { data, resource: issuer }) {
/* istanbul ignore next: Webpack API no provide `loaderContext.hot` for testing */
if (this.hot && PluginService.useHotUpdate()) {
content = this.injectHotScript(content);
Expand Down
31 changes: 14 additions & 17 deletions src/Loader/Preprocessors/Ejs/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,19 +13,17 @@ const preprocessor = (loaderContext, options) => {
const { rootContext } = loaderContext;

return {
externalData: '{}',

/**
* Render template into HTML.
* Called for rendering of template defined as entry point.
*
* @param {string} template
* @param {string} source The template source code.
* @param {string} resourcePath
* @param {{}} data
* @return {string}
*/
render: (template, { resourcePath, data = {} }) =>
Ejs.render(template, data, {
render: (source, { resourcePath, data = {} }) =>
Ejs.render(source, data, {
async: false,
root: rootContext, // root path for includes with an absolute path (e.g., /file.html)
...options,
Expand All @@ -38,13 +36,13 @@ const preprocessor = (loaderContext, options) => {
*
* TODO: add support for the `include`
*
* @param {string} template
* @param {string} source The template source code.
* @param {string} resourcePath
* @param {{}} data
* @return {string}
*/
compile: (template, { resourcePath, data = {} }) => {
let source = Ejs.compile(template, {
compile: (source, { resourcePath, data = {} }) => {
let templateFunction = Ejs.compile(source, {
client: true,
compileDebug: false,
root: rootContext, // root path for includes with an absolute path (e.g., /file.html)
Expand All @@ -53,26 +51,25 @@ const preprocessor = (loaderContext, options) => {
filename: resourcePath, // allow including a partial relative to the template
}).toString();

this.externalData = stringifyData(data);

return source.replace(`var __output = "";`, 'locals = Object.assign(__data__, locals); var __output = "";');
return templateFunction
.replace(`var __output = "";`, 'locals = Object.assign(__data__, locals); var __output = "";')
.replaceAll('include(', 'require(');
},

/**
* Export the compiled template function contained resolved source asset files.
* Note: this method is required for `compile` mode.
*
* @param {string} content The source code of the template function.
* @param {string} templateFunction The source code of the template function.
* @param {{}} data The object with variables passed in template.
* @return {string} The exported template function.
*/
export: (content) => {
export: (templateFunction, { data }) => {
// the name of template function in generated code
const fnName = 'anonymous';
const exportFunction = 'anonymous';
const exportCode = 'module.exports=';

content = content.replaceAll('include(', 'require(');

return `var __data__ = ${this.externalData};` + content + `;${exportCode}${fnName};`;
return `var __data__ = ${stringifyData(data)};` + templateFunction + `;${exportCode}${exportFunction};`;
},
};
};
Expand Down
71 changes: 39 additions & 32 deletions src/Loader/Preprocessors/Eta/index.js
Original file line number Diff line number Diff line change
@@ -1,14 +1,8 @@
const path = require('path');
const { escapeSequences } = require('../../Utils');
const { stringifyData } = require('../../Utils');
const { loadModule } = require('../../../Common/FileUtils');
const { yellow } = require('ansis');

const compileModeWarning = () => {
// TODO: warning
console.log(
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.`
);
};
const includeRegexp = /=include\((?<file>.+?)(?:\)|,\s*{(?<data>.+?)}\))/g;

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

const eta = new Eta({
useWith: true, // allow using variables in template without `it.` scope
useWith: true, // allow using variables in template without `it.` namespace
...options,
views, // directory that contains templates
});
Expand All @@ -36,54 +30,67 @@ const preprocessor = (loaderContext, options) => {
* Render template into HTML.
* Called for rendering of template defined as entry point.
*
* @param {string} template
* @param {string} source The template source code.
* @param {string} resourcePath
* @param {{}} data
* @return {string}
*/
render: async
? (template, { resourcePath, data = {} }) => {
return eta.renderStringAsync(template, data);
? (source, { resourcePath, data = {} }) => {
return eta.renderStringAsync(source, data);
}
: (template, { resourcePath, data = {} }) => {
return eta.renderString(template, data);
: (source, { resourcePath, data = {} }) => {
return eta.renderString(source, data);
},

/**
* Compile template into template function.
* Called when a template is loaded in JS in `compile` mode.
*
* Note:
* Eta does not compile the template into template function source code,
* so we compile the template into an HTML string that will be wrapped in a function for client-side compatibility.
*
* @param {string} template
* @param {string} source The template source code.
* @param {string} resourcePath
* @param {{}} data
* @return {string}
*/
compile: async
? (template, { resourcePath, data = {} }) => {
compileModeWarning();
return eta.renderStringAsync(template, data);
}
: (template, { resourcePath, data = {} }) => {
compileModeWarning();
return eta.renderString(template, data);
},
compile: (source, { resourcePath, data = {} }) => {
const varName = options.varName || 'it';
const eta = new Eta({
useWith: true, // allow using variables in template without `it.` namespace
...options,
views,
});

// parse and replace the partial file and data
// include("./file.eta") => require("./file.eta")({...it, ...{}})
// include('./file.eta', { name: 'eta' }) => require('./file.eta')({...it, ...{name: 'eta'}})
const templateFunctionBody = eta
.compileToString(source)
.replaceAll(includeRegexp, `=require($<file>)({...${varName}, ...{$<data>}})`);

return `function(${varName}){${templateFunctionBody}}`;
},

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

return `const ${fnName} = () => '` + escapeSequences(content) + `';${exportCode}${fnName};`;
return `
var { Eta } = require('${runtimeFile}');
var eta = new Eta(${stringifyData(options)});
var __data__ = ${stringifyData(data)};
var etaFn = ${templateFunction};
var ${exportFunction} = (context) => etaFn.bind(eta)(Object.assign(__data__, context));
${exportCode}${exportFunction};`;
},
};
};
Expand Down
Loading

0 comments on commit d278fce

Please sign in to comment.