Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 36 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -241,6 +241,42 @@ summary
23.28x faster than ejs.render
```

## micro-template-serialize

`micro-template-serialize` is a CLI tool for precompiling multiple template files into a single ESM (ECMAScript Module) JavaScript file. This is especially useful for environments where dynamic function generation (such as with `new Function()`) is not allowed, or for delivering precompiled templates to browsers or serverless environments.

### Why use it?
- **Security:** Avoids the use of `new Function()` at runtime, which is often restricted in secure or serverless environments.
- **Performance:** Templates are precompiled, so rendering is fast and does not require parsing or compiling templates at runtime.
- **Convenience:** Bundles multiple templates into a single importable module.

### Usage

```sh
micro-template-serialize <input1.tmpl> <input2.tmpl> ... --output <output.js> [--root <dir>]
```

- `<input1.tmpl> ...` : List of template files to serialize.
- `--output <output.js>` : Output JavaScript file (required).
- `--root <dir>` : Root directory for template IDs (default: current directory).

#### Example

```sh
micro-template-serialize test/data-test1.tmpl test/data-fizzbuzz.tmpl --output templates.js --root test
```

This will generate a file named `templates.js` in the current directory. You can then import the generated module in your JavaScript code:

```js
import { extended as template } from './templates.js';
const result = template('main', { foo: 'world', baz: 'baz!' });
console.log('render result:', result);
```

- If a template does not contain a `<!--meta.keys=[...]-->` comment, a warning will be shown.
- The output file is an ESM module (use `import` to load it).

LICENSE
-------

Expand Down
84 changes: 84 additions & 0 deletions bin/micro-template-serialize.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
#!/usr/bin/env node

import { serializeTemplates } from '../lib/serializer.js';
import { promises as fs } from 'fs';
import path from 'path';

const USAGE = `
Usage: micro-template-serialize <input1.tmpl> ... --output <output.js> [--root <dir>]

Purpose:
Reads one or more template files, extracts their content and meta information
(such as keys defined in <!--meta.keys=[...]-->), and serializes them into a JavaScript file.
The template ID is determined by the relative path from the root directory (without extension).

This tool is especially useful for environments where dynamic function generation
(such as with new Function()) is not allowed, as it outputs pre-serialized ESM modules.

Arguments:
<input1.tmpl> ... List of template files to serialize.
--output <output.js> Output JavaScript file (required).
--root <dir> Root directory for template IDs (default: current directory).
--help, -h Show this help message.

Example:
micro-template-serialize test/data-test1.tmpl test/data-fizzbuzz.tmpl --output templates.js --root test

And this will generate a file named 'templates.js' in the current directory.
You can then import the generated module in your JavaScript code:

import { extended as template } from './templates.js';
const result = template('main', { foo: 'world', baz: 'baz!' });
console.log('render result:', result);

Notes:
- If a template does not contain a <!--meta.keys=[...]--> comment, a warning will be shown.
- The output file will contain the serialized templates using the serializeTemplates function.
- The output file is an ESM module (use 'import' to load it).
`;

// --- 引数パース ---
const args = process.argv.slice(2);
if (args.includes('--help') || args.includes('-h')) {
console.log(USAGE);
process.exit(0);
}
let outputFile;
let rootDir = process.cwd();
const inputFiles = [];
for (let i = 0; i < args.length; i++) {
if (args[i] === '--output') {
outputFile = args[++i];
} else
if (args[i] === '--root') {
rootDir = args[++i];
} else {
inputFiles.push(args[i]);
}
}
if (!outputFile || inputFiles.length === 0) {
console.error('Usage: micro-template-serialize <input1.tmpl> ... --output templates.js [--root <dir>]');
process.exit(1);
}

// --- テンプレートファイル読み込み ---
const templates = {};
for (const file of inputFiles) {
// id を rootDir からの相対パス(拡張子除く)にする
const relPath = path.relative(rootDir, file);
const id = relPath.replace(path.extname(relPath), '');
const source = await fs.readFile(file, 'utf-8');
templates[id] = { source };
source.replace(/<!--meta\.(\w+)=(.+?)-->/g, (match, key, value) => {
templates[id][key] = JSON.parse(value);
return ''; // Remove the comment
});
if (!templates[id].keys) {
console.warn(`Warning: Template "${id}" does not have keys defined. Please add <!--meta.keys=["key1", "key2"]--> in the template file.`);
templates[id].keys = [];
}
}

const code = serializeTemplates(templates);
await fs.writeFile(outputFile, code);
console.log(`Wrote: ${outputFile}`);
19 changes: 10 additions & 9 deletions index.html
Original file line number Diff line number Diff line change
Expand Up @@ -166,6 +166,14 @@
<header>
<h1>micro-template.js</h1>
<p class="subtitle">A minimal, blazing fast JavaScript template engine for hackers.</p>
<div style="margin:1em 0 0.5em 0;">
<img src="https://img.shields.io/npm/v/micro-template.svg?style=flat-square" alt="npm version"
style="vertical-align:middle; margin-right:8px;">
<img src="https://img.shields.io/github/stars/cho45/micro-template.js?style=flat-square&label=GitHub+Stars"
alt="GitHub stars" style="vertical-align:middle; margin-right:8px;">
<img src="https://img.shields.io/npm/l/micro-template.svg?style=flat-square" alt="MIT License"
style="vertical-align:middle;">
</div>
</header>
<section class="features">
<h2>Features</h2>
Expand All @@ -177,16 +185,8 @@ <h2>Features</h2>
</ul>
</section>
<section class="usage">
<h2>Usage Example</h2>
<h2>Getting Started</h2>
<div class="usage-blocks">
<div class="usage-block">
<h3>Basic</h3>
<pre><code class="language-js">import { template } from 'micro-template';

const result = template('&lt;div&gt;&lt;%= message %&gt;&lt;/div&gt;', { message: 'Hello, inline!' });
console.log(result); // &lt;div&gt;Hello, inline!&lt;/div&gt;
</code></pre>
</div>
<div class="usage-block">
<h3>In HTML</h3>
<pre><code class="language-html">&lt;script type="application/x-template" id="tmpl1"&gt;
Expand All @@ -200,6 +200,7 @@ <h3>In HTML</h3>
</div>
<div class="usage-block">
<h3>In Node.js</h3>
<pre><code>npm install micro-template</code></pre>
<pre><code class="language-js">import fs from 'node:fs';
import { template } from 'micro-template';

Expand Down
7 changes: 4 additions & 3 deletions lib/micro-template.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ const template = function (id, data) {
if (arguments.length < 2) throw new Error('template() must be called with (template, data)');
const me = template, isArray = Array.isArray(data), keys = isArray ? data : Object.keys(data || {}), key = `data:${id}:${keys.sort().join(':')}`;
if (!me.cache.has(key)) me.cache.set(key, (function () {
let name = id, string = /^[\w\-]+$/.test(id) ? me.get(id): (name = `template-${Math.random().toString(36).slice(2)}`, id); // no warnings
let name = id, string = /^[/\w\-]+$/.test(id) ? me.get(id): (name = `template-${Math.random().toString(36).slice(2)}`, id); // no warnings
let line = 1;
const body = (
`try {` +
Expand All @@ -27,8 +27,9 @@ const template = function (id, data) {
`//# sourceURL=${name}\n` +
`//# sourceMappingURL=data:application/json,${encodeURIComponent(JSON.stringify({version:3, file:name, sources:[`${name}.ejs`], sourcesContent:[string], mappings:";;AAAA;"+Array(line-1).fill('AACA').join(';')}))}`
);
const func = new Function("__this", ...keys, body);
return function (stash) { return func.call(null, me.context = { escapeHTML: me.escapeHTML, line: 1, ret : '', stash: stash }, ...keys.map(key => stash[key])) };
const compiled = new Function("__this", ...keys, body);
const ret = function (stash) { return compiled.call(null, me.context = { escapeHTML: me.escapeHTML, line: 1, ret : '', stash: stash }, ...keys.map(key => stash[key])) };
return (ret.compiled = compiled, ret.keys = keys, ret);
})());
return isArray ? me.cache.get(key) : me.cache.get(key)(data);
}
Expand Down
37 changes: 37 additions & 0 deletions lib/serializer.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
#!/usr/bin/env node

