@nci-gis/js-tmpl 0.0.1-beta.2 → 0.1.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/README.md +78 -126
- package/package.json +36 -3
- package/src/cli/args.js +37 -16
- package/src/cli/main.js +24 -11
- package/src/cli/usage.js +17 -0
- package/src/config/defaults.js +8 -6
- package/src/config/loader.js +32 -18
- package/src/config/resolver.js +59 -53
- package/src/config/valuePartials.js +114 -0
- package/src/config/view.js +123 -3
- package/src/engine/contentRenderer.js +24 -7
- package/src/engine/partials.js +81 -33
- package/src/engine/pathFormula.js +74 -0
- package/src/engine/pathRenderer.js +62 -11
- package/src/engine/pathSegment.js +49 -0
- package/src/engine/renderDirectory.js +17 -11
- package/src/engine/treeWalker.js +43 -11
- package/src/index.js +2 -2
- package/src/types.js +30 -0
- package/src/utils/fs.js +5 -4
- package/src/utils/namespacing.js +92 -0
- package/src/utils/object.js +9 -4
package/README.md
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
# js-tmpl
|
|
2
2
|
|
|
3
3
|
> A lightweight, deterministic file templating engine built on Handlebars.
|
|
4
|
+
>
|
|
4
5
|
> An explicit file templating engine for developers who care about **control, predictability, and composability**.
|
|
5
6
|
|
|
6
7
|
[](https://www.npmjs.com/package/@nci-gis/js-tmpl)
|
|
@@ -44,6 +45,7 @@ Most templating tools fail in one of two ways:
|
|
|
44
45
|
js-tmpl sits intentionally in between.
|
|
45
46
|
|
|
46
47
|
See [Motivation](docs/Motivation.md) - The full story.
|
|
48
|
+
|
|
47
49
|
See [Design Principles](docs/PRINCIPLES.md) - Core philosophy guiding all decisions.
|
|
48
50
|
|
|
49
51
|
## Features
|
|
@@ -76,10 +78,11 @@ js-tmpl will search for a project config file in **exactly these locations**, in
|
|
|
76
78
|
|
|
77
79
|
Everything else must be **explicitly specified**:
|
|
78
80
|
|
|
79
|
-
- ✅ **Values file**
|
|
80
|
-
- ✅ **
|
|
81
|
-
- ✅ **
|
|
82
|
-
- ✅ **
|
|
81
|
+
- ✅ **Values file** — Optional via `--values` flag or `valuesFile` config (VP-8)
|
|
82
|
+
- ✅ **Values directory** — Optional via `--values-dir` flag or `valuesDir` config (VP-6)
|
|
83
|
+
- ✅ **Template directory** — Must be in config or defaults to `templates/`
|
|
84
|
+
- ✅ **Output directory** — Must be in config or defaults to `dist/`
|
|
85
|
+
- ✅ **Partials directory** — Must be in config; not loaded if omitted
|
|
83
86
|
|
|
84
87
|
### Override Auto-Discovery
|
|
85
88
|
|
|
@@ -137,14 +140,9 @@ templates/
|
|
|
137
140
|
**Template content** (`templates/${project.name}/config.json.hbs`):
|
|
138
141
|
|
|
139
142
|
```handlebars
|
|
140
|
-
{
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
"server": {
|
|
144
|
-
"port": {{config.port}},
|
|
145
|
-
"host": "{{config.host}}"
|
|
146
|
-
}
|
|
147
|
-
}
|
|
143
|
+
{ "name": "{{project.name}}", "version": "{{project.version}}", "server": {
|
|
144
|
+
"port":
|
|
145
|
+
{{config.port}}, "host": "{{config.host}}" } }
|
|
148
146
|
```
|
|
149
147
|
|
|
150
148
|
### 3. Render templates
|
|
@@ -163,7 +161,7 @@ import { resolveConfig, renderDirectory } from '@nci-gis/js-tmpl';
|
|
|
163
161
|
const config = resolveConfig({
|
|
164
162
|
valuesFile: './values.yaml',
|
|
165
163
|
templateDir: './templates',
|
|
166
|
-
outDir: './dist'
|
|
164
|
+
outDir: './dist',
|
|
167
165
|
});
|
|
168
166
|
|
|
169
167
|
await renderDirectory(config);
|
|
@@ -190,14 +188,11 @@ CLI arguments
|
|
|
190
188
|
|
|
191
189
|
### View Model
|
|
192
190
|
|
|
193
|
-
Templates receive a view object
|
|
191
|
+
Templates receive a view object containing your values data plus an `env` object with allowlisted environment variables. The `env` key is reserved — if your values file contains a top-level `env` key, a warning is logged and it is overwritten.
|
|
194
192
|
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
env: process.env // Environment variables
|
|
199
|
-
}
|
|
200
|
-
```
|
|
193
|
+
Use `envKeys` and `envPrefix` in your config file or via CLI (`--env-keys`, `--env-prefix`) to control which environment variables are exposed. Without either, `env` is an empty object `{}`.
|
|
194
|
+
|
|
195
|
+
See [docs/API.md](docs/API.md#view-object) for full details, examples, and recommended conventions.
|
|
201
196
|
|
|
202
197
|
Access in templates:
|
|
203
198
|
|
|
@@ -218,23 +213,44 @@ templates/
|
|
|
218
213
|
→ dist/production/config-my-app.yaml
|
|
219
214
|
```
|
|
220
215
|
|
|
216
|
+
Use `$if{var}` / `$ifn{var}` as whole directory segments to conditionally
|
|
217
|
+
include or skip files based on view data:
|
|
218
|
+
|
|
219
|
+
```text
|
|
220
|
+
templates/
|
|
221
|
+
├── common.yaml.hbs
|
|
222
|
+
├── $if{prod}/
|
|
223
|
+
│ └── alerts.yaml.hbs → written only when view.prod is truthy
|
|
224
|
+
└── $ifn{prod}/
|
|
225
|
+
└── debug-panel.yaml.hbs → written only when view.prod is falsy
|
|
226
|
+
```
|
|
227
|
+
|
|
228
|
+
Guards are directory-only, whole-segment, and throw loudly on missing
|
|
229
|
+
variables. See [API docs](docs/API.md#path-guards--conditional-files) for
|
|
230
|
+
the full semantics and rejected variants.
|
|
231
|
+
|
|
221
232
|
### Partial System
|
|
222
233
|
|
|
223
|
-
|
|
234
|
+
Each render pass uses an isolated Handlebars instance. Directory structure maps to partial names:
|
|
224
235
|
|
|
225
236
|
```text
|
|
226
237
|
templates.partials/
|
|
227
|
-
|
|
238
|
+
├── header.hbs → {{> header}}
|
|
239
|
+
├── components/
|
|
240
|
+
│ ├── button.hbs → {{> components.button}}
|
|
241
|
+
│ └── forms/
|
|
242
|
+
│ └── login.hbs → {{> components.forms.login}}
|
|
228
243
|
```
|
|
229
244
|
|
|
230
|
-
|
|
245
|
+
**`@` directories** flatten their contents (filename only, no namespace):
|
|
231
246
|
|
|
232
247
|
```text
|
|
233
|
-
|
|
234
|
-
└──
|
|
235
|
-
└── metadata.hbs → {{> common.metadata}}
|
|
248
|
+
├── @helpers/
|
|
249
|
+
│ └── date.hbs → {{> date}}
|
|
236
250
|
```
|
|
237
251
|
|
|
252
|
+
Duplicate partial names throw an error. Names must be alphanumeric + underscore only. See [API docs](docs/API.md#partial-system) for details.
|
|
253
|
+
|
|
238
254
|
## Mental Model
|
|
239
255
|
|
|
240
256
|
> ⚠️ Design note
|
|
@@ -243,7 +259,7 @@ templates.partials/
|
|
|
243
259
|
Think of js-tmpl as a function:
|
|
244
260
|
|
|
245
261
|
```text
|
|
246
|
-
(
|
|
262
|
+
f(config, values/view, input templates) → files (output)
|
|
247
263
|
```
|
|
248
264
|
|
|
249
265
|
There is no hidden state, no lifecycle, and no side effects.
|
|
@@ -257,14 +273,22 @@ js-tmpl render [options]
|
|
|
257
273
|
|
|
258
274
|
### Options
|
|
259
275
|
|
|
260
|
-
| Option | Description
|
|
261
|
-
| ------------------------ |
|
|
262
|
-
| `-c, --values FILE` | Values file (
|
|
263
|
-
|
|
|
264
|
-
| `-
|
|
265
|
-
| `-
|
|
266
|
-
| `-
|
|
267
|
-
|
|
|
276
|
+
| Option | Description | Default |
|
|
277
|
+
| ------------------------ | ---------------------------------------- | --------------- |
|
|
278
|
+
| `-c, --values FILE` | Values file (`.yaml` / `.yml` / `.json`) | Optional |
|
|
279
|
+
| `--values-dir DIR` | Value-partials root (namespaced by path) | Optional |
|
|
280
|
+
| `-t, --template-dir DIR` | Template directory | `templates` |
|
|
281
|
+
| `-o, --out DIR` | Output directory | `dist` |
|
|
282
|
+
| `-p, --partials-dir DIR` | Partials directory | None (skipped) |
|
|
283
|
+
| `-x, --ext EXT` | Template extension | `.hbs` |
|
|
284
|
+
| `--config-file FILE` | Explicit config file | Auto-discovered |
|
|
285
|
+
| `--env-keys KEYS` | Comma-separated env var names to expose | None |
|
|
286
|
+
| `--env-prefix PREFIX` | Auto-include env vars with this prefix | None |
|
|
287
|
+
|
|
288
|
+
Both `--values` and `--values-dir` are optional (VP-8, VP-6). If neither is
|
|
289
|
+
supplied, `view` is `{ env: {...} }` only. Missing `{{var}}` in a template
|
|
290
|
+
throws with the template's relative path and variable name (VP-9, strict
|
|
291
|
+
mode).
|
|
268
292
|
|
|
269
293
|
### Examples of Usage
|
|
270
294
|
|
|
@@ -278,111 +302,35 @@ js-tmpl render \
|
|
|
278
302
|
--template-dir ./my-templates \
|
|
279
303
|
--out ./output
|
|
280
304
|
|
|
281
|
-
# Multi-environment
|
|
282
|
-
NODE_ENV=production js-tmpl render --values prod-values.yaml
|
|
305
|
+
# Multi-environment (allowlist NODE_ENV to use it in templates)
|
|
306
|
+
NODE_ENV=production js-tmpl render --values prod-values.yaml --env-keys NODE_ENV
|
|
283
307
|
```
|
|
284
308
|
|
|
285
309
|
## Programmatic API
|
|
286
310
|
|
|
287
|
-
See [docs/API.md](docs/API.md) for
|
|
288
|
-
|
|
289
|
-
### Import
|
|
290
|
-
|
|
291
|
-
```javascript
|
|
292
|
-
import { resolveConfig, renderDirectory } from '@nci-gis/js-tmpl';
|
|
293
|
-
```
|
|
294
|
-
|
|
295
|
-
### resolveConfig(options)
|
|
296
|
-
|
|
297
|
-
Resolves configuration with proper precedence.
|
|
298
|
-
|
|
299
|
-
**Parameters:**
|
|
300
|
-
|
|
301
|
-
- `options.valuesFile` (string, required) - Path to values file
|
|
302
|
-
- `options.templateDir` (string) - Template directory path
|
|
303
|
-
- `options.partialsDir` (string) - Partials directory path
|
|
304
|
-
- `options.outDir` (string) - Output directory path
|
|
305
|
-
- `options.extname` (string) - Template file extension
|
|
306
|
-
- `options.configFile` (string) - Explicit config file path
|
|
307
|
-
|
|
308
|
-
**Returns:** Resolved configuration object
|
|
309
|
-
|
|
310
|
-
### renderDirectory(config)
|
|
311
|
-
|
|
312
|
-
Executes the rendering process.
|
|
313
|
-
|
|
314
|
-
**Parameters:**
|
|
315
|
-
|
|
316
|
-
- `config` (object) - Configuration from `resolveConfig`
|
|
317
|
-
|
|
318
|
-
**Returns:** Promise that resolves when rendering completes
|
|
319
|
-
|
|
320
|
-
### Example
|
|
321
|
-
|
|
322
|
-
```javascript
|
|
323
|
-
import { resolveConfig, renderDirectory } from '@nci-gis/js-tmpl';
|
|
324
|
-
|
|
325
|
-
const config = resolveConfig({
|
|
326
|
-
valuesFile: './values.yaml',
|
|
327
|
-
templateDir: './templates',
|
|
328
|
-
partialsDir: './partials',
|
|
329
|
-
outDir: './dist'
|
|
330
|
-
});
|
|
331
|
-
|
|
332
|
-
await renderDirectory(config);
|
|
333
|
-
console.log('✅ Rendering complete');
|
|
334
|
-
```
|
|
335
|
-
|
|
336
|
-
## Project Configuration
|
|
337
|
-
|
|
338
|
-
Create `js-tmpl.config.yaml` in your project root:
|
|
339
|
-
|
|
340
|
-
```yaml
|
|
341
|
-
templateDir: templates
|
|
342
|
-
partialsDir: templates.partials
|
|
343
|
-
outDir: dist
|
|
344
|
-
extname: .hbs
|
|
345
|
-
```
|
|
346
|
-
|
|
347
|
-
Auto-discovered config files (in order):
|
|
348
|
-
|
|
349
|
-
1. `js-tmpl.config.yaml`
|
|
350
|
-
2. `js-tmpl.config.yml`
|
|
351
|
-
3. `js-tmpl.config.json`
|
|
352
|
-
4. `config/js-tmpl.yaml`
|
|
353
|
-
5. `config/js-tmpl.json`
|
|
311
|
+
See [docs/API.md](docs/API.md) for the complete API reference — parameters, return types, config file format, and advanced usage.
|
|
354
312
|
|
|
355
313
|
## Examples
|
|
356
314
|
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
-
|
|
361
|
-
|
|
362
|
-
-
|
|
315
|
+
- [examples/yaml-templates/](examples/yaml-templates/) — complete walkthrough:
|
|
316
|
+
dynamic paths with `${env.NODE_ENV}`, Handlebars features (loops,
|
|
317
|
+
conditionals), root and namespaced partials, multi-format output.
|
|
318
|
+
- [examples/path-guards/](examples/path-guards/) — conditional files via
|
|
319
|
+
`$if{var}` / `$ifn{var}` whole-segment path guards.
|
|
320
|
+
- [examples/value-partials/](examples/value-partials/) — composing `view`
|
|
321
|
+
from multiple structured files via `--values-dir` (directory-as-namespace,
|
|
322
|
+
no merge, `@`-flatten escape).
|
|
363
323
|
|
|
364
324
|
## Testing
|
|
365
325
|
|
|
366
|
-
This project has comprehensive test coverage
|
|
367
|
-
|
|
368
|
-
- 156 tests
|
|
369
|
-
- 99.8% line coverage
|
|
370
|
-
- 99.7% branch coverage
|
|
326
|
+
This project has comprehensive automated test coverage across unit and integration suites.
|
|
327
|
+
Current coverage remains above 99% line coverage with high branch coverage as well.
|
|
371
328
|
|
|
372
329
|
See [tests/README.md](tests/README.md) for testing documentation.
|
|
373
330
|
|
|
374
331
|
## Development Principles
|
|
375
332
|
|
|
376
|
-
js-tmpl follows
|
|
377
|
-
|
|
378
|
-
1. **Engine First, CLI Second** - Programmatic API is primary
|
|
379
|
-
2. **Explicit Over Implicit** - No magic or hidden conventions
|
|
380
|
-
3. **Deterministic Over Clever** - Predictable behavior
|
|
381
|
-
4. **Separation of Concerns** - Each layer has one responsibility
|
|
382
|
-
5. **Composable Over Monolithic** - Small, focused functions
|
|
383
|
-
6. **Simple Over Feature-Rich** - Minimal API surface
|
|
384
|
-
|
|
385
|
-
See [docs/PRINCIPLES.md](docs/PRINCIPLES.md) for details.
|
|
333
|
+
js-tmpl follows six core design principles — engine-first, explicit, deterministic, separated, composable, and simple. See [docs/PRINCIPLES.md](docs/PRINCIPLES.md) for the full philosophy.
|
|
386
334
|
|
|
387
335
|
## Roadmap
|
|
388
336
|
|
|
@@ -414,7 +362,7 @@ For security concerns, see [SECURITY.md](SECURITY.md).
|
|
|
414
362
|
|
|
415
363
|
## License
|
|
416
364
|
|
|
417
|
-
|
|
365
|
+
See [LICENSE](LICENSE).
|
|
418
366
|
|
|
419
367
|
## Learn More
|
|
420
368
|
|
|
@@ -431,3 +379,7 @@ MIT © pasxd245
|
|
|
431
379
|
- [Examples](examples/) - Working examples and templates
|
|
432
380
|
- [Issue Tracker](https://github.com/nci-gis/js-tmpl/issues) - Report bugs or request features
|
|
433
381
|
- [NPM Package](https://www.npmjs.com/package/@nci-gis/js-tmpl) - Package registry
|
|
382
|
+
|
|
383
|
+
## Transparency
|
|
384
|
+
|
|
385
|
+
AI-assisted development (e.g., Claude Code, Copilot) was used for scaffolding and iteration.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nci-gis/js-tmpl",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.1.0",
|
|
4
4
|
"description": "The pure JavaScript templating engine that uses handlebars.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -12,16 +12,26 @@
|
|
|
12
12
|
}
|
|
13
13
|
},
|
|
14
14
|
"scripts": {
|
|
15
|
+
"prepare": "husky",
|
|
15
16
|
"test": "node --test $(find tests -name '*.test.js')",
|
|
16
17
|
"test:watch": "node --test --watch $(find tests -name '*.test.js')",
|
|
17
18
|
"test:coverage": "node --experimental-test-coverage --test $(find tests -name '*.test.js')",
|
|
19
|
+
"format": "pnpm format:code && pnpm format:md",
|
|
20
|
+
"format:check": "pnpm format:code:check && pnpm format:md:check",
|
|
21
|
+
"format:code": "prettier --write src/ tests/",
|
|
22
|
+
"format:code:check": "prettier --check src/ tests/",
|
|
23
|
+
"format:md": "prettier --write \"*.md\" \"docs/**/*.md\" \"tests/**/*.md\" \"examples/**/*.md\"",
|
|
24
|
+
"format:md:check": "prettier --check \"*.md\" \"docs/**/*.md\" \"tests/**/*.md\" \"examples/**/*.md\"",
|
|
18
25
|
"lint": "eslint src/ tests/",
|
|
19
26
|
"lint:fix": "eslint src/ tests/ --fix",
|
|
20
27
|
"build": "echo 'No build step required for pure JS library'",
|
|
21
28
|
"start": "node src/cli/main.js",
|
|
22
29
|
"help": "node src/cli/main.js --help",
|
|
23
30
|
"dev": "node src/cli/main.js",
|
|
24
|
-
"tool": "node src/cli/main.js"
|
|
31
|
+
"tool": "node src/cli/main.js",
|
|
32
|
+
"docs:check-links": "remark --frail --quiet *.md docs/*.md tests/README.md examples/**/*.md",
|
|
33
|
+
"docs:check-exports": "node scripts/check-doc-exports.js",
|
|
34
|
+
"docs:check": "pnpm docs:check-links && pnpm docs:check-exports"
|
|
25
35
|
},
|
|
26
36
|
"keywords": [
|
|
27
37
|
"js",
|
|
@@ -56,8 +66,31 @@
|
|
|
56
66
|
"js-yaml": "^4.1.1"
|
|
57
67
|
},
|
|
58
68
|
"devDependencies": {
|
|
69
|
+
"@commitlint/cli": "^20.5.0",
|
|
70
|
+
"@commitlint/config-conventional": "^20.5.0",
|
|
59
71
|
"@eslint/js": "^9.39.2",
|
|
72
|
+
"@types/js-yaml": "^4.0.9",
|
|
73
|
+
"@types/node": "^25.5.2",
|
|
60
74
|
"eslint": "^9.39.2",
|
|
61
|
-
"eslint-plugin-simple-import-sort": "^12.1.1"
|
|
75
|
+
"eslint-plugin-simple-import-sort": "^12.1.1",
|
|
76
|
+
"husky": "^9.1.7",
|
|
77
|
+
"lint-staged": "^16.4.0",
|
|
78
|
+
"prettier": "^3.8.1",
|
|
79
|
+
"remark-cli": "^12.0.1",
|
|
80
|
+
"remark-validate-links": "^13.1.0",
|
|
81
|
+
"typescript": "^6.0.2"
|
|
82
|
+
},
|
|
83
|
+
"lint-staged": {
|
|
84
|
+
"src/**/*.js": [
|
|
85
|
+
"prettier --write",
|
|
86
|
+
"eslint --fix"
|
|
87
|
+
],
|
|
88
|
+
"tests/**/*.js": [
|
|
89
|
+
"prettier --write",
|
|
90
|
+
"eslint --fix"
|
|
91
|
+
],
|
|
92
|
+
"**/*.md": [
|
|
93
|
+
"prettier --write"
|
|
94
|
+
]
|
|
62
95
|
}
|
|
63
96
|
}
|
package/src/cli/args.js
CHANGED
|
@@ -1,49 +1,70 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Parse CLI arguments.
|
|
3
|
-
*
|
|
3
|
+
*
|
|
4
4
|
* @param {string[]} args
|
|
5
|
-
* @returns {
|
|
5
|
+
* @returns {import('../types.js').CliArgs}
|
|
6
6
|
*/
|
|
7
7
|
export function parseArgs(args) {
|
|
8
|
-
|
|
8
|
+
/** @type {import('../types.js').CliArgs} */
|
|
9
|
+
const opts = { command: 'render' };
|
|
9
10
|
|
|
10
11
|
let i = 0;
|
|
11
12
|
while (i < args.length) {
|
|
12
13
|
const a = args[i];
|
|
13
14
|
|
|
14
15
|
switch (a) {
|
|
15
|
-
case
|
|
16
|
-
|
|
16
|
+
case '-h':
|
|
17
|
+
case '--help':
|
|
18
|
+
opts.command = 'help';
|
|
17
19
|
break;
|
|
18
20
|
|
|
19
|
-
case
|
|
20
|
-
|
|
21
|
+
case 'render':
|
|
22
|
+
opts.command = 'render';
|
|
23
|
+
break;
|
|
24
|
+
|
|
25
|
+
case '-t':
|
|
26
|
+
case '--template-dir':
|
|
21
27
|
opts.templateDir = args[++i];
|
|
22
28
|
break;
|
|
23
29
|
|
|
24
|
-
case
|
|
25
|
-
case
|
|
30
|
+
case '-c':
|
|
31
|
+
case '--values':
|
|
26
32
|
opts.valuesFile = args[++i];
|
|
27
33
|
break;
|
|
28
34
|
|
|
29
|
-
case
|
|
30
|
-
|
|
35
|
+
case '--values-dir':
|
|
36
|
+
opts.valuesDir = args[++i];
|
|
37
|
+
break;
|
|
38
|
+
|
|
39
|
+
case '-o':
|
|
40
|
+
case '--out':
|
|
31
41
|
opts.outDir = args[++i];
|
|
32
42
|
break;
|
|
33
43
|
|
|
34
|
-
case
|
|
35
|
-
case
|
|
44
|
+
case '-p':
|
|
45
|
+
case '--partials-dir':
|
|
36
46
|
opts.partialsDir = args[++i];
|
|
37
47
|
break;
|
|
38
48
|
|
|
39
|
-
case
|
|
49
|
+
case '--config-file':
|
|
40
50
|
opts.configFile = args[++i];
|
|
41
51
|
break;
|
|
42
52
|
|
|
43
|
-
case
|
|
44
|
-
case
|
|
53
|
+
case '-x':
|
|
54
|
+
case '--ext':
|
|
45
55
|
opts.extname = args[++i];
|
|
46
56
|
break;
|
|
57
|
+
|
|
58
|
+
case '--env-keys':
|
|
59
|
+
opts.envKeys = args[++i]
|
|
60
|
+
.split(',')
|
|
61
|
+
.map((s) => s.trim())
|
|
62
|
+
.filter(Boolean);
|
|
63
|
+
break;
|
|
64
|
+
|
|
65
|
+
case '--env-prefix':
|
|
66
|
+
opts.envPrefix = args[++i];
|
|
67
|
+
break;
|
|
47
68
|
}
|
|
48
69
|
// next argument:
|
|
49
70
|
i++;
|
package/src/cli/main.js
CHANGED
|
@@ -1,14 +1,21 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
|
-
import { resolveConfig } from
|
|
4
|
-
import { renderDirectory } from
|
|
5
|
-
import { parseArgs } from
|
|
3
|
+
import { resolveConfig } from '../config/resolver.js';
|
|
4
|
+
import { renderDirectory } from '../engine/renderDirectory.js';
|
|
5
|
+
import { parseArgs } from './args.js';
|
|
6
|
+
import { USAGE } from './usage.js';
|
|
6
7
|
|
|
8
|
+
/** @returns {Promise<void>} */
|
|
7
9
|
export async function main() {
|
|
8
10
|
const cli = parseArgs(process.argv);
|
|
9
11
|
|
|
10
|
-
if (cli.command
|
|
11
|
-
console.
|
|
12
|
+
if (cli.command === 'help') {
|
|
13
|
+
console.log(USAGE);
|
|
14
|
+
return;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
if (cli.command !== 'render') {
|
|
18
|
+
console.error('Unknown command:', cli.command);
|
|
12
19
|
process.exit(1);
|
|
13
20
|
}
|
|
14
21
|
|
|
@@ -16,13 +23,19 @@ export async function main() {
|
|
|
16
23
|
|
|
17
24
|
await renderDirectory(cfg);
|
|
18
25
|
|
|
19
|
-
console.log(
|
|
26
|
+
console.log('✔ js-tmpl completed.');
|
|
20
27
|
}
|
|
21
28
|
|
|
22
29
|
// Execute main function if this file is run directly
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
30
|
+
const isDirectRun =
|
|
31
|
+
process.argv[1] &&
|
|
32
|
+
import.meta.url.endsWith(process.argv[1].replaceAll('\\', '/'));
|
|
33
|
+
|
|
34
|
+
if (isDirectRun) {
|
|
35
|
+
try {
|
|
36
|
+
await main();
|
|
37
|
+
} catch (error) {
|
|
38
|
+
console.error('Error:', error);
|
|
39
|
+
process.exit(1);
|
|
40
|
+
}
|
|
28
41
|
}
|
package/src/cli/usage.js
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/** CLI usage text. */
|
|
2
|
+
export const USAGE = `Usage: js-tmpl [command] [options]
|
|
3
|
+
|
|
4
|
+
Commands:
|
|
5
|
+
render Render templates (default)
|
|
6
|
+
|
|
7
|
+
Options:
|
|
8
|
+
-t, --template-dir <dir> Template directory (default: templates)
|
|
9
|
+
-c, --values <file> Values file (.yaml / .yml / .json) — optional
|
|
10
|
+
--values-dir <dir> Value-partials root, namespaced into view by path
|
|
11
|
+
-o, --out <dir> Output directory (default: dist)
|
|
12
|
+
-p, --partials-dir <dir> Partials directory
|
|
13
|
+
-x, --ext <ext> Template extension (default: .hbs)
|
|
14
|
+
--config-file <file> Project config file
|
|
15
|
+
--env-keys <keys> Comma-separated env var names to expose (default: none)
|
|
16
|
+
--env-prefix <prefix> Auto-include env vars with this prefix (e.g. JS_TMPL_)
|
|
17
|
+
-h, --help Show this help message`;
|
package/src/config/defaults.js
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
export const DEFAULTS = {
|
|
2
|
-
templateDir:
|
|
3
|
-
partialsDir:
|
|
4
|
-
valuesDir:
|
|
5
|
-
valuesFile:
|
|
6
|
-
outDir:
|
|
7
|
-
extname:
|
|
2
|
+
templateDir: 'templates',
|
|
3
|
+
partialsDir: '',
|
|
4
|
+
valuesDir: '',
|
|
5
|
+
valuesFile: '',
|
|
6
|
+
outDir: 'dist',
|
|
7
|
+
extname: '.hbs',
|
|
8
|
+
envKeys: [],
|
|
9
|
+
envPrefix: '',
|
|
8
10
|
};
|
package/src/config/loader.js
CHANGED
|
@@ -1,30 +1,30 @@
|
|
|
1
|
-
import fs from
|
|
2
|
-
import path from
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
3
|
|
|
4
|
-
import YAML from
|
|
4
|
+
import YAML from 'js-yaml';
|
|
5
5
|
|
|
6
6
|
/**
|
|
7
7
|
* Load YAML or JSON values.
|
|
8
8
|
*
|
|
9
9
|
* @param {string} filePath Path to YAML or JSON file.
|
|
10
|
-
* @returns {
|
|
10
|
+
* @returns {Record<string, unknown>} Parsed values.
|
|
11
11
|
*/
|
|
12
12
|
export function loadYamlOrJson(filePath) {
|
|
13
13
|
// Check if file exists before attempting to read
|
|
14
14
|
if (!fs.existsSync(filePath)) {
|
|
15
15
|
throw new Error(
|
|
16
16
|
`Values file not found: ${filePath}\n` +
|
|
17
|
-
|
|
17
|
+
'Check that the file exists and the path is correct.',
|
|
18
18
|
);
|
|
19
19
|
}
|
|
20
20
|
|
|
21
|
-
const raw = fs.readFileSync(filePath,
|
|
21
|
+
const raw = fs.readFileSync(filePath, 'utf8');
|
|
22
22
|
|
|
23
23
|
if (/\.ya?ml$/i.test(filePath)) {
|
|
24
|
-
return YAML.load(raw) || {};
|
|
24
|
+
return /** @type {Record<string, unknown>} */ (YAML.load(raw) || {});
|
|
25
25
|
}
|
|
26
26
|
if (/\.json$/i.test(filePath)) {
|
|
27
|
-
return JSON.parse(raw);
|
|
27
|
+
return /** @type {Record<string, unknown>} */ (JSON.parse(raw));
|
|
28
28
|
}
|
|
29
29
|
|
|
30
30
|
throw new Error(`Unsupported values file: ${filePath}`);
|
|
@@ -35,31 +35,45 @@ export function loadYamlOrJson(filePath) {
|
|
|
35
35
|
*
|
|
36
36
|
* @param {string} cwd Current working directory.
|
|
37
37
|
* @param {string} [explicitFile] Explicit config file path.
|
|
38
|
-
* @returns {
|
|
38
|
+
* @returns {Record<string, unknown> | null} Parsed config object or null if not found.
|
|
39
39
|
*/
|
|
40
40
|
export function loadProjectConfig(cwd, explicitFile) {
|
|
41
|
+
if (explicitFile) {
|
|
42
|
+
const abs = path.isAbsolute(explicitFile)
|
|
43
|
+
? explicitFile
|
|
44
|
+
: path.join(cwd, explicitFile);
|
|
45
|
+
if (!fs.existsSync(abs)) {
|
|
46
|
+
throw new Error(
|
|
47
|
+
`Config file not found: ${abs}\n` +
|
|
48
|
+
'The --config-file path was explicitly provided but does not exist.',
|
|
49
|
+
);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
41
53
|
const candidates = explicitFile
|
|
42
54
|
? [explicitFile]
|
|
43
55
|
: [
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
path.join(
|
|
48
|
-
path.join(
|
|
56
|
+
'js-tmpl.config.yaml',
|
|
57
|
+
'js-tmpl.config.yml',
|
|
58
|
+
'js-tmpl.config.json',
|
|
59
|
+
path.join('config', 'js-tmpl.yaml'),
|
|
60
|
+
path.join('config', 'js-tmpl.json'),
|
|
49
61
|
];
|
|
50
62
|
|
|
51
63
|
for (const rel of candidates) {
|
|
52
64
|
const abs = path.isAbsolute(rel) ? rel : path.join(cwd, rel);
|
|
53
|
-
if (!fs.existsSync(abs)) {
|
|
65
|
+
if (!fs.existsSync(abs)) {
|
|
66
|
+
continue;
|
|
67
|
+
}
|
|
54
68
|
|
|
55
|
-
const raw = fs.readFileSync(abs,
|
|
69
|
+
const raw = fs.readFileSync(abs, 'utf8');
|
|
56
70
|
|
|
57
71
|
if (/\.ya?ml$/i.test(abs)) {
|
|
58
|
-
return YAML.load(raw) || {};
|
|
72
|
+
return /** @type {Record<string, unknown>} */ (YAML.load(raw) || {});
|
|
59
73
|
}
|
|
60
74
|
|
|
61
75
|
if (/\.json$/i.test(abs)) {
|
|
62
|
-
return JSON.parse(raw);
|
|
76
|
+
return /** @type {Record<string, unknown>} */ (JSON.parse(raw));
|
|
63
77
|
}
|
|
64
78
|
}
|
|
65
79
|
|