miki-template 1.3.7 → 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,20 @@
1
+ ## v1.3.7 — Template discovery improvements
2
+
3
+ - Add recursive/app-style template discovery so projects can place
4
+ templates in nested `templates/` folders (Django-style) and have
5
+ them discovered automatically.
6
+ - `setupExpress` now expands `app.get('views')` to include nested
7
+ directories that contain template files so `res.render('name')`
8
+ works for templates located in project-level or package-level
9
+ `templates/` directories.
10
+ - Expose `findTemplateInViews(name, roots)` helper and
11
+ `setAppTemplateDirNames()/getAppTemplateDirNames()` to configure
12
+ app-style template folder names.
13
+ - Improve `res.render` fallback to use the recursive finder before
14
+ throwing Express's "Failed to lookup view" error.
15
+ - Update docs and API reference with usage examples and migration
16
+ notes.
17
+
18
+ CI: runs lint + tests (all passing locally). If you'd like a more
19
+ comprehensive changelog, I can expand this with links to issues and
20
+ code snippets.
package/API_REFERENCE.md CHANGED
@@ -107,6 +107,33 @@ app.get('/', (req, res) => {
107
107
  });
108
108
  ```
109
109
 
110
+ ### Template discovery helpers
111
+
112
+ `miki-template` exposes helpers to discover templates across multiple
113
+ `views` roots and to configure what directory names are considered
114
+ app-style template folders (e.g. `templates` or `app_templates`). These
115
+ are useful for projects that place templates in nested app folders or
116
+ package-level `templates/` directories.
117
+
118
+ #### `findTemplateInViews(name, viewsDirs)`
119
+
120
+ Search for a template by `name` across the provided `viewsDirs` array
121
+ or single string. Performs direct resolution first, then a recursive
122
+ search for bare filenames. Returns the absolute file path or `null`.
123
+
124
+ Example:
125
+ ```js
126
+ const found = require('miki-template').findTemplateInViews('detail', ['./views', './templates']);
127
+ ```
128
+
129
+ #### `setAppTemplateDirNames(names)` / `getAppTemplateDirNames()`
130
+
131
+ Configure and retrieve the directory names treated as app-style
132
+ template folders when scanning the project tree. The default is
133
+ `['templates']`. Use `setAppTemplateDirNames(['templates','app_templates'])`
134
+ to include additional conventions.
135
+
136
+
110
137
  ---
111
138
 
112
139
  ### `registerTag(name, parserFn)`
package/README.md CHANGED
@@ -132,6 +132,16 @@ app.listen(3000);
132
132
 
133
133
  > `setupExpress` calls `app.engine()`, `app.set('views')`, and `app.set('view engine')` for you, and patches `res.render` so `view#partial` is dispatched to the partial renderer (not the file system). It works equally well for `.miki` files — just pass `extension: 'miki'`.
134
134
 
135
+ Note on template discovery: `setupExpress` now expands the `app.get('views')`
136
+ value to include nested directories that contain template files. This
137
+ means templates placed in project-level `templates/`, package-level
138
+ `packages/*/templates/...`, or app-specific folders (e.g. `app_templates/`)
139
+ will be discovered automatically when calling `res.render('name')`.
140
+
141
+ If your project uses a different convention than `templates`, call
142
+ `setAppTemplateDirNames()` to customize the names that the engine
143
+ recognizes when scanning for app-style template folders.
144
+
135
145
  **The classic, fully manual setup still works** if you prefer it:
136
146
 
137
147
  ```javascript
@@ -1,17 +1,17 @@
1
1
  [
2
2
  {
3
3
  "name": "small",
4
- "syncAvgMs": "0.04",
4
+ "syncAvgMs": "0.01",
5
5
  "asyncAvgMs": "0.03"
6
6
  },
7
7
  {
8
8
  "name": "medium",
9
- "syncAvgMs": "0.00",
9
+ "syncAvgMs": "0.01",
10
10
  "asyncAvgMs": "0.01"
11
11
  },
12
12
  {
13
13
  "name": "large",
14
- "syncAvgMs": "0.00",
14
+ "syncAvgMs": "0.01",
15
15
  "asyncAvgMs": "0.01"
16
16
  }
17
17
  ]
package/docs/api.md CHANGED
@@ -12,6 +12,9 @@ This document lists the public API exported by **miki-template** for developers
12
12
  | `express(options?)` | `express(object?) → function` | Factory that returns a view-engine function suitable for `app.engine(...)`. Honors `view#partial` selectors. | `app.engine('html', miki.express());` |
