miki-template 1.3.6 → 2.0.0

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.
@@ -0,0 +1,9 @@
1
+ {
2
+ "name": "live-test",
3
+ "version": "1.0.0",
4
+ "private": true,
5
+ "dependencies": {
6
+ "express": "^5.2.1",
7
+ "miki-template": "file:../miki-template-2.0.0.tgz"
8
+ }
9
+ }
@@ -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>
@@ -0,0 +1,38 @@
1
+ const express = require('express');
2
+ const path = require('path');
3
+ const miki = require('miki-template');
4
+
5
+ const app = express();
6
+ const views = path.join(__dirname, 'views');
7
+ const templatesRoot = path.join(__dirname, 'templates');
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.
18
+
19
+ app.get('/', (req, res) => {
20
+ res.render('index', { name: 'Live NPM' });
21
+ });
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
+
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,8 @@
1
+ <html>
2
+ <head>
3
+ <title>{% block title %}Base Title{% endblock %}</title>
4
+ </head>
5
+ <body>
6
+ {% block content %}Base content{% endblock %}
7
+ </body>
8
+ </html>
@@ -0,0 +1,7 @@
1
+ {% extends 'base.html' %}
2
+ {% block title %}Child - {{ title }}{% endblock %}
3
+ {% block content %}
4
+ <h2>Child Content</h2>
5
+ <p>{{ message }}</p>
6
+ {{ block.super }}
7
+ {% endblock %}
@@ -0,0 +1 @@
1
+ <html><head><title>{{ name }}</title></head><body><h1>Hello {{ name }}</h1></body></html>
@@ -894,7 +894,7 @@ function activate(context) {
894
894
  const selection = editor.selection;
895
895
  const selectedText = editor.document.getText(selection);
896
896
  editor.edit(editBuilder => {
897
- editBuilder.replace(selection, `{% block ${1:name} %}\n${selectedText}\n{% endblock %}`);
897
+ editBuilder.replace(selection, '{% block ${1:name} %}\n' + selectedText + '\n{% endblock %}');
898
898
  });
899
899
  }),
900
900
 
@@ -904,7 +904,7 @@ function activate(context) {
904
904
  const selection = editor.selection;
905
905
  const selectedText = editor.document.getText(selection);
906
906
  editor.edit(editBuilder => {
907
- editBuilder.replace(selection, `{% for ${1:item} in ${2:items} %}\n${selectedText}\n{% endfor %}`);
907
+ editBuilder.replace(selection, '{% for ${1:item} in ${2:items} %}\n' + selectedText + '\n{% endfor %}');
908
908
  });
909
909
  }),
910
910
 
@@ -914,7 +914,7 @@ function activate(context) {
914
914
  const selection = editor.selection;
915
915
  const selectedText = editor.document.getText(selection);
916
916
  editor.edit(editBuilder => {
917
- editBuilder.replace(selection, `{% if ${1:condition} %}\n${selectedText}\n{% endif %}`);
917
+ editBuilder.replace(selection, '{% if ${1:condition} %}\n' + selectedText + '\n{% endif %}');
918
918
  });
919
919
  }),
920
920
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "miki-template",
3
- "version": "1.3.6",
3
+ "version": "2.0.0",
4
4
  "description": "Django-Style template engine for Node.js and Express",
5
5
  "main": "src/index.js",
6
6
  "exports": {
@@ -10,13 +10,6 @@
10
10
  "default": "./src/index.js"
11
11
  }
12
12
  },
13
- "scripts": {
14
- "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'})\"",
15
- "bench": "node benchmarks/run.js",
16
- "release:patch": "npm version patch --no-git-tag-version -m \"chore(release): v%s\" && git push origin main",
17
- "release:minor": "npm version minor --no-git-tag-version -m \"chore(release): v%s\" && git push origin main",
18
- "release:major": "npm version major --no-git-tag-version -m \"chore(release): v%s\" && git push origin main"
19
- },
20
13
  "keywords": [
21
14
  "django",
22
15
  "template",
@@ -32,6 +25,16 @@
32
25
  "he": "^1.2.0"
33
26
  },
34
27
  "devDependencies": {
35
- "express": "^5.2.1"
28
+ "express": "^5.2.1",
29
+ "eslint": "^8.50.0"
30
+ }
31
+ ,
32
+ "scripts": {
33
+ "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
+ "lint": "eslint . -f unix",
35
+ "bench": "node benchmarks/run.js",
36
+ "release:patch": "npm version patch --no-git-tag-version -m \"chore(release): v%s\" && git push origin main",
37
+ "release:minor": "npm version minor --no-git-tag-version -m \"chore(release): v%s\" && git push origin main",
38
+ "release:major": "npm version major --no-git-tag-version -m \"chore(release): v%s\" && git push origin main"
36
39
  }
