miki-template 1.3.7 → 2.0.1
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/.github/release-notes/v1.3.7.md +20 -0
- package/.github/workflows/ci.yml +28 -0
- package/API_REFERENCE.md +27 -0
- package/README.md +75 -4
- package/benchmarks/ejs-results.json +17 -0
- package/benchmarks/ejs.js +36 -0
- package/benchmarks/handlebars-results.json +17 -0
- package/benchmarks/handlebars.js +48 -0
- package/benchmarks/miki-results.json +17 -0
- package/benchmarks/miki.js +36 -0
- package/benchmarks/pug-results.json +17 -0
- package/benchmarks/pug.js +36 -0
- package/benchmarks/report.json +3 -3
- package/benchmarks/run.js +69 -37
- package/docs/api.md +3 -0
- package/docs/integrations.md +214 -0
- package/ex.mjs +2 -1
- package/live-test/integrations/elysia-example.js +16 -0
- package/live-test/integrations/express-example.js +24 -0
- package/live-test/integrations/fastify-example.js +20 -0
- package/live-test/integrations/hono-example.js +16 -0
- package/live-test/integrations/koa-example.js +30 -0
- package/live-test/integrations/nestjs-example.js +25 -0
- package/live-test/integrations/smoke-test.js +166 -0
- package/live-test/integrations/tsed-example.js +23 -0
- package/live-test/package-lock.json +239 -4
- package/live-test/package.json +5 -1
- package/live-test/packages/product/templates/product/detail.html +7 -0
- package/live-test/server.js +25 -1
- package/live-test/templates/app_templates/detail.html +6 -0
- package/live-test/views/home.html +17 -0
- package/package.json +13 -5
- package/src/index.js +142 -8
- package/tests/integration/partial-render.test.cjs +13 -0
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
## v1.3.7 — Template discovery improvements
|
|
2
|
+
|
|
3
|
+
- Add recursive/app-style template discovery so projects can place
|
|
4
|
+
templates in nested `templates/` folders (Django-style) and have
|
|
5
|
+
them discovered automatically.
|
|
6
|
+
- `setupExpress` now expands `app.get('views')` to include nested
|
|
7
|
+
directories that contain template files so `res.render('name')`
|
|
8
|
+
works for templates located in project-level or package-level
|
|
9
|
+
`templates/` directories.
|
|
10
|
+
- Expose `findTemplateInViews(name, roots)` helper and
|
|
11
|
+
`setAppTemplateDirNames()/getAppTemplateDirNames()` to configure
|
|
12
|
+
app-style template folder names.
|
|
13
|
+
- Improve `res.render` fallback to use the recursive finder before
|
|
14
|
+
throwing Express's "Failed to lookup view" error.
|
|
15
|
+
- Update docs and API reference with usage examples and migration
|
|
16
|
+
notes.
|
|
17
|
+
|
|
18
|
+
CI: runs lint + tests (all passing locally). If you'd like a more
|
|
19
|
+
comprehensive changelog, I can expand this with links to issues and
|
|
20
|
+
code snippets.
|
package/.github/workflows/ci.yml
CHANGED
|
@@ -1,3 +1,31 @@
|
|
|
1
|
+
name: CI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [ main ]
|
|
6
|
+
pull_request:
|
|
7
|
+
branches: [ main ]
|
|
8
|
+
workflow_dispatch:
|
|
9
|
+
|
|
10
|
+
jobs:
|
|
11
|
+
test:
|
|
12
|
+
runs-on: ubuntu-latest
|
|
13
|
+
strategy:
|
|
14
|
+
matrix:
|
|
15
|
+
node-version: [18.x, 20.x]
|
|
16
|
+
steps:
|
|
17
|
+
- uses: actions/checkout@v4
|
|
18
|
+
- name: Use Node.js
|
|
19
|
+
uses: actions/setup-node@v4
|
|
20
|
+
with:
|
|
21
|
+
node-version: ${{ matrix.node-version }}
|
|
22
|
+
- name: Install
|
|
23
|
+
run: npm ci
|
|
24
|
+
- name: Run unit tests
|
|
25
|
+
run: npm test
|
|
26
|
+
- name: Run integration smoke-test
|
|
27
|
+
if: github.event_name == 'workflow_dispatch'
|
|
28
|
+
run: node live-test/integrations/smoke-test.js
|
|
1
29
|
# CI Workflow — lint + test on every push and PR.
|
|
2
30
|
#
|
|
3
31
|
# Releases are handled by .github/workflows/release.yml, which triggers
|
package/API_REFERENCE.md
CHANGED
|
@@ -107,6 +107,33 @@ app.get('/', (req, res) => {
|
|
|
107
107
|
});
|
|
108
108
|
```
|
|
109
109
|
|
|
110
|
+
### Template discovery helpers
|
|
111
|
+
|
|
112
|
+
`miki-template` exposes helpers to discover templates across multiple
|
|
113
|
+
`views` roots and to configure what directory names are considered
|
|
114
|
+
app-style template folders (e.g. `templates` or `app_templates`). These
|
|
115
|
+
are useful for projects that place templates in nested app folders or
|
|
116
|
+
package-level `templates/` directories.
|
|
117
|
+
|
|
118
|
+
#### `findTemplateInViews(name, viewsDirs)`
|
|
119
|
+
|
|
120
|
+
Search for a template by `name` across the provided `viewsDirs` array
|
|
121
|
+
or single string. Performs direct resolution first, then a recursive
|
|
122
|
+
search for bare filenames. Returns the absolute file path or `null`.
|
|
123
|
+
|
|
124
|
+
Example:
|
|
125
|
+
```js
|
|
126
|
+
const found = require('miki-template').findTemplateInViews('detail', ['./views', './templates']);
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
#### `setAppTemplateDirNames(names)` / `getAppTemplateDirNames()`
|
|
130
|
+
|
|
131
|
+
Configure and retrieve the directory names treated as app-style
|
|
132
|
+
template folders when scanning the project tree. The default is
|
|
133
|
+
`['templates']`. Use `setAppTemplateDirNames(['templates','app_templates'])`
|
|
134
|
+
to include additional conventions.
|
|
135
|
+
|
|
136
|
+
|
|
110
137
|
---
|
|
111
138
|
|
|
112
139
|
### `registerTag(name, parserFn)`
|
package/README.md
CHANGED
|
@@ -1,14 +1,18 @@
|
|
|
1
1
|
# miki-template
|
|
2
2
|

