miki-template 1.3.6 → 1.3.7

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": "1.3.6"
8
+ }
9
+ }
@@ -0,0 +1,14 @@
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
+
8
+ miki.setupExpress(app, { extension: 'html', views });
9
+
10
+ app.get('/', (req, res) => {
11
+ res.render('index', { name: 'Live NPM' });
12
+ });
13
+
14
+ app.listen(3002, () => console.log('Live test app listening on 3002'));
@@ -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": "1.3.7",
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)}`
@@ -827,6 +951,11 @@ function setupExpress(app, opts = {}) {
827
951
  break;
828
952
  }
829
953
  }
954
+ // Fallback: recursive search for templates in subfolders
955
+ if (!filePath) {
956
+ const found = findTemplateInViews(fileName, [viewsDir]);
957
+ if (found) filePath = found;
958
+ }
830
959
  if (!filePath) {
831
960
  const err = new Error(
832
961
  `Failed to lookup view "${view}" in views directory "${viewsDir}"`
@@ -910,6 +1039,11 @@ function expressPartialRenderer() {
910
1039
  filePath = null;
911
1040
  } catch { filePath = null; }
912
1041
  }
1042
+ // Fallback: recursive search for templates in subfolders
1043
+ if (!filePath) {
1044
+ const found = findTemplateInViews(fileName, Array.isArray(viewsDir) ? viewsDir : [viewsDir]);
1045
+ if (found) filePath = found;
1046
+ }
913
1047
  if (!filePath) {
914
1048
  return res.status(404).send(
915
1049
  `Template not found: '${fileName}' in '${viewsDir}'`
@@ -933,6 +1067,10 @@ function expressPartialRenderer() {
933
1067
  }
934
1068
 
935
1069
  module.exports = {
1070
+ // Export the finder to allow unit tests to call it directly
1071
+ findTemplateInViews,
1072
+ setAppTemplateDirNames,
1073
+ getAppTemplateDirNames,
936
1074
  compile,
937
1075
  render,
938
1076
  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
+ });