37
40
  }
package/src/esm.mjs CHANGED
@@ -44,6 +44,11 @@ const {
44
44
  escapeHtml
45
45
  } = cjsModule;
46
46
 
47
+ // Re-export the finder for ESM tests
48
+ export const findTemplateInViews = cjsModule.findTemplateInViews;
49
+ export const setAppTemplateDirNames = cjsModule.setAppTemplateDirNames;
50
+ export const getAppTemplateDirNames = cjsModule.getAppTemplateDirNames;
51
+
47
52
  export {
48
53
  compile,
49
54
  render,
package/src/index.js CHANGED
@@ -69,6 +69,97 @@ function normalizeViews(views) {
69
69
  });
70
70
  }
71
71
 
72
+ // Find a template file by name in the provided views directories.
73
+ // Supports searching recursively through subdirectories when the
74
+ // template name is a bare name (no path separators). Returns the
75
+ // absolute path to the first matching file, or null if not found.
76
+ function findTemplateInViews(templateName, viewsDirs) {
77
+ if (!templateName) return null;
78
+ const hasExt = /\.[a-z0-9]+$/i.test(templateName);
79
+ // Candidate basenames to look for when doing recursive search
80
+ const candidateBasenames = hasExt ? [path.basename(templateName)] : [path.basename(templateName) + '.html', path.basename(templateName) + '.miki'];
81
+
82
+ // Normalize templateName's separators to the platform so direct
83
+ // resolves work when callers use forward slashes on Windows.
84
+ const templateNameNorm = templateName.replace(/\//g, path.sep);
85
+
86
+ for (const dir of viewsDirs) {
87
+ // Also consider app-style 'templates' directories nested inside
88
+ // the views root (e.g. project/app/templates/...)
89
+ const appTemplateDirs = findTemplatesDirsUnder(dir);
90
+ const searchDirs = [dir, ...appTemplateDirs];
91
+ for (const sdir of searchDirs) {
92
+ // Try direct resolution: if caller provided a path (like "nested/index")
93
+ // resolve it relative to the search dir and try supported extensions.
94
+ const basePath = path.resolve(sdir, templateNameNorm);
95
+ if (hasExt) {
96
+ try { if (fs.existsSync(basePath)) return basePath; } catch {}
97
+ } else {
98
+ try { if (fs.existsSync(basePath + '.html')) return basePath + '.html'; } catch {}
99
+ try { if (fs.existsSync(basePath + '.miki')) return basePath + '.miki'; } catch {}
100
+ }
101
+
102
+ // If templateName is a bare name (no path separators), search
103
+ // recursively under the search dir for matching filenames.
104
+ if (!templateName.includes('/') && !templateName.includes(path.sep)) {
105
+ const stack = [sdir];
106
+ while (stack.length) {
107
+ const cur = stack.pop();
108
+ let entries;
109
+ try { entries = fs.readdirSync(cur, { withFileTypes: true }); } catch (e) { continue; }
110
+ for (const ent of entries) {
111
+ const p = path.join(cur, ent.name);
112
+ if (ent.isDirectory()) {
113
+ stack.push(p);
114
+ continue;
115
+ }
116
+ if (!ent.isFile()) continue;
117
+ const relative = path.relative(path.resolve(sdir), p);
118
+ if (relative.startsWith('..') || path.isAbsolute(relative)) continue;
119
+ for (const candBasename of candidateBasenames) {
120
+ if (ent.name === candBasename) return p;
121
+ }
122
+ }
123
+ }
124
+ }
125
+ }
126
+ }
127
+ return null;
128
+ }
129
+
130
+ // Search for "app-style" templates directories under the provided
131
+ // views directories. Many projects place templates inside an app
132
+ // submodule under `appname/templates/...`. This helper will locate
133
+ // any `templates` directories and search them for candidates.
134
+ let appTemplateDirNames = ['templates'];
135
+
136
+ function setAppTemplateDirNames(names) {
137
+ if (!names) return;
138
+ if (Array.isArray(names)) appTemplateDirNames = names.slice();
139
+ else if (typeof names === 'string') appTemplateDirNames = [names];
140
+ }
141
+
142
+ function getAppTemplateDirNames() {
143
+ return appTemplateDirNames.slice();
144
+ }
145
+
146
+ function findTemplatesDirsUnder(dir) {
147
+ const results = [];
148
+ const stack = [dir];
149
+ while (stack.length) {
150
+ const cur = stack.pop();
151
+ let entries;
152
+ try { entries = fs.readdirSync(cur, { withFileTypes: true }); } catch (e) { continue; }
153
+ for (const ent of entries) {
154
+ const p = path.join(cur, ent.name);
155
+ if (!ent.isDirectory()) continue;
156
+ if (appTemplateDirNames.includes(ent.name)) results.push(p);
157
+ stack.push(p);
158
+ }
159
+ }
160
+ return results;
161
+ }
162
+
72
163
  // Inject the registration functions so libraries can activate without
73
164
  // triggering a circular require. This must happen BEFORE the
74
165
  // auto-activation loop below.
@@ -101,15 +192,28 @@ function readParentSource(parentName, viewsDirs) {
101
192
  if (relative.startsWith('..') || path.isAbsolute(relative)) {
102
193
  throw new Error(`Extends tag attempted path traversal outside allowed views: '${parentName}'`);
103
194
  }
104
- const fileContent = fs.readFileSync(fullPath, 'utf8');
105
- // Populate the LRU cache and return the value
106
- return getParentSource(key, () => fileContent);
195
+ if (fs.existsSync(fullPath)) {
196
+ const fileContent = fs.readFileSync(fullPath, 'utf8');
197
+ // Populate the LRU cache and return the value
198
+ return getParentSource(key, () => fileContent);
199
+ }
107
200
  } catch (e) {
108
201
  if (e.message && e.message.startsWith('Extends tag attempted path traversal')) {
109
202
  throw e;
110
203
  }
111
204
  }
112
205
  }
