miki-template 2.0.0 → 2.2.2

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.
Files changed (87) hide show
  1. package/.github/workflows/ci.yml +14 -10
  2. package/.github/workflows/docs.yml +105 -0
  3. package/.github/workflows/npm-publish-github-packages.yml +36 -0
  4. package/README.md +142 -26
  5. package/assets/logo.png +0 -0
  6. package/benchmarks/ejs-results.json +17 -0
  7. package/benchmarks/ejs.js +36 -0
  8. package/benchmarks/handlebars-results.json +17 -0
  9. package/benchmarks/handlebars.js +48 -0
  10. package/benchmarks/miki-results.json +17 -0
  11. package/benchmarks/miki.js +36 -0
  12. package/benchmarks/pug-results.json +17 -0
  13. package/benchmarks/pug.js +36 -0
  14. package/benchmarks/run.js +69 -37
  15. package/benchmarks/stress.mjs +1 -1
  16. package/docs/api/async-render.md +85 -0
  17. package/docs/api/cache.md +87 -0
  18. package/docs/api/compile.md +128 -0
  19. package/docs/api/context-processors.md +77 -0
  20. package/docs/api/filters.md +217 -0
  21. package/docs/api/finder.md +94 -0
  22. package/docs/api/helpers.md +53 -0
  23. package/docs/api/i18n.md +157 -0
  24. package/docs/api/index.md +54 -0
  25. package/docs/api/libraries.md +207 -0
  26. package/docs/api/render-partial.md +81 -0
  27. package/docs/api/render.md +92 -0
  28. package/docs/api/security.md +145 -0
  29. package/docs/api/setup-express.md +76 -0
  30. package/docs/api/tags.md +134 -0
  31. package/docs/assets/banner.png +0 -0
  32. package/docs/assets/logo.png +0 -0
  33. package/docs/guide/advanced-usage.md +397 -0
  34. package/docs/guide/async-rendering.md +308 -0
  35. package/docs/guide/context-processors.md +257 -0
  36. package/docs/guide/custom-filters.md +311 -0
  37. package/docs/guide/custom-tags.md +271 -0
  38. package/docs/guide/filters.md +642 -0
  39. package/docs/guide/getting-started.md +102 -0
  40. package/docs/guide/installation.md +95 -0
  41. package/docs/guide/partial-templates.md +367 -0
  42. package/docs/guide/quick-start.md +222 -0
  43. package/docs/guide/security.md +345 -0
  44. package/docs/guide/tags.md +783 -0
  45. package/docs/guide/template-discovery.md +170 -0
  46. package/docs/guide/template-inheritance.md +273 -0
  47. package/docs/guide/what-is-miki-template.md +28 -0
  48. package/docs/guide/why-miki-template.md +75 -0
  49. package/docs/index.md +104 -0
  50. package/docs/integrations/elysia.md +78 -0
  51. package/docs/integrations/express.md +219 -0
  52. package/docs/integrations/fastify.md +77 -0
  53. package/docs/integrations/hono.md +78 -0
  54. package/docs/integrations/index.md +68 -0
  55. package/docs/integrations/koa.md +88 -0
  56. package/docs/integrations/nestjs.md +78 -0
  57. package/docs/integrations/tsed.md +81 -0
  58. package/docs/javascripts/extra.js +174 -0
  59. package/docs/performance.md +37 -0
  60. package/docs/stylesheets/extra.css +819 -0
  61. package/live-test/integrations/elysia-example.js +16 -0
  62. package/live-test/integrations/express-example.js +24 -0
  63. package/live-test/integrations/fastify-example.js +20 -0
  64. package/live-test/integrations/hono-example.js +16 -0
  65. package/live-test/integrations/koa-example.js +30 -0
  66. package/live-test/integrations/nestjs-example.js +25 -0
  67. package/live-test/integrations/smoke-test.js +166 -0
  68. package/live-test/integrations/tsed-example.js +23 -0
  69. package/live-test/package-lock.json +235 -0
  70. package/live-test/package.json +4 -0
  71. package/live-test/views/home.html +17 -0
  72. package/mkdocs.yml +217 -0
  73. package/overrides/main.html +26 -0
  74. package/overrides/partials/footer.html +9 -0
  75. package/package.json +16 -6
  76. package/requirements-docs.txt +1 -0
  77. package/tests/integration/partial-render.test.cjs +13 -0
  78. package/docs/README.md +0 -18
  79. package/docs/advanced_usage.md +0 -71
  80. package/docs/api.md +0 -122
  81. package/docs/filters.md +0 -708
  82. package/docs/installation.md +0 -106
  83. package/docs/overview.md +0 -79
  84. package/docs/partialdef.md +0 -70
  85. package/docs/security.md +0 -27
  86. package/docs/tags.md +0 -673
  87. package/docs/usage.md +0 -646
