mutorjs 1.5.9 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,1665 +1,861 @@
1
- # Mutor.js — Official Documentation
2
-
3
- > **Version:** Current Stable
4
- > **Author:** Onah Victor
5
- > **Repository:** [github.com/allAboutJS/Mutor.js](https://github.com/allAboutJS/Mutor.js)
6
- > **Language:** TypeScript (zero runtime dependencies)
7
- > **License:** See repository
8
-
9
- ---
10
-
11
- ## Table of Contents
12
-
13
- 1. [Introduction](#1-introduction)
14
- 2. [Installation](#2-installation)
15
- 3. [Quick Start](#3-quick-start)
16
- 4. [Core Concepts](#4-core-concepts)
17
- 5. [Syntax Guide](#5-syntax-guide)
18
- 6. [Logic System](#6-logic-system)
19
- 7. [Expression System](#7-expression-system)
20
- 8. [Includes](#8-includes)
21
- 9. [Components](#9-components)
22
- 10. [Rendering APIs](#10-rendering-apis)
23
- 11. [Security Architecture](#11-security-architecture)
24
- 12. [Sandbox System](#12-sandbox-system)
25
- 13. [Property Access Validation](#13-property-access-validation)
26
- 14. [Namespace System](#14-namespace-system)
27
- 15. [Cache System](#15-cache-system)
28
- 16. [Configuration Reference](#16-configuration-reference)
29
- 17. [Delimiter Customization](#17-delimiter-customization)
30
- 18. [Whitespace Control](#18-whitespace-control)
31
- 19. [Escaping Rules](#19-escaping-rules)
32
- 20. [Error Handling](#20-error-handling)
33
- 21. [CLI](#21-cli)
34
- 22. [Performance](#22-performance)
35
- 23. [Environment Support](#23-environment-support)
36
- 24. [Browser Usage](#24-browser-usage)
37
- 25. [Server Usage](#25-server-usage)
38
- 26. [Advanced Examples](#26-advanced-examples)
39
- 27. [Best Practices](#27-best-practices)
40
- 28. [Security Best Practices](#28-security-best-practices)
41
- 29. [Real-World Use Cases](#29-real-world-use-cases)
42
- 30. [Design Tradeoffs](#30-design-tradeoffs)
43
- 31. [Future Roadmap](#31-future-roadmap)
44
- 32. [FAQ](#32-faq)
45
- 33. [API Reference](#33-api-reference)
46
-
47
- ---
48
-
49
- ## 1. Introduction
50
-
51
- Mutor.js is a secure, compiler-oriented template engine written entirely in TypeScript. It is designed for environments where template output correctness, security, and execution speed all matter simultaneously — conditions under which most template engines require a tradeoff.
52
-
53
- Mutor.js takes a different approach. Rather than performing string interpolation at runtime or relying on `eval`-based tricks, it processes templates through a full compiler pipeline: tokenization, AST construction, semantic analysis, security validation, code generation, and finally compilation into a native JavaScript function. That compiled function is then cached and reused for every subsequent render, making cold-path overhead a one-time cost.
54
-
55
- **Mutor.js is not:**
56
-
57
- - A frontend framework.
58
- - A JSX alternative.
59
- - An SSR framework or a React-like rendering system.
60
-
61
- It is a template engine. It accepts a template string and a context object. It returns a rendered string. That output can be HTML, plain text, XML, or any other string-based format. What you build on top of Mutor.js — a static site generator, a server-side rendering layer, an email renderer — is your concern. Mutor.js concerns itself exclusively with doing that one job securely and fast.
62
-
63
- **Mutor.js is:**
64
-
65
- - A zero-dependency, TypeScript-native template engine.
66
- - A compiler that produces JavaScript functions, not an interpreter.
67
- - A security-first system with layered compile-time and runtime protection.
68
- - A configurable engine that does not assume your use case.
69
-
70
- ---
71
-
72
- ## 2. Installation
73
-
74
- Mutor.js is distributed as an npm package. It requires Node.js 14+ or any ES6-compatible runtime.
75
-
76
- ```bash
77
- npm install mutorjs
78
- ```
79
-
80
- ```bash
81
- yarn add mutorjs
82
- ```
83
-
84
- ```bash
85
- pnpm add mutorjs
86
- ```
1
+ # Mutor.js
87
2
 
88
- Both CommonJS and ESM module formats are supported. The package ships pre-compiled TypeScript output with bundled type declarations.
3
+ Mutor.js is a small, fast templating engine for people who want templates to be expressive without turning them into a second application runtime.
89
4
 
90
- ### Subpath Exports
91
-
92
- Mutor.js exposes two entry points:
93
-
94
- | Import Path | Description |
95
- |---|---|
96
- | `mutorjs` | Universal entry point. Safe for both browser and Node.js environments. Does not include file-system APIs. |
97
- | `mutorjs/server` | Server-only entry point. Includes `renderFile` and file-system-dependent functionality. Do not import this in browser bundles. |
98
-
99
- ```js
100
- // Universal (browser + server)
101
- import Mutor from "mutorjs";
102
-
103
- // Server-only
104
- import Mutor from "mutorjs/server";
105
- ```
5
+ It gives you interpolation, conditionals, loops, partials, file rendering, async values, escaping, and a guarded expression system. It does not ship layout inheritance on purpose. In Mutor, complex pages are built from partials/components and the right context.
106
6
 
107
- ---
108
-
109
- ## 3. Quick Start
110
-
111
- ### Basic Render
112
-
113
- ```js
7
+ ```ts
114
8
  import Mutor from "mutorjs";
115
9
 
116
10
  const mutor = new Mutor();
117
11
 
118
- const output = mutor.render(
119
- `<h1>{{ greeting }}, {{ user.name }}!</h1>`,
120
- { greeting: "Hello", user: { name: "Onah" } }
121
- );
12
+ const html = mutor.render("Hello, {{ user.name }}.", {
13
+ user: { name: "Ada" },
14
+ });
122
15
 
123
- console.log(output);
124
- // <h1>Hello, Onah!</h1>
16
+ console.log(html); // Hello, Ada.
125
17
  ```
126
18
 
127
- ### Compile Once, Execute Many Times
128
-
129
- ```js
130
- import Mutor from "mutorjs";
131
-
132
- const mutor = new Mutor();
133
-
134
- const fn = mutor.compile(`<p>{{ item.title }}</p>`);
19
+ ## Install
135
20
 
136
- const items = [
137
- { title: "Alpha" },
138
- { title: "Beta" },
139
- { title: "Gamma" },
140
- ];
141
-
142
- const rendered = items.map(item => fn({ item }));
21
+ ```sh
22
+ npm install mutorjs
143
23
  ```
144
24
 
145
- ### File Rendering (Server Only)
146
-
147
- ```js
148
- import Mutor from "mutorjs/server";
25
+ ```sh
26
+ yarn add mutorjs
27
+ ```
149
28
 
150
- const mutor = new Mutor();
29
+ ```sh
30
+ pnpm add mutorjs
31
+ ```
151
32
 
152
- const html = await mutor.renderFile("./views/index.html", {
153
- title: "Dashboard",
154
- user: req.user,
155
- });
33
+ ## Why Mutor
156
34
 
157
- res.send(html);
158
- ```
35
+ - Small template language with familiar JavaScript-like expressions.
36
+ - HTML escaping is on by default.
37
+ - Dangerous properties such as `constructor`, `prototype`, and `__proto__` are blocked.
38
+ - Function calls are restricted by default.
39
+ - Partials/components are first-class.
40
+ - Server rendering supports file includes and directory builds.
41
+ - Async values work through `Mutor::await`.
42
+ - Cache entries can be inspected, reset, or invalidated.
159
43
 
160
- ### Component Rendering
44
+ ## Quick Start
161
45
 
162
- ```js
46
+ ```ts
163
47
  import Mutor from "mutorjs";
164
48
 
165
49
  const mutor = new Mutor();
166
50
 
167
- mutor.registerComponent("card", `
168
- <div class="card">
169
- <h2>{{ card.title }}</h2>
170
- <p>{{ card.body }}</p>
171
- </div>
172
- `);
51
+ const template = `
52
+ {{ if user.admin }}
53
+ <strong>{{ user.name }}</strong>
54
+ {{ else }}
55
+ <span>{{ user.name }}</span>
56
+ {{ endif }}
57
+ `;
173
58
 
174
- const output = mutor.renderComponent("card", {
175
- card: { title: "Notice", body: "This is a component." }
59
+ const html = mutor.render(template, {
60
+ user: { name: "Grace", admin: true },
176
61
  });
177
62
  ```
178
63
 
179
- ---
180
-
181
- ## 4. Core Concepts
64
+ By default, strings are escaped before they are written:
182
65
 
183
- ### Templates Are Compiled, Not Interpreted
184
-
185
- Every template processed by Mutor.js is compiled into a JavaScript function. This happens once per unique template string. On subsequent calls with the same template, the compiled function is retrieved from cache and invoked directly. There is no parsing, no AST traversal, no validation on the hot path — only a function call.
186
-
187
- This distinction matters at scale. A template engine that interprets its AST on every render pays a traversal cost proportional to the template's complexity on every single request. Mutor.js pays that cost exactly once.
66
+ ```ts
67
+ mutor.render("{{ value }}", {
68
+ value: "<script>alert('nope')</script>",
69
+ });
70
+ // &lt;script&gt;alert(&#39;nope&#39;)&lt;/script&gt;
71
+ ```
188
72
 
189
- ### Security Is Architectural, Not Bolt-On
73
+ Disable escaping only when you know the output is already safe:
190
74
 
191
- Most template engines treat security as a configuration layer — a set of filters you can optionally enable. In Mutor.js, security is woven into the compilation pipeline itself. The semantic analysis phase validates all property access, identifies potentially unsafe patterns, and either rewrites them into guarded forms or rejects them outright. By the time a compiled function reaches runtime, the security decisions have already been made.
75
+ ```ts
76
+ const mutor = new Mutor({ autoEscape: false });
77
+ ```
192
78
 
193
- This means you cannot accidentally bypass security by misconfiguring a flag. The dangerous paths do not exist in the compiled output.
79
+ ## Template Syntax
194
80
 
195
- ### Context Is an Isolated Scope
81
+ Mutor expressions live inside `{{ ... }}`.
196
82
 
197
- Templates execute inside a restricted scope. The context object you provide is the entire world available to the template. There is no access to the global object, no access to `process`, no access to `require`, no access to `window`. The sandbox is enforced at code generation time: the generated function receives only what you pass in.
83
+ ```html
84
+ <h1>{{ title }}</h1>
85
+ ```
198
86
 
199
- ### Zero Dependencies
87
+ ### Comments
200
88
 
201
- Mutor.js has no runtime dependencies. It is self-contained TypeScript. This is a deliberate architectural constraint. Every dependency you introduce to a security-critical system is a potential attack surface. Mutor.js owns its entire stack — tokenizer, parser, AST, code generator, runtime, cache — and each of those components is auditable without pulling in third-party code.
89
+ Comments are removed from the rendered output.
202
90
 
203
- ---
91
+ ```html
92
+ {{# This will not render }}
93
+ ```
204
94
 
205
- ## 5. Syntax Guide
95
+ ### Whitespace Control
206
96
 
207
- Mutor.js uses a structured tag syntax. It is not raw JavaScript embedded in HTML. The template language is a defined, bounded DSL that the compiler can reason about completely.
97
+ Use `~` next to a tag to trim whitespace on that side.
208
98
 
209
- ### Interpolation
99
+ ```html
100
+ Hello {{~ name ~}} !
101
+ ```
210
102
 
211
- The primary mechanism for outputting values is the interpolation block:
103
+ With `name = "Ada"`, that renders:
212
104
 
213
105
  ```html
214
- {{ expression }}
106
+ HelloAda!
215
107
  ```
216
108
 
217
- The result of the expression is HTML-escaped by default and inserted into the output at that position.
109
+ ### Escaped Tags
110
+
111
+ Prefix an opening tag with the escape delimiter when you want the tag to appear as text.
218
112
 
219
113
  ```html
220
- <p>{{ user.name }}</p>
221
- <p>{{ product.price * 1.1 }}</p>
222
- <p>{{ user.bio ?? "No bio provided." }}</p>
114
+ \{{ name }}
223
115
  ```
224
116
 
225
- ### Whitespace Trimming
226
-
227
- By default, Mutor.js preserves whitespace around tags exactly as written. The `~` directive instructs the engine to strip whitespace (including newlines) from the relevant side of the tag.
117
+ That renders:
228
118
 
229
119
  ```html
230
- {{~ expression }} <!-- trim left whitespace -->
231
- {{ expression ~}} <!-- trim right whitespace -->
232
- {{~ expression ~}} <!-- trim both sides -->
120
+ {{ name }}
233
121
  ```
234
122
 
235
- This is particularly useful when working with `for` loops and `if` blocks to prevent unwanted blank lines in output.
123
+ If `preserveEscapeDelimiter` is enabled, the escape delimiter is kept too.
236
124
 
237
- ### Comments
125
+ ## Values And Literals
238
126
 
239
- Comments use the `#` prefix inside the opening delimiter:
127
+ Mutor supports simple literals:
240
128
 
241
129
  ```html
242
- {{# This is a comment. It produces no output. }}
130
+ {{ "hello" }}
131
+ {{ 'hello' }}
132
+ {{ `hello` }}
133
+ {{ 42 }}
134
+ {{ 3.14 }}
135
+ {{ 1e-3 }}
136
+ {{ true }}
137
+ {{ false }}
138
+ {{ null }}
139
+ {{ undefined }}
243
140
  ```
244
141
 
245
- Comments are multiline by default:
142
+ Mutor does not allow JavaScript object literals, array literals, function literals, arrow functions, or constructors inside templates:
246
143
 
247
144
  ```html
248
- {{#
249
- This is a block comment.
250
- It spans multiple lines.
251
- None of this appears in output.
252
- }}
145
+ {{ { name: "Ada" } }} <!-- not allowed -->
146
+ {{ [1, 2, 3] }} <!-- not allowed -->
147
+ {{ function() {} }} <!-- not allowed -->
148
+ {{ () => {} }} <!-- not allowed -->
149
+ {{ new User() }} <!-- not allowed -->
253
150
  ```
254
151
 
255
- ### Escaped Opening Tag
256
-
257
- To output the literal opening delimiter (e.g., `{{`) without triggering template parsing, prefix it with a backslash:
152
+ When you need an array or object value in a template expression, pass it in the context or create it from JSON:
258
153
 
259
154
  ```html
260
- \{{ this is rendered literally as {{ }}
155
+ {{ JSON::parse("[1,2,3]")[0] }}
156
+ {{ JSON::parse('{"name":"Ada"}').name }}
261
157
  ```
262
158
 
263
- Whether the escape character itself is preserved in the output is controlled by `keepOpeningTagEscapeDelimiter` in configuration.
264
-
265
- ### Tag Summary
266
-
267
- | Syntax | Purpose |
268
- |---|---|
269
- | `{{ expr }}` | Interpolation (auto-escaped) |
270
- | `{{~ expr }}` | Interpolation, trim left |
271
- | `{{ expr ~}}` | Interpolation, trim right |
272
- | `{{~ expr ~}}` | Interpolation, trim both |
273
- | `{{# ... }}` | Comment (no output) |
274
- | `\{{` | Escaped literal delimiter |
275
- | `{{ if ... }}` | Conditional block open |
276
- | `{{ else if ... }}` | Conditional branch |
277
- | `{{ else }}` | Default branch |
278
- | `{{ for x of ... }}` | Iteration (value) |
279
- | `{{ for x in ... }}` | Iteration (key) |
280
- | `{{ end }}` | Closes any open block |
159
+ Passing values through context is usually cleaner:
281
160
 
282
- ---
283
-
284
- ## 6. Logic System
161
+ ```ts
162
+ mutor.render("{{ user.name }}", {
163
+ user: { name: "Ada" },
164
+ });
165
+ ```
285
166
 
286
- Mutor.js provides structured control flow via `if`, `else if`, `else`, `for`, and `end` tags. These are not arbitrary JavaScript — they are a fixed set of recognized logic directives that the parser handles explicitly.
167
+ ## Expressions
287
168
 
288
- ### Conditionals
169
+ Mutor expressions are intentionally familiar:
289
170
 
290
171
  ```html
291
- {{ if condition }}
292
- ...
293
- {{ end }}
294
- ```
172
+ {{ user.name }}
173
+ {{ user?.profile?.name }}
174
+ {{ user["name"] }}
175
+ {{ count + 1 }}
176
+ {{ price * quantity }}
177
+ {{ score >= 80 }}
178
+ {{ admin && active }}
179
+ {{ name ?? "Anonymous" }}
180
+ {{ admin ? "Admin" : "Member" }}
181
+ ```
182
+
183
+ Supported expression pieces include:
184
+
185
+ - property access with `.` and `[]`
186
+ - optional chaining with `?.`
187
+ - arithmetic operators such as `+`, `-`, `*`, `/`, `%`, `**`
188
+ - comparison operators such as `>`, `<`, `>=`, `<=`
189
+ - equality operators `==` and `!=`
190
+ - logical operators `&&`, `||`, `??`
191
+ - bitwise operators `&`, `|`, `^`, `>>`, `<<`
192
+ - unary operators `!`, `+`, `-`
193
+ - ternaries with `condition ? yes : no`
194
+ - grouping with parentheses
195
+
196
+ ## Conditionals
295
197
 
296
198
  ```html
297
- {{ if user.role == "admin" }}
298
- <a href="/admin">Admin Panel</a>
299
- {{ else if user.role == "moderator" }}
300
- <a href="/moderate">Moderation Queue</a>
199
+ {{ if user.admin }}
200
+ <strong>{{ user.name }}</strong>
201
+ {{ else if user.active }}
202
+ <span>{{ user.name }}</span>
301
203
  {{ else }}
302
- <a href="/dashboard">Dashboard</a>
303
- {{ end }}
204
+ <em>Inactive user</em>
205
+ {{ endif }}
304
206
  ```
305
207
 
306
- Conditions are arbitrary expressions. Any expression that evaluates to a truthy or falsy value is valid. Comparison operators, boolean operators, nullish coalescing, optional chaining, and ternaries are all valid inside condition expressions.
307
-
308
- ```html
309
- {{ if items.length > 0 && user.isActive }}
310
- ...
311
- {{ end }}
312
- ```
208
+ ## Loops
313
209
 
314
- ### Iteration
315
-
316
- Mutor.js supports two forms of `for` iteration, mirroring JavaScript's `for...of` and `for...in` semantics:
317
-
318
- **`for...of` — iterate over values:**
210
+ Use `of` for arrays and iterable values:
319
211
 
320
212
  ```html
321
213
  {{ for item of items }}
322
- <li>{{ item.name }}</li>
323
- {{ end }}
214
+ <li>{{ item }}</li>
215
+ {{ endfor }}
324
216
  ```
325
217
 
326
- **`for...in` iterate over keys:**
218
+ You can add an optional second binding for the index:
327
219
 
328
220
  ```html
329
- {{ for key in record }}
330
- <dt>{{ key }}</dt>
331
- {{ end }}
221
+ {{ for item, index of items }}
222
+ <li>{{ index }}: {{ item }}</li>
223
+ {{ endfor }}
332
224
  ```
333
225
 
334
- **Nested loops:**
226
+ Use `in` for object keys:
335
227
 
336
228
  ```html
337
- {{ for category of categories }}
338
- <section>
339
- <h2>{{ category.name }}</h2>
340
- {{ for product of category.products }}
341
- <p>{{ product.title }} — {{ product.price }}</p>
342
- {{ end }}
343
- </section>
344
- {{ end }}
345
- ```
346
-
347
- ### Block Termination
348
-
349
- Every `if` block and every `for` block must be closed with `{{ end }}`. Unclosed blocks are a compile-time error. The compiler tracks block depth and reports the location (line and column) of the unclosed block.
350
-
351
- > **Note:** There is no `break` or `continue` directive in the current version. Complex filtering logic belongs in the context object, not the template.
352
-
353
- ---
354
-
355
- ## 7. Expression System
356
-
357
- Mutor.js expressions are a defined subset of JavaScript expressions, parsed by Mutor.js's own expression parser — meaning the engine controls exactly what is and is not expressible.
358
-
359
- ### Supported Expression Constructs
360
-
361
- | Category | Examples |
362
- |---|---|
363
- | Arithmetic | `a + b`, `price * 1.2`, `total / count`, `n % 2`, `2 ** 8` |
364
- | Comparison | `a == b`, `a != b`, `a > b`, `a >= b`, `a < b`, `a <= b` |
365
- | Boolean | `a && b`, `a \|\| b`, `!flag` |
366
- | Ternary | `condition ? valueA : valueB` |
367
- | Nullish Coalescing | `value ?? "default"` |
368
- | Optional Chaining | `user?.address?.city` |
369
- | Property Access | `obj.prop`, `obj["key"]` |
370
- | Array Access | `arr[0]`, `arr[index]` |
371
- | Namespace Access | `Math::floor(n)`, `JSON::stringify(data)` |
372
- | Bitwise | `a & b`, `a \| b`, `a ^ b`, `a >> b`, `a << b` |
373
- | Function Calls | `fn(args)` — only when `allowFnCalls: true` |
374
- | String Literals | `'single'`, `"double"`, `` `backtick` `` |
375
-
376
- ### Operator Precedence
377
-
378
- Mutor.js follows standard JavaScript operator precedence:
379
-
380
- | Precedence (high → low) | Operators |
381
- |---|---|
382
- | Unary | `!`, unary `-` |
383
- | Exponentiation | `**` |
384
- | Multiplicative | `*`, `/`, `%` |
385
- | Additive | `+`, `-` |
386
- | Shift | `<<`, `>>` |
387
- | Relational | `<`, `>`, `<=`, `>=` |
388
- | Equality | `==`, `!=` |
389
- | Bitwise AND | `&` |
390
- | Bitwise XOR | `^` |
391
- | Bitwise OR | `\|` |
392
- | Logical AND | `&&` |
393
- | Logical OR | `\|\|` |
394
- | Nullish Coalescing | `??` |
395
- | Conditional (Ternary) | `? :` |
396
-
397
- When in doubt, use explicit parentheses:
229
+ {{ for key in user }}
230
+ <p>{{ key }}</p>
231
+ {{ endfor }}
232
+ ```
233
+
234
+ You can add an optional second binding for the value:
398
235
 
399
236
  ```html
400
- {{ (a + b) * c }}
401
- {{ user.age >= 18 ? "adult" : "minor" }}
237
+ {{ for key, value in user }}
238
+ <p>{{ key }} = {{ value }}</p>
239
+ {{ endfor }}
402
240
  ```
403
241
 
404
- ### Strings
405
-
406
- String literals support single quotes, double quotes, and backticks. Backtick strings are treated as static strings — template interpolation (`${...}`) inside strings does not evaluate expressions. The backtick form exists to allow strings containing both single and double quotes conveniently.
242
+ `break` and `continue` are available inside loops:
407
243
 
408
244
  ```html
409
- {{ "Hello, world" }}
410
- {{ 'It\'s fine' }}
411
- {{ `This has "quotes" and 'apostrophes'` }}
245
+ {{ for item of items }}
246
+ {{ if item.hidden }}{{ continue }}{{ endif }}
247
+ {{ item.name }}
248
+ {{ if item.last }}{{ break }}{{ endif }}
249
+ {{ endfor }}
412
250
  ```
413
251
 
414
- ### Function Calls
252
+ ## Partials And Composition
415
253
 
416
- Function calls are disabled by default. This prevents templates from invoking arbitrary functions that may exist on context objects, which is the correct default for most use cases.
254
+ Mutor does not have layouts. That is a design choice.
417
255
 
418
- To enable function calls:
256
+ Instead, build pages from partials/components and pass the context they need. This keeps the core smaller and makes composition explicit.
419
257
 
420
- ```js
421
- const mutor = new Mutor({ allowFnCalls: true });
422
- ```
423
-
424
- When enabled, calls like `item.name.toUpperCase()` or `Math::floor(price)` are permitted. Namespace function calls (`Math::floor`, etc.) are permitted regardless of `allowFnCalls` because they are routed through the controlled namespace system.
258
+ ```ts
259
+ import Mutor from "mutorjs";
425
260
 
426
- > **Warning:** Enabling `allowFnCalls` expands the trusted surface area of your templates. Ensure the context objects you pass contain only functions you intend to expose. Do not pass raw service objects or request objects as top-level context when `allowFnCalls` is enabled.
261
+ const mutor = new Mutor({ autoEscape: false });
427
262
 
428
- ---
263
+ mutor.registerComponent(
264
+ "shell",
265
+ `
266
+ <!doctype html>
267
+ <html>
268
+ <head><title>{{ title }}</title></head>
269
+ <body>
270
+ {{ Mutor::include("nav") }}
271
+ <main>{{ content }}</main>
272
+ </body>
273
+ </html>
274
+ `,
275
+ );
429
276
 
430
- ## 8. Includes
277
+ mutor.registerComponent(
278
+ "nav",
279
+ `
280
+ <nav>
281
+ {{ for item of nav }}
282
+ <a href="{{ item.href }}">{{ item.label }}</a>
283
+ {{ endfor }}
284
+ </nav>
285
+ `,
286
+ );
431
287
 
432
- Includes allow a template to embed another template file at a specific location in its output. The included template is compiled and cached independently, sharing the same cache as the parent instance.
288
+ const page = mutor.render('{{ Mutor::include("shell") }}', {
289
+ title: "Dashboard",
290
+ content: "<h1>Welcome</h1>",
291
+ nav: [
292
+ { href: "/", label: "Home" },
293
+ { href: "/settings", label: "Settings" },
294
+ ],
295
+ });
296
+ ```
433
297
 
434
- ### Syntax
298
+ If no context is passed to an include, it inherits the parent context:
435
299
 
436
300
  ```html
437
- {{ Mutor::include("./partials/header.html") }}
438
- {{ Mutor::include("./partials/nav.html", navLinks) }}
301
+ {{ Mutor::include("profile-card") }}
439
302
  ```
440
303
 
441
- When no context argument is provided, the include inherits the current rendering context automatically. For direct access to the current context, use `Mutor::$$context`:
304
+ Pass a different context when the partial should render against a smaller or different value:
442
305
 
443
306
  ```html
444
- {{ Mutor::include("./partials/footer.html", Mutor::$$context) }}
307
+ {{ Mutor::include("profile-card", user) }}
445
308
  ```
446
309
 
447
- ### Include Resolution
310
+ Inside any template or partial, the current context is available as `Mutor::$$context`:
448
311
 
449
- Include paths are resolved relative to the file being rendered when using `renderFile`. In a browser environment, include paths are treated as names of components registered with `mutor.registerComponent`. Attempting to include a non-registered component, or a file that does not exist, throws an error.
312
+ ```html
313
+ <pre>{{ JSON::stringify(Mutor::$$context, 2) }}</pre>
314
+ ```
450
315
 
451
- ### Circular Include Detection
316
+ That is useful for generic components that render the value they were given:
452
317
 
453
- Mutor.js tracks the include chain across all recursive resolutions. If a template attempts to include a file already in the current include stack, the engine throws rather than entering infinite recursion:
318
+ ```ts
319
+ mutor.registerComponent("badge", `<span>{{ Mutor::$$context }}</span>`);
454
320
 
455
- ```
456
- MutorError: Circular include detected: ./partials/a.html → ./partials/b.html → ./partials/a.html
321
+ mutor.render('{{ Mutor::include("badge", "New") }}', {});
322
+ // <span>New</span>
457
323
  ```
458
324
 
459
- ---
325
+ ## Server Rendering
460
326
 
461
- ## 9. Components
327
+ Use the server entry when templates live on disk.
462
328
 
463
- Components are named templates registered on a Mutor instance. They are the browser-compatible alternative to file-based includes, and are also useful on the server when you want to manage templates programmatically.
464
-
465
- ### Registering a Component
466
-
467
- ```js
468
- mutor.registerComponent("alert", `
469
- <div class="alert alert--{{ alert.type }}">
470
- <strong>{{ alert.title }}</strong>
471
- <p>{{ alert.message }}</p>
472
- </div>
473
- `);
474
- ```
329
+ ```ts
330
+ import Mutor from "mutorjs/server";
475
331
 
476
- ### Rendering a Component
332
+ const mutor = new Mutor({
333
+ rootDir: "./views",
334
+ });
477
335
 
478
- ```js
479
- const html = mutor.renderComponent("alert", {
480
- alert: {
481
- type: "error",
482
- title: "Validation Failed",
483
- message: "Email address is required.",
484
- }
336
+ const html = mutor.renderFile("./pages/home.html", {
337
+ title: "Home",
485
338
  });
486
339
  ```
487
340
 
488
- ### Components vs. Includes
489
-
490
- | Feature | Components | Includes |
491
- |---|---|---|
492
- | Template source | In-memory string | File system |
493
- | Browser-compatible | Yes | No |
494
- | Circular detection | Yes | Yes |
495
- | Cache | Same instance cache | Same instance cache |
496
- | Context handling | Explicit argument | Explicit or `$$context` |
497
-
498
- Components are compiled and cached on first `renderComponent` call. Subsequent calls hit the cache directly.
499
-
500
- ---
341
+ Server includes resolve relative to the file currently being rendered:
501
342
 
502
- ## 10. Rendering APIs
503
-
504
- ### `compile(template: string): (context: object) => string`
505
-
506
- Compiles a template string into an executable function. The resulting function accepts a context object and returns the rendered string.
343
+ ```html
344
+ <!-- pages/home.html -->
345
+ {{ Mutor::include("../partials/header.html") }}
507
346
 
508
- ```js
509
- const fn = mutor.compile(`<title>{{ page.title }}</title>`);
510
- const output = fn({ page: { title: "Home" } });
347
+ <h1>{{ title }}</h1>
511
348
  ```
512
349
 
513
- Calling `compile` with the same template string twice returns the cached compiled function.
514
-
515
- ### `render(template: string, context: object): string`
350
+ Use the `@/` alias to resolve from `rootDir`:
516
351
 
517
- Convenience wrapper around `compile`. Compiles the template if not already cached, then immediately invokes it with the provided context.
518
-
519
- ```js
520
- const output = mutor.render(`<p>{{ msg }}</p>`, { msg: "Hello" });
352
+ ```html
353
+ {{ Mutor::include("@/partials/header.html") }}
521
354
  ```
522
355
 
523
- ### `renderFile(path: string, context: object): Promise<string>`
356
+ ### Build A Directory
524
357
 
525
- Server-only. Reads the template file from disk, compiles it, and renders it with the given context. The compiled result is cached by resolved file path — repeated calls with the same path do not re-read the file or re-compile.
358
+ `buildDir` renders matching template files and copies everything else.
526
359
 
527
- ```js
528
- const html = await mutor.renderFile("./views/home.html", context);
360
+ ```ts
361
+ await mutor.buildDir("./site", "./dist", {
362
+ title: "Mutor Site",
363
+ });
529
364
  ```
530
365
 
531
- > This method is not available when importing from `mutorjs` (the universal entry point). Import from `mutorjs/server` to access it.
532
-
533
- ### `registerComponent(name: string, template: string): void`
534
-
535
- Registers a named template string as a component on the current Mutor instance. Compilation is deferred to first render.
536
-
537
- ### `renderComponent(name: string, context: object): string`
538
-
539
- Renders a previously registered component by name. Throws `MutorComponentError` if the component has not been registered.
540
-
541
- ### Instance Isolation
542
-
543
- Each `Mutor` instance maintains its own compilation cache and component registry. Two instances do not share state. This is important in multi-tenant or per-request environments where isolation between rendering contexts is required.
366
+ By default, `.html` and `.txt` files are rendered. `node_modules` and `.git` are skipped.
544
367
 
545
- ---
368
+ ### Compile A Directory
546
369
 
547
- ## 11. Security Architecture
370
+ `compileDir` precompiles matching files into the cache.
548
371
 
549
- Security in Mutor.js is not an optional layer. It is the reason the compilation pipeline exists in the form it does.
550
-
551
- ### Threat Model
552
-
553
- Mutor.js is designed for environments where the template may reference user-controlled data, and in some configurations, where the template itself may originate from less-trusted sources. The primary threats are:
554
-
555
- - **Cross-site scripting (XSS):** Injecting HTML or JavaScript through interpolated values.
556
- - **Prototype pollution access:** Using `__proto__`, `constructor`, or `prototype` to escape the context object and reach dangerous properties.
557
- - **Unintended function invocation:** Calling methods on context objects that were not intended to be exposed to templates.
558
- - **Global scope leakage:** Accessing `process`, `window`, `global`, `require`, or other environmental globals.
559
- - **Computed property injection:** Using dynamic property names to bypass static property validation.
560
-
561
- ### Defense Layers
562
-
563
- Mutor.js employs defense in depth across three distinct phases:
564
-
565
- **Layer 1 — Compile-time analysis**
566
-
567
- Every property access in the template is analyzed during compilation. Static accesses (`obj.prop`, `obj["key"]`) are validated against the forbidden property list. Dynamic accesses (`obj[expression]`) are rewritten into calls to a runtime guard helper. Access to `__proto__`, `constructor`, or `prototype` is unconditionally rejected — there is no configuration override.
568
-
569
- **Layer 2 — Code generation sandboxing**
570
-
571
- The generated function runs inside a closed scope. It receives only the context object as its argument. It has no reference to any global, no `this` binding to the environment, and no access to `require` or `import`.
572
-
573
- **Layer 3 — Runtime context validation**
574
-
575
- Before the compiled function executes, Mutor.js validates the context object. Prototype chains are checked, and objects with polluted prototypes or non-standard getters and setters on built-in prototype properties are rejected. This prevents an attacker from crafting a context object that passes compile-time checks but exploits runtime behavior.
576
-
577
- ### Output Escaping
578
-
579
- All interpolated values are HTML-escaped by default:
580
-
581
- | Character | Escaped As |
582
- |---|---|
583
- | `&` | `&amp;` |
584
- | `<` | `&lt;` |
585
- | `>` | `&gt;` |
586
- | `"` | `&quot;` |
587
- | `'` | `&#39;` |
588
-
589
- Auto-escaping is controlled by the `autoEscape` configuration option. Setting `autoEscape: false` disables this behavior globally.
590
-
591
- > **Warning:** Do not disable `autoEscape` unless you have a specific, justified reason and you understand the full implications. Disabled auto-escaping in combination with user-controlled context data is an XSS vector.
592
-
593
- ---
594
-
595
- ## 12. Sandbox System
596
-
597
- The sandbox ensures compiled templates cannot access anything outside the explicitly provided context.
598
-
599
- ### What the Sandbox Prevents
600
-
601
- - Access to JavaScript globals (`window`, `process`, `global`, `require`, `globalThis`)
602
- - Access to `this` in a way that reaches the outer environment
603
- - Access to prototype chain escape vectors (`__proto__`, `constructor`, `prototype`)
604
-
605
- ### Function Call Sandboxing
606
-
607
- When `allowFnCalls: false` (the default), the compiler rejects any expression that involves a function invocation on a non-namespace identifier. `user.name.toUpperCase()` would be rejected. `Math::floor(price)` is permitted because it routes through the namespace system.
608
-
609
- When `allowFnCalls: true`, method calls on context properties are permitted. The sandbox still prevents access to globals; it only allows invocation of functions that exist on the context object you explicitly pass in.
610
-
611
- ### Forbidden Property List
612
-
613
- The default forbidden properties are `__proto__`, `constructor`, and `prototype`. These are always enforced and cannot be removed. You can extend the list for your specific threat model:
614
-
615
- ```js
616
- const mutor = new Mutor({
617
- forbiddenProps: new Set(["__proto__", "constructor", "prototype", "valueOf", "toString"]),
618
- });
372
+ ```ts
373
+ await mutor.compileDir("./views");
619
374
  ```
620
375
 
621
- > **Note:** If you add a property to both `allowedProps` and `forbiddenProps`, `allowedProps` takes precedence and the property will not be blocked.
622
-
623
- ---
376
+ After that, `renderFile` can use cached compiled templates.
624
377
 
625
- ## 13. Property Access Validation
378
+ ## Async Templates
626
379
 
627
- ### Static Property Access
380
+ Use `Mutor::await` when a value may be a promise.
628
381
 
629
- ```js
630
- user.name // static — property name known at compile time
631
- user["email"] // static — property name known at compile time
382
+ ```html
383
+ Hello, {{ (Mutor::await(userPromise)).name }}
632
384
  ```
633
385
 
634
- For static access, the compiler checks the property name against the forbidden list at compile time. If the name is forbidden, compilation fails immediately with a clear error.
635
-
636
- ### Dynamic Property Access
386
+ Use the async render methods for templates that use `Mutor::await`:
637
387
 
638
- ```js
639
- obj[someVariable] // dynamic property name not known until runtime
388
+ ```ts
389
+ const html = await mutor.renderAsync(
390
+ "Hello, {{ Mutor::await(namePromise) }}",
391
+ {
392
+ namePromise: Promise.resolve("Ada"),
393
+ },
394
+ );
640
395
  ```
641
396
 
642
- Dynamic access cannot be fully validated at compile time. Mutor.js handles this by rewriting all dynamic property accesses through a runtime guard that validates the resolved property name against the forbidden list before performing the access. Even if an attacker sets `someVariable = "__proto__"` at runtime, the guard catches it.
643
-
644
- ---
397
+ Server and component APIs also have async forms:
645
398
 
646
- ## 14. Namespace System
647
-
648
- The namespace system provides controlled access to JavaScript built-in utilities inside templates without exposing the full global scope.
649
-
650
- ### Syntax
651
-
652
- ```html
653
- {{ Namespace::identifier }}
654
- {{ Namespace::method(args) }}
399
+ ```ts
400
+ await mutor.renderFileAsync("./page.html", context);
401
+ await mutor.renderAsyncComponent("card", context);
655
402
  ```
656
403
 
657
- The `::` operator is Mutor.js-specific. It does not exist in standard JavaScript. It is parsed by Mutor.js and translated into safe, controlled lookups at code generation time.
658
-
659
- ### Available Namespaces
404
+ Good to know: `Mutor::await` makes the compiled template async. Prefer the async APIs for templates that use it.
660
405
 
661
- | Namespace | Accessible Members |
662
- |---|---|
663
- | `Math` | All `Math` static methods and properties (`floor`, `ceil`, `round`, `max`, `min`, `abs`, `sqrt`, `pow`, `PI`, etc.) |
664
- | `JSON` | `stringify`, `parse` |
665
- | `Object` | `keys`, `values`, `entries`, `assign`, `fromEntries` |
666
- | `Array` | `isArray`, `from` |
667
- | `String` | `fromCharCode` |
668
- | `Number` | `isInteger`, `isFinite`, `isNaN`, `parseInt`, `parseFloat` |
669
- | `Date` | `now`, `getFullYear` (static forms) |
670
- | `Mutor` | `include`, `$$context` (engine directives) |
406
+ ## Namespaces
671
407
 
672
- ### Examples
408
+ Namespaces are trusted helper groups available from templates. Namespace calls are allowed even when normal function calls are disabled.
673
409
 
674
410
  ```html
675
- <p>Price: {{ Math::floor(product.price) }} USD</p>
676
- <p>Keys: {{ Object::keys(record).length }}</p>
677
- <p>Data: {{ JSON::stringify(debug.payload) }}</p>
678
- <p>Valid: {{ Array::isArray(items) ? "yes" : "no" }}</p>
411
+ {{ Math::max(10, 20) }}
412
+ {{ Array::range(1, 3) }}
413
+ {{ Object::keys(user) }}
414
+ {{ JSON::stringify(user) }}
415
+ {{ String::capitalize(name) }}
416
+ {{ Date::iso() }}
417
+ {{ URL::encode(query) }}
679
418
  ```
680
419
 
681
- ### The `Mutor` Namespace
420
+ Useful built-ins include:
682
421
 
683
- The `Mutor` namespace is reserved for engine directives:
684
-
685
- - `Mutor::include(path)` includes a template file, inheriting the current context
686
- - `Mutor::include(path, context)` includes a template file with an explicit context
687
- - `Mutor::$$context` a reference to the current rendering context object
688
-
689
- ---
690
-
691
- ## 15. Cache System
692
-
693
- Every `Mutor` instance maintains an internal LRU (Least Recently Used) cache of compiled template functions.
694
-
695
- ### Cache Behavior
696
-
697
- - Templates are cached by their source string (for `compile` and `render`) or by their resolved absolute file path (for `renderFile`).
698
- - Cache lookups are O(1).
699
- - When a template is accessed, it is promoted to the most-recently-used position.
700
- - When the cache reaches its configured `maxSize`, the least-recently-used entry is evicted.
701
-
702
- ### Cache Configuration
422
+ | Namespace | Examples |
423
+ | --- | --- |
424
+ | `JSON` | `stringify`, `parse` |
425
+ | `Object` | `keys`, `values`, `entries`, `hasOwn`, `fromEntries`, `pick`, `omit` |
426
+ | `Array` | `isArray`, `from`, `of`, `unique`, `compact`, `chunk`, `range` |
427
+ | `Number` | `isFinite`, `isNaN`, `isInteger`, `parseInt`, `parseFloat`, `clamp`, `toFixed`, `random` |
428
+ | `String` | `fromCharCode`, `capitalize` |
429
+ | `Math` | `abs`, `floor`, `ceil`, `round`, `sqrt`, `pow`, `max`, `min`, `PI` |
430
+ | `Date` | `now`, `parse`, `new`, `iso`, `timestamp` |
431
+ | `Boolean` | `valueOf` |
432
+ | `URL` | `encode`, `decode` |
433
+ | `Mutor` | `include`, `await`, `$$context` |
434
+
435
+ ## Security Model
436
+
437
+ Mutor is designed to keep templates useful without handing them the whole JavaScript runtime.
438
+
439
+ By default:
440
+
441
+ - HTML strings are escaped.
442
+ - Function calls from context values are disabled.
443
+ - Namespace calls are allowed.
444
+ - Dangerous property names are blocked.
445
+ - Computed property access is validated.
446
+ - Template expressions are parsed by Mutor, not executed as arbitrary JavaScript source.
447
+
448
+ Blocked properties include:
449
+
450
+ ```txt
451
+ __proto__
452
+ constructor
453
+ prototype
454
+ __defineGetter__
455
+ __defineSetter__
456
+ __lookupGetter__
457
+ __lookupSetter__
458
+ caller
459
+ callee
460
+ arguments
461
+ ```
462
+
463
+ You can allow or forbid additional properties:
703
464
 
704
- ```js
465
+ ```ts
705
466
  const mutor = new Mutor({
706
- cache: {
707
- active: true,
708
- maxSize: 50 * 1024 * 1024, // 50MB
709
- }
467
+ allowedProps: new Set(["constructor"]),
468
+ forbiddenProps: new Set(["passwordHash"]),
710
469
  });
711
470
  ```
712
471
 
713
- | Option | Type | Default | Description |
714
- |---|---|---|---|
715
- | `active` | `boolean` | `true` | Whether caching is enabled. Set to `false` to recompile on every render (useful for development). |
716
- | `maxSize` | `number` | `52428800` (50MB) | Maximum cache memory in bytes. When exceeded, LRU eviction runs. |
472
+ Use `allowFnCalls` deliberately:
717
473
 
718
- ### Disabling the Cache
719
-
720
- ```js
474
+ ```ts
721
475
  const mutor = new Mutor({
722
- cache: { active: false }
476
+ allowFnCalls: true,
723
477
  });
724
478
  ```
725
479
 
726
- With the cache disabled, every `render` or `renderFile` call runs the full compiler pipeline. This is useful in development for picking up template changes without restarting the process, but is not appropriate for production.
727
-
728
- ### Manual Cache Control
480
+ With `allowFnCalls: false`, this is blocked:
729
481
 
730
- ```js
731
- // Clear the entire cache
732
- mutor.reset();
733
-
734
- // Get cache diagnostics (memory usage, entry count)
735
- const diagnostics = mutor.getDiagnostics();
482
+ ```html
483
+ {{ user.deleteAccount() }}
736
484
  ```
737
485
 
738
- > **Note:** `reset()` is synchronous and removes all compiled functions from the instance cache. The next render of any template will trigger recompilation.
739
-
740
- ### Instance Isolation
486
+ Mutor is a template engine, not a complete sandbox for hostile code. If users can write templates, keep the default restrictions unless you have a reason to loosen them.
741
487
 
742
- Each instance has an independent cache with its own size limit. If you need strict per-tenant or per-request memory isolation, use separate `Mutor` instances.
743
-
744
- ---
745
-
746
- ## 16. Configuration Reference
747
-
748
- The full configuration object with all defaults:
488
+ ## Configuration
749
489
 
750
490
  ```ts
751
- interface MutorConfig {
752
- build?: {
753
- include?: Set<string>; // File extensions to scan (server only)
754
- exclude?: Set<string>; // Directories to exclude from scanning
755
- };
756
- autoEscape?: boolean; // Default: true
757
- allowedProps?: Set<string>; // Explicitly whitelisted property names
758
- forbiddenProps?: Set<string>; // Additional forbidden property names
759
- allowFnCalls?: boolean; // Default: false
760
- delimiters?: {
761
- openingTag?: string; // Default: "{{"
762
- closingTag?: string; // Default: "}}"
763
- openingTagEscape?: string; // Default: "\\"
764
- whitespaceTrim?: string; // Default: "~"
765
- commentTag?: string; // Default: "#"
766
- };
767
- keepOpeningTagEscapeDelimiter?: boolean; // Default: false
768
- cache?: {
769
- active?: boolean; // Default: true
770
- maxSize?: number; // Default: 50 * 1024 * 1024 (50MB)
771
- };
772
- }
773
- ```
774
-
775
- ### Default Configuration
776
-
777
- ```js
778
- const defaultConfig = {
779
- build: {
780
- include: new Set([".html", ".txt"]),
781
- exclude: new Set(["node_modules", ".git"]),
782
- },
491
+ const mutor = new Mutor({
783
492
  autoEscape: true,
784
- allowedProps: new Set(),
785
- forbiddenProps: new Set(["__proto__", "constructor", "prototype"]),
786
493
  allowFnCalls: false,
494
+ preserveEscapeDelimiter: false,
495
+ debugRuntimeErrors: false,
496
+ rootDir: "./views",
497
+ cache: {
498
+ active: true,
499
+ maxSize: 50 * 1024 * 1024,
500
+ },
787
501
  delimiters: {
788
502
  openingTag: "{{",
789
503
  closingTag: "}}",
790
504
  openingTagEscape: "\\",
791
505
  whitespaceTrim: "~",
506
+ commentTag: "#",
792
507
  },
793
- keepOpeningTagEscapeDelimiter: false,
794
- cache: {
795
- active: true,
796
- maxSize: 50 * 1024 * 1024,
508
+ build: {
509
+ include: new Set([".html", ".txt"]),
510
+ exclude: new Set(["node_modules", ".git"]),
797
511
  },
798
- };
512
+ onIncludeFail: "throw",
513
+ });
799
514
  ```
800
515
 
801
- ### `allowedProps`
802
-
803
- A `Set<string>` of property names that are explicitly permitted regardless of other validation rules.
804
-
805
- ### `forbiddenProps`
516
+ ### `autoEscape`
806
517
 
807
- A `Set<string>` of additional property names to block. The built-in forbidden set (`__proto__`, `constructor`, `prototype`) is always enforced and merged with any values you provide here.
518
+ Escapes HTML-sensitive characters in strings. Enabled by default.
808
519
 
809
- ### `build.include` and `build.exclude`
520
+ ### `allowFnCalls`
810
521
 
811
- Used by the server-side file scanner when pre-compiling a directory of templates. `include` is a set of file extensions to process. `exclude` is a set of directory names to skip.
522
+ Allows templates to call functions from context values. Disabled by default.
812
523
 
813
- ---
524
+ Namespace calls such as `Math::max(1, 2)` are still allowed.
814
525
 
815
- ## 17. Delimiter Customization
526
+ ### `delimiters`
816
527
 
817
- All delimiter-related strings are fully configurable.
528
+ Customize tags and control markers.
818
529
 
819
- ### Changing Delimiters
820
-
821
- ```js
530
+ ```ts
822
531
  const mutor = new Mutor({
823
532
  delimiters: {
824
- openingTag: "<%",
825
- closingTag: "%>",
826
- openingTagEscape: "\\",
827
- whitespaceTrim: "-",
828
- commandTag: "#",
829
- }
533
+ openingTag: "{%",
534
+ closingTag: "%}",
535
+ },
830
536
  });
831
537
  ```
832
538
 
833
- With this configuration, templates use `<%` and `%>`:
834
-
835
- ```html
836
- <p><% user.name %></p>
837
- <%- if user.isAdmin -%>
838
- <a href="/admin">Admin</a>
839
- <%- end -%>
840
- ```
539
+ ### `preserveEscapeDelimiter`
841
540
 
842
- ### Constraints on Delimiter Choice
541
+ Controls whether escaped opening tags keep their escape marker.
843
542
 
844
- - Opening and closing delimiters must be distinct strings.
845
- - Delimiters should not be substrings of each other (e.g., `{` and `{{` would cause scanner ambiguity).
846
- - The whitespace trim character must be a single character.
847
- - The escape character must be a single character.
543
+ ### `rootDir`
848
544
 
849
- ### Delimiter Change and Cache Invalidation
545
+ Used by the server renderer for `@/` includes.
850
546
 
851
- If you change delimiters at runtime by creating a new `Mutor` instance with different configuration, the new instance has a separate cache. Templates compiled under one delimiter configuration are not compatible with a differently configured instance — the instance and its configuration are inseparable.
547
+ ### `cache`
852
548
 
853
- ---
549
+ Controls compiled template caching.
854
550
 
855
- ## 18. Whitespace Control
551
+ ```ts
552
+ const mutor = new Mutor({
553
+ cache: {
554
+ active: true,
555
+ maxSize: 10 * 1024 * 1024,
556
+ },
557
+ });
558
+ ```
856
559
 
857
- Mutor.js provides precise whitespace control via the `~` directive (or your configured `whitespaceTrim` character).
560
+ ### `build`
858
561
 
859
- ### Without Whitespace Trimming
562
+ Controls which files `buildDir` and `compileDir` process.
860
563
 
861
- ```html
862
- <ul>
863
- {{ for item of items }}
864
- <li>{{ item.name }}</li>
865
- {{ end }}
866
- </ul>
564
+ ```ts
565
+ const mutor = new Mutor({
566
+ build: {
567
+ include: new Set([".html", ".md"]),
568
+ exclude: new Set(["node_modules", ".git", "drafts"]),
569
+ },
570
+ });
867
571
  ```
868
572
 
869
- Output:
573
+ ### `onIncludeFail`
870
574
 
871
- ```html
872
- <ul>
873
-
874
- <li>Alpha</li>
875
-
876
- <li>Beta</li>
877
-
878
- </ul>
575
+ Controls what happens when an include fails.
576
+
577
+ ```ts
578
+ const mutor = new Mutor({
579
+ onIncludeFail: "throw", // "throw" | "ignore" | "ignoreLog"
580
+ });
879
581
  ```
880
582
 
881
- The blank lines come from the newlines around the `for` and `end` tags.
583
+ ### `onIncludeError`
882
584
 
883
- ### With Whitespace Trimming
585
+ Return fallback content for failed includes.
884
586
 
885
- ```html
886
- <ul>
887
- {{~ for item of items ~}}
888
- <li>{{ item.name }}</li>
889
- {{~ end ~}}
890
- </ul>
587
+ ```ts
588
+ const mutor = new Mutor({
589
+ onIncludeFail: "ignore",
590
+ onIncludeError(meta, err) {
591
+ return `<!-- include failed: ${meta.path} -->`;
592
+ },
593
+ });
891
594
  ```
892
595
 
893
- Output:
596
+ ### `debugRuntimeErrors`
894
597
 
895
- ```html
896
- <ul>
897
- <li>Alpha</li>
898
- <li>Beta</li>
899
- </ul>
900
- ```
598
+ Wraps runtime failures with template source context.
901
599
 
902
- The `~` on the opening tag trims whitespace to the left of that tag; `~` on the closing tag trims whitespace to the right. Trim directives work on interpolation tags as well as logic tags.
600
+ ```ts
601
+ const mutor = new Mutor({
602
+ debugRuntimeErrors: true,
603
+ allowFnCalls: true,
604
+ });
605
+ ```
903
606
 
904
- ---
607
+ This is helpful during development because errors point back to the template line and column.
905
608
 
906
- ## 19. Escaping Rules
609
+ ## Cache
907
610
 
908
- ### Auto-Escape (Default)
611
+ Mutor caches compiled templates by identifier or absolute file path.
909
612
 
910
- When `autoEscape: true`, every dynamic value is HTML-escaped before insertion. A function call that returns a value like `<script>alert(1)</script>` renders as:
613
+ For registered components:
911
614
 
912
- ```
913
- &lt;script&gt;alert(1)&lt;/script&gt;
615
+ ```ts
616
+ mutor.registerComponent("card", "<article>{{ title }}</article>");
617
+ mutor.invalidateCacheEntry("card");
914
618
  ```
915
619
 
916
- ### Disabling Auto-Escape
620
+ For server files:
917
621
 
918
- ```js
919
- const mutor = new Mutor({ autoEscape: false });
622
+ ```ts
623
+ mutor.renderFile("./views/page.html", context);
624
+ mutor.invalidateCacheEntry("./views/page.html");
920
625
  ```
921
626
 
922
- With auto-escaping disabled, values are interpolated verbatim. This is appropriate only when generating non-HTML output (e.g., plain text) or when context data has been sanitized elsewhere.
627
+ The next render recompiles the template.
923
628
 
924
- ### Escaping the Delimiter
629
+ Inspect cache usage:
925
630
 
926
- To output the literal opening delimiter without triggering parsing:
927
-
928
- ```html
929
- \{{ this is not a template tag }}
631
+ ```ts
632
+ mutor.getDiagnostics();
930
633
  ```
931
634
 
932
- The `keepOpeningTagEscapeDelimiter` option controls whether the backslash itself appears in output:
933
-
934
- | `keepOpeningTagEscapeDelimiter` | Template | Output |
935
- |---|---|---|
936
- | `false` (default) | `\{{ expr }}` | `{{ expr }}` |
937
- | `true` | `\{{ expr }}` | `\{{ expr }}` |
938
-
939
- ---
940
-
941
- ## 20. Error Handling
942
-
943
- Mutor.js produces typed errors with detailed source location information.
944
-
945
- ### Error Types
946
-
947
- | Error Class | Thrown When |
948
- |---|---|
949
- | `MutorParseError` | The tokenizer or parser cannot process the input |
950
- | `MutorSecurityError` | A forbidden property access or unsafe construct is detected |
951
- | `MutorCompileError` | Code generation or compilation fails |
952
- | `MutorRuntimeError` | A runtime context validation failure occurs |
953
- | `MutorIncludeError` | An include file cannot be found or a circular include is detected |
954
- | `MutorComponentError` | A component is rendered that has not been registered |
955
-
956
- ### Error Structure
957
-
958
- All error types extend a base `MutorError` class:
635
+ Example result:
959
636
 
960
637
  ```ts
961
- class MutorError extends Error {
962
- code: string; // Machine-readable error code
963
- source?: string; // Original template content (truncated)
964
- location?: {
965
- line: number;
966
- column: number;
967
- offset: number;
968
- };
638
+ {
639
+ bytesUsed: 1200,
640
+ bytesMax: 52428800,
641
+ readableUsed: "0.00 MB",
642
+ readableMax: "50.00 MB",
643
+ totalEntries: 2,
644
+ percentFull: 0,
645
+ avgTemplateSize: 600
969
646
  }
970
647
  ```
971
648
 
972
- ### Error Messages
973
-
974
- Because every token is position-tracked, errors reference exact source locations:
649
+ Clear all cache entries and restore default config:
975
650
 
651
+ ```ts
652
+ mutor.reset();
976
653
  ```
977
- MutorSecurityError: Forbidden property access: "constructor"
978
- at line 4, column 12 in template "views/user-profile.html"
979
654
 
980
- MutorParseError: Unclosed block: "if" block opened at line 7, column 3 was never closed.
655
+ Good to know: Mutor does not watch files. If a template file changes while cache is active, call `invalidateCacheEntry`, call `reset`, or disable cache in development.
981
656
 
982
- MutorIncludeError: Circular include detected.
983
- Include chain: views/layout.html → views/partials/nav.html → views/layout.html
984
- ```
657
+ ## API
985
658
 
986
- ### Handling Errors
659
+ ### Core API
987
660
 
988
- ```js
989
- import { MutorSecurityError, MutorParseError } from "mutorjs";
661
+ Import from `mutorjs`:
990
662
 
991
- try {
992
- const output = mutor.render(template, context);
993
- } catch (err) {
994
- if (err instanceof MutorSecurityError) {
995
- // Log and reject the template — do not attempt to render
996
- logger.error("Template security violation", { code: err.code, location: err.location });
997
- throw err;
998
- }
999
- if (err instanceof MutorParseError) {
1000
- // Template has a syntax error — report to developer
1001
- throw err;
1002
- }
1003
- throw err;
1004
- }
663
+ ```ts
664
+ import Mutor from "mutorjs";
1005
665
  ```
1006
666
 
1007
- ---
1008
-
1009
- ## 21. CLI
1010
-
1011
- Mutor.js ships a command-line interface for compiling and rendering templates outside of JavaScript code.
1012
-
1013
- ### Installation
1014
-
1015
- Install the package globally to use the `mutor` command directly:
1016
-
1017
- ```bash
1018
- npm install -g mutorjs
1019
- ```
667
+ #### `render(template, context)`
1020
668
 
1021
- Or use it locally via `npx`:
669
+ Renders a template string.
1022
670
 
1023
- ```bash
1024
- npx mutor <command> <input> [options]
671
+ ```ts
672
+ mutor.render("Hello {{ name }}", { name: "Ada" });
1025
673
  ```
1026
674
 
1027
- ### Commands
1028
-
1029
- #### `compile <template>`
675
+ #### `renderAsync(template, context)`
1030
676
 
1031
- Compiles a single template file to its intermediate compiled form.
677
+ Renders a template string through a promise. Use this when the template uses `Mutor::await`.
1032
678
 
1033
- ```bash
1034
- mutor compile ./views/index.html --out ./dist/index.html
679
+ ```ts
680
+ await mutor.renderAsync("{{ Mutor::await(value) }}", {
681
+ value: Promise.resolve("done"),
682
+ });
1035
683
  ```
1036
684
 
1037
- If `--out` is omitted, the compiled output is printed to stdout.
685
+ #### `compile(template)`
1038
686
 
1039
- #### `build <dir>`
687
+ Compiles a template and returns a reusable function.
1040
688
 
1041
- Renders all templates in a directory using a JSON data source.
689
+ ```ts
690
+ const renderGreeting = mutor.compile("Hello {{ name }}");
1042
691
 
1043
- ```bash
1044
- mutor build ./views --data ./data.json --out ./dist
692
+ renderGreeting({ name: "Ada" });
693
+ renderGreeting({ name: "Grace" });
1045
694
  ```
1046
695
 
1047
- Both `--data` and `--out` are required for `build`.
1048
-
1049
- #### `render <template>`
696
+ #### `registerComponent(identifier, template)`
1050
697
 
1051
- Compiles and immediately renders a single template using a JSON data source.
698
+ Registers a reusable component/partial.
1052
699
 
1053
- ```bash
1054
- mutor render ./views/email.html --data ./context.json --out ./dist/email.html
700
+ ```ts
701
+ mutor.registerComponent("button", "<button>{{ label }}</button>");
1055
702
  ```
1056
703
 
1057
- If `--out` is omitted, the rendered output is printed to stdout.
704
+ #### `renderComponent(identifier, context)`
1058
705
 
1059
- ### Options
1060
-
1061
- | Option | Description |
1062
- |---|---|
1063
- | `--out <path>` | Output file or directory. Defaults to stdout for `compile` and `render`. |
1064
- | `--data <path>` | JSON file to use as the render context. Required for `build` and `render`. |
1065
- | `--config <path>` | JSON config file passed to the Mutor instance. |
1066
- | `--version` | Print the installed version and exit. |
1067
- | `--help` | Print usage information and exit. |
1068
-
1069
- ### Exit Codes
1070
-
1071
- | Code | Meaning |
1072
- |---|---|
1073
- | `0` | Success |
1074
- | `1` | Runtime error (I/O failure, render failure, etc.) |
1075
- | `2` | Argument error (unknown flag, missing required value, wrong file type) |
1076
-
1077
- ### Configuration via `--config`
1078
-
1079
- You can pass any `MutorConfig`-compatible JSON file to `--config`:
1080
-
1081
- ```json
1082
- {
1083
- "autoEscape": true,
1084
- "allowFnCalls": false,
1085
- "cache": {
1086
- "active": true,
1087
- "maxSize": 52428800
1088
- }
1089
- }
1090
- ```
706
+ Renders a registered component.
1091
707
 
1092
- ```bash
1093
- mutor build ./views --data ./data.json --out ./dist --config ./mutor.config.json
708
+ ```ts
709
+ mutor.renderComponent("button", { label: "Save" });
1094
710
  ```
1095
711
 
1096
- ### Examples
1097
-
1098
- ```bash
1099
- # Compile a template and print to stdout
1100
- mutor compile ./views/home.html
1101
-
1102
- # Compile a template to a file
1103
- mutor compile ./views/home.html --out ./dist/home.html
1104
-
1105
- # Render a template with data, write to file
1106
- mutor render ./views/email.html --data ./payload.json --out ./out/email.html
712
+ #### `renderAsyncComponent(identifier, context)`
1107
713
 
1108
- # Build an entire views directory
1109
- mutor build ./views --data ./site.json --out ./dist
714
+ Async component rendering.
1110
715
 
1111
- # Build with a custom config
1112
- mutor build ./views --data ./site.json --out ./dist --config ./mutor.config.json
716
+ ```ts
717
+ await mutor.renderAsyncComponent("button", context);
1113
718
  ```
1114
719
 
1115
- ---
1116
-
1117
- ## 22. Performance
1118
-
1119
- ### Compilation vs. Execution
1120
-
1121
- Mutor.js makes a deliberate tradeoff: spend more time at compile time so that runtime is as fast as possible.
1122
-
1123
- **Compilation cost** — The full pipeline (tokenize → parse → analyze → validate → generate → compile) is more work than engines like EJS or Eta perform. Mutor.js runs semantic analysis and security validation steps that those engines skip. This cost is paid once per unique template per process lifetime.
1124
-
1125
- **Execution cost** — The compiled output is a native JavaScript function. Executing it is as fast as executing any other JavaScript function of equivalent complexity. There is no interpreter, no AST walker, no regex match — only a function call.
1126
-
1127
- The compilation cost is amortized across all renders of that template. For a server process that handles thousands of requests, a single template might be compiled once and executed hundreds of thousands of times. The per-request cost is execution only.
1128
-
1129
- ### Benchmarks
1130
-
1131
- The following benchmarks are indicative, based on internal measurements on a modern machine with Node.js 20:
1132
-
1133
- **Full pipeline (compile + execute):**
1134
-
1135
- | Engine | Relative Performance |
1136
- |---|---|
1137
- | Eta | 1× (baseline) |
1138
- | **Mutor.js** | **~0.8–0.9×** |
1139
- | EJS | ~0.4× |
1140
- | Nunjucks | ~0.15× |
1141
- | Handlebars | ~0.12× |
1142
-
1143
- **Raw execution (cache-warm, pre-compiled):**
1144
-
1145
- | Engine | Relative Performance |
1146
- |---|---|
1147
- | **Mutor.js** | **1× (top tier)** |
1148
- | Eta | ~0.8–0.9× |
1149
- | EJS | ~0.3× |
1150
- | Nunjucks | ~0.08× |
1151
- | Handlebars | ~0.06× |
1152
-
1153
- Mutor.js is slightly behind Eta on first-compile benchmarks, which is expected — Eta does minimal validation. On the execution-only path (cache warm), Mutor.js leads. That is the path that matters under production load.
1154
-
1155
- > **Note:** These benchmarks do not account for real-world I/O or middleware overhead. Profile your specific use case.
720
+ #### `invalidateCacheEntry(identifier)`
1156
721
 
1157
- ---
722
+ Removes a cached component entry.
1158
723
 
1159
- ## 23. Environment Support
1160
-
1161
- | Environment | Supported | Notes |
1162
- |---|---|---|
1163
- | Node.js 14+ | ✅ | Full support including `renderFile` |
1164
- | Node.js 18+ (recommended) | ✅ | Best performance |
1165
- | Browser (modern) | ✅ | Use universal entry point; `renderFile` unavailable |
1166
- | Deno | ✅ (via npm compat) | `renderFile` available if Deno FS APIs are mapped |
1167
- | Bun | ✅ | Full support |
1168
- | ESM | ✅ | Native ESM build available |
1169
- | CommonJS | ✅ | CJS build available |
1170
- | TypeScript | ✅ | Type declarations shipped with package |
1171
- | Minimum requirement | ES6 | Requires `const`, arrow functions, template literals, `Set`, `Map` |
1172
-
1173
- ---
1174
-
1175
- ## 24. Browser Usage
1176
-
1177
- In browser environments, import from the universal entry point:
1178
-
1179
- ```js
1180
- import Mutor from "mutorjs";
724
+ ```ts
725
+ mutor.invalidateCacheEntry("button");
1181
726
  ```
1182
727
 
1183
- The universal entry point excludes all file-system APIs. The following methods are available: `compile`, `render`, `registerComponent`, `renderComponent`. The `renderFile` method is not available and will throw a `MutorError` if called.
728
+ #### `addConfig(config)`
1184
729
 
1185
- ### Managing Templates in the Browser
730
+ Updates the engine config.
1186
731
 
1187
- Since file-system access is unavailable, templates must be loaded manually and registered as components:
732
+ ```ts
733
+ mutor.addConfig({ autoEscape: false });
734
+ ```
1188
735
 
1189
- ```js
1190
- const templateString = await fetch("/templates/card.html").then(r => r.text());
736
+ #### `restoreDefaultConfig()`
1191
737
 
1192
- mutor.registerComponent("card", templateString);
738
+ Restores the default config.
1193
739
 
1194
- const html = mutor.renderComponent("card", { card: data });
1195
- document.getElementById("app").innerHTML = html;
740
+ ```ts
741
+ mutor.restoreDefaultConfig();
1196
742
  ```
1197
743
 
1198
- ---
1199
-
1200
- ## 25. Server Usage
744
+ #### `getDiagnostics()`
1201
745
 
1202
- Import from the server entry point:
746
+ Returns cache diagnostics.
1203
747
 
1204
- ```js
1205
- import Mutor from "mutorjs/server";
748
+ ```ts
749
+ mutor.getDiagnostics();
1206
750
  ```
1207
751
 
1208
- The server entry point includes all functionality from the universal entry point plus `renderFile` and pre-compilation scanning via the `build` configuration.
752
+ #### `reset()`
1209
753
 
1210
- ### Express Integration
754
+ Restores default config and clears cached templates.
1211
755
 
1212
- ```js
1213
- import express from "express";
1214
- import Mutor from "mutorjs/server";
1215
-
1216
- const app = express();
1217
- const mutor = new Mutor();
1218
-
1219
- app.get("/", async (req, res) => {
1220
- const html = await mutor.renderFile("./views/home.html", {
1221
- user: req.user,
1222
- page: { title: "Home" },
1223
- });
1224
- res.send(html);
1225
- });
756
+ ```ts
757
+ mutor.reset();
1226
758
  ```
1227
759
 
1228
- ### Pre-Warming the Cache
760
+ ### Server API
1229
761
 
1230
- In production, pre-compile all templates during application startup to eliminate compilation latency on first request:
762
+ Import from `mutorjs/server`:
1231
763
 
1232
- ```js
764
+ ```ts
1233
765
  import Mutor from "mutorjs/server";
1234
-
1235
- const mutor = new Mutor();
1236
-
1237
- async function warmCache(viewsDir) {
1238
- const files = await getHtmlFiles(viewsDir); // your file enumeration logic
1239
- await Promise.all(files.map(f => mutor.renderFile(f, {})));
1240
- console.log(`Warmed ${files.length} templates.`);
1241
- }
1242
-
1243
- await warmCache("./views");
1244
766
  ```
1245
767
 
1246
- Calling `renderFile` with an empty context during startup compiles and caches every template. Subsequent production renders with real context data hit the cache.
1247
-
1248
- ---
1249
-
1250
- ## 26. Advanced Examples
768
+ #### `renderFile(path, context)`
1251
769
 
1252
- ### Dashboard Template with Nested Data
770
+ Renders a template file.
1253
771
 
1254
- ```html
1255
- {{# Project dashboard template #}}
1256
- <main class="dashboard">
1257
- <header>
1258
- <h1>{{ org.name }} — Dashboard</h1>
1259
- <p>Logged in as {{ user.displayName }}</p>
1260
- </header>
1261
-
1262
- {{~ if projects.length > 0 ~}}
1263
- <section class="projects">
1264
- {{ for project of projects }}
1265
- <article class="project-card">
1266
- <h2>{{ project.name }}</h2>
1267
- <p class="budget">
1268
- Budget: {{ Math::floor(project.budget) }} / {{ Math::floor(project.budgetTotal) }} USD
1269
- </p>
1270
- {{ if project.status == "active" }}
1271
- <span class="badge badge--active">Active</span>
1272
- {{ else if project.status == "review" }}
1273
- <span class="badge badge--review">In Review</span>
1274
- {{ else }}
1275
- <span class="badge badge--inactive">Inactive</span>
1276
- {{ end }}
1277
- <ul class="tags">
1278
- {{ for tag of project.tags }}
1279
- <li class="tag">{{ tag }}</li>
1280
- {{ end }}
1281
- </ul>
1282
- </article>
1283
- {{ end }}
1284
- </section>
1285
- {{~ else ~}}
1286
- <p class="empty-state">No projects found.</p>
1287
- {{~ end ~}}
1288
- </main>
1289
- ```
1290
-
1291
- ### Component with Namespace Utilities
1292
-
1293
- ```js
1294
- mutor.registerComponent("data-table", `
1295
- <table>
1296
- <thead>
1297
- <tr>
1298
- {{ for col of table.columns }}
1299
- <th>{{ col.label }}</th>
1300
- {{ end }}
1301
- </tr>
1302
- </thead>
1303
- <tbody>
1304
- {{ for row of table.rows }}
1305
- <tr>
1306
- {{ for col of table.columns }}
1307
- <td>{{ row[col.key] ?? "—" }}</td>
1308
- {{ end }}
1309
- </tr>
1310
- {{ end }}
1311
- </tbody>
1312
- <tfoot>
1313
- <tr>
1314
- <td colspan="{{ table.columns.length }}">
1315
- {{ table.rows.length }} {{ table.rows.length == 1 ? "record" : "records" }}
1316
- </td>
1317
- </tr>
1318
- </tfoot>
1319
- </table>
1320
- `);
1321
- ```
1322
-
1323
- ### Include with Explicit Context Forwarding
1324
-
1325
- ```html
1326
- {{# views/layout.html #}}
1327
- <!DOCTYPE html>
1328
- <html lang="en">
1329
- <head>
1330
- <title>{{ page.title }} — {{ site.name }}</title>
1331
- {{ Mutor::include("./partials/head-meta.html", Mutor::$$context) }}
1332
- </head>
1333
- <body>
1334
- {{ Mutor::include("./partials/nav.html", { nav: nav, user: user }) }}
1335
- <main>
1336
- {{ Mutor::include(page.template, Mutor::$$context) }}
1337
- </main>
1338
- {{ Mutor::include("./partials/footer.html", { site: site }) }}
1339
- </body>
1340
- </html>
772
+ ```ts
773
+ mutor.renderFile("./views/home.html", context);
1341
774
  ```
1342
775
 
1343
- ### Pre-compilation with Custom Forbidden Properties
776
+ #### `renderFileAsync(path, context)`
1344
777
 
1345
- ```js
1346
- import Mutor from "mutorjs/server";
778
+ Async file rendering.
1347
779
 
1348
- const mutor = new Mutor({
1349
- forbiddenProps: new Set([
1350
- "__proto__",
1351
- "constructor",
1352
- "prototype",
1353
- "valueOf",
1354
- "toString",
1355
- "hasOwnProperty",
1356
- ]),
1357
- allowFnCalls: false,
1358
- cache: {
1359
- active: true,
1360
- maxSize: 100 * 1024 * 1024,
1361
- },
1362
- });
780
+ ```ts
781
+ await mutor.renderFileAsync("./views/home.html", context);
1363
782
  ```
1364
783
 
1365
- ---
1366
-
1367
- ## 27. Best Practices
784
+ #### `buildDir(src, destination, context)`
1368
785
 
1369
- **Keep templates thin.** Templates should handle layout and presentation. Business logic, data transformations, filtering, and sorting belong in the context preparation layer, not the template.
786
+ Renders a directory into another directory.
1370
787
 
1371
- **Pre-warm the cache in production.** Use the startup cache warming pattern to eliminate first-request compilation latency. Templates should be compiled before traffic arrives.
1372
-
1373
- **Use a single shared `Mutor` instance per process.** Cache efficiency is maximized when all requests share the same compiled template cache. Use separate instances only when you need genuine isolation (e.g., per-tenant configuration differences).
1374
-
1375
- **Pass minimal context.** Only expose in the context what the template actually needs. Avoid passing entire service objects, database clients, or request objects as context.
1376
-
1377
- **Validate context data before passing it in.** Mutor.js validates the context's prototype chain but does not validate the semantic meaning of your data. Validate user input before it enters the render context.
1378
-
1379
- **Use `for...of` for arrays, `for...in` for objects.** This mirrors JavaScript semantics and makes templates readable to any JavaScript developer.
1380
-
1381
- **Name includes and components descriptively.** Component names and include paths are the template's interface contract. `renderComponent("card")` is readable; `renderComponent("c1")` is not.
1382
-
1383
- **Test your templates.** Templates are code. Render them with fixture contexts in your test suite to catch regressions in output structure.
1384
-
1385
- ---
1386
-
1387
- ## 28. Security Best Practices
1388
-
1389
- **Never disable `autoEscape` when rendering user-controlled data.** If you must render raw HTML from a trusted source, consider using a separate Mutor instance with `autoEscape: false` specifically for that purpose, so the risk is isolated.
1390
-
1391
- **Never pass `req`, `res`, database clients, or service objects as context.** These objects have methods and properties that, with `allowFnCalls: true`, could be invoked from a template. Prepare a clean, minimal context object explicitly.
1392
-
1393
- **Enable `allowFnCalls` only when necessary and with a clear scope.** If you need method calls on specific types (e.g., string formatting), consider the planned custom namespace feature instead.
1394
-
1395
- **Extend `forbiddenProps` conservatively.** Add property names you know are dangerous in your specific context. `valueOf` and `toString` are candidates in many applications.
1396
-
1397
- **Treat `MutorSecurityError` as a hard stop.** If a security error is thrown during compilation, do not attempt to render the template. Log it, alert on it, and investigate.
1398
-
1399
- **Do not use `cache: { active: false }` in production.** Beyond the performance cost, a disabled cache means every render triggers recompilation — and recompilation triggers full security analysis on every request, which while correct, is unnecessary overhead.
1400
-
1401
- **Audit templates that come from external sources.** If your application allows user-defined templates to be compiled, review them with the same scrutiny as user-provided code. Mutor.js's sandbox is strong, but defense in depth applies here too.
1402
-
1403
- ---
1404
-
1405
- ## 29. Real-World Use Cases
1406
-
1407
- ### Server-Side HTML Rendering (Node.js)
1408
-
1409
- The primary use case. A web application that serves HTML pages from templates. Mutor.js compiles the view templates at startup, and each request invokes the compiled functions with a prepared context.
1410
-
1411
- ### Email Template Rendering
1412
-
1413
- Email templates have similar requirements to HTML templates: variable interpolation, conditional sections (e.g., showing a section only if a promo code is attached), iteration over lists (e.g., order items). Mutor.js's auto-escaping is important for preventing XSS in email clients that render HTML, and the predictable output makes it easy to test email rendering in CI.
1414
-
1415
- ### Static Site Generation
1416
-
1417
- A static site generator can use Mutor.js to compile page templates and render them with content sourced from a CMS, markdown files, or a database. The CLI's `build` command makes this straightforward for file-based workflows. The compilation cache ensures that re-builds are fast: only changed templates need recompilation.
1418
-
1419
- ### Configuration File Generation
1420
-
1421
- Mutor.js can render structured text formats like JSON, YAML, or NGINX configuration files, not just HTML. Disable `autoEscape` for non-HTML output and use the template language to generate configuration files with environment-specific values.
1422
-
1423
- ### Report Generation
1424
-
1425
- Server-side report generation that produces HTML output for PDF conversion. Mutor.js's deterministic, secure rendering is well-suited for environments where the template must process potentially large and complex data structures into formatted output.
1426
-
1427
- ---
1428
-
1429
- ## 30. Design Tradeoffs
1430
-
1431
- ### Compilation Cost for Execution Speed
1432
-
1433
- Mutor.js is slower to compile than Eta because it does substantially more work during compilation. This is acceptable because the compiled output executes faster, and compilation happens once per template per process lifetime.
1434
-
1435
- **Impact:** Mutor.js is not ideal for environments where templates change on every request and caching is disabled. It excels in environments with a stable set of templates and many requests.
1436
-
1437
- ### Bounded Template Language for Security
1438
-
1439
- Mutor.js does not allow arbitrary JavaScript in templates. You cannot write a `while` loop, a `try/catch`, a variable declaration, or an IIFE. This is a hard constraint from the security architecture.
1440
-
1441
- **Impact:** Complex logic must live in the context object, not the template. This is the correct design for large applications — templates should be thin views, not business logic containers — but it requires a different authoring mindset.
1442
-
1443
- ### Zero Dependencies for Auditability
1444
-
1445
- Mutor.js has no third-party dependencies. Every dependency in a security-critical system is a potential attack surface. Mutor.js owns its entire stack, which makes it fully auditable and immune to supply chain attacks through compromised dependencies.
1446
-
1447
- **Impact:** Mutor.js does not benefit from improvements in third-party parsing libraries or utility packages. Every capability must be built and maintained internally.
1448
-
1449
- ---
1450
-
1451
- ## 31. Future Roadmap
1452
-
1453
- The following features are planned or under active development and are not available in the current stable release.
1454
-
1455
- ### Template Source Maps *(Planned)*
1456
-
1457
- Source map generation so that runtime errors in compiled functions can be mapped back to their originating template locations — the natural next step from the position tracking already in the tokenizer.
1458
-
1459
- ### Extended Namespace Registry *(Planned)*
1460
-
1461
- An API to register custom namespaces:
1462
-
1463
- ```js
1464
- mutor.registerNamespace("Utils", {
1465
- formatCurrency: (n) => `$${n.toFixed(2)}`,
1466
- slugify: (s) => s.toLowerCase().replace(/\s+/g, "-"),
1467
- });
788
+ ```ts
789
+ await mutor.buildDir("./site", "./dist", context);
1468
790
  ```
1469
791
 
1470
- This would allow project-specific utilities to be exposed into templates via the controlled namespace mechanism rather than requiring `allowFnCalls: true`.
1471
-
1472
- ### Async Template Rendering *(Under Consideration)*
1473
-
1474
- Support for templates that include asynchronous expressions — for example, inline data fetching or async namespace methods. Architecturally complex and not yet committed.
1475
-
1476
- ### Template Language Server Protocol Support *(Future)*
1477
-
1478
- An LSP implementation for Mutor.js template syntax, enabling editor tooling: syntax highlighting, error diagnostics, completion, and hover docs.
1479
-
1480
- ### Compiler Plugin API *(Future)*
1481
-
1482
- An API for hooking into the compilation pipeline at defined extension points (post-parse, post-analyze, post-generate), enabling custom AST transformations, custom security policies, and code generation optimizations without forking the core.
1483
-
1484
- ---
1485
-
1486
- ## 32. FAQ
1487
-
1488
- **Q: Is Mutor.js production-ready?**
792
+ #### `compileDir(src)`
1489
793
 
1490
- Yes. The core engine rendering, compilation, caching, security, components, includes — is stable and production-ready. The CLI is also available as of the current release.
1491
-
1492
- **Q: Can I use Mutor.js with Express/Fastify/Koa?**
1493
-
1494
- Yes. Mutor.js is a rendering function, not a framework. Any server framework that lets you produce a string response can use it. See §25 Server Usage for an Express example.
1495
-
1496
- **Q: Why is function calling disabled by default?**
1497
-
1498
- Templates that invoke arbitrary functions on context objects are harder to reason about and audit than templates that only read data. Disabling function calls by default makes the common case — read-only data rendering — safe without configuration, and forces an explicit opt-in for the more powerful (and potentially more risky) capability.
1499
-
1500
- **Q: Can I extend the namespace system with my own namespaces?**
1501
-
1502
- Not in the current version. This is planned as a future feature. For now, all logic that requires custom functions should be called in the context preparation layer and the result passed into the context.
1503
-
1504
- **Q: Why is template interpolation inside backtick strings not supported?**
1505
-
1506
- Backtick strings in Mutor.js are static strings, not JavaScript template literals. Supporting `${...}` inside strings would require a nested expression parser inside the string tokenizer, significantly increasing complexity. The use case is better handled outside the string: `prefix + value + suffix` as a concatenation expression.
1507
-
1508
- **Q: How does Mutor.js handle `null` and `undefined` in interpolation?**
1509
-
1510
- By default, `null` and `undefined` values render as empty strings. This prevents `undefined` from appearing literally in output. Use the nullish coalescing operator (`??`) to provide explicit defaults when you need them.
1511
-
1512
- **Q: Can two Mutor instances share a cache?**
1513
-
1514
- No. Each instance has an isolated cache. If you want cache sharing, use a single instance.
1515
-
1516
- **Q: Is there a way to render without escaping for a specific tag?**
1517
-
1518
- In the current version, auto-escaping is a global configuration setting, not per-tag. To render a raw value, either use `autoEscape: false` on the instance, sanitize the value before it enters the context, or use a separate instance configured without auto-escaping.
1519
-
1520
- ---
1521
-
1522
- ## 33. API Reference
1523
-
1524
- ### `new Mutor(config?: MutorConfig)`
1525
-
1526
- Creates a new Mutor instance with the given configuration merged over the defaults.
794
+ Precompiles matching files in a directory into the cache.
1527
795
 
1528
796
  ```ts
1529
- const mutor = new Mutor({
1530
- autoEscape: true,
1531
- allowFnCalls: false,
1532
- cache: { active: true, maxSize: 50 * 1024 * 1024 },
1533
- });
797
+ await mutor.compileDir("./views");
1534
798
  ```
1535
799
 
1536
- ---
800
+ #### `invalidateCacheEntry(path)`
1537
801
 
1538
- ### `mutor.compile(template: string): CompiledTemplate`
802
+ Removes a cached file entry. The server renderer resolves the path before removing it.
1539
803
 
1540
804
  ```ts
1541
- type CompiledTemplate = (context: Record<string, unknown>) => string;
805
+ mutor.invalidateCacheEntry("./views/home.html");
1542
806
  ```
1543
807
 
1544
- Compiles a template string and returns the compiled function. The compilation result is stored in the instance cache.
1545
-
1546
- **Throws:**
1547
- - `MutorParseError` — if the template contains a syntax error
1548
- - `MutorSecurityError` — if the template contains a forbidden construct
1549
- - `MutorCompileError` — if code generation fails
1550
-
1551
- ---
1552
-
1553
- ### `mutor.render(template: string, context: Record<string, unknown>): string`
1554
-
1555
- Compiles (or retrieves from cache) and immediately renders a template with the given context.
1556
-
1557
- **Throws:** Same as `compile`, plus `MutorRuntimeError` for runtime context validation failures.
1558
-
1559
- ---
1560
-
1561
- ### `mutor.renderFile(path: string, context: Record<string, unknown>): Promise<string>`
808
+ ## CLI
1562
809
 
1563
- *Server entry point only (`mutorjs/server`).*
810
+ Mutor ships with a small CLI.
1564
811
 
1565
- Reads, compiles, and renders a template file. The resolved absolute path is used as the cache key.
1566
-
1567
- **Throws:** Same as `render`, plus `MutorIncludeError` if the file cannot be read.
1568
-
1569
- ---
1570
-
1571
- ### `mutor.registerComponent(name: string, template: string): void`
1572
-
1573
- Registers a named component template. Compilation is deferred to first render.
1574
-
1575
- **Throws:** `MutorError` if `name` is empty or if `template` is not a string.
1576
-
1577
- ---
1578
-
1579
- ### `mutor.renderComponent(name: string, context: Record<string, unknown>): string`
1580
-
1581
- Renders a registered component by name.
1582
-
1583
- **Throws:** `MutorComponentError` if the component has not been registered. Same compile/runtime errors as `render` on first call.
1584
-
1585
- ---
1586
-
1587
- ### `mutor.clearCache(): void`
1588
-
1589
- Removes all entries from the instance's compilation cache.
1590
-
1591
- ---
1592
-
1593
- ### `mutor.getCacheSize(): number`
1594
-
1595
- Returns the estimated current memory usage of the cache in bytes.
1596
-
1597
- ---
1598
-
1599
- ### `mutor.getCacheEntryCount(): number`
1600
-
1601
- Returns the number of compiled templates currently stored in the cache.
1602
-
1603
- ---
1604
-
1605
- ### `mutor.addConfig(config: Partial<MutorConfig>): void`
1606
-
1607
- Merges additional configuration into the instance after construction. Useful when configuration is loaded asynchronously (e.g., from a file) after the instance is created.
1608
-
1609
- ---
812
+ ```sh
813
+ mutor <command> <input> [options]
814
+ ```
1610
815
 
1611
- ### Error Classes
816
+ ### Render A File
1612
817
 
1613
- ```ts
1614
- import {
1615
- MutorError,
1616
- MutorParseError,
1617
- MutorSecurityError,
1618
- MutorCompileError,
1619
- MutorRuntimeError,
1620
- MutorIncludeError,
1621
- MutorComponentError,
1622
- } from "mutorjs";
818
+ ```sh
819
+ mutor render ./views/home.html --data ./data.json --out ./dist/home.html
1623
820
  ```
1624
821
 
1625
- All error classes extend `MutorError extends Error` and carry:
822
+ Without `--out`, the result is printed to stdout.
1626
823
 
1627
- | Property | Type | Description |
1628
- |---|---|---|
1629
- | `code` | `string` | Machine-readable error code |
1630
- | `message` | `string` | Human-readable description |
1631
- | `source` | `string \| undefined` | Originating template source (truncated) |
1632
- | `location` | `SourceLocation \| undefined` | `{ line, column, offset }` |
824
+ ### Build A Directory
1633
825
 
1634
- ---
826
+ ```sh
827
+ mutor build ./site --data ./data.json --out ./dist
828
+ ```
1635
829
 
1636
- ### `MutorConfig` Type Reference
830
+ ### Compile A Template
1637
831
 
1638
- ```ts
1639
- interface MutorConfig {
1640
- build?: {
1641
- include?: Set<string>;
1642
- exclude?: Set<string>;
1643
- };
1644
- autoEscape?: boolean;
1645
- allowedProps?: Set<string>;
1646
- forbiddenProps?: Set<string>;
1647
- allowFnCalls?: boolean;
1648
- delimiters?: {
1649
- openingTag?: string;
1650
- closingTag?: string;
1651
- openingTagEscape?: string;
1652
- whitespaceTrim?: string;
1653
- };
1654
- keepOpeningTagEscapeDelimiter?: boolean;
1655
- cache?: {
1656
- active?: boolean;
1657
- maxSize?: number;
1658
- };
1659
- }
832
+ ```sh
833
+ mutor compile ./views/home.html --out ./compiled.txt
1660
834
  ```
1661
835
 
1662
- ---
836
+ ### Options
1663
837
 
1664
- *Mutor.js engineered for correctness, security, and speed.*
1665
- *Built by Onah Victor. Contributions welcome under the project's guidelines.*
838
+ | Option | Meaning |
839
+ | --- | --- |
840
+ | `--data <path>` | JSON data file used as render context |
841
+ | `--out <path>` | Output file or directory |
842
+ | `--config <path>` | JSON config file |
843
+ | `--version` | Print the installed version |
844
+ | `--help` | Show CLI help |
845
+
846
+ ## Good To Know
847
+
848
+ - Mutor has no layout inheritance. Compose pages from partials/components and context.
849
+ - Includes inherit their parent context when no context is passed.
850
+ - The current context is available as `Mutor::$$context`.
851
+ - Use async render methods when templates use `Mutor::await`.
852
+ - File cache does not watch the filesystem.
853
+ - Function calls from context values are disabled by default.
854
+ - Array/object/function/class literals are not part of the template language.
855
+ - Use `JSON::parse(...)` or pass data through context when you need arrays or objects.
856
+ - Auto-escaping only changes strings. Non-string values are returned as they are.
857
+ - Missing includes can throw, return empty output, log, or use `onIncludeError`, depending on config.
858
+
859
+ ## License
860
+
861
+ ISC