206
+
207
+ // Fallback: try recursive search through subfolders in the provided
208
+ // views directories. This enables Django-like behavior where a
209
+ // template may be placed in a nested folder and referenced by name.
210
+ const found = findTemplateInViews(parentName, viewsDirs);
211
+ if (found) {
212
+ const key = path.dirname(found) + '\0' + parentName;
213
+ const fileContent = fs.readFileSync(found, 'utf8');
214
+ return getParentSource(key, () => fileContent);
215
+ }
216
+
113
217
  throw new Error(`Template not found: '${parentName}' in directories ${JSON.stringify(viewsDirs)}`);
114
218
  }
115
219
 
@@ -135,6 +239,18 @@ async function renderASTAsync(nodes, context) {
135
239
 
136
240
  const fileContent = readParentSource(parentName, viewsDirs);
137
241
 
242
+ // If parent not found by direct resolution, try recursive search
243
+ // (search subfolders) using the new helper. This ensures extends
244
+ // can locate parent templates placed in nested directories.
245
+ if (!fileContent) {
246
+ const found = findTemplateInViews(parentName, viewsDirs);
247
+ if (found) {
248
+ const fileContent2 = fs.readFileSync(found, 'utf8');
249
+ const key = path.dirname(found) + '\0' + parentName;
250
+ return getParentSource(key, () => fileContent2);
251
+ }
252
+ }
253
+
138
254
  const parentTokens = tokenize(fileContent);
139
255
  const parentParser = new Parser(parentTokens, getTagRegistry());
140
256
  const parentNodes = parentParser.parse();
@@ -427,6 +543,14 @@ function renderPartialFromFile(fileName, partialName, contextObj, options) {
427
543
  }
428
544
  if (loaded) break;
429
545
  }
