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.
- package/.github/release-notes/v1.3.7.md +20 -0
- package/API_REFERENCE.md +27 -0
- package/README.md +10 -0
- package/benchmarks/report.json +3 -3
- package/docs/api.md +3 -0
- package/ex.mjs +2 -1
- package/live-test/package-lock.json +4 -4
- package/live-test/package.json +1 -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/package.json +1 -1
- package/src/index.js +142 -8
|
@@ -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
|
package/benchmarks/report.json
CHANGED
|
@@ -1,17 +1,17 @@
|
|
|
1
1
|
[
|
|
2
2
|
{
|
|
3
3
|
"name": "small",
|
|
4
|
-
"syncAvgMs": "0.
|
|
4
|
+
"syncAvgMs": "0.01",
|
|
5
5
|
"asyncAvgMs": "0.03"
|
|
6
6
|
},
|
|
7
7
|
{
|
|
8
8
|
"name": "medium",
|
|
9
|
-
"syncAvgMs": "0.
|
|
9
|
+
"syncAvgMs": "0.01",
|
|
10
10
|
"asyncAvgMs": "0.01"
|
|
11
11
|
},
|
|
12
12
|
{
|
|
13
13
|
"name": "large",
|
|
14
|
-
"syncAvgMs": "0.
|
|
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": "
|
|
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.
|
|
519
|
-
"resolved": "
|
|
520
|
-
"integrity": "sha512-
|
|
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",
|
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'));
|
package/package.json
CHANGED
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) {
|