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.
@@ -9,9 +9,60 @@
9
9
  "version": "1.0.0",
10
10
  "dependencies": {
11
11
  "express": "^5.2.1",
12
- "miki-template": "1.3.6"
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.6",
519
- "resolved": "https://registry.npmjs.org/miki-template/-/miki-template-1.3.6.tgz",
520
- "integrity": "sha512-xB658VrwCMAg8abpMzKF8ZKlnYgMaX7eimpxSx/GR35sor5kW/bhqiYr54HXpNux3d0Qgv7uUPszid3MFYcpSA==",
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",
@@ -4,6 +4,10 @@
4
4
  "private": true,
5
5
  "dependencies": {
6
6
  "express": "^5.2.1",
7
- "miki-template": "1.3.6"
7
+ "miki-template": "file:../miki-template-2.0.0.tgz"
8
+ },
9
+ "devDependencies": {
10
+ "elysia": "^1.4.30",
11
+ "hono": "^4.13.7"
8
12
  }
9
13
  }
@@ -0,0 +1,7 @@
1
+ <html>
2
+ <head><title>Product Detail</title></head>
3
+ <body>
4
+ <h1>Product: {{ name }}</h1>
5
+ <p>Price: {{ price }}</p>
6
+ </body>
7
+ </html>
@@ -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
- miki.setupExpress(app, { extension: 'html', views });
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,6 @@
1
+ <html>
2
+ <head><title>App Template</title></head>
3
+ <body>
4
+ <h1>App-level template: {{ item }}</h1>
5
+ </body>
6
+ </html>
@@ -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": "1.3.7",
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
- "esm"
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
- "eslint": "^8.50.0"
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
- const found = findTemplateInViews(fileName, [viewsDir]);
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
- if (cb) {
989
- return originalRender.call(this, view, opts, cb);
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
- const found = findTemplateInViews(fileName, Array.isArray(viewsDir) ? viewsDir : [viewsDir]);
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
+ });