546
+ // Fallback: try recursive search if not loaded
547
+ if (!loaded) {
548
+ const found = findTemplateInViews(fileName, viewsDirs);
549
+ if (found) {
550
+ fileContent = fs.readFileSync(found, 'utf8');
551
+ loaded = true;
552
+ }
553
+ }
430
554
  if (!loaded) {
431
555
  throw new Error(
432
556
  `Template not found: '${fileName}' in directories ${JSON.stringify(viewsDirs)}`
@@ -770,11 +894,80 @@ function setupExpress(app, opts = {}) {
770
894
  // Install the raw engine so Express can use it
771
895
  app.engine(ext, async ? __expressAsync : __express);
772
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
+
773
949
  // Capture config in a closure so patchedRender can use it even
774
950
  // when called before the request handler runs.
775
951
  const configExt = ext;
776
952
  const configViews = opts.views;
777
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
+
778
971
  // Capture the original res.render so we can dispatch on #partial
779
972
  const originalRender = app.response.render;
780
973
  app.response.render = function patchedRender(view, locals, callback) {
@@ -827,6 +1020,21 @@ function setupExpress(app, opts = {}) {
827
1020
  break;
828
1021
  }
829
1022
  }
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.
1027
+ if (!filePath) {
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);
1036
+ if (found) filePath = found;
1037
+ }
830
1038
  if (!filePath) {
831
1039
  const err = new Error(
832
1040
  `Failed to lookup view "${view}" in views directory "${viewsDir}"`
@@ -855,11 +1063,55 @@ function setupExpress(app, opts = {}) {
855
1063
  }
856
1064
  }
857
1065
 
858
- // No partial selector: behave exactly like the original res.render
859
- if (cb) {
860
- 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;
861
1114
  }
862
- return originalRender.call(this, view, opts);
863
1115
  };
864
1116
  }
865
1117
 
@@ -910,6 +1162,22 @@ function expressPartialRenderer() {
910
1162
  filePath = null;
911
1163
  } catch { filePath = null; }
912
1164
  }
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.
1169
+ if (!filePath) {
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);
1179
+ if (found) filePath = found;
1180
+ }
913
1181
  if (!filePath) {
914
1182
  return res.status(404).send(
915
1183
  `Template not found: '${fileName}' in '${viewsDir}'`
@@ -933,6 +1201,10 @@ function expressPartialRenderer() {
933
1201
  }
934
1202
 
935
1203
  module.exports = {
1204
+ // Export the finder to allow unit tests to call it directly
1205
+ findTemplateInViews,
1206
+ setAppTemplateDirNames,
1207
+ getAppTemplateDirNames,
936
1208
  compile,
937
1209
  render,
938
1210
  asyncRender,
@@ -0,0 +1,19 @@
1
+ const test = require('node:test');
2
+ const assert = require('node:assert');
3
+ const path = require('path');
4
+ const fs = require('fs');
5
+ const { findTemplateInViews, setAppTemplateDirNames } = require('../src/index.js');
6
+
7
+ test('Finder honors custom app template dir names', () => {
8
+ const fixtures = path.join(__dirname, 'fixtures', 'views-appdirs');
9
+ // Create a small fixture structure
10
+ if (!fs.existsSync(path.join(fixtures, 'product', 'site'))){
11
+ fs.mkdirSync(path.join(fixtures, 'product', 'site'), { recursive: true });
12
+ }
13
+ const fname = path.join(fixtures, 'product', 'site', 'detail.html');
14
+ fs.writeFileSync(fname, '<p>detail</p>');
15
+
16
+ setAppTemplateDirNames(['site']);
17
+ const found = findTemplateInViews('detail', [fixtures]);
18
+ assert.ok(found && fs.existsSync(found));
19
+ });
@@ -0,0 +1,17 @@
1
+ const test = require('node:test');
2
+ const assert = require('node:assert');
3
+ const path = require('path');
4
+ const fs = require('fs');
5
+ const { findTemplateInViews } = require('../src/index.js');
6
+
7
+ test('Template finder - nested subfolder', () => {
8
+ const views = [path.join(__dirname, 'fixtures', 'views')];
9
+ const found = findTemplateInViews('nested/index', views);
10
+ assert.ok(found && fs.existsSync(found));
11
+ });
12
+
13
+ test('Template finder - bare name searches subfolders', () => {
14
+ const views = [path.join(__dirname, 'fixtures', 'views')];
15
+ const found = findTemplateInViews('deepfile', views);
16
+ assert.ok(found && fs.existsSync(found));
17
+ });
@@ -0,0 +1 @@
1
+ <h1>Nested Index</h1>
@@ -0,0 +1 @@
1
+ <p>Included: {{ item }}</p>
@@ -0,0 +1 @@
1
+ <div>Deep file</div>
@@ -0,0 +1 @@
1
+ <p>detail</p>
@@ -0,0 +1,13 @@
1
+ import assert from 'node:assert';
2
+ import path from 'node:path';
3
+ import { fileURLToPath } from 'node:url';
4
+ import test from 'node:test';
5
+ import { findTemplateInViews } from '../../src/esm.mjs';
6
+
7
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
8
+
9
+ test('ESM Template finder - nested index', () => {
10
+ const views = [path.join(__dirname, '..', 'fixtures', 'views')];
11
+ const found = findTemplateInViews('nested/index', views);
12
+ assert.ok(found && typeof found === 'string');
13
+ });