miki-template 2.3.1 → 2.3.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 (159) hide show
  1. package/package.json +45 -1
  2. package/.eslintrc.json +0 -16
  3. package/.github/release-notes/v1.3.1.md +0 -55
  4. package/.github/release-notes/v1.3.3.md +0 -77
  5. package/.github/release-notes/v1.3.7.md +0 -20
  6. package/.github/workflows/ci.yml +0 -42
  7. package/.github/workflows/docs.yml +0 -107
  8. package/.github/workflows/release.yml +0 -107
  9. package/AGENT.md +0 -71
  10. package/API_REFERENCE.md +0 -341
  11. package/CHANGELOG.md +0 -173
  12. package/CODE_OF_CONDUCT.md +0 -14
  13. package/CONTRIBUTING.md +0 -27
  14. package/ROADMAP.md +0 -40
  15. package/assets/banner.png +0 -0
  16. package/assets/logo.png +0 -0
  17. package/benchmarks/ejs-results.json +0 -17
  18. package/benchmarks/ejs.js +0 -38
  19. package/benchmarks/handlebars-results.json +0 -17
  20. package/benchmarks/handlebars.js +0 -45
  21. package/benchmarks/miki-results.json +0 -17
  22. package/benchmarks/miki.js +0 -39
  23. package/benchmarks/pug-results.json +0 -17
  24. package/benchmarks/pug.js +0 -38
  25. package/benchmarks/report.json +0 -17
  26. package/benchmarks/run.js +0 -81
  27. package/benchmarks/stress.mjs +0 -647
  28. package/benchmarks/templates/large.dtpl +0 -7
  29. package/benchmarks/templates/medium.dtpl +0 -3
  30. package/benchmarks/templates/small.dtpl +0 -7
  31. package/context/component.md +0 -109
  32. package/context/prd.md +0 -131
  33. package/context/project-structure.md +0 -33
  34. package/dir/base.html +0 -23
  35. package/dir/cmpnt.html +0 -11
  36. package/dir/footer.html +0 -3
  37. package/dir/home.html +0 -80
  38. package/dir/index.html +0 -80
  39. package/dir/navbar.html +0 -9
  40. package/docs/api/async-render.md +0 -170
  41. package/docs/api/cache.md +0 -174
  42. package/docs/api/compile.md +0 -256
  43. package/docs/api/context-processors.md +0 -154
  44. package/docs/api/filters.md +0 -437
  45. package/docs/api/finder.md +0 -188
  46. package/docs/api/helpers.md +0 -106
  47. package/docs/api/i18n.md +0 -314
  48. package/docs/api/index.md +0 -108
  49. package/docs/api/libraries.md +0 -414
  50. package/docs/api/render-partial.md +0 -162
  51. package/docs/api/render.md +0 -184
  52. package/docs/api/security.md +0 -290
  53. package/docs/api/setup-express.md +0 -152
  54. package/docs/api/tags.md +0 -268
  55. package/docs/assets/banner.png +0 -0
  56. package/docs/assets/logo.png +0 -0
  57. package/docs/filter.md +0 -0
  58. package/docs/guide/advanced-usage.md +0 -794
  59. package/docs/guide/async-rendering.md +0 -616
  60. package/docs/guide/context-processors.md +0 -514
  61. package/docs/guide/custom-filters.md +0 -622
  62. package/docs/guide/custom-tags.md +0 -542
  63. package/docs/guide/filters.md +0 -1314
  64. package/docs/guide/getting-started.md +0 -204
  65. package/docs/guide/installation.md +0 -190
  66. package/docs/guide/partial-templates.md +0 -734
  67. package/docs/guide/quick-start.md +0 -444
  68. package/docs/guide/security.md +0 -690
  69. package/docs/guide/tags.md +0 -1566
  70. package/docs/guide/template-discovery.md +0 -340
  71. package/docs/guide/template-inheritance.md +0 -546
  72. package/docs/guide/what-is-miki-template.md +0 -28
  73. package/docs/guide/why-miki-template.md +0 -75
  74. package/docs/index.md +0 -86
  75. package/docs/integrations/elysia.md +0 -80
  76. package/docs/integrations/express.md +0 -219
  77. package/docs/integrations/fastify.md +0 -79
  78. package/docs/integrations/hono.md +0 -80
  79. package/docs/integrations/index.md +0 -68
  80. package/docs/integrations/koa.md +0 -90
  81. package/docs/integrations/nestjs.md +0 -80
  82. package/docs/integrations/tsed.md +0 -83
  83. package/docs/performance.md +0 -74
  84. package/eslint.config.mjs +0 -42
  85. package/ex.mjs +0 -36
  86. package/live-test/integrations/elysia-example.js +0 -16
  87. package/live-test/integrations/express-example.js +0 -24
  88. package/live-test/integrations/fastify-example.js +0 -20
  89. package/live-test/integrations/hono-example.js +0 -16
  90. package/live-test/integrations/koa-example.js +0 -30
  91. package/live-test/integrations/nestjs-example.js +0 -25
  92. package/live-test/integrations/smoke-test.js +0 -166
  93. package/live-test/integrations/tsed-example.js +0 -23
  94. package/live-test/package-lock.json +0 -1150
  95. package/live-test/package.json +0 -13
  96. package/live-test/packages/product/templates/product/detail.html +0 -7
  97. package/live-test/server.js +0 -38
  98. package/live-test/templates/app_templates/detail.html +0 -6
  99. package/live-test/views/base.html +0 -8
  100. package/live-test/views/child.html +0 -7
  101. package/live-test/views/home.html +0 -17
  102. package/live-test/views/index.html +0 -1
  103. package/miki-template-extension/.github/workflows/ci.yml +0 -116
  104. package/miki-template-extension/.vscodeignore +0 -7
  105. package/miki-template-extension/CHANGELOG.md +0 -99
  106. package/miki-template-extension/LICENSE +0 -21
  107. package/miki-template-extension/README.md +0 -273
  108. package/miki-template-extension/extension.js +0 -1013
  109. package/miki-template-extension/icon.png +0 -0
  110. package/miki-template-extension/icon.svg +0 -10
  111. package/miki-template-extension/miki-template-1.7.1.vsix +0 -0
  112. package/miki-template-extension/package.json +0 -280
  113. package/miki-template-extension/snippets/miki-template.json +0 -717
  114. package/miki-template-extension/syntaxes/language-configuration.json +0 -114
  115. package/miki-template-extension/syntaxes/miki-template.tmLanguage.json +0 -355
  116. package/miki-template-extension/tests/grammar-tests.json +0 -162
  117. package/miki-template-extension/tests/run-grammar-tests.js +0 -82
  118. package/mkdocs.yml +0 -195
  119. package/overrides/main.html +0 -26
  120. package/requirements-docs.txt +0 -2
  121. package/sample-app/package-lock.json +0 -901
  122. package/sample-app/package.json +0 -9
  123. package/sample-app/server.js +0 -14
  124. package/sample-app/views/index.html +0 -1
  125. package/scripts/build-vsix.js +0 -129
  126. package/scripts/build-vsix.ps1 +0 -15
  127. package/snippets/miki-template.json +0 -177
  128. package/src/asyncRender.js +0 -21
  129. package/src/types.d.ts +0 -107
  130. package/syntaxes/language-configuration.json +0 -26
  131. package/syntaxes/miki-template.tmLanguage.json +0 -146
  132. package/tests/asyncRender.test.js +0 -17
  133. package/tests/base.html +0 -6
  134. package/tests/child.html +0 -3
  135. package/tests/context_processors.test.js +0 -13
  136. package/tests/esm.test.mjs +0 -61
  137. package/tests/filters.test.js +0 -263
  138. package/tests/finder-appdirs.test.js +0 -19
  139. package/tests/finder.test.js +0 -17
  140. package/tests/fixtures/views/nested/index.html +0 -1
  141. package/tests/fixtures/views/partial.html +0 -1
  142. package/tests/fixtures/views/sub/deepfile.html +0 -1
  143. package/tests/fixtures/views-appdirs/product/site/detail.html +0 -1
  144. package/tests/include_security.test.js +0 -9
  145. package/tests/integration/README.md +0 -32
  146. package/tests/integration/features.test.cjs +0 -1681
  147. package/tests/integration/features.test.mjs +0 -1697
  148. package/tests/integration/finder.esm.test.mjs +0 -13
  149. package/tests/integration/partial-render.test.cjs +0 -13
  150. package/tests/integration/templates/base.miki +0 -6
  151. package/tests/integration/templates/child.miki +0 -6
  152. package/tests/integration/templates/index.html +0 -17
  153. package/tests/lexer.test.js +0 -45
  154. package/tests/parser.test.js +0 -57
  155. package/tests/partial.html +0 -1
  156. package/tests/partialdef.test.js +0 -79
  157. package/tests/production_checks.js +0 -57
  158. package/tests/security.test.js +0 -28
  159. package/tests/tags.test.js +0 -233
