NODE-JHS2 is a dynamic template engine for Node.js that lets you embed server-side JavaScript directly in HTML files using <?jhs ... ?> delimiters — similar to PHP, but powered by JavaScript.
It is a complete rewrite of node-jhs, with improved security, sandboxed execution, XSS protection, and a cleaner API.
Template files use the .jhs extension and integrate seamlessly with native Node.js HTTPS servers and custom middleware chains.
| Feature | node-jhs v1 | NODE-JHS2 v2 |
|---|---|---|
| XSS auto-escaping | ❌ | ✅ |
| VM sandbox execution | ❌ | ✅ |
require() filtering |
❌ | ✅ |
| Execution timeout | ❌ | ✅ 5s |
raw() helper |
❌ | ✅ |
include() support |
❌ | ✅ |
echo() function |
❌ | ✅ |
| Template caching | ✅ | ✅ |
| Configurable delimiters | ✅ | ✅ |
⚠️ Breaking change: The internal compilation and execution model has been completely rewritten. See Migration from v1.
| Feature | v2.0.0 | v2.1.0 |
|---|---|---|
echo(raw(...)) |
rendered [object Object] |
✅ prints trusted markup, unescaped |
| Template cache | stale until clearCache() or restart |
✅ revalidated against the file's mtime (hot reload) |
| Unit tests | 13 | 17 |
<?= raw(...) ?> already bypassed escaping in v2.0.0 — v2.1.0 extends the same sentinel to echo(), matching the wallermax-server Rust port.
Wrap the markup in raw(), keep the data escaped with escapeHtml():
<?jhs
users.forEach(user => {
echo(raw('<li>' + escapeHtml(user.name) + '</li>\n'));
});
?>
⚠️ raw()returns a sentinel object: concatenate inside the call.raw(a) + bflattens the sentinel and renders[object Object]b.
With cache: true (the default), every render re-checks the cached file's modification time and recompiles automatically when it changes — edit a template, save, render again: no clearCache(), no restart.
await engine.render('page.jhs', data); // compiles and caches
// ...edit views/page.jhs on disk...
await engine.render('page.jhs', data); // recompiles — fresh contentnode demo-raw-mtime.jsThe demo script at the repo root prints nine numbered checks: escaped vs raw output, echo(raw()), the mixed-trust pattern, the [object Object] quirk, require('url') inside a template, and the mtime hot reload.
- 🔖 PHP-like syntax — use
<?jhs ... ?>to run JavaScript inside HTML <?= expr ?>shorthand for outputting values- 🛡️ Auto XSS protection — HTML escaping enabled by default
- ⚡ Template caching with hot reload — compiled templates are cached and revalidated against the file's
mtime - 🔒 Sandboxed execution — templates run inside a Node.js
vmcontext - 📦
requirefiltering — block dangerous or unwanted modules - 🔁
include()support — embed sub-templates with shared data - ⏱️ Execution timeout — 5-second limit per template to prevent hangs
npm install node-jhs2Or clone and use locally:
git clone https://github.com/justo-tapiador/node-jhs2.git<!DOCTYPE html>
<html>
<head>
<title><?= title ?></title>
</head>
<body>
<h1>Hello, <?= name ?>!</h1>
<?jhs if (items.length > 0) { ?>
<ul>
<?jhs items.forEach(item => { ?>
<li><?= item ?></li>
<?jhs }); ?>
</ul>
<?jhs } ?>
</body>
</html>const https = require('https');
const fs = require('fs');
const JSTemplateEngine = require('node-jhs2');
const engine = new JSTemplateEngine({ viewsPath: './views' });
https.createServer({
key: fs.readFileSync('./ssl/key.pem'),
cert: fs.readFileSync('./ssl/cert.pem')
}, async (req, res) => {
const html = await engine.render('index.jhs', {
title: 'My App',
name: 'World',
items: ['Apple', 'Banana', 'Cherry']
});
res.writeHead(200, { 'Content-Type': 'text/html' });
res.end(html);
}).listen(443);<?= variableName ?><?jhs
const greeting = 'Hello World';
let count = 0;
?><?jhs if (user.isAdmin) { ?>
<p>Welcome, Admin!</p>
<?jhs } else { ?>
<p>Access denied.</p>
<?jhs } ?><?jhs users.forEach(user => { ?>
<li><?= user.name ?> — <?= user.email ?></li>
<?jhs }); ?><?jhs const nav = await include('partials/nav.jhs', { active: 'home' }); ?>
<?= raw(nav) ?>
include()inherits the parent template's data by default.
<?= raw('<strong>Bold</strong>') ?>
<?jhs echo(raw('<strong>Bold</strong>')); ?>Since v2.1.0,
echo()acceptsraw()sentinels as well — see What's new in v2.1.0.
<?jhs echo('Hello ', username, '!'); ?>
echo()escapes its arguments, just like<?= ?>. To print trusted markup from a code block, wrap it inraw():echo(raw('<li>item</li>')).
NODE-JHS2 is designed to work naturally with custom middleware chains on native Node.js HTTPS servers — no Express required.
The recommended pattern is a MiddlewareManager class that chains async functions, with one middleware that attaches res.renderTemplate() to the response object. All subsequent middleware and routes then call it directly.
const server = new MiddlewareManager();
// ── Attach res.renderTemplate() ──────────────────────────────────────────────
server.use((req, res, next) => {
res.renderTemplate = async (templatePath, data = {}, statusCode = 200) => {
const html = await templateEngine.render(templatePath, { ...data, req, res });
res.writeHead(statusCode, { 'Content-Type': 'text/html; charset=utf-8' });
res.end(html);
};
next();
});
// ── Serve .jhs files automatically from /public ──────────────────────────────
server.use(async (req, res, next) => {
const filePath = path.join(PUBLIC_DIR, req.pathname);
if (path.extname(filePath) === '.jhs') {
await res.renderTemplate(filePath, { user: req.user });
return;
}
next();
});
// ── Application routes ────────────────────────────────────────────────────────
server.use(async (req, res, next) => {
if (req.pathname === '/contact' && req.method === 'GET') {
await res.renderTemplate('contact.jhs', { title: 'Contact Us' });
} else {
next();
}
});
// ── Start HTTPS server ────────────────────────────────────────────────────────
https.createServer(sslOptions, (req, res) => server.execute(req, res))
.listen(443);See the full working example in examples/https-server.js.
| Option | Type | Default | Description |
|---|---|---|---|
viewsPath |
string |
./views |
Directory where .jhs template files are stored |
cache |
boolean |
true |
Cache compiled templates in memory (revalidated against file mtime) |
openTag |
string |
<?jhs |
Opening tag for code blocks |
closeTag |
string |
?> |
Closing tag |
echoTag |
string |
<?= |
Opening tag for output expressions |
encoding |
string |
utf-8 |
File encoding |
autoEscape |
boolean |
true |
Auto-escape HTML output to prevent XSS |
banned_require |
string[] |
['vm','jhs2'] |
Modules blocked from use inside templates |
Renders a .jhs file from disk.
const html = await engine.render('index.jhs', { title: 'Home' });Renders a template directly from a string.
const html = await engine.renderString('<p><?= msg ?></p>', { msg: 'Hi!' });Clears the in-memory template cache.
Since v2.1.0 the cache revalidates itself against each file's
mtime, so picking up template edits no longer requiresclearCache()— it remains useful for freeing memory or forcing recompilation.
Clears Node.js require cache entries matching a pattern (useful in development).
JSTemplateEngine.clearRequireCache('.jhs');All values output via <?= ?> or echo() are HTML-escaped by default:
| Character | Escaped as |
|---|---|
& |
& |
< |
< |
> |
> |
" |
" |
' |
' |
Use raw() to output trusted HTML without escaping — and pair it with escapeHtml() for any data inside it (the mixed-trust pattern):
<?= raw(trustedHtmlString) ?>
<?jhs echo(raw('<li>' + escapeHtml(userInput) + '</li>')); ?>Templates run inside a Node.js vm.createContext() sandbox with a 5-second execution timeout. Only explicitly whitelisted globals are available inside templates.
Block specific modules from use inside templates:
const engine = new JSTemplateEngine({
banned_require: ['child_process', 'fs', 'net']
});vm and jhs2 are always blocked by default.
If you are migrating from node-jhs v1:
- Template syntax is unchanged — your
.jhsfiles work as-is. autoEscapeis now on by default. If you were outputting raw HTML via<?= ?>, wrap it withraw():<?= raw(myHtml) ?>.require()is now filtered.vmandjhs2are always blocked. Add others viabanned_require.- Constructor options are backwards compatible — no changes needed.
NODE-JHS2/
├── index.js # Main engine module
├── package.json
├── README.md
├── LICENSE
├── .gitignore
├── CHANGELOG.md
├── demo-raw-mtime.js # Runnable demo: raw() sentinel + mtime hot reload
├── examples/
│ ├── https-server.js # Full middleware-based HTTPS server example
│ ├── server.js # Minimal HTTPS server example
│ └── views/
│ ├── index.jhs
│ └── partials/
│ └── nav.jhs
└── test/
└── engine.test.js # Unit tests (17 tests)
Pull requests are welcome. For major changes, open an issue first.
- Fork the repo
- Create your branch:
git checkout -b feature/my-feature - Commit:
git commit -m 'Add my feature' - Push:
git push origin feature/my-feature - Open a Pull Request
- jhs-instantiator — Hierarchical Instantiator companion module: progressively degrades multi-rank JHS templates down to plain HTML.
MIT — see LICENSE for details.