13
13
  | `setupExpress(app, opts?)` | `setupExpress(expressApp, object?) → void` | **One-line Express integration.** Wires `app.engine(...)`, `app.set('views')`, and patches `res.render` so `res.render('view#partial', ...)` returns just that partial. Options: `{ extension?, views?, async? }`. | `miki.setupExpress(app, { extension: 'html', views: './views' });` |
14
14
  | `expressPartialRenderer()` | `expressPartialRenderer() → function` | Express middleware that adds `res.renderPartial(view, locals)`. Useful as a drop-in HTMX helper without the full `setupExpress` shim. | `app.use(miki.expressPartialRenderer());` |
15
+ | `findTemplateInViews(name, viewsDirs)` | `findTemplateInViews(string, string[]|string) → string|null` | Search for a template by name across one or more `views` roots. Performs direct resolution first (supports explicit paths and extensions), then a recursive search for bare filenames in subdirectories. Returns the absolute file path or `null` if not found. | `miki.findTemplateInViews('detail', ['./views', './templates'])` |
16
+ | `setAppTemplateDirNames(names)` | `setAppTemplateDirNames(string[]|string) → void` | Configure which directory names are treated as app-style template folders when scanning (default: `['templates']`). Useful when projects use a different convention. | `miki.setAppTemplateDirNames(['templates','app_templates'])` |
17
+ | `getAppTemplateDirNames()` | `getAppTemplateDirNames() → string[]` | Retrieve the current configured app-template directory names. | `const names = miki.getAppTemplateDirNames()` |
15
18
  | `renderPartialFromFile(filePath, partialName, context?, options?)` | `renderPartialFromFile(string, string, object?, object?) → string` | Load a file from disk and render only the named `{% partialdef %}`. | `miki.renderPartialFromFile('views/home.html', 'card', { user });` |
16
19
  | `renderPartialFromSource(source, partialName, context?, options?)` | `renderPartialFromSource(string, string, object?, object?) → string` | Render a single named partial directly from a template string. Walks the AST (and `extends` chain) to discover partials nested inside blocks. | `miki.renderPartialFromSource(src, 'card', ctx, { views });` |
17
20
  | `stripExpressContext(options)` | `stripExpressContext(object) → object` | Remove Express framework keys (`_locals`, `settings`, `cache`) from an options object. | `const ctx = stripExpressContext(res.locals);` |
package/ex.mjs CHANGED
@@ -26,10 +26,11 @@ const dir=path.join(process.cwd(),"dir")
26
26
  {name:"miki", email:"jack@miki.com",address:"kumba"},
27
27
  {name:"luis",email:"luis@miki.com",address:"kumba"}
28
28
  ]
29
- res.render("index",{name:"miki-template context", users:users, data:data})
29
+ res.render("index#card",{name:"miki-template context", users:users, data:data})
30
30
  // res.send(content)
31
31
  })
32
32
 
33
+
33
34
  app.listen(3000, () => {
34
35
  console.log('Server is running on port 3000 click: http://localhost:3000')
35
36
  } )
@@ -9,7 +9,7 @@
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
13
  }
14
14
  },
15
15
  "node_modules/accepts": {
@@ -515,9 +515,9 @@
515
515
  }
516
516
  },
517
517
  "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==",
518
+ "version": "1.3.7",
519
+ "resolved": "file:../miki-template-2.0.0.tgz",
520
+ "integrity": "sha512-FGTWjPS3CTm3X4+dnRKqHwqQUKgqtz/0VNTjN1BQjBcgwhXnEbVYg06VZmBtZ8SYr6z12JIX3LGj2pQog3h84Q==",
521
521
  "license": "MIT",
522
522
  "dependencies": {
523
523
  "date-fns": "^3.6.0",
@@ -4,6 +4,6 @@
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
8
  }
9
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>
@@ -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>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "miki-template",
3
- "version": "1.3.7",
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": {
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) {