@@ -1,647 +0,0 @@
1
- /**
2
- * benchmarks/stress.mjs
3
- *
4
- * Strict, opinionated stress + performance benchmark for miki-template.
5
- *
6
- * Dimensions measured:
7
- * 1. CORRECTNESS — every render must produce byte-identical output to
8
- * the first render (deterministic) and pass a fixed
9
- * set of golden-string assertions.
10
- * 2. COMPILE SPEED — templates/compile second, broken down by template
11
- * size and template shape (flat, inheritance, for, partial).
12
- * 3. RENDER SPEED — renders/second for small / medium / large / inheritance
13
- * templates, both cold and warm.
14
- * 4. CACHE BEHAVIOR — second compile of the same source must be a cache
15
- * hit; partials included via {% include %} must NOT
16
- * cause cross-template cache pollution.
17
- * 5. SCALE — single template with N items rendered in a for loop.
18
- * Linear-or-better growth vs N.
19
- * 6. ENDURANCE — run 100k renders back-to-back. No memory growth
20
- * reported by process.memoryUsage beyond a bounded
21
- * factor (we measure delta RSS, not absolute RSS).
22
- * 7. PARTIAL OVERHEAD — renderPartialFromSource vs render() overhead.
23
- * 8. ASYNC OVERHEAD — asyncRender vs render() overhead.
24
- * 9. CONCURRENCY — 50+ parallel asyncRender calls complete in bounded
25
- * wall time and produce the same output as serial.
26
- *
27
- * The script exits with code 1 on any failure. All numbers are reported
28
- * in a single human-readable summary.
29
- */
30
-
31
- import { compile, render, asyncRender, renderPartialFromSource, clearCache } from '../src/esm.mjs';
32
- import { performance } from 'node:perf_hooks';
33
- import process from 'node:process';
34
- import fs from 'node:fs';
35
- import os from 'node:os';
36
- import path from 'node:path';
37
-
38
- const RED = '\x1b[31m';
39
- const GREEN = '\x1b[32m';
40
- const YEL = '\x1b[33m';
41
- const DIM = '\x1b[2m';
42
- const RST = '\x1b[0m';
43
-
44
- const results = [];
45
- let failures = 0;
46
-
47
- function record(name, ok, info) {
48
- results.push({ name, ok, info });
49
- if (!ok) failures++;
50
- const tag = ok ? `${GREEN}PASS${RST}` : `${RED}FAIL${RST}`;
51
- const extra = info ? ` ${DIM}${info}${RST}` : '';
52
- console.log(` ${tag} ${name}${extra}`);
53
- }
54
-
55
- function section(title) {
56
- console.log(`\n${YEL}== ${title} ==${RST}`);
57
- }
58
-
59
- function median(arr) {
60
- const s = [...arr].sort((a, b) => a - b);
61
- const m = Math.floor(s.length / 2);
62
- return s.length % 2 ? s[m] : (s[m - 1] + s[m]) / 2;
63
- }
64
-
65
- function fmt(n, unit = 'ms') {
66
- if (n >= 1000) return (n / 1000).toFixed(2) + 'k ' + unit;
67
- if (n < 10) return n.toFixed(3) + ' ' + unit;
68
- if (n < 100) return n.toFixed(2) + ' ' + unit;
69
- return n.toFixed(1) + ' ' + unit;
70
- }
71
-
72
- function bytes(n) {
73
- if (n >= 1024 * 1024) return (n / 1024 / 1024).toFixed(2) + ' MB';
74
- if (n >= 1024) return (n / 1024).toFixed(2) + ' KB';
75
- return n + ' B';
76
- }
77
-
78
- function rssMB() {
79
- return process.memoryUsage().rss / 1024 / 1024;
80
- }
81
-
82
- // ------------------------------------------------------------------
83
- // Templates
84
- // ------------------------------------------------------------------
85
- const SMALL = `Hello {{ name|title }}, today is {{ date|default:"a day"|truncatewords:3 }}.`;
86
-
87
- const MEDIUM = `<article>
88
- <h1>{{ post.title|upper }}</h1>
89
- <p class="meta">By {{ post.author.name }} on {{ post.date|date_format:"yyyy-MM-dd" }}</p>
90
- {% if post.featured %}<span class="badge">Featured</span>{% endif %}
91
- <div class="body">
92
- {{ post.body|truncatewords:80 }}
93
- </div>
94
- <ul class="tags">
95
- {% for tag in post.tags %}<li>{{ tag|lower }}</li>{% empty %}
96
- <li>No tags</li>{% endfor %}
97
- </ul>
98
- <footer>
99
- {% for c in post.comments %}{% if c.approved %}
100
- <div class="comment">{{ c.author }}: {{ c.text|truncatewords:30 }}</div>
101
- {% endif %}{% endfor %}
102
- </footer>
103
- </article>`;
104
-
105
- const LARGE = `<!doctype html>
106
- <html lang="en">
107
- <head>
108
- <meta charset="utf-8">
109
- <title>{{ page.title|default:"Site"|title }}</title>
110
- <meta name="description" content="{{ page.description|truncatewords:20 }}">
111
- {% for css in page.stylesheets %}<link rel="stylesheet" href="{{ css }}">
112
- {% endfor %}{% for js in page.scripts %}<script src="{{ js }}"></script>
113
- {% endfor %}
114
- </head>
115
- <body class="{% cycle 'theme-a' 'theme-b' 'theme-c' %}">
116
- <header>
117
- <h1>{{ site.name }}</h1>
118
- <nav>{% for item in nav %}<a href="{{ item.href }}">{{ item.label }}</a>{% endfor %}</nav>
119
- </header>
120
- <main>
121
- {% for section in sections %}
122
- <section id="{{ section.slug }}">
123
- <h2>{{ section.title }}</h2>
124
- {% if section.kind == 'grid' %}
125
- <div class="grid">{% for item in section.items %}<div class="cell">
126
- <h3>{{ item.title }}</h3>
127
- <p>{{ item.summary|truncatewords:40 }}</p>
128
- <span class="price">{{ item.price|floatformat:2 }}</span>
129
- </div>{% endfor %}</div>
130
- {% elif section.kind == 'list' %}
131
- <ul>{% for item in section.items %}<li>
132
- <a href="{{ item.href }}">{{ item.label }}</a>
133
- <small>{{ item.note|default:""|truncatewords:5 }}</small>
134
- </li>{% endfor %}</ul>
135
- {% endif %}
136
- </section>
137
- {% endfor %}
138
- </main>
139
- <aside>
140
- <h3>Recent</h3>
141
- <ol>{% for r in recent %}<li>{{ r.title }}</li>{% endfor %}</ol>
142
- </aside>
143
- <footer>
144
- <p>&copy; {{ year }} {{ site.name }}. All rights reserved.</p>
145
- {% if user %}<p>Signed in as {{ user.name }} ({{ user.email }})</p>{% endif %}
146
- </footer>
147
- </body>
148
- </html>`;
149
-
150
- const INHERIT_BASE = `<!doctype html>
151
- <html><head><title>{% block title %}Default{% endblock %}</title></head>
152
- <body>
153
- <header>{% block header %}Default header{% endblock %}</header>
154
- <main>{% block content %}Default content{% endblock %}</main>
155
- <footer>{% block footer %}{{ copyright }}{% endblock %}</footer>
156
- </body></html>`;
157
-
158
- const INHERIT_CHILD = `{% extends "base.dtpl" %}
159
- {% block title %}{{ super_title|default:"Page" }}{% endblock %}
160
- {% block content %}
161
- <h1>{{ heading }}</h1>
162
- {% for item in items %}
163
- <div class="item">
164
- <h2>{{ item.name|title }}</h2>
165
- <p>{{ item.body|truncatewords:20 }}</p>
166
- {% if item.featured %}<span class="badge">Featured</span>{% endif %}
167
- </div>
168
- {% endfor %}
169
- {% endblock %}`;
170
-
171
- const PARTIAL_TPL = `{% partialdef card %}
172
- <div class="card">
173
- <h3>{{ title|default:"Untitled" }}</h3>
174
- <p>{{ body|truncatewords:30 }}</p>
175
- {% if featured %}<em>Featured</em>{% endif %}
176
- </div>
177
- {% endpartialdef %}
178
- {% for entry in entries %}
179
- {% partial card with title=entry.title body=entry.body featured=entry.featured %}
180
- {% endfor %}`;
181
-
182
- const templates = { SMALL, MEDIUM, LARGE, INHERIT_BASE, INHERIT_CHILD, PARTIAL_TPL };
183
-
184
- // ------------------------------------------------------------------
185
- // Data
186
- // ------------------------------------------------------------------
187
- function makeData(seed = 1) {
188
- const tags = ['Node.js', 'Express', 'Django', 'Jinja', 'HTMX', 'Templates', 'Performance'];
189
- const sections = [];
190
- for (let s = 0; s < 6; s++) {
191
- const items = [];
192
- for (let i = 0; i < 12; i++) {
193
- items.push({
194
- title: `Item ${seed}-${s}-${i}`,
195
- summary: `Summary text for item number ${i} in section ${s} with enough padding to make truncation interesting ${'.'.repeat(i)}`,
196
- price: (i * 3.7 + s).toFixed(2),
197
- href: `/items/${s}/${i}`,
198
- label: `Section ${s} Item ${i}`,
199
- note: i % 3 === 0 ? null : `note-${i}`,
200
- });
201
- }
202
- sections.push({
203
- slug: `section-${s}`,
204
- title: `Section ${s}`,
205
- kind: s % 2 === 0 ? 'grid' : 'list',
206
- items,
207
- });
208
- }
209
- return {
210
- name: 'alice',
211
- date: 'a wonderful day',
212
- post: {
213
- title: 'hello world',
214
- author: { name: 'Alice' },
215
- date: new Date(Date.UTC(2024, 5, 1, 12)),
216
- featured: true,
217
- body: '<p>Lorem ipsum dolor sit amet, consectetur adipiscing elit.</p>'.repeat(5),
218
- tags: tags.slice(0, seed + 2),
219
- comments: Array.from({ length: 6 }, (_, i) => ({
220
- author: `user${i}`,
221
- text: 'great post '.repeat(3 + (i % 4)),
222
- approved: i % 2 === 0,
223
- })),
224
- },
225
- page: {
226
- title: 'home',
227
- description: 'a'.repeat(120),
228
- stylesheets: ['/static/main.css', '/static/theme.css'],
229
- scripts: ['/static/app.js'],
230
- },
231
- site: { name: 'miki-template' },
232
- nav: [
233
- { href: '/', label: 'Home' },
234
- { href: '/about', label: 'About' },
235
- { href: '/contact', label: 'Contact' },
236
- ],
237
- sections,
238
- recent: Array.from({ length: 5 }, (_, i) => ({ title: `Recent ${i}` })),
239
- year: 2026,
240
- user: { name: 'alice', email: 'alice@example.com' },
241
- super_title: 'Inherit Test',
242
- heading: 'Welcome',
243
- items: Array.from({ length: 20 }, (_, i) => ({
244
- name: `item ${i}`,
245
- body: 'description '.repeat(8 + i) + 'end',
246
- featured: i % 3 === 0,
247
- })),
248
- entries: Array.from({ length: 50 }, (_, i) => ({
249
- title: `Entry ${i}`,
250
- body: 'body text '.repeat(5 + (i % 7)),
251
- featured: i % 4 === 0,
252
- })),
253
- copyright: '© 2026',
254
- };
255
- }
256
-
257
- // ------------------------------------------------------------------
258
- // Set up an inheritance fixture on disk
259
- // ------------------------------------------------------------------
260
- const FIXTURES_DIR = fs.mkdtempSync(path.join(os.tmpdir(), 'miki-stress-'));
261
- fs.writeFileSync(path.join(FIXTURES_DIR, 'base.dtpl'), INHERIT_BASE);
262
-
263
- // ------------------------------------------------------------------
264
- // 1. CORRECTNESS
265
- // ------------------------------------------------------------------
266
- section('1. CORRECTNESS');
267
-
268
- function check(name, fn) {
269
- try {
270
- fn();
271
- record(name, true, '');
272
- } catch (e) {
273
- record(name, false, typeof e === 'string' ? e : e.message);
274
- }
275
- }
276
-
277
- function assert(cond, msg) {
278
- if (!cond) throw new Error(msg || 'assertion failed');
279
- }
280
-
281
- check('small template renders expected output', () => {
282
- const c = compile(SMALL);
283
- const out = c.render({ name: 'alice', date: 'today' });
284
- assert(out.includes('Alice'), `expected 'Alice' in output, got: ${out}`);
285
- assert(out.includes('today'), `expected 'today' in output, got: ${out}`);
286
- });
287
-
288
- check('medium template renders expected output', () => {
289
- const c = compile(MEDIUM);
290
- const out = c.render(makeData());
291
- assert(out.includes('HELLO WORLD'), 'missing upper-cased title');
292
- assert(out.includes('Featured</span>'), 'missing Featured span');
293
- const li = (out.match(/<li>/g) || []).length;
294
- assert(li >= 3, `expected >=3 <li>, got ${li}`);
295
- assert(out.includes('2024-'), 'missing date_format output');
296
- });
297
-
298
- check('large template renders expected output', () => {
299
- const c = compile(LARGE);
300
- const out = c.render(makeData());
301
- assert(out.includes('miki-template'), 'missing site name');
302
- assert(out.match(/<section id="section-\d+">/), 'no section match');
303
- const hrefs = (out.match(/href="\/items\//g) || []).length;
304
- assert(hrefs === 36, `expected 36 item hrefs (3 list sections × 12), got ${hrefs}`);
305
- });
306
-
307
- check('inheritance: child overrides title and content', () => {
308
- const child = compile(INHERIT_CHILD, { views: FIXTURES_DIR });
309
- const out = child.render(makeData());
310
- assert(out.includes('Inherit Test'), `missing child title; output: ${out.slice(0, 200)}`);
311
- // Items are rendered with |title so names are Title-Cased: 'item 1' -> 'Item 1'
312
- assert(out.includes('Item 1'), 'no item loop content (expected "Item 1" from |title)');
313
- assert(out.includes('Featured</span>'), 'no Featured span from child block');
314
- assert(out.includes('© 2026'), 'no base footer block');
315
- });
316
-
317
- check('partial: {% partial %} renders the body for every entry', () => {
318
- const c = compile(PARTIAL_TPL);
319
- const out = c.render(makeData());
320
- const cardCount = (out.match(/class="card"/g) || []).length;
321
- assert(cardCount === 50, `expected 50 cards, got ${cardCount}`);
322
- assert((out.match(/<em>Featured<\/em>/g) || []).length > 0, 'no Featured span in partials');
323
- });
324
-
325
- check('determinism: 1000 renders produce identical output', () => {
326
- const c = compile(LARGE);
327
- const data = makeData();
328
- const first = c.render(data);
329
- for (let i = 0; i < 1000; i++) {
330
- const o = c.render(data);
331
- assert(o === first, `render ${i} differs from first`);
332
- }
333
- });
334
-
335
- check('auto-escape: HTML in context is escaped', () => {
336
- const c = compile('{{ html }}');
337
- const out = c.render({ html: '<script>alert(1)</script>' });
338
- assert(out === '&lt;script&gt;alert(1)&lt;/script&gt;', `got: ${out}`);
339
- });
340
-
341
- check('safe filter bypasses auto-escape', () => {
342
- const c = compile('{{ html|safe }}');
343
- const out = c.render({ html: '<b>ok</b>' });
344
- assert(out === '<b>ok</b>', `got: ${out}`);
345
- });
346
-
347
- check('date filter multi-token: yyyy-MM-dd renders correctly', () => {
348
- const c = compile('{{ d|date:"yyyy-MM-dd" }}');
349
- const d = new Date(Date.UTC(2024, 5, 15, 12, 0, 0));
350
- const yyyy = String(d.getFullYear()).padStart(4, '0');
351
- const MM = String(d.getMonth() + 1).padStart(2, '0');
352
- const dd = String(d.getDate()).padStart(2, '0');
353
- const out = c.render({ d });
354
- assert(out === `${yyyy}-${MM}-${dd}`, `got ${out}, expected ${yyyy}-${MM}-${dd}`);
355
- });
356
-
357
- check('time filter multi-token: HH:mm:ss renders correctly', () => {
358
- const c = compile('{{ d|time:"HH:mm:ss" }}');
359
- const d = new Date(Date.UTC(2024, 5, 15, 9, 7, 3));
360
- const HH = String(d.getHours()).padStart(2, '0');
361
- const mm = String(d.getMinutes()).padStart(2, '0');
362
- const ss = String(d.getSeconds()).padStart(2, '0');
363
- const out = c.render({ d });
364
- assert(out === `${HH}:${mm}:${ss}`, `got ${out}, expected ${HH}:${mm}:${ss}`);
365
- });
366
-
367
- check('default vs default_if_none (Django parity)', () => {
368
- const c = compile('{{ x|default:"D" }}|{{ x|default_if_none:"N" }}');
369
- const r1 = c.render({ x: 'v' });
370
- const r2 = c.render({ x: '' });
371
- const r3 = c.render({ x: null });
372
- const r4 = c.render({});
373
- assert(r1 === 'v|v', `x=v: got ${r1}, expected v|v`);
374
- assert(r2 === 'D|', `x=empty: got ${r2}, expected D|`); // default_if_none does NOT fallback on ''
375
- assert(r3 === 'D|N', `x=null: got ${r3}, expected D|N`);
376
- assert(r4 === 'D|N', `undef: got ${r4}, expected D|N`);
377
- });
378
-
379
- // ------------------------------------------------------------------
380
- // 2. COMPILE SPEED
381
- // ------------------------------------------------------------------
382
- section('2. COMPILE SPEED');
383
-
384
- function benchCompile(name, tpl, runs = 200) {
385
- for (let i = 0; i < 5; i++) compile(tpl);
386
- clearCache();
387
- const times = [];
388
- for (let i = 0; i < runs; i++) {
389
- const t0 = performance.now();
390
- compile(tpl);
391
- times.push(performance.now() - t0);
392
- }
393
- return { name, runs, medianMs: median(times), minMs: Math.min(...times), maxMs: Math.max(...times) };
394
- }
395
-
396
- const compileResults = {};
397
- for (const [name, tpl] of Object.entries(templates)) {
398
- const r = benchCompile(name, tpl, name === 'LARGE' ? 100 : 300);
399
- compileResults[name] = r;
400
- // Thresholds tuned to a normal 4-core Windows runner.
401
- const limit = name === 'LARGE' ? 15 : (name.startsWith('INHERIT') ? 8 : 6);
402
- record(`compile: ${name} (${bytes(tpl.length)})`, r.medianMs < limit,
403
- `median ${fmt(r.medianMs)} (min ${fmt(r.minMs)}, max ${fmt(r.maxMs)})`);
404
- }
405
-
406
- // ------------------------------------------------------------------
407
- // 3. RENDER SPEED
408
- // ------------------------------------------------------------------
409
- section('3. RENDER SPEED');
410
-
411
- function benchRender(tpl, data, runs = 5000, opts = {}) {
412
- const c = compile(tpl, opts);
413
- for (let i = 0; i < 50; i++) c.render(data);
414
- const times = [];
415
- for (let i = 0; i < runs; i++) {
416
- const t0 = performance.now();
417
- c.render(data);
418
- times.push(performance.now() - t0);
419
- }
420
- return { medianMs: median(times), minMs: Math.min(...times), runs };
421
- }
422
-
423
- function renderSpeedRecord(name, tpl, data, runs, thresholdRps, opts = {}) {
424
- const r = benchRender(tpl, data, runs, opts);
425
- const rps = 1000 / r.medianMs;
426
- record(`${name} ≥ ${thresholdRps.toLocaleString()} rps`, rps >= thresholdRps,
427
- `${Math.round(rps).toLocaleString()} rps (median ${fmt(r.medianMs)})`);
428
- }
429
-
430
- const data = makeData();
431
- renderSpeedRecord('render: small', SMALL, data, 20000, 50000);
432
- renderSpeedRecord('render: medium', MEDIUM, data, 5000, 1000);
433
- renderSpeedRecord('render: large', LARGE, data, 2000, 1000);
434
- renderSpeedRecord('render: inherit child', INHERIT_CHILD, data, 2000, 1000, { views: FIXTURES_DIR });
435
- renderSpeedRecord('render: partials (50 entries)', PARTIAL_TPL, data, 1000, 500);
436
-
437
- // ------------------------------------------------------------------
438
- // 4. CACHE BEHAVIOR
439
- // ------------------------------------------------------------------
440
- section('4. CACHE BEHAVIOR');
441
-
442
- check('cache: second compile of same source hits cache', () => {
443
- const t0 = performance.now();
444
- compile(SMALL);
445
- const first = performance.now() - t0;
446
- const t1 = performance.now();
447
- compile(SMALL);
448
- const second = performance.now() - t1;
449
- assert(second * 2 <= first, `expected cached compile ≥ 2× faster (first ${fmt(first)}, cached ${fmt(second)})`);
450
- });
451
-
452
- check('cache: partial template does not pollute sibling cache', () => {
453
- const a = compile(PARTIAL_TPL);
454
- const b = compile(MEDIUM);
455
- const aOut = a.render(makeData());
456
- const bOut = b.render(makeData());
457
- const a2 = compile(PARTIAL_TPL);
458
- const b2 = compile(MEDIUM);
459
- assert(a2.render(makeData()) === aOut, 'partial cache poisoned after medium compile');
460
- assert(b2.render(makeData()) === bOut, 'medium cache poisoned after partial compile');
461
- });
462
-
463
- check('cache: function-valued options do not poison cache key', () => {
464
- const fn = () => '/';
465
- const t1 = performance.now();
466
- compile(SMALL, { urlHelper: fn });
467
- const first = performance.now() - t1;
468
- const t2 = performance.now();
469
- compile(SMALL, { urlHelper: fn });
470
- const second = performance.now() - t2;
471
- assert(second <= first * 1.5, `cached compile should be ≤ 1.5× first (first ${fmt(first)}, cached ${fmt(second)})`);
472
- });
473
-
474
- // ------------------------------------------------------------------
475
- // 5. SCALE
476
- // ------------------------------------------------------------------
477
- section('5. SCALE');
478
-
479
- function scaleRender(tpl, itemCount) {
480
- const c = compile(tpl);
481
- const data = { items: Array.from({ length: itemCount }, (_, i) => ({ i, v: i * 2 })) };
482
- for (let i = 0; i < 10; i++) c.render(data);
483
- const times = [];
484
- for (let i = 0; i < 50; i++) {
485
- const t0 = performance.now();
486
- c.render(data);
487
- times.push(performance.now() - t0);
488
- }
489
- return { count: itemCount, medianMs: median(times) };
490
- }
491
-
492
- const scaleTpl = `{% for x in items %}<li>{{ x.i }}: {{ x.v|add:1 }}</li>{% endfor %}`;
493
- const s100 = scaleRender(scaleTpl, 100);
494
- const s1000 = scaleRender(scaleTpl, 1000);
495
- const s10000 = scaleRender(scaleTpl, 10000);
496
- const s50000 = scaleRender(scaleTpl, 50000);
497
-
498
- const ratio = s10000.medianMs / s100.medianMs;
499
- record(`scale: 10k items ≤ 200× time of 100 items`, ratio < 200,
500
- `100→${fmt(s100.medianMs)}, 1k→${fmt(s1000.medianMs)}, 10k→${fmt(s10000.medianMs)}, 50k→${fmt(s50000.medianMs)} (10k/100 = ${ratio.toFixed(1)}x)`);
501
- record(`scale: 50k items renders in < 2s`, s50000.medianMs < 2000,
502
- `${fmt(s50000.medianMs)} median`);
503
-
504
- // ------------------------------------------------------------------
505
- // 6. ENDURANCE (100k renders, watch RSS delta)
506
- // ------------------------------------------------------------------
507
- section('6. ENDURANCE');
508
-
509
- const beforeRss = rssMB();
510
- const enduranceTpl = MEDIUM;
511
- const enduranceData = makeData();
512
- const endC = compile(enduranceTpl);
513
- for (let i = 0; i < 1000; i++) endC.render(enduranceData);
514
-
515
- const tStart = performance.now();
516
- let endRssMax = beforeRss;
517
- for (let i = 0; i < 100000; i++) {
518
- endC.render(enduranceData);
519
- if (i % 10000 === 0) {
520
- const r = rssMB();
521
- if (r > endRssMax) endRssMax = r;
522
- }
523
- }
524
- const dur = performance.now() - tStart;
525
- const afterRss = rssMB();
526
- const rssDelta = endRssMax - beforeRss;
527
- const endRps = 100000 / dur;
528
-
529
- record('endurance: 100k renders complete in < 30s', dur < 30000,
530
- `${dur.toFixed(0)} ms total, ${Math.round(endRps).toLocaleString()} rps avg`);
531
- record('endurance: RSS growth < 80 MB', rssDelta < 80,
532
- `before ${beforeRss.toFixed(1)} MB, peak ${endRssMax.toFixed(1)} MB (Δ +${rssDelta.toFixed(1)} MB)`);
533
- record('endurance: output is still correct at end', (() => {
534
- const expected = endC.render(enduranceData);
535
- return expected.includes('HELLO WORLD') && expected.includes('Featured');
536
- })());
537
-
538
- // ------------------------------------------------------------------
539
- // 7. PARTIAL OVERHEAD
540
- // ------------------------------------------------------------------
541
- section('7. PARTIAL RENDERING');
542
-
543
- check('renderPartialFromSource: finds partial inside block in extends chain', () => {
544
- const src = `{% extends "fakebase" %}{% block content %}{% partialdef greet %}<p>Hi {{ who }}</p>{% endpartialdef %}{% endblock %}`;
545
- const out = renderPartialFromSource(src, 'greet', { who: 'World' });
546
- assert(out.includes('Hi World'), `got: ${out}`);
547
- });
548
-
549
- function partialSpeedCheck() {
550
- const src = `{% partialdef c %}<li>{{ name }}: {{ score|add:1 }}</li>{% endpartialdef %}{% for x in xs %}{% partial c with name=x.name score=x.score %}{% endfor %}`;
551
- const data = { xs: Array.from({ length: 20 }, (_, i) => ({ name: `n${i}`, score: i })) };
552
- for (let i = 0; i < 20; i++) renderPartialFromSource(src, 'c', data);
553
- const t0 = performance.now();
554
- for (let i = 0; i < 500; i++) renderPartialFromSource(src, 'c', data);
555
- const partialMs = (performance.now() - t0) / 500;
556
-
557
- const c = compile(src);
558
- for (let i = 0; i < 20; i++) c.render(data);
559
- const t1 = performance.now();
560
- for (let i = 0; i < 500; i++) c.render(data);
561
- const fullMs = (performance.now() - t1) / 500;
562
-
563
- const overhead = partialMs < Math.max(fullMs * 5, 5);
564
- record('renderPartialFromSource < 5x render() median', overhead,
565
- `partial ${fmt(partialMs)} / full ${fmt(fullMs)} (${(partialMs / fullMs).toFixed(2)}x)`);
566
- }
567
-
568
- // ------------------------------------------------------------------
569
- // 8. ASYNC OVERHEAD
570
- // ------------------------------------------------------------------
571
- section('8. ASYNC RENDERING');
572
-
573
- (async () => {
574
- partialSpeedCheck();
575
-
576
- const c = compile(MEDIUM);
577
- const data = makeData();
578
- for (let i = 0; i < 50; i++) { c.render(data); await asyncRender(MEDIUM, data); }
579
-
580
- const syncTimes = [];
581
- for (let i = 0; i < 1000; i++) {
582
- const t0 = performance.now();
583
- c.render(data);
584
- syncTimes.push(performance.now() - t0);
585
- }
586
- const syncMed = median(syncTimes);
587
-
588
- const asyncTimes = [];
589
- for (let i = 0; i < 200; i++) {
590
- const t0 = performance.now();
591
- await asyncRender(MEDIUM, data);
592
- asyncTimes.push(performance.now() - t0);
593
- }
594
- const asyncMed = median(asyncTimes);
595
-
596
- record('asyncRender: serial overhead < 10x sync', asyncMed < Math.max(syncMed * 10, 5),
597
- `sync ${fmt(syncMed)} / async ${fmt(asyncMed)}`);
598
-
599
- const N = 50;
600
- const tPar = performance.now();
601
- const parResults = await Promise.all(
602
- Array.from({ length: N }, (_, i) => asyncRender(MEDIUM, makeData(i + 1)))
603
- );
604
- const parDur = performance.now() - tPar;
605
- record('asyncRender: 50 concurrent complete in < 1s', parDur < 1000,
606
- `${parDur.toFixed(0)} ms for 50 parallel (${(parDur / N).toFixed(1)} ms/each)`);
607
- record('asyncRender: concurrent outputs all match serial', (() => {
608
- const ser = render(MEDIUM, makeData(1));
609
- return parResults[0] === ser;
610
- })());
611
-
612
- // ------------------------------------------------------------------
613
- // 9. CONCURRENCY
614
- // ------------------------------------------------------------------
615
- section('9. CONCURRENCY');
616
-
617
- const tConc = performance.now();
618
- const conc = await Promise.all(
619
- Array.from({ length: 200 }, () => asyncRender(LARGE, data))
620
- );
621
- const concDur = performance.now() - tConc;
622
- record('asyncRender: 200 concurrent LARGE in < 5s', concDur < 5000,
623
- `${concDur.toFixed(0)} ms (${(concDur / 200).toFixed(1)} ms/each)`);
624
- record('asyncRender: concurrent outputs are all identical', (() => {
625
- const first = conc[0];
626
- for (const o of conc) if (o !== first) return false;
627
- return true;
628
- })());
629
-
630
- // ------------------------------------------------------------------
631
- // SUMMARY
632
- // ------------------------------------------------------------------
633
- console.log(`\n${YEL}== SUMMARY ==${RST}`);
634
- const pass = results.filter(r => r.ok).length;
635
- const fail = results.length - pass;
636
- console.log(` ${GREEN}PASS${RST}: ${pass}`);
637
- console.log(` ${fail === 0 ? GREEN : RED}FAIL${RST}: ${fail}`);
638
- console.log(` TOTAL: ${results.length}`);
639
- if (fail > 0) {
640
- console.log(`\n${RED}Failed checks:${RST}`);
641
- for (const r of results) if (!r.ok) console.log(` - ${r.name}${r.info ? ' ' + DIM + r.info + RST : ''}`);
642
- try { fs.rmSync(FIXTURES_DIR, { recursive: true, force: true }); } catch {}
643
- process.exit(1);
644
- }
645
- console.log(`\n${GREEN}All checks passed.${RST}`);
646
- try { fs.rmSync(FIXTURES_DIR, { recursive: true, force: true }); } catch {}
647
- })();
@@ -1,7 +0,0 @@
1
- // large.dtpl
2
- {% for i in range(0, 1000) %}
3
- <div class="item">
4
- <h2>Item {{ i }}</h2>
5
- <p>{{ "Lorem ipsum dolor sit amet, consectetur adipiscing elit. " | repeat:5 }}</p>
6
- </div>
7
- {% endfor %}
@@ -1,3 +0,0 @@
1
- // medium.dtpl
2
- {% for i in range(0, 100) %}Item {{ i }}
3
- {% endfor %}
@@ -1,7 +0,0 @@
1
- // small.dtpl
2
- Hello {{ name|upper }}!
3
- {% if showList %}
4
- <ul>
5
- {% for item in items %}<li>{{ item }}</li>{% endfor %}
6
- </ul>
7
- {% endif %}