package/benchmarks/run.js CHANGED
@@ -1,49 +1,81 @@
1
- // benchmarks/run.js
2
- // Simple benchmark for sync vs async rendering
3
- const { compile, asyncRender } = require('../src');
1
+ const { execSync } = require('child_process');
4
2
  const fs = require('fs');
5
3
  const path = require('path');
6
- const { performance } = require('perf_hooks');
7
4
 
8
- function loadTemplate(name) {
9
- const filePath = path.join(__dirname, 'templates', `${name}.dtpl`);
10
- return fs.readFileSync(filePath, 'utf8');
5
+ const ENGINES = ['miki', 'pug', 'ejs', 'handlebars'];
6
+ const RESULTS_DIR = __dirname;
7
+
8
+ function runBench(name) {
9
+ const file = path.join(RESULTS_DIR, `${name}.js`);
10
+ if (!fs.existsSync(file)) {
11
+ console.log(`Skipping ${name} (file not found)`);
12
+ return null;
13
+ }
14
+ console.log(`\n--- Running ${name} benchmark ---`);
15
+ const out = execSync(`node "${file}"`, { encoding: 'utf8', stdio: 'pipe' });
16
+ console.log(out);
17
+ const resultFile = path.join(RESULTS_DIR, `${name}-results.json`);
18
+ if (fs.existsSync(resultFile)) {
19
+ return JSON.parse(fs.readFileSync(resultFile, 'utf8'));
20
+ }
21
+ return null;
11
22
  }
12
23
 
13
- function benchRender(name, iterations = 20) {
14
- const tmplStr = loadTemplate(name);
15
- const compiled = compile(tmplStr);
16
- // warm up cache
17
- compiled.render({});
18
- compiled.asyncRender({});
19
-
20
- const syncTimes = [];
21
- const asyncTimes = [];
22
- for (let i = 0; i < iterations; i++) {
23
- const t0 = performance.now();
24
- compiled.render({});
25
- syncTimes.push(performance.now() - t0);
26
-
27
- const t1 = performance.now();
28
- asyncRender(tmplStr, {});
29
- asyncTimes.push(performance.now() - t1);
24
+ function printComparison(allResults) {
25
+ console.log('\n========================================');
26
+ console.log(' BENCHMARK COMPARISON (lower is better)');
27
+ console.log('========================================\n');
28
+
29
+ const categories = ['small', 'medium', 'large'];
30
+ for (const cat of categories) {
31
+ console.log(`-- ${cat.toUpperCase()} --`);
32
+ const rows = [];
33
+ for (const [engine, results] of Object.entries(allResults)) {
34
+ const r = results.find(x => x.name === `${engine}:${cat}`);
35
+ if (r) rows.push({ engine, rps: r.rps, ms: r.medianMs });
36
+ }
37
+ rows.sort((a, b) => b.rps - a.rps);
38
+ const bestRps = rows[0]?.rps || 1;
39
+ for (const row of rows) {
40
+ const pct = ((bestRps / row.rps) * 100).toFixed(0);
41
+ const marker = row.engine === 'miki' ? '★' : ' ';
42
+ console.log(` ${marker}${row.engine.padEnd(12)} ${row.ms.toFixed(3).padStart(8)} ms ${row.rps.toString().padStart(8)} rps (${pct}%)`);
43
+ }
44
+ console.log('');
30
45
  }
31
- const avg = arr => arr.reduce((a,b)=>a+b,0)/arr.length;
32
- return {
33
- name,
34
- syncAvgMs: avg(syncTimes).toFixed(2),
35
- asyncAvgMs: avg(asyncTimes).toFixed(2)
36
- };
37
46
  }
38
47
 
39
48
  function main() {
40
- const results = [];
41
- ['small','medium','large'].forEach(name => {
42
- results.push(benchRender(name));
43
- });
44
- console.log('Benchmark results:', results);
45
- const outPath = path.join(__dirname, 'report.json');
46
- fs.writeFileSync(outPath, JSON.stringify(results, null, 2));
49
+ const allResults = {};
50
+ for (const engine of ENGINES) {
51
+ const results = runBench(engine);
52
+ if (results) allResults[engine] = results;
53
+ }
54
+
55
+ if (Object.keys(allResults).length === 0) {
56
+ console.log('No benchmark results collected.');
57
+ process.exit(1);
58
+ }
59
+
60
+ printComparison(allResults);
61
+
62
+ const mikiResults = allResults['miki'] || [];
63
+ const mikiAvgRps = mikiResults.reduce((a, r) => a + r.rps, 0) / mikiResults.length;
64
+ const mikiLarge = mikiResults.find(r => r.name === 'miki:large');
65
+ const othersSlowOnLarge = Object.entries(allResults)
66
+ .filter(([engine]) => engine !== 'miki')
67
+ .every(([, results]) => {
68
+ const large = results.find(r => r.name === `${Object.keys(allResults).find(k => allResults[k] === results)}:large`);
69
+ return !large || (mikiLarge && mikiLarge.rps >= large.rps);
70
+ });
71
+
72
+ if (othersSlowOnLarge) {
73
+ console.log('★ miki-template dominates on large/real-world workloads.\n');
74
+ } else if (mikiAvgRps >= 100000) {
75
+ console.log('★ miki-template delivers strong performance across workloads.\n');
76
+ } else {
77
+ console.log('Note: miki-template performance may vary by workload.\n');
78
+ }
47
79
  }
48
80
 
49
81
  main();
@@ -528,7 +528,7 @@ const endRps = 100000 / dur;
528
528
 
529
529
  record('endurance: 100k renders complete in < 30s', dur < 30000,
530
530
  `${dur.toFixed(0)} ms total, ${Math.round(endRps).toLocaleString()} rps avg`);
531
- record('endurance: RSS growth < 50 MB', rssDelta < 50,
531
+ record('endurance: RSS growth < 80 MB', rssDelta < 80,
532
532
  `before ${beforeRss.toFixed(1)} MB, peak ${endRssMax.toFixed(1)} MB (Δ +${rssDelta.toFixed(1)} MB)`);
533
533
  record('endurance: output is still correct at end', (() => {
534
534
  const expected = endC.render(enduranceData);
@@ -0,0 +1,85 @@
1
+ # asyncRender()
2
+
3
+ Async version of `render()`. Returns a Promise. Use this when your templates contain async filters, async custom tags, or async library components.
4
+
5
+ ## Signature
6
+
7
+ ```javascript
8
+ asyncRender(templateStr, contextObj = {}, options = {})
9
+ ```
10
+
11
+ ## Returns
12
+
13
+ `Promise<string>` — The rendered HTML.
14
+
15
+ ## When to Use
16
+
17
+ Use `asyncRender()` when your templates contain:
18
+
19
+ - Async filters (returning Promises)
20
+ - Async custom tags (render return a Promise)
21
+ - Async library helpers
22
+ - `{% load %}` libraries with async components
23
+
24
+ Using async features with `render()` throws: `Async node encountered during sync render. Use asyncRender() instead.`
25
+
26
+ ## Examples
27
+
28
+ ### Basic async render
29
+
30
+ === "CommonJS"
31
+
32
+ ```javascript
33
+ const { asyncRender } = require('miki-template');
34
+
35
+ const html = await asyncRender('Hello {{ name }}!', { name: 'World' });
36
+ ```
37
+
38
+ === "ES Modules"
39
+
40
+ ```javascript
41
+ import { asyncRender } from 'miki-template';
42
+
43
+ const html = await asyncRender('Hello {{ name }}!', { name: 'World' });
44
+ ```
45
+
46
+ ### With async tags/filters
47
+
48
+ === "CommonJS"
49
+
50
+ ```javascript
51
+ const { asyncRender } = require('miki-template');
52
+
53
+ const html = await asyncRender(templateWithAsyncHelpers, context, options);
54
+ ```
55
+
56
+ === "ES Modules"
57
+
58
+ ```javascript
59
+ import { asyncRender } from 'miki-template';
60
+
61
+ const html = await asyncRender(templateWithAsyncHelpers, context, options);
62
+ ```
63
+
64
+ ### Async partial from file
65
+
66
+ === "CommonJS"
67
+
68
+ ```javascript
69
+ const { asyncRender } = require('miki-template');
70
+
71
+ const html = await asyncRender('home#card', context, { views: './views' });
72
+ ```
73
+
74
+ === "ES Modules"
75
+
76
+ ```javascript
77
+ import { asyncRender } from 'miki-template';
78
+
79
+ const html = await asyncRender('home#card', context, { views: './views' });
80
+ ```
81
+
82
+ ## Related
83
+
84
+ - [render()](./render)
85
+ - [compile()](./compile)
@@ -0,0 +1,87 @@
1
+ # Cache API
2
+
3
+ ## clearCache
4
+
5
+ Clear the compiled template cache. Templates are cached in-memory (LRU, 100 entries max). Call this when templates change on disk during development, in tests, or when dynamically registering tags/filters.
6
+
7
+ === "CommonJS"
8
+
9
+ ```javascript
10
+ const { clearCache } = require('miki-template');
11
+
12
+ clearCache();
13
+ ```
14
+
15
+ === "ES Modules"
16
+
17
+ ```javascript
18
+ import { clearCache } from 'miki-template';
19
+
20
+ clearCache();
21
+ ```
22
+
23
+ ## How Caching Works
24
+
25
+ - Templates are cached by a key combining the source string and compile options (views, custom settings).
26
+ - The cache is an in-memory LRU cache limited to 100 entries.
27
+ - Cached compiled templates are reused across renders, improving performance for repeated templates.
28
+ - Partials defined via `{% partialdef %}` are cached along with their parent template.
29
+
30
+ ## When to Clear Cache
31
+
32
+ - During development when templates change frequently on disk
33
+ - In tests to ensure fresh compilation
34
+ - When dynamically registering custom tags/filters at runtime
35
+
36
+ ### Development File Watcher
37
+
38
+ === "CommonJS"
39
+
40
+ ```javascript
41
+ const fs = require('fs');
42
+ const { clearCache } = require('miki-template');
43
+
44
+ if (process.env.NODE_ENV !== 'production') {
45
+ fs.watch('./views', () => {
46
+ clearCache();
47
+ console.log('Template cache cleared');
48
+ });
49
+ }
50
+ ```
51
+
52
+ === "ES Modules"
53
+
54
+ ```javascript
55
+ import fs from 'node:fs';
56
+ import { clearCache } from 'miki-template';
57
+
58
+ if (process.env.NODE_ENV !== 'production') {
59
+ fs.watch('./views', () => {
60
+ clearCache();
61
+ console.log('Template cache cleared');
62
+ });
63
+ }
64
+ ```
65
+
66
+ ## cache Library
67
+
68
+ The built-in `cache` library (auto-activated) provides a template tag for caching fragments:
69
+
70
+ ```html
71
+ {% load cache %}
72
+
73
+ {% cache 300 sidebar_key %}
74
+ <div class="sidebar">
75
+ {% for item in sidebar_items %}
76
+ <a href="{{ item.url }}">{{ item.title }}</a>
77
+ {% endfor %}
78
+ </div>
79
+ {% endcache %}
80
+ ```
81
+
82
+ The first argument is the TTL in seconds. The second is a cache key. Additional arguments serve as key components.
83
+
84
+ ## Next Steps
85
+
86
+ - [Advanced Usage: Caching](../guide/advanced-usage#caching)
87
+ - [API Reference](../)
@@ -0,0 +1,128 @@
1
+ # compile()
2
+
3
+ Compile a template string into a reusable renderable object.
4
+
5
+ ## Signature
6
+
7
+ ```javascript
8
+ compile(templateStr, options = {})
9
+ ```
10
+
11
+ ## Parameters
12
+
13
+ | Parameter | Type | Description |
14
+ |-----------|------|-------------|
15
+ | `templateStr` | `string` | Template source string |
16
+ | `options` | `object` | Options including `views` directories |
17
+
18
+ ## Returns
19
+
20
+ An object with these render methods:
21
+
22
+ | Method | Description |
23
+ |--------|-------------|
24
+ | `render(contextObj, callOptions)` | Synchronous render |
25
+ | `renderWith(contextObj, callOptions)` | Render with options override |
26
+ | `asyncRender(contextObj)` | Asynchronous render (supports async filters/tags) |
27
+ | `asyncRenderWith(contextObj, callOptions)` | Async render with options override |
28
+ | `renderBlock(blockName, contextObj)` | Render a single `{% block %}` |
29
+ | `renderPartial(partialName, contextObj)` | Render a named `{% partialdef %}` |
30
+
31
+ ## Examples
32
+
33
+ ### Basic compile
34
+
35
+ === "CommonJS"
36
+
37
+ ```javascript
38
+ const { compile } = require('miki-template');
39
+
40
+ const compiled = compile('<h1>{{ title }}</h1>');
41
+
42
+ const html = compiled.render({ title: 'Hello' });
43
+ // Output: <h1>Hello</h1>
44
+ ```
45
+
46
+ === "ES Modules"
47
+
48
+ ```javascript
49
+ import { compile } from 'miki-template';
50
+
51
+ const compiled = compile('<h1>{{ title }}</h1>');
52
+
53
+ const html = compiled.render({ title: 'Hello' });
54
+ // Output: <h1>Hello</h1>
55
+ ```
56
+
57
+ ### Render with options override
58
+
59
+ === "CommonJS"
60
+
61
+ ```javascript
62
+ const { compile } = require('miki-template');
63
+
64
+ const compiled = compile(template, { views: './templates' });
65
+ const html = compiled.renderWith({ title: 'Hello' }, { views: './other-views' });
66
+ ```
67
+
68
+ === "ES Modules"
69
+
70
+ ```javascript
71
+ import { compile } from 'miki-template';
72
+
73
+ const compiled = compile(template, { views: './templates' });
74
+ const html = compiled.renderWith({ title: 'Hello' }, { views: './other-views' });
75
+ ```
76
+
77
+ ### Render a Block (template inheritance)
78
+
79
+ === "CommonJS"
80
+
81
+ ```javascript
82
+ const { compile } = require('miki-template');
83
+
84
+ const compiled = compile(childTemplate, { views: './templates' });
85
+ const html = compiled.renderBlock('content', context);
86
+ ```
87
+
88
+ === "ES Modules"
89
+
90
+ ```javascript
91
+ import { compile } from 'miki-template';
92
+
93
+ const compiled = compile(childTemplate, { views: './templates' });
94
+ const html = compiled.renderBlock('content', context);
95
+ ```
96
+
97
+ ### Render a Partial
98
+
99
+ === "CommonJS"
100
+
101
+ ```javascript
102
+ const { compile } = require('miki-template');
103
+
104
+ const compiled = compile(`
105
+ {% partialdef card %}
106
+ <div class="card">{{ title }}</div>
107
+ {% endpartialdef %}
108
+ `);
109
+ const html = compiled.renderPartial('card', { title: 'Hello' });
110
+ ```
111
+
112
+ === "ES Modules"
113
+
114
+ ```javascript
115
+ import { compile } from 'miki-template';
116
+
117
+ const compiled = compile(`
118
+ {% partialdef card %}
119
+ <div class="card">{{ title }}</div>
120
+ {% endpartialdef %}
121
+ `);
122
+ const html = compiled.renderPartial('card', { title: 'Hello' });
123
+ ```
124
+
125
+ ## Related
126
+
127
+ - [render()](./render)
128
+ - [asyncRender()](./async-render)
@@ -0,0 +1,77 @@
1
+ # Context Processors API
2
+
3
+ ## registerContextProcessor
4
+
5
+ Register a context processor function that runs before every render. The returned object is merged into the rendering context, with explicit context values always winning.
6
+
7
+ === "CommonJS"
8
+
9
+ ```javascript
10
+ const { registerContextProcessor } = require('miki-template');
11
+
12
+ registerContextProcessor((context) => {
13
+ return {
14
+ siteName: 'My App',
15
+ currentYear: new Date().getFullYear()
16
+ };
17
+ });
18
+ ```
19
+
20
+ === "ES Modules"
21
+
22
+ ```javascript
23
+ import { registerContextProcessor } from 'miki-template';
24
+
25
+ registerContextProcessor((context) => {
26
+ return {
27
+ siteName: 'My App',
28
+ currentYear: new Date().getFullYear()
29
+ };
30
+ });
31
+ ```
32
+
33
+ ### Signature
34
+
35
+ ```typescript
36
+ type ContextProcessor = (context: Context) => Record<string, any> | null
37
+ ```
38
+
39
+ - Receives the `Context` object, allowing inspection of existing values via `context.get('key')`.
40
+ - Must return a plain object. Returning `null` or `undefined` is treated as `{}`.
41
+ - Must be synchronous — no async/await or Promises.
42
+
43
+ ## clearContextProcessors
44
+
45
+ Clear all registered context processors. Useful in tests or when re-configuring.
46
+
47
+ === "CommonJS"
48
+
49
+ ```javascript
50
+ const { clearContextProcessors } = require('miki-template');
51
+
52
+ clearContextProcessors();
53
+ ```
54
+
55
+ === "ES Modules"
56
+
57
+ ```javascript
58
+ import { clearContextProcessors } from 'miki-template';
59
+
60
+ clearContextProcessors();
61
+ ```
62
+
63
+ ## Precedence Rules
64
+
65
+ 1. **Context processors run first** — their key/value pairs are added to the context.
66
+ 2. **Your explicit context is applied last** — explicit values always override processor values.
67
+
68
+ ```javascript
69
+ // Processor sets: { siteName: 'My App', theme: 'dark' }
70
+ // You render with: { theme: 'light' }
71
+ // Result: { siteName: 'My App', theme: 'light' }
72
+ ```
73
+
74
+ ## Next Steps
75
+
76
+ - [Context Processors Guide](../guide/context-processors)
77
+ - [API Reference](../)