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
|
@@ -9,9 +9,60 @@
|
|
|
9
9
|
"version": "1.0.0",
|
|
10
10
|
"dependencies": {
|
|
11
11
|
"express": "^5.2.1",
|
|
12
|
-
"miki-template": "
|
|
12
|
+
"miki-template": "file:../miki-template-2.0.0.tgz"
|
|
13
|
+
},
|
|
14
|
+
"devDependencies": {
|
|
15
|
+
"elysia": "^1.4.30",
|
|
16
|
+
"hono": "^4.13.7"
|
|
13
17
|
}
|
|
14
18
|
},
|
|
19
|
+
"node_modules/@borewit/text-codec": {
|
|
20
|
+
"version": "0.2.2",
|
|
21
|
+
"resolved": "https://registry.npmjs.org/@borewit/text-codec/-/text-codec-0.2.2.tgz",
|
|
22
|
+
"integrity": "sha512-DDaRehssg1aNrH4+2hnj1B7vnUGEjU6OIlyRdkMd0aUdIUvKXrJfXsy8LVtXAy7DRvYVluWbMspsRhz2lcW0mQ==",
|
|
23
|
+
"dev": true,
|
|
24
|
+
"license": "MIT",
|
|
25
|
+
"peer": true,
|
|
26
|
+
"funding": {
|
|
27
|
+
"type": "github",
|
|
28
|
+
"url": "https://github.com/sponsors/Borewit"
|
|
29
|
+
}
|
|
30
|
+
},
|
|
31
|
+
"node_modules/@sinclair/typebox": {
|
|
32
|
+
"version": "0.34.52",
|
|
33
|
+
"resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.34.52.tgz",
|
|
34
|
+
"integrity": "sha512-XiMQh7qqVlxZzcVD+kkGMNGMzcTrDMLWI7S4x7z1MkCkbDPrekpZXEUK0eZqZFMuHQg2a2DZOcDIh9o5v3Gonw==",
|
|
35
|
+
"dev": true,
|
|
36
|
+
"license": "MIT",
|
|
37
|
+
"peer": true
|
|
38
|
+
},
|
|
39
|
+
"node_modules/@tokenizer/inflate": {
|
|
40
|
+
"version": "0.4.1",
|
|
41
|
+
"resolved": "https://registry.npmjs.org/@tokenizer/inflate/-/inflate-0.4.1.tgz",
|
|
42
|
+
"integrity": "sha512-2mAv+8pkG6GIZiF1kNg1jAjh27IDxEPKwdGul3snfztFerfPGI1LjDezZp3i7BElXompqEtPmoPx6c2wgtWsOA==",
|
|
43
|
+
"dev": true,
|
|
44
|
+
"license": "MIT",
|
|
45
|
+
"peer": true,
|
|
46
|
+
"dependencies": {
|
|
47
|
+
"debug": "^4.4.3",
|
|
48
|
+
"token-types": "^6.1.1"
|
|
49
|
+
},
|
|
50
|
+
"engines": {
|
|
51
|
+
"node": ">=18"
|
|
52
|
+
},
|
|
53
|
+
"funding": {
|
|
54
|
+
"type": "github",
|
|
55
|
+
"url": "https://github.com/sponsors/Borewit"
|
|
56
|
+
}
|
|
57
|
+
},
|
|
58
|
+
"node_modules/@tokenizer/token": {
|
|
59
|
+
"version": "0.3.0",
|
|
60
|
+
"resolved": "https://registry.npmjs.org/@tokenizer/token/-/token-0.3.0.tgz",
|
|
61
|
+
"integrity": "sha512-OvjF+z51L3ov0OyAU0duzsYuvO01PH7x4t6DJx+guahgTnBHkhJdG7soQeTSFLWN3efnHyibZ4Z8l2EuWwJN3A==",
|
|
62
|
+
"dev": true,
|
|
63
|
+
"license": "MIT",
|
|
64
|
+
"peer": true
|
|
65
|
+
},
|
|
15
66
|
"node_modules/accepts": {
|
|
16
67
|
"version": "2.0.0",
|
|
17
68
|
"resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz",
|
|
@@ -196,6 +247,49 @@
|
|
|
196
247
|
"integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==",
|
|
197
248
|
"license": "MIT"
|
|
198
249
|
},
|
|
250
|
+
"node_modules/elysia": {
|
|
251
|
+
"version": "1.4.30",
|
|
252
|
+
"resolved": "https://registry.npmjs.org/elysia/-/elysia-1.4.30.tgz",
|
|
253
|
+
"integrity": "sha512-S2qqV0CbM4faB1hQQ5IYoeYnAUinjHlzRduxaBc4OoNqh0GjOc8k6tt3hv5ozWcG6Svr6hugw6JL4zNke4jwfA==",
|
|
254
|
+
"dev": true,
|
|
255
|
+
"license": "MIT",
|
|
256
|
+
"dependencies": {
|
|
257
|
+
"cookie": "^1.1.1",
|
|
258
|
+
"exact-mirror": "^0.2.7",
|
|
259
|
+
"fast-decode-uri-component": "^1.0.1",
|
|
260
|
+
"memoirist": "^0.4.0"
|
|
261
|
+
},
|
|
262
|
+
"peerDependencies": {
|
|
263
|
+
"@sinclair/typebox": ">= 0.34.0 < 1",
|
|
264
|
+
"@types/bun": ">= 1.2.0",
|
|
265
|
+
"exact-mirror": ">= 0.0.9",
|
|
266
|
+
"file-type": ">= 20.0.0",
|
|
267
|
+
"openapi-types": ">= 12.0.0",
|
|
268
|
+
"typescript": ">= 5.0.0"
|
|
269
|
+
},
|
|
270
|
+
"peerDependenciesMeta": {
|
|
271
|
+
"@types/bun": {
|
|
272
|
+
"optional": true
|
|
273
|
+
},
|
|
274
|
+
"typescript": {
|
|
275
|
+
"optional": true
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
},
|
|
279
|
+
"node_modules/elysia/node_modules/cookie": {
|
|
280
|
+
"version": "1.1.1",
|
|
281
|
+
"resolved": "https://registry.npmjs.org/cookie/-/cookie-1.1.1.tgz",
|
|
282
|
+
"integrity": "sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==",
|
|
283
|
+
"dev": true,
|
|
284
|
+
"license": "MIT",
|
|
285
|
+
"engines": {
|
|
286
|
+
"node": ">=18"
|
|
287
|
+
},
|
|
288
|
+
"funding": {
|
|
289
|
+
"type": "opencollective",
|
|
290
|
+
"url": "https://opencollective.com/express"
|
|
291
|
+
}
|
|
292
|
+
},
|
|
199
293
|
"node_modules/encodeurl": {
|
|
200
294
|
"version": "2.0.0",
|
|
201
295
|
"resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz",
|
|
@@ -250,6 +344,21 @@
|
|
|
250
344
|
"node": ">= 0.6"
|
|
251
345
|
}
|
|
252
346
|
},
|
|
347
|
+
"node_modules/exact-mirror": {
|
|
348
|
+
"version": "0.2.7",
|
|
349
|
+
"resolved": "https://registry.npmjs.org/exact-mirror/-/exact-mirror-0.2.7.tgz",
|
|
350
|
+
"integrity": "sha512-+MeEmDcLA4o/vjK2zujgk+1VTxPR4hdp23qLqkWfStbECtAq9gmsvQa3LW6z/0GXZyHJobrCnmy1cdeE7BjsYg==",
|
|
351
|
+
"dev": true,
|
|
352
|
+
"license": "MIT",
|
|
353
|
+
"peerDependencies": {
|
|
354
|
+
"@sinclair/typebox": "^0.34.15"
|
|
355
|
+
},
|
|
356
|
+
"peerDependenciesMeta": {
|
|
357
|
+
"@sinclair/typebox": {
|
|
358
|
+
"optional": true
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
},
|
|
253
362
|
"node_modules/express": {
|
|
254
363
|
"version": "5.2.1",
|
|
255
364
|
"resolved": "https://registry.npmjs.org/express/-/express-5.2.1.tgz",
|
|
@@ -293,6 +402,33 @@
|
|
|
293
402
|
"url": "https://opencollective.com/express"
|
|
294
403
|
}
|
|
295
404
|
},
|
|
405
|
+
"node_modules/fast-decode-uri-component": {
|
|
406
|
+
"version": "1.0.1",
|
|
407
|
+
"resolved": "https://registry.npmjs.org/fast-decode-uri-component/-/fast-decode-uri-component-1.0.1.tgz",
|
|
408
|
+
"integrity": "sha512-WKgKWg5eUxvRZGwW8FvfbaH7AXSh2cL+3j5fMGzUMCxWBJ3dV3a7Wz8y2f/uQ0e3B6WmodD3oS54jTQ9HVTIIg==",
|
|
409
|
+
"dev": true,
|
|
410
|
+
"license": "MIT"
|
|
411
|
+
},
|
|
412
|
+
"node_modules/file-type": {
|
|
413
|
+
"version": "22.0.2",
|
|
414
|
+
"resolved": "https://registry.npmjs.org/file-type/-/file-type-22.0.2.tgz",
|
|
415
|
+
"integrity": "sha512-0H8TsCUGBLx+V5adH3EY52hTAcyLKbV1D4gq5cIOJ6DnQAHeV9Z2Hhuc5CoBX4YmvB2oL+JIC84z0qO7JsCoNw==",
|
|
416
|
+
"dev": true,
|
|
417
|
+
"license": "MIT",
|
|
418
|
+
"peer": true,
|
|
419
|
+
"dependencies": {
|
|
420
|
+
"@tokenizer/inflate": "^0.4.1",
|
|
421
|
+
"strtok3": "^10.3.5",
|
|
422
|
+
"token-types": "^6.1.2",
|
|
423
|
+
"uint8array-extras": "^1.5.0"
|
|
424
|
+
},
|
|
425
|
+
"engines": {
|
|
426
|
+
"node": ">=22"
|
|
427
|
+
},
|
|
428
|
+
"funding": {
|
|
429
|
+
"url": "https://github.com/sindresorhus/file-type?sponsor=1"
|
|
430
|
+
}
|
|
431
|
+
},
|
|
296
432
|
"node_modules/finalhandler": {
|
|
297
433
|
"version": "2.1.1",
|
|
298
434
|
"resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.1.tgz",
|
|
@@ -423,6 +559,16 @@
|
|
|
423
559
|
"he": "bin/he"
|
|
424
560
|
}
|
|
425
561
|
},
|
|
562
|
+
"node_modules/hono": {
|
|
563
|
+
"version": "4.13.7",
|
|
564
|
+
"resolved": "https://registry.npmjs.org/hono/-/hono-4.13.7.tgz",
|
|
565
|
+
"integrity": "sha512-c8/gF9ac8Y78/agExVocyLevgR+JlpNB444Py0FSX8pJoPdYUfUzRcXtYEYGwt6l19qIlVZPN5Mfsw9jFShmQQ==",
|
|
566
|
+
"dev": true,
|
|
567
|
+
"license": "MIT",
|
|
568
|
+
"engines": {
|
|
569
|
+
"node": ">=16.9.0"
|
|
570
|
+
}
|
|
571
|
+
},
|
|
426
572
|
"node_modules/http-errors": {
|
|
427
573
|
"version": "2.0.1",
|
|
428
574
|
"resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz",
|
|
@@ -459,6 +605,28 @@
|
|
|
459
605
|
"url": "https://opencollective.com/express"
|
|
460
606
|
}
|
|
461
607
|
},
|
|
608
|
+
"node_modules/ieee754": {
|
|
609
|
+
"version": "1.2.1",
|
|
610
|
+
"resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz",
|
|
611
|
+
"integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==",
|
|
612
|
+
"dev": true,
|
|
613
|
+
"funding": [
|
|
614
|
+
{
|
|
615
|
+
"type": "github",
|
|
616
|
+
"url": "https://github.com/sponsors/feross"
|
|
617
|
+
},
|
|
618
|
+
{
|
|
619
|
+
"type": "patreon",
|
|
620
|
+
"url": "https://www.patreon.com/feross"
|
|
621
|
+
},
|
|
622
|
+
{
|
|
623
|
+
"type": "consulting",
|
|
624
|
+
"url": "https://feross.org/support"
|
|
625
|
+
}
|
|
626
|
+
],
|
|
627
|
+
"license": "BSD-3-Clause",
|
|
628
|
+
"peer": true
|
|
629
|
+
},
|
|
462
630
|
"node_modules/inherits": {
|
|
463
631
|
"version": "2.0.4",
|
|
464
632
|
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
|
|
@@ -502,6 +670,13 @@
|
|
|
502
670
|
"url": "https://opencollective.com/express"
|
|
503
671
|
}
|
|
504
672
|
},
|
|
673
|
+
"node_modules/memoirist": {
|
|
674
|
+
"version": "0.4.0",
|
|
675
|
+
"resolved": "https://registry.npmjs.org/memoirist/-/memoirist-0.4.0.tgz",
|
|
676
|
+
"integrity": "sha512-zxTgA0mSYELa66DimuNQDvyLq36AwDlTuVRbnQtB+VuTcKWm5Qc4z3WkSpgsFWHNhexqkIooqpv4hdcqrX5Nmg==",
|
|
677
|
+
"dev": true,
|
|
678
|
+
"license": "MIT"
|
|
679
|
+
},
|
|
505
680
|
"node_modules/merge-descriptors": {
|
|
506
681
|
"version": "2.0.0",
|
|
507
682
|
"resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-2.0.0.tgz",
|
|
@@ -515,9 +690,9 @@
|
|
|
515
690
|
}
|
|
516
691
|
},
|
|
517
692
|
"node_modules/miki-template": {
|
|
518
|
-
"version": "1.3.
|
|
519
|
-
"resolved": "
|
|
520
|
-
"integrity": "sha512-
|
|
693
|
+
"version": "1.3.7",
|
|
694
|
+
"resolved": "file:../miki-template-2.0.0.tgz",
|
|
695
|
+
"integrity": "sha512-FGTWjPS3CTm3X4+dnRKqHwqQUKgqtz/0VNTjN1BQjBcgwhXnEbVYg06VZmBtZ8SYr6z12JIX3LGj2pQog3h84Q==",
|
|
521
696
|
"license": "MIT",
|
|
522
697
|
"dependencies": {
|
|
523
698
|
"date-fns": "^3.6.0",
|
|
@@ -617,6 +792,14 @@
|
|
|
617
792
|
"wrappy": "1"
|
|
618
793
|
}
|
|
619
794
|
},
|
|
795
|
+
"node_modules/openapi-types": {
|
|
796
|
+
"version": "12.1.3",
|
|
797
|
+
"resolved": "https://registry.npmjs.org/openapi-types/-/openapi-types-12.1.3.tgz",
|
|
798
|
+
"integrity": "sha512-N4YtSYJqghVu4iek2ZUvcN/0aqH1kRDuNqzcycDxhOUpg7GdvLa2F3DgS6yBNhInhv2r/6I0Flkn7CqL8+nIcw==",
|
|
799
|
+
"dev": true,
|
|
800
|
+
"license": "MIT",
|
|
801
|
+
"peer": true
|
|
802
|
+
},
|
|
620
803
|
"node_modules/parseurl": {
|
|
621
804
|
"version": "1.3.3",
|
|
622
805
|
"resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz",
|
|
@@ -847,6 +1030,24 @@
|
|
|
847
1030
|
"node": ">= 0.8"
|
|
848
1031
|
}
|
|
849
1032
|
},
|
|
1033
|
+
"node_modules/strtok3": {
|
|
1034
|
+
"version": "10.3.5",
|
|
1035
|
+
"resolved": "https://registry.npmjs.org/strtok3/-/strtok3-10.3.5.tgz",
|
|
1036
|
+
"integrity": "sha512-ki4hZQfh5rX0QDLLkOCj+h+CVNkqmp/CMf8v8kZpkNVK6jGQooMytqzLZYUVYIZcFZ6yDB70EfD8POcFXiF5oA==",
|
|
1037
|
+
"dev": true,
|
|
1038
|
+
"license": "MIT",
|
|
1039
|
+
"peer": true,
|
|
1040
|
+
"dependencies": {
|
|
1041
|
+
"@tokenizer/token": "^0.3.0"
|
|
1042
|
+
},
|
|
1043
|
+
"engines": {
|
|
1044
|
+
"node": ">=18"
|
|
1045
|
+
},
|
|
1046
|
+
"funding": {
|
|
1047
|
+
"type": "github",
|
|
1048
|
+
"url": "https://github.com/sponsors/Borewit"
|
|
1049
|
+
}
|
|
1050
|
+
},
|
|
850
1051
|
"node_modules/toidentifier": {
|
|
851
1052
|
"version": "1.0.1",
|
|
852
1053
|
"resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz",
|
|
@@ -856,6 +1057,26 @@
|
|
|
856
1057
|
"node": ">=0.6"
|
|
857
1058
|
}
|
|
858
1059
|
},
|
|
1060
|
+
"node_modules/token-types": {
|
|
1061
|
+
"version": "6.1.2",
|
|
1062
|
+
"resolved": "https://registry.npmjs.org/token-types/-/token-types-6.1.2.tgz",
|
|
1063
|
+
"integrity": "sha512-dRXchy+C0IgK8WPC6xvCHFRIWYUbqqdEIKPaKo/AcTUNzwLTK6AH7RjdLWsEZcAN/TBdtfUw3PYEgPr5VPr6ww==",
|
|
1064
|
+
"dev": true,
|
|
1065
|
+
"license": "MIT",
|
|
1066
|
+
"peer": true,
|
|
1067
|
+
"dependencies": {
|
|
1068
|
+
"@borewit/text-codec": "^0.2.1",
|
|
1069
|
+
"@tokenizer/token": "^0.3.0",
|
|
1070
|
+
"ieee754": "^1.2.1"
|
|
1071
|
+
},
|
|
1072
|
+
"engines": {
|
|
1073
|
+
"node": ">=14.16"
|
|
1074
|
+
},
|
|
1075
|
+
"funding": {
|
|
1076
|
+
"type": "github",
|
|
1077
|
+
"url": "https://github.com/sponsors/Borewit"
|
|
1078
|
+
}
|
|
1079
|
+
},
|
|
859
1080
|
"node_modules/type-is": {
|
|
860
1081
|
"version": "2.1.0",
|
|
861
1082
|
"resolved": "https://registry.npmjs.org/type-is/-/type-is-2.1.0.tgz",
|
|
@@ -887,6 +1108,20 @@
|
|
|
887
1108
|
"url": "https://opencollective.com/express"
|
|
888
1109
|
}
|
|
889
1110
|
},
|
|
1111
|
+
"node_modules/uint8array-extras": {
|
|
1112
|
+
"version": "1.5.0",
|
|
1113
|
+
"resolved": "https://registry.npmjs.org/uint8array-extras/-/uint8array-extras-1.5.0.tgz",
|
|
1114
|
+
"integrity": "sha512-rvKSBiC5zqCCiDZ9kAOszZcDvdAHwwIKJG33Ykj43OKcWsnmcBRL09YTU4nOeHZ8Y2a7l1MgTd08SBe9A8Qj6A==",
|
|
1115
|
+
"dev": true,
|
|
1116
|
+
"license": "MIT",
|
|
1117
|
+
"peer": true,
|
|
1118
|
+
"engines": {
|
|
1119
|
+
"node": ">=18"
|
|
1120
|
+
},
|
|
1121
|
+
"funding": {
|
|
1122
|
+
"url": "https://github.com/sponsors/sindresorhus"
|
|
1123
|
+
}
|
|
1124
|
+
},
|
|
890
1125
|
"node_modules/unpipe": {
|
|
891
1126
|
"version": "1.0.0",
|
|
892
1127
|
"resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz",
|
package/live-test/package.json
CHANGED
package/live-test/server.js
CHANGED
|
@@ -4,11 +4,35 @@ const miki = require('miki-template');
|
|
|
4
4
|
|
|
5
5
|
const app = express();
|
|
6
6
|
const views = path.join(__dirname, 'views');
|
|
7
|
+
const templatesRoot = path.join(__dirname, 'templates');
|
|
7
8
|
|
|
8
|
-
|
|
9
|
+
// Demonstrate multiple discovery roots: explicit views dir,
|
|
10
|
+
// plus a top-level `templates/` (Django-style) and app-style nested folders
|
|
11
|
+
// discovered anywhere under the project root.
|
|
12
|
+
miki.setupExpress(app, { extension: 'html', views: [views, templatesRoot, __dirname] });
|
|
13
|
+
|
|
14
|
+
// Note: `miki.setupExpress` may expand `app.get('views')` to include
|
|
15
|
+
// nested template directories (project-level `templates/`,
|
|
16
|
+
// app-style `packages/*/templates/...`, etc.). The live-test app
|
|
17
|
+
// intentionally passed multiple roots to demonstrate discovery.
|
|
9
18
|
|
|
10
19
|
app.get('/', (req, res) => {
|
|
11
20
|
res.render('index', { name: 'Live NPM' });
|
|
12
21
|
});
|
|
13
22
|
|
|
23
|
+
// Render a nested child template that extends base.html located in views/
|
|
24
|
+
app.get('/child', (req, res) => {
|
|
25
|
+
res.render('child', { title: 'From Live', message: 'Hello from child' });
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
// Render a template located under templates/app_templates/detail.html
|
|
29
|
+
|
|
30
|
+
app.get('/app-detail', (req, res) => {
|
|
31
|
+
res.render('detail', { item: 'Widget 42' });
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
app.get('/product', (req, res) => {
|
|
35
|
+
res.render('product/detail', { name: 'Gizmo', price: '$19.99' });
|
|
36
|
+
});
|
|
37
|
+
|
|
14
38
|
app.listen(3002, () => console.log('Live test app listening on 3002'));
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html>
|
|
3
|
+
<head>
|
|
4
|
+
<title>Home</title>
|
|
5
|
+
</head>
|
|
6
|
+
<body>
|
|
7
|
+
<h1>Welcome, {{ user }}</h1>
|
|
8
|
+
|
|
9
|
+
{% partialdef card %}
|
|
10
|
+
<div class="card">Card: {{ title }}</div>
|
|
11
|
+
{% endpartialdef %}
|
|
12
|
+
|
|
13
|
+
<div id="content">
|
|
14
|
+
{% partial card %}
|
|
15
|
+
</div>
|
|
16
|
+
</body>
|
|
17
|
+
</html>
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "miki-template",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "2.0.1",
|
|
4
4
|
"description": "Django-Style template engine for Node.js and Express",
|
|
5
5
|
"main": "src/index.js",
|
|
6
6
|
"exports": {
|
|
@@ -16,7 +16,11 @@
|
|
|
16
16
|
"engine",
|
|
17
17
|
"express",
|
|
18
18
|
"nodejs",
|
|
19
|
-
"
|
|
19
|
+
"bun",
|
|
20
|
+
"esm",
|
|
21
|
+
"koa",
|
|
22
|
+
"fastify",
|
|
23
|
+
"nestjs"
|
|
20
24
|
],
|
|
21
25
|
"author": "",
|
|
22
26
|
"license": "MIT",
|
|
@@ -25,10 +29,14 @@
|
|
|
25
29
|
"he": "^1.2.0"
|
|
26
30
|
},
|
|
27
31
|
"devDependencies": {
|
|
32
|
+
"koa": "^3.2.1",
|
|
33
|
+
"fastify": "^5.12.3",
|
|
34
|
+
"ejs": "^6.0.1",
|
|
35
|
+
"eslint": "^8.50.0",
|
|
28
36
|
"express": "^5.2.1",
|
|
29
|
-
"
|
|
30
|
-
|
|
31
|
-
,
|
|
37
|
+
"handlebars": "^4.7.9",
|
|
38
|
+
"pug": "^3.0.4"
|
|
39
|
+
},
|
|
32
40
|
"scripts": {
|
|
33
41
|
"test": "node -e \"const fs=require('fs');const path=require('path');function walk(d){const out=[];for(const e of fs.readdirSync(d,{withFileTypes:true})){const p=path.join(d,e.name);if(e.isDirectory())out.push(...walk(p));else if(/\\\\.test\\\\.(cjs|mjs|js)$/.test(e.name))out.push(p);}return out;}const files=walk('tests');require('child_process').execSync('node --test '+files.join(' '),{stdio:'inherit'})\"",
|
|
34
42
|
"lint": "eslint . -f unix",
|
package/src/index.js
CHANGED
|
@@ -894,11 +894,80 @@ function setupExpress(app, opts = {}) {
|
|
|
894
894
|
// Install the raw engine so Express can use it
|
|
895
895
|
app.engine(ext, async ? __expressAsync : __express);
|
|
896
896
|
|
|
897
|
+
// Expand configured view roots to include nested directories that
|
|
898
|
+
// actually contain template files. Express will only check the
|
|
899
|
+
// directories returned by `app.get('views')`, so if templates live
|
|
900
|
+
// inside nested app folders (e.g. `packages/*/templates/...`), we
|
|
901
|
+
// must include those directories explicitly so `res.render('name')`
|
|
902
|
+
// succeeds without additional work by users.
|
|
903
|
+
function expandViewsForExpress(viewsInput, maxDepth = 6) {
|
|
904
|
+
const seen = new Set();
|
|
905
|
+
const results = [];
|
|
906
|
+
const roots = normalizeViews(viewsInput || (app.get && app.get('views')) || ['.']);
|
|
907
|
+
const isTemplateFile = name => /\.(html|miki)$/i.test(name);
|
|
908
|
+
|
|
909
|
+
for (const r of roots) {
|
|
910
|
+
let rootPath;
|
|
911
|
+
try { rootPath = path.resolve(r); } catch { continue; }
|
|
912
|
+
if (seen.has(rootPath)) continue;
|
|
913
|
+
seen.add(rootPath);
|
|
914
|
+
// BFS/DFS limited traversal
|
|
915
|
+
const stack = [{ dir: rootPath, depth: 0 }];
|
|
916
|
+
while (stack.length) {
|
|
917
|
+
const cur = stack.pop();
|
|
918
|
+
let entries;
|
|
919
|
+
try { entries = fs.readdirSync(cur.dir, { withFileTypes: true }); } catch { continue; }
|
|
920
|
+
let hasTemplate = false;
|
|
921
|
+
for (const ent of entries) {
|
|
922
|
+
if (ent.isFile() && isTemplateFile(ent.name)) {
|
|
923
|
+
hasTemplate = true;
|
|
924
|
+
break;
|
|
925
|
+
}
|
|
926
|
+
}
|
|
927
|
+
if (hasTemplate) {
|
|
928
|
+
if (!seen.has(cur.dir)) {
|
|
929
|
+
seen.add(cur.dir);
|
|
930
|
+
results.push(cur.dir);
|
|
931
|
+
}
|
|
932
|
+
}
|
|
933
|
+
if (cur.depth < maxDepth) {
|
|
934
|
+
for (const ent of entries) {
|
|
935
|
+
if (!ent.isDirectory()) continue;
|
|
936
|
+
const name = ent.name;
|
|
937
|
+
if (name === 'node_modules' || name === '.git' || name === 'dist' || name === 'build') continue;
|
|
938
|
+
const child = path.join(cur.dir, name);
|
|
939
|
+
if (!seen.has(child)) stack.push({ dir: child, depth: cur.depth + 1 });
|
|
940
|
+
}
|
|
941
|
+
}
|
|
942
|
+
}
|
|
943
|
+
// Always include the root itself so existing layouts still work
|
|
944
|
+
if (!results.includes(rootPath)) results.unshift(rootPath);
|
|
945
|
+
}
|
|
946
|
+
return results;
|
|
947
|
+
}
|
|
948
|
+
|
|
897
949
|
// Capture config in a closure so patchedRender can use it even
|
|
898
950
|
// when called before the request handler runs.
|
|
899
951
|
const configExt = ext;
|
|
900
952
|
const configViews = opts.views;
|
|
901
953
|
|
|
954
|
+
// Expand and set the app 'views' so Express's resolver can find
|
|
955
|
+
// templates placed in nested directories. Only do this when a
|
|
956
|
+
// non-empty views value exists (we don't want to change defaults
|
|
957
|
+
// if user intentionally left it unset).
|
|
958
|
+
try {
|
|
959
|
+
const currentViews = app.get && app.get('views') ? app.get('views') : configViews || app.get && app.get('views');
|
|
960
|
+
if (currentViews) {
|
|
961
|
+
const expanded = expandViewsForExpress(currentViews);
|
|
962
|
+
if (expanded && expanded.length) {
|
|
963
|
+
app.set('views', expanded);
|
|
964
|
+
}
|
|
965
|
+
}
|
|
966
|
+
} catch (e) {
|
|
967
|
+
// Don't crash setupExpress if view expansion fails; fallback to
|
|
968
|
+
// whatever the app already had configured.
|
|
969
|
+
}
|
|
970
|
+
|
|
902
971
|
// Capture the original res.render so we can dispatch on #partial
|
|
903
972
|
const originalRender = app.response.render;
|
|
904
973
|
app.response.render = function patchedRender(view, locals, callback) {
|
|
@@ -951,9 +1020,19 @@ function setupExpress(app, opts = {}) {
|
|
|
951
1020
|
break;
|
|
952
1021
|
}
|
|
953
1022
|
}
|
|
954
|
-
// Fallback: recursive search for templates in subfolders
|
|
1023
|
+
// Fallback: recursive search for templates in subfolders. If
|
|
1024
|
+
// the configured views is an array, search all normalized
|
|
1025
|
+
// entries rather than only the first directory so app-style
|
|
1026
|
+
// templates under other roots are discovered.
|
|
955
1027
|
if (!filePath) {
|
|
956
|
-
|
|
1028
|
+
let searchDirs = [viewsDir];
|
|
1029
|
+
if (Array.isArray(viewsDir)) {
|
|
1030
|
+
searchDirs = normalizeViews(viewsDir);
|
|
1031
|
+
} else if (Array.isArray(configViews)) {
|
|
1032
|
+
// If setupExpress was given an array, include its normalized form
|
|
1033
|
+
searchDirs = normalizeViews(configViews);
|
|
1034
|
+
}
|
|
1035
|
+
const found = findTemplateInViews(fileName, searchDirs);
|
|
957
1036
|
if (found) filePath = found;
|
|
958
1037
|
}
|
|
959
1038
|
if (!filePath) {
|
|
@@ -984,11 +1063,55 @@ function setupExpress(app, opts = {}) {
|
|
|
984
1063
|
}
|
|
985
1064
|
}
|
|
986
1065
|
|
|
987
|
-
// No partial selector: behave exactly like the original res.render
|
|
988
|
-
|
|
989
|
-
|
|
1066
|
+
// No partial selector: behave exactly like the original res.render.
|
|
1067
|
+
// If Express fails to locate the view (no recursive lookup),
|
|
1068
|
+
// fall back to our recursive/app-style finder and invoke the
|
|
1069
|
+
// engine directly with the resolved file path.
|
|
1070
|
+
try {
|
|
1071
|
+
if (cb) {
|
|
1072
|
+
return originalRender.call(this, view, opts, cb);
|
|
1073
|
+
}
|
|
1074
|
+
return originalRender.call(this, view, opts);
|
|
1075
|
+
} catch (err) {
|
|
1076
|
+
// Detect Express view-not-found error and attempt fallback
|
|
1077
|
+
if (err && typeof err.message === 'string' && err.message.includes('Failed to lookup view')) {
|
|
1078
|
+
// Determine normalized view roots to search. Prefer arrays
|
|
1079
|
+
// returned by app.get('views') (Express supports arrays).
|
|
1080
|
+
let roots = [];
|
|
1081
|
+
try {
|
|
1082
|
+
const appViews = this.req && this.req.app ? this.req.app.get('views') : null;
|
|
1083
|
+
if (appViews) roots = roots.concat(Array.isArray(appViews) ? normalizeViews(appViews) : [appViews]);
|
|
1084
|
+
} catch (e) {}
|
|
1085
|
+
if (configViews) roots = roots.concat(Array.isArray(configViews) ? normalizeViews(configViews) : [configViews]);
|
|
1086
|
+
// If expandViewsForExpress was used earlier, app.get('views')
|
|
1087
|
+
// may already be an expanded array of candidate dirs. Ensure
|
|
1088
|
+
// uniqueness and absolute resolution.
|
|
1089
|
+
roots = roots.filter(Boolean).map(r => path.resolve(r));
|
|
1090
|
+
roots = Array.from(new Set(roots));
|
|
1091
|
+
|
|
1092
|
+
// Try to find the template file using our recursive finder.
|
|
1093
|
+
const searchRoots = roots.length ? roots : [process.cwd()];
|
|
1094
|
+
const found = findTemplateInViews(view, searchRoots);
|
|
1095
|
+
if (found) {
|
|
1096
|
+
// Invoke the engine directly with the resolved file path.
|
|
1097
|
+
try {
|
|
1098
|
+
if (cb) return (__express)(found, opts, cb);
|
|
1099
|
+
return __express(found, opts, (err2, html) => {
|
|
1100
|
+
if (err2) {
|
|
1101
|
+
if (cb) return cb(err2);
|
|
1102
|
+
throw err2;
|
|
1103
|
+
}
|
|
1104
|
+
this.send(html);
|
|
1105
|
+
});
|
|
1106
|
+
} catch (e2) {
|
|
1107
|
+
if (cb) return cb(e2);
|
|
1108
|
+
throw e2;
|
|
1109
|
+
}
|
|
1110
|
+
}
|
|
1111
|
+
}
|
|
1112
|
+
// Not our error or fallback failed — rethrow
|
|
1113
|
+
throw err;
|
|
990
1114
|
}
|
|
991
|
-
return originalRender.call(this, view, opts);
|
|
992
1115
|
};
|
|
993
1116
|
}
|
|
994
1117
|
|
|
@@ -1039,9 +1162,20 @@ function expressPartialRenderer() {
|
|
|
1039
1162
|
filePath = null;
|
|
1040
1163
|
} catch { filePath = null; }
|
|
1041
1164
|
}
|
|
1042
|
-
// Fallback: recursive search for templates in subfolders
|
|
1165
|
+
// Fallback: recursive search for templates in subfolders. Prefer
|
|
1166
|
+
// searching all normalized view roots so templates in sibling
|
|
1167
|
+
// folders (project-level `templates/`, packages/*/templates, etc.)
|
|
1168
|
+
// are found.
|
|
1043
1169
|
if (!filePath) {
|
|
1044
|
-
|
|
1170
|
+
let searchDirs = Array.isArray(viewsDir) ? normalizeViews(viewsDir) : [viewsDir];
|
|
1171
|
+
// Also include the app-level configured views array if present
|
|
1172
|
+
try {
|
|
1173
|
+
const appViews = req.app.get('views');
|
|
1174
|
+
if (Array.isArray(appViews)) {
|
|
1175
|
+
searchDirs = searchDirs.concat(normalizeViews(appViews));
|
|
1176
|
+
}
|
|
1177
|
+
} catch (e) {}
|
|
1178
|
+
const found = findTemplateInViews(fileName, searchDirs);
|
|
1045
1179
|
if (found) filePath = found;
|
|
1046
1180
|
}
|
|
1047
1181
|
if (!filePath) {
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
const test = require('node:test');
|
|
2
|
+
const assert = require('assert');
|
|
3
|
+
const path = require('path');
|
|
4
|
+
const miki = require('../../src/index.js');
|
|
5
|
+
|
|
6
|
+
test('engine partial render via render() and asyncRender()', async () => {
|
|
7
|
+
const views = path.resolve(__dirname, '..', '..', 'live-test', 'views');
|
|
8
|
+
const sync = miki.render('home#card', { user: 'Tester', title: 'T' }, { views });
|
|
9
|
+
assert.ok(sync.includes('Card'), 'sync partial should contain Card');
|
|
10
|
+
|
|
11
|
+
const asyncRes = await miki.asyncRender('home#card', { user: 'Tester', title: 'T' }, { views });
|
|
12
|
+
assert.ok(asyncRes.includes('Card'), 'async partial should contain Card');
|
|
13
|
+
});
|