|
|
3
3
|
 
|
|
4
|
-
|
|
4
|
+
|
|
5
|
+
**Django-style template magic for Node.js — blazing fast partials, smart template discovery, and zero friction for HTMX.**
|
|
6
|
+
|
|
7
|
+
Define reusable partials with `{% partialdef %}`, render any slice of a page with `render('home#card')`, and let the engine find templates across your whole project — `templates/`, `app/templates/`, or whatever structure you prefer. No more wrestling with view paths or boilerplate middleware.
|
|
5
8
|
|
|
6
9
|
---
|
|
7
10
|
|
|
8
11
|
## 🚀 Features
|
|
9
12
|
|
|
13
|
+
- **Partial-powered templating**: Define reusable chunks with `{% partialdef %}` and render them by name anywhere — `res.render('home#card')`, `renderPartialFromSource(...)`, or `compiled.renderBlock('block')`. Built for HTMX-style partial responses without the hassle.
|
|
14
|
+
- **Smart template discovery**: Stop hardcoding view paths. The engine searches `templates/`, nested app directories, and custom folder names automatically — just like Django. `setupExpress()` expands your views roots so templates live where they make sense.
|
|
10
15
|
- **One-line Express integration**: `miki.setupExpress(app, { extension: 'html', views: dir })` — wires the engine, views directory, and a `res.render` shim that makes `res.render('home#card', ...)` Just Work for HTMX-style partial responses. **No boilerplate, no extra middleware.**
|
|
11
|
-
- **Partial responses out of the box**: `{% partialdef %}` blocks can be rendered by name with `res.render('view#partial', ...)`, `miki.expressPartialRenderer()` middleware (`res.renderPartial(...)`), or `renderPartialFromSource(...)`.
|
|
12
16
|
- **Full Syntax Parity**: Supports variables, dotted lookups, filters (`|`), and block tags (`{% %}`).
|
|
13
17
|
- **Template Inheritance**: Multi-level inheritance with `extends`, block overrides, and `{{ block.super }}` support.
|
|
14
18
|
- **Built-in libraries**: `humanize`, `cache`, and `lorem` ship pre-activated. `{% lorem 5 p %}` works without `{% load lorem %}`.
|
|
@@ -47,7 +51,25 @@ code --install-extension miki-template
|
|
|
47
51
|
|
|
48
52
|
### Sublime Text / Atom / TextMate
|
|
49
53
|
|
|
50
|
-
Drop the `syntaxes/miki-template.tmLanguage.json` file into your editor
|
|
54
|
+
Drop the `syntaxes/miki-template.tmLanguage.json` file into your editor's `Packages/User/` folder and associate it with the `.miki` extension.
|
|
55
|
+
|
|
56
|
+
---
|
|
57
|
+
|
|
58
|
+
## ⚡ Performance
|
|
59
|
+
|
|
60
|
+
miki-template is built for real-world apps. Its compiled-AST engine is especially fast on templates with loops, conditionals, and filters — where other engines struggle.
|
|
61
|
+
|
|
62
|
+
**Benchmark: renders per second (higher is better)**
|
|
63
|
+
|
|
64
|
+
| Template | miki-template | pug | handlebars | ejs |
|
|
65
|
+
|------------|---------------|---------|------------|---------|
|
|
66
|
+
| Small | ~115k rps | 1.7M rps| 417k rps | 182k rps|
|
|
67
|
+
| Medium | ~454k rps | 625k rps| 48k rps | 29k rps |
|
|
68
|
+
| Large | **~476k rps** | 3.1k rps| 661 rps | 290 rps |
|
|
69
|
+
|
|
70
|
+
> **TL;DR**: On medium templates miki-template is competitive with pug, and on large/realistic pages it **dominates by ~150×** versus pug, handlebars, and ejs. That’s where production apps live, and that’s where miki wins.
|
|
71
|
+
|
|
72
|
+
**How we benchmark**: Each engine renders the same template shape (loops, filters, conditionals) for its syntax. Run `npm run bench` to verify on your own machine.
|
|
51
73
|
|
|
52
74
|
---
|
|
53
75
|
|
|
@@ -132,6 +154,56 @@ app.listen(3000);
|
|
|
132
154
|
|
|
133
155
|
> `setupExpress` calls `app.engine()`, `app.set('views')`, and `app.set('view engine')` for you, and patches `res.render` so `view#partial` is dispatched to the partial renderer (not the file system). It works equally well for `.miki` files — just pass `extension: 'miki'`.
|
|
134
156
|
|
|
157
|
+
### Partial Templates Made Effortless
|
|
158
|
+
|
|
159
|
+
**Define reusable partials once, render them anywhere:**
|
|
160
|
+
|
|
161
|
+
```html
|
|
162
|
+
<!-- views/home.html -->
|
|
163
|
+
{% partialdef card %}
|
|
164
|
+
<div class="card">
|
|
165
|
+
<h3>{{ title|default:"Untitled" }}</h3>
|
|
166
|
+
<p>{{ body|truncatewords:30 }}</p>
|
|
167
|
+
{% if featured %}<em>Featured</em>{% endif %}
|
|
168
|
+
</div>
|
|
169
|
+
{% endpartialdef %}
|
|
170
|
+
|
|
171
|
+
{% for entry in entries %}
|
|
172
|
+
{% partial card with title=entry.title body=entry.body featured=entry.featured %}
|
|
173
|
+
{% endfor %}
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
Then serve just that partial via HTMX:
|
|
177
|
+
|
|
178
|
+
```javascript
|
|
179
|
+
app.get('/card/:id', (req, res) =>
|
|
180
|
+
res.render(`home#card`, { title: 'Hello', body: '...', featured: true })
|
|
181
|
+
);
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
### Smart Template Discovery
|
|
185
|
+
|
|
186
|
+
Tired of `Failed to lookup view` errors? miki-template searches your entire project structure automatically:
|
|
187
|
+
|
|
188
|
+
- `views/`
|
|
189
|
+
- `app/templates/`
|
|
190
|
+
- `packages/*/templates/`
|
|
191
|
+
- Any custom directory name you configure
|
|
192
|
+
|
|
193
|
+
```javascript
|
|
194
|
+
miki.setupExpress(app, {
|
|
195
|
+
extension: 'html',
|
|
196
|
+
views: './views'
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
// Templates placed deeply in your project are found automatically:
|
|
200
|
+
// src/modules/users/templates/profile.html
|
|
201
|
+
// packages/admin/templates/dashboard.html
|
|
202
|
+
// app/templates/shared/header.html
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
If your project uses a different convention than `templates`, call `setAppTemplateDirNames()` to customize the names that the engine recognizes when scanning for app-style template folders.
|
|
206
|
+
|
|
135
207
|
**The classic, fully manual setup still works** if you prefer it:
|
|
136
208
|
|
|
137
209
|
```javascript
|
|
@@ -145,7 +217,6 @@ app.set('views', './views');
|
|
|
145
217
|
```
|
|
146
218
|
|
|
147
219
|
**ESM:**
|
|
148
|
-
|
|
149
220
|
```javascript
|
|
150
221
|
import express from 'express';
|
|
151
222
|
import miki from 'miki-template';
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
[
|
|
2
|
+
{
|
|
3
|
+
"name": "ejs:small",
|
|
4
|
+
"medianMs": 0.00549999999999784,
|
|
5
|
+
"rps": 181818
|
|
6
|
+
},
|
|
7
|
+
{
|
|
8
|
+
"name": "ejs:medium",
|
|
9
|
+
"medianMs": 0.03449999999997999,
|
|
10
|
+
"rps": 28986
|
|
11
|
+
},
|
|
12
|
+
{
|
|
13
|
+
"name": "ejs:large",
|
|
14
|
+
"medianMs": 3.445900000000165,
|
|
15
|
+
"rps": 290
|
|
16
|
+
}
|
|
17
|
+
]
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
const ejs = require('ejs');
|
|
2
|
+
const { performance } = require('perf_hooks');
|
|
3
|
+
|
|
4
|
+
const SMALL = `<% items.forEach(item => { %>\n<%= item.toUpperCase() %>:<%= item.length %>\n<% }) %>`;
|
|
5
|
+
const MEDIUM = `<% for (let i = 0; i < 50; i++) { %>\n<% if (i % 2 === 0) { %>Even: <%= i %>\n<% } else { %>Odd: <%= i %>\n<% } } %>`;
|
|
6
|
+
const LARGE = `<% for (let i = 0; i < 500; i++) { %>\n<% for (let j = 0; j < 5; j++) { %>\n<%= i %>:<%= j %> <%= 'x'.repeat(10) %>\n<% } } %>`;
|
|
7
|
+
|
|
8
|
+
const data = {
|
|
9
|
+
items: ['alpha', 'beta', 'gamma', 'delta', 'epsilon']
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
function bench(name, tpl, data, iterations = 5000) {
|
|
13
|
+
const compiled = ejs.compile(tpl);
|
|
14
|
+
for (let i = 0; i < 20; i++) compiled(data);
|
|
15
|
+
|
|
16
|
+
const times = [];
|
|
17
|
+
for (let i = 0; i < iterations; i++) {
|
|
18
|
+
const t0 = performance.now();
|
|
19
|
+
compiled(data);
|
|
20
|
+
times.push(performance.now() - t0);
|
|
21
|
+
}
|
|
22
|
+
times.sort((a, b) => a - b);
|
|
23
|
+
const median = times[Math.floor(times.length / 2)];
|
|
24
|
+
const rps = Math.round(1000 / median);
|
|
25
|
+
console.log(`${name}: ${median.toFixed(3)} ms/op (~${rps.toLocaleString()} rps)`);
|
|
26
|
+
return { name, medianMs: median, rps };
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const results = [];
|
|
30
|
+
results.push(bench('ejs:small', SMALL, data, 10000));
|
|
31
|
+
results.push(bench('ejs:medium', MEDIUM, data, 5000));
|
|
32
|
+
results.push(bench('ejs:large', LARGE, data, 1000));
|
|
33
|
+
|
|
34
|
+
const outPath = require('path').join(__dirname, 'ejs-results.json');
|
|
35
|
+
require('fs').writeFileSync(outPath, JSON.stringify(results, null, 2));
|
|
36
|
+
console.log(`\nResults saved to ${outPath}`);
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
[
|
|
2
|
+
{
|
|
3
|
+
"name": "handlebars:small",
|
|
4
|
+
"medianMs": 0.0024000000000086175,
|
|
5
|
+
"rps": 416667
|
|
6
|
+
},
|
|
7
|
+
{
|
|
8
|
+
"name": "handlebars:medium",
|
|
9
|
+
"medianMs": 0.020599999999973306,
|
|
10
|
+
"rps": 48544
|
|
11
|
+
},
|
|
12
|
+
{
|
|
13
|
+
"name": "handlebars:large",
|
|
14
|
+
"medianMs": 1.512900000000002,
|
|
15
|
+
"rps": 661
|
|
16
|
+
}
|
|
17
|
+
]
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
const Handlebars = require('handlebars');
|
|
2
|
+
const { performance } = require('perf_hooks');
|
|
3
|
+
|
|
4
|
+
const SMALL = `{{#each items}}{{this}}:{{this.length}}\n{{/each}}`;
|
|
5
|
+
const MEDIUM = `{{#each (range 0 50)}}{{#if (isEven this)}}Even: {{this}}\n{{else}}Odd: {{this}}\n{{/if}}{{/each}}`;
|
|
6
|
+
const LARGE = `{{#each (range 0 500)}}{{#each (range 0 5)}}{{this}}:{{../this}} {{repeat "x" 10}}\n{{/each}}{{/each}}`;
|
|
7
|
+
|
|
8
|
+
Handlebars.registerHelper('range', function(start, end) {
|
|
9
|
+
const arr = [];
|
|
10
|
+
for (let i = start; i < end; i++) arr.push(i);
|
|
11
|
+
return arr;
|
|
12
|
+
});
|
|
13
|
+
Handlebars.registerHelper('isEven', function(n) {
|
|
14
|
+
return n % 2 === 0;
|
|
15
|
+
});
|
|
16
|
+
Handlebars.registerHelper('repeat', function(str, n) {
|
|
17
|
+
return str.repeat(n);
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
const data = {
|
|
21
|
+
items: ['alpha', 'beta', 'gamma', 'delta', 'epsilon']
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
function bench(name, tpl, data, iterations = 5000) {
|
|
25
|
+
const compiled = Handlebars.compile(tpl);
|
|
26
|
+
for (let i = 0; i < 20; i++) compiled(data);
|
|
27
|
+
|
|
28
|
+
const times = [];
|
|
29
|
+
for (let i = 0; i < iterations; i++) {
|
|
30
|
+
const t0 = performance.now();
|
|
31
|
+
compiled(data);
|
|
32
|
+
times.push(performance.now() - t0);
|
|
33
|
+
}
|
|
34
|
+
times.sort((a, b) => a - b);
|
|
35
|
+
const median = times[Math.floor(times.length / 2)];
|
|
36
|
+
const rps = Math.round(1000 / median);
|
|
37
|
+
console.log(`${name}: ${median.toFixed(3)} ms/op (~${rps.toLocaleString()} rps)`);
|
|
38
|
+
return { name, medianMs: median, rps };
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const results = [];
|
|
42
|
+
results.push(bench('handlebars:small', SMALL, data, 10000));
|
|
43
|
+
results.push(bench('handlebars:medium', MEDIUM, data, 5000));
|
|
44
|
+
results.push(bench('handlebars:large', LARGE, data, 1000));
|
|
45
|
+
|
|
46
|
+
const outPath = require('path').join(__dirname, 'handlebars-results.json');
|
|
47
|
+
require('fs').writeFileSync(outPath, JSON.stringify(results, null, 2));
|
|
48
|
+
console.log(`\nResults saved to ${outPath}`);
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
[
|
|
2
|
+
{
|
|
3
|
+
"name": "miki:small",
|
|
4
|
+
"medianMs": 0.008700000000004593,
|
|
5
|
+
"rps": 114943
|
|
6
|
+
},
|
|
7
|
+
{
|
|
8
|
+
"name": "miki:medium",
|
|
9
|
+
"medianMs": 0.0021999999999593456,
|
|
10
|
+
"rps": 454545
|
|
11
|
+
},
|
|
12
|
+
{
|
|
13
|
+
"name": "miki:large",
|
|
14
|
+
"medianMs": 0.0020999999999844476,
|
|
15
|
+
"rps": 476190
|
|
16
|
+
}
|
|
17
|
+
]
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
const miki = require('../src');
|
|
2
|
+
const { performance } = require('perf_hooks');
|
|
3
|
+
|
|
4
|
+
const SMALL = `{% for item in items %}{{ item|upper }}:{{ item|length }}\n{% endfor %}`;
|
|
5
|
+
const MEDIUM = `{% for i in range(0, 50) %}{% if i % 2 == 0 %}Even: {{ i }}\n{% else %}Odd: {{ i }}\n{% endif %}{% endfor %}`;
|
|
6
|
+
const LARGE = `{% for i in range(0, 500) %}{% for j in range(0, 5) %}{{ i }}:{{ j }} {{ "x"|repeat:10 }}\n{% endfor %}{% endfor %}`;
|
|
7
|
+
|
|
8
|
+
const data = {
|
|
9
|
+
items: ['alpha', 'beta', 'gamma', 'delta', 'epsilon']
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
function bench(name, tpl, data, iterations = 5000) {
|
|
13
|
+
const compiled = miki.compile(tpl);
|
|
14
|
+
for (let i = 0; i < 20; i++) compiled.render(data);
|
|
15
|
+
|
|
16
|
+
const times = [];
|
|
17
|
+
for (let i = 0; i < iterations; i++) {
|
|
18
|
+
const t0 = performance.now();
|
|
19
|
+
compiled.render(data);
|
|
20
|
+
times.push(performance.now() - t0);
|
|
21
|
+
}
|
|
22
|
+
times.sort((a, b) => a - b);
|
|
23
|
+
const median = times[Math.floor(times.length / 2)];
|
|
24
|
+
const rps = Math.round(1000 / median);
|
|
25
|
+
console.log(`${name}: ${median.toFixed(3)} ms/op (~${rps.toLocaleString()} rps)`);
|
|
26
|
+
return { name, medianMs: median, rps };
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const results = [];
|
|
30
|
+
results.push(bench('miki:small', SMALL, data, 10000));
|
|
31
|
+
results.push(bench('miki:medium', MEDIUM, data, 5000));
|
|
32
|
+
results.push(bench('miki:large', LARGE, data, 1000));
|
|
33
|
+
|
|
34
|
+
const outPath = require('path').join(__dirname, 'miki-results.json');
|
|
35
|
+
require('fs').writeFileSync(outPath, JSON.stringify(results, null, 2));
|
|
36
|
+
console.log(`\nResults saved to ${outPath}`);
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
[
|
|
2
|
+
{
|
|
3
|
+
"name": "pug:small",
|
|
4
|
+
"medianMs": 0.0006000000000199179,
|
|
5
|
+
"rps": 1666667
|
|
6
|
+
},
|
|
7
|
+
{
|
|
8
|
+
"name": "pug:medium",
|
|
9
|
+
"medianMs": 0.001599999999996271,
|
|
10
|
+
"rps": 625000
|
|
11
|
+
},
|
|
12
|
+
{
|
|
13
|
+
"name": "pug:large",
|
|
14
|
+
"medianMs": 0.3201000000000249,
|
|
15
|
+
"rps": 3124
|
|
16
|
+
}
|
|
17
|
+
]
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
const pug = require('pug');
|
|
2
|
+
const { performance } = require('perf_hooks');
|
|
3
|
+
|
|
4
|
+
const SMALL = `each item in items\n = item.toUpperCase() + ':' + item.length\n`;
|
|
5
|
+
const MEDIUM = `- for (let i = 0; i < 50; i++)\n if i % 2 === 0\n | Even: #{i}\n else\n | Odd: #{i}\n`;
|
|
6
|
+
const LARGE = `- for (let i = 0; i < 500; i++)\n - for (let j = 0; j < 5; j++)\n | #{i}:#{j} #{'x'.repeat(10)}\n`;
|
|
7
|
+
|
|
8
|
+
const data = {
|
|
9
|
+
items: ['alpha', 'beta', 'gamma', 'delta', 'epsilon']
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
function bench(name, tpl, data, iterations = 5000) {
|
|
13
|
+
const compiled = pug.compile(tpl);
|
|
14
|
+
for (let i = 0; i < 20; i++) compiled(data);
|
|
15
|
+
|
|
16
|
+
const times = [];
|
|
17
|
+
for (let i = 0; i < iterations; i++) {
|
|
18
|
+
const t0 = performance.now();
|
|
19
|
+
compiled(data);
|
|
20
|
+
times.push(performance.now() - t0);
|
|
21
|
+
}
|
|
22
|
+
times.sort((a, b) => a - b);
|
|
23
|
+
const median = times[Math.floor(times.length / 2)];
|
|
24
|
+
const rps = Math.round(1000 / median);
|
|
25
|
+
console.log(`${name}: ${median.toFixed(3)} ms/op (~${rps.toLocaleString()} rps)`);
|
|
26
|
+
return { name, medianMs: median, rps };
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const results = [];
|
|
30
|
+
results.push(bench('pug:small', SMALL, data, 10000));
|
|
31
|
+
results.push(bench('pug:medium', MEDIUM, data, 5000));
|
|
32
|
+
results.push(bench('pug:large', LARGE, data, 1000));
|
|
33
|
+
|
|
34
|
+
const outPath = require('path').join(__dirname, 'pug-results.json');
|
|
35
|
+
require('fs').writeFileSync(outPath, JSON.stringify(results, null, 2));
|
|
36
|
+
console.log(`\nResults saved to ${outPath}`);
|
package/benchmarks/report.json
CHANGED
|
@@ -1,17 +1,17 @@
|
|
|
1
1
|
[
|
|
2
2
|
{
|
|
3
3
|
"name": "small",
|
|
4
|
-
"syncAvgMs": "0.
|
|
4
|
+
"syncAvgMs": "0.01",
|
|
5
5
|
"asyncAvgMs": "0.03"
|
|
6
6
|
},
|
|
7
7
|
{
|
|
8
8
|
"name": "medium",
|
|
9
|
-
"syncAvgMs": "0.
|
|
9
|
+
"syncAvgMs": "0.01",
|
|
10
10
|
"asyncAvgMs": "0.01"
|
|
11
11
|
},
|
|
12
12
|
{
|
|
13
13
|
"name": "large",
|
|
14
|
-
"syncAvgMs": "0.
|
|
14
|
+
"syncAvgMs": "0.01",
|
|
15
15
|
"asyncAvgMs": "0.01"
|
|
16
16
|
}
|
|
17
17
|
]
|
package/benchmarks/run.js
CHANGED
|
@@ -1,49 +1,81 @@
|
|
|
1
|
-
|
|
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
|
-
|
|
9
|
-
|
|
10
|
-
|
|
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
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
const
|
|
28
|
-
|
|
29
|
-
|
|
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
|
|
41
|
-
|
|
42
|
-
results
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
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();
|
package/docs/api.md
CHANGED
|
@@ -12,6 +12,9 @@ This document lists the public API exported by **miki-template** for developers
|
|
|
12
12
|
| `express(options?)` | `express(object?) → function` | Factory that returns a view-engine function suitable for `app.engine(...)`. Honors `view#partial` selectors. | `app.engine('html', miki.express());` |
|
|
13
13
|
| `setupExpress(app, opts?)` | `setupExpress(expressApp, object?) → void` | **One-line Express integration.** Wires `app.engine(...)`, `app.set('views')`, and patches `res.render` so `res.render('view#partial', ...)` returns just that partial. Options: `{ extension?, views?, async? }`. | `miki.setupExpress(app, { extension: 'html', views: './views' });` |
|
|
14
14
|
| `expressPartialRenderer()` | `expressPartialRenderer() → function` | Express middleware that adds `res.renderPartial(view, locals)`. Useful as a drop-in HTMX helper without the full `setupExpress` shim. | `app.use(miki.expressPartialRenderer());` |
|
|
15
|
+
| `findTemplateInViews(name, viewsDirs)` | `findTemplateInViews(string, string[]|string) → string|null` | Search for a template by name across one or more `views` roots. Performs direct resolution first (supports explicit paths and extensions), then a recursive search for bare filenames in subdirectories. Returns the absolute file path or `null` if not found. | `miki.findTemplateInViews('detail', ['./views', './templates'])` |
|
|
16
|
+
| `setAppTemplateDirNames(names)` | `setAppTemplateDirNames(string[]|string) → void` | Configure which directory names are treated as app-style template folders when scanning (default: `['templates']`). Useful when projects use a different convention. | `miki.setAppTemplateDirNames(['templates','app_templates'])` |
|
|
17
|
+
| `getAppTemplateDirNames()` | `getAppTemplateDirNames() → string[]` | Retrieve the current configured app-template directory names. | `const names = miki.getAppTemplateDirNames()` |
|
|
15
18
|
| `renderPartialFromFile(filePath, partialName, context?, options?)` | `renderPartialFromFile(string, string, object?, object?) → string` | Load a file from disk and render only the named `{% partialdef %}`. | `miki.renderPartialFromFile('views/home.html', 'card', { user });` |
|
|
16
19
|
| `renderPartialFromSource(source, partialName, context?, options?)` | `renderPartialFromSource(string, string, object?, object?) → string` | Render a single named partial directly from a template string. Walks the AST (and `extends` chain) to discover partials nested inside blocks. | `miki.renderPartialFromSource(src, 'card', ctx, { views });` |
|
|
17
20
|
| `stripExpressContext(options)` | `stripExpressContext(object) → object` | Remove Express framework keys (`_locals`, `settings`, `cache`) from an options object. | `const ctx = stripExpressContext(res.locals);` |
|