import { template, extended } from './micro-template.js';

export function serializeTemplates(target) {
template.get = id => target[id].source;
template.cache.clear();

let serialized = 'const compiled = {};\n';

for (const [id, entry] of Object.entries(target)) {
const keys = entry.keys || [];
console.log(`Compiling template: ${id}`);
Copy link

Copilot AI Jun 15, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Consider removing or gating the console.log statement to avoid unwanted logging in production builds.

Suggested change
console.log(`Compiling template: ${id}`);
if (process.env.NODE_ENV !== 'production') {
console.log(`Compiling template: ${id}`);
}

Copilot uses AI. Check for mistakes.
const func = extended(id, keys);
const compiled = func.compiled;
serialized += `compiled['${id}'] = ` + compiled.toString().replace(/\/\/#.*/g, '') + ';\n';
serialized += `compiled['${id}'].keys = ` + JSON.stringify(func.keys) + ';\n';
}

serialized += `const regexp = /[<>"'&]/;\n`;
serialized += `const escapeHTML = ` + template.escapeHTML.toString() + ';\n';

serialized += `const template = ` + (function (id, stash) {
const me = template;
const func = compiled[id];
if (!func) {
throw new Error(`Template "${id}" not found.`);
}
return func.call(null, me.context = { escapeHTML, line: 1, ret: '', stash }, ...func.keys.map(key => stash[key]));
}).toString() + ';\n';

serialized += `const extended = ` + extended.toString() + ';\n';

serialized += `export { template, extended };\n`;

return serialized;
}
5 changes: 5 additions & 0 deletions misc/serialized.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
#!/usr/bin/env node
import { extended as template } from '../_serialized.js';

const result = template('main', { foo: 'world', baz: 'baz!' });
console.log('render result:', result);
8 changes: 8 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,14 @@
"bench": "node --expose-gc ./misc/benchmark.js",
"test:types": "npx tsc --lib es2015 --noEmit test/types.test.ts"
},
"bin": {
"micro-template-serialize": "./bin/micro-template-serialize.js"
},
"exports": {
".": "./lib/micro-template.js",
"./micro-template": "./lib/micro-template.js",
"./serializer": "./lib/serializer.js"
},
"keywords": [
"template",
"engine",
Expand Down
5 changes: 5 additions & 0 deletions test/serialize/footer.tmpl
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
<!--meta.keys=["year"] -->

<footer>
<p>Footer content <%= year %></p>
</footer>
11 changes: 11 additions & 0 deletions test/serialize/main.tmpl
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
<!--meta.keys=["foo", "baz"] -->

HEADER
<% wrapper('wrapper', function () { %>
hello <%= foo %>,
<% [1,2].forEach( () => { %>
and <%= baz %>
<% }) %>
<% }) %>
<% include('footer', { year: 2025 }) %>
FOOTER
5 changes: 5 additions & 0 deletions test/serialize/wrapper.tmpl
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
<!--meta.keys=["content"] -->

BEFORE CONTENT
<%=raw content %>
AFTER CONTENT
Loading