@nci-gis/js-tmpl 0.0.1-beta.2 → 0.0.1
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 +38 -115
- package/package.json +30 -3
- package/src/cli/args.js +33 -16
- package/src/cli/main.js +24 -11
- package/src/cli/usage.js +16 -0
- package/src/config/defaults.js +8 -6
- package/src/config/loader.js +32 -18
- package/src/config/resolver.js +23 -14
- package/src/config/view.js +48 -1
- package/src/engine/contentRenderer.js +9 -5
- package/src/engine/partials.js +107 -31
- package/src/engine/pathRenderer.js +7 -4
- package/src/engine/renderDirectory.js +16 -10
- package/src/engine/treeWalker.js +18 -7
- package/src/index.js +2 -2
- package/src/types.js +30 -0
- package/src/utils/fs.js +5 -4
- 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
|
|
@@ -79,7 +81,7 @@ Everything else must be **explicitly specified**:
|
|
|
79
81
|
- ✅ **Values file** - Required via `--values` flag or `valuesFile` config
|
|
80
82
|
- ✅ **Template directory** - Must be in config or defaults to `templates/`
|
|
81
83
|
- ✅ **Output directory** - Must be in config or defaults to `dist/`
|
|
82
|
-
- ✅ **Partials directory** - Must be in config
|
|
84
|
+
- ✅ **Partials directory** - Must be in config; not loaded if omitted
|
|
83
85
|
|
|
84
86
|
### Override Auto-Discovery
|
|
85
87
|
|
|
@@ -137,14 +139,9 @@ templates/
|
|
|
137
139
|
**Template content** (`templates/${project.name}/config.json.hbs`):
|
|
138
140
|
|
|
139
141
|
```handlebars
|
|
140
|
-
{
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
"server": {
|
|
144
|
-
"port": {{config.port}},
|
|
145
|
-
"host": "{{config.host}}"
|
|
146
|
-
}
|
|
147
|
-
}
|
|
142
|
+
{ "name": "{{project.name}}", "version": "{{project.version}}", "server": {
|
|
143
|
+
"port":
|
|
144
|
+
{{config.port}}, "host": "{{config.host}}" } }
|
|
148
145
|
```
|
|
149
146
|
|
|
150
147
|
### 3. Render templates
|
|
@@ -163,7 +160,7 @@ import { resolveConfig, renderDirectory } from '@nci-gis/js-tmpl';
|
|
|
163
160
|
const config = resolveConfig({
|
|
164
161
|
valuesFile: './values.yaml',
|
|
165
162
|
templateDir: './templates',
|
|
166
|
-
outDir: './dist'
|
|
163
|
+
outDir: './dist',
|
|
167
164
|
});
|
|
168
165
|
|
|
169
166
|
await renderDirectory(config);
|
|
@@ -190,14 +187,11 @@ CLI arguments
|
|
|
190
187
|
|
|
191
188
|
### View Model
|
|
192
189
|
|
|
193
|
-
Templates receive a view object
|
|
190
|
+
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
191
|
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
env: process.env // Environment variables
|
|
199
|
-
}
|
|
200
|
-
```
|
|
192
|
+
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 `{}`.
|
|
193
|
+
|
|
194
|
+
See [docs/API.md](docs/API.md#view-object) for full details, examples, and recommended conventions.
|
|
201
195
|
|
|
202
196
|
Access in templates:
|
|
203
197
|
|
|
@@ -220,21 +214,26 @@ templates/
|
|
|
220
214
|
|
|
221
215
|
### Partial System
|
|
222
216
|
|
|
223
|
-
|
|
217
|
+
Each render pass uses an isolated Handlebars instance. Directory structure maps to partial names:
|
|
224
218
|
|
|
225
219
|
```text
|
|
226
220
|
templates.partials/
|
|
227
|
-
|
|
221
|
+
├── header.hbs → {{> header}}
|
|
222
|
+
├── components/
|
|
223
|
+
│ ├── button.hbs → {{> components.button}}
|
|
224
|
+
│ └── forms/
|
|
225
|
+
│ └── login.hbs → {{> components.forms.login}}
|
|
228
226
|
```
|
|
229
227
|
|
|
230
|
-
|
|
228
|
+
**`@` directories** flatten their contents (filename only, no namespace):
|
|
231
229
|
|
|
232
230
|
```text
|
|
233
|
-
|
|
234
|
-
└──
|
|
235
|
-
└── metadata.hbs → {{> common.metadata}}
|
|
231
|
+
├── @helpers/
|
|
232
|
+
│ └── date.hbs → {{> date}}
|
|
236
233
|
```
|
|
237
234
|
|
|
235
|
+
Duplicate partial names throw an error. Names must be alphanumeric + underscore only. See [API docs](docs/API.md#partial-system) for details.
|
|
236
|
+
|
|
238
237
|
## Mental Model
|
|
239
238
|
|
|
240
239
|
> ⚠️ Design note
|
|
@@ -257,14 +256,16 @@ js-tmpl render [options]
|
|
|
257
256
|
|
|
258
257
|
### Options
|
|
259
258
|
|
|
260
|
-
| Option | Description
|
|
261
|
-
| ------------------------ |
|
|
262
|
-
| `-c, --values FILE` | Values file (YAML/JSON)
|
|
263
|
-
| `-t, --template-dir DIR` | Template directory
|
|
264
|
-
| `-o, --out DIR` | Output directory
|
|
265
|
-
| `-p, --partials-dir DIR` | Partials directory
|
|
266
|
-
| `-x, --ext EXT` | Template extension
|
|
267
|
-
| `--config-file FILE` | Explicit config file
|
|
259
|
+
| Option | Description | Default |
|
|
260
|
+
| ------------------------ | --------------------------------------- | --------------- |
|
|
261
|
+
| `-c, --values FILE` | Values file (YAML/JSON) | **Required** |
|
|
262
|
+
| `-t, --template-dir DIR` | Template directory | `templates` |
|
|
263
|
+
| `-o, --out DIR` | Output directory | `dist` |
|
|
264
|
+
| `-p, --partials-dir DIR` | Partials directory | None (skipped) |
|
|
265
|
+
| `-x, --ext EXT` | Template extension | `.hbs` |
|
|
266
|
+
| `--config-file FILE` | Explicit config file | Auto-discovered |
|
|
267
|
+
| `--env-keys KEYS` | Comma-separated env var names to expose | None |
|
|
268
|
+
| `--env-prefix PREFIX` | Auto-include env vars with this prefix | None |
|
|
268
269
|
|
|
269
270
|
### Examples of Usage
|
|
270
271
|
|
|
@@ -278,79 +279,13 @@ js-tmpl render \
|
|
|
278
279
|
--template-dir ./my-templates \
|
|
279
280
|
--out ./output
|
|
280
281
|
|
|
281
|
-
# Multi-environment
|
|
282
|
-
NODE_ENV=production js-tmpl render --values prod-values.yaml
|
|
282
|
+
# Multi-environment (allowlist NODE_ENV to use it in templates)
|
|
283
|
+
NODE_ENV=production js-tmpl render --values prod-values.yaml --env-keys NODE_ENV
|
|
283
284
|
```
|
|
284
285
|
|
|
285
286
|
## Programmatic API
|
|
286
287
|
|
|
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`
|
|
288
|
+
See [docs/API.md](docs/API.md) for the complete API reference — parameters, return types, config file format, and advanced usage.
|
|
354
289
|
|
|
355
290
|
## Examples
|
|
356
291
|
|
|
@@ -363,26 +298,14 @@ See [examples/yaml-templates/](examples/yaml-templates/) for a complete working
|
|
|
363
298
|
|
|
364
299
|
## Testing
|
|
365
300
|
|
|
366
|
-
This project has comprehensive test coverage
|
|
367
|
-
|
|
368
|
-
- 156 tests
|
|
369
|
-
- 99.8% line coverage
|
|
370
|
-
- 99.7% branch coverage
|
|
301
|
+
This project has comprehensive automated test coverage across unit and integration suites.
|
|
302
|
+
Current coverage remains above 99% line coverage with high branch coverage as well.
|
|
371
303
|
|
|
372
304
|
See [tests/README.md](tests/README.md) for testing documentation.
|
|
373
305
|
|
|
374
306
|
## Development Principles
|
|
375
307
|
|
|
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.
|
|
308
|
+
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
309
|
|
|
387
310
|
## Roadmap
|
|
388
311
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nci-gis/js-tmpl",
|
|
3
|
-
"version": "0.0.1
|
|
3
|
+
"version": "0.0.1",
|
|
4
4
|
"description": "The pure JavaScript templating engine that uses handlebars.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -12,16 +12,22 @@
|
|
|
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": "prettier --write src/ tests/",
|
|
20
|
+
"format:check": "prettier --check src/ tests/",
|
|
18
21
|
"lint": "eslint src/ tests/",
|
|
19
22
|
"lint:fix": "eslint src/ tests/ --fix",
|
|
20
23
|
"build": "echo 'No build step required for pure JS library'",
|
|
21
24
|
"start": "node src/cli/main.js",
|
|
22
25
|
"help": "node src/cli/main.js --help",
|
|
23
26
|
"dev": "node src/cli/main.js",
|
|
24
|
-
"tool": "node src/cli/main.js"
|
|
27
|
+
"tool": "node src/cli/main.js",
|
|
28
|
+
"docs:check-links": "remark --frail --quiet *.md docs/*.md tests/README.md examples/**/*.md",
|
|
29
|
+
"docs:check-exports": "node scripts/check-doc-exports.js",
|
|
30
|
+
"docs:check": "pnpm docs:check-links && pnpm docs:check-exports"
|
|
25
31
|
},
|
|
26
32
|
"keywords": [
|
|
27
33
|
"js",
|
|
@@ -57,7 +63,28 @@
|
|
|
57
63
|
},
|
|
58
64
|
"devDependencies": {
|
|
59
65
|
"@eslint/js": "^9.39.2",
|
|
66
|
+
"@types/js-yaml": "^4.0.9",
|
|
67
|
+
"@types/node": "^25.5.2",
|
|
60
68
|
"eslint": "^9.39.2",
|
|
61
|
-
"eslint-plugin-simple-import-sort": "^12.1.1"
|
|
69
|
+
"eslint-plugin-simple-import-sort": "^12.1.1",
|
|
70
|
+
"husky": "^9.1.7",
|
|
71
|
+
"lint-staged": "^16.4.0",
|
|
72
|
+
"prettier": "^3.8.1",
|
|
73
|
+
"remark-cli": "^12.0.1",
|
|
74
|
+
"remark-validate-links": "^13.1.0",
|
|
75
|
+
"typescript": "^6.0.2"
|
|
76
|
+
},
|
|
77
|
+
"lint-staged": {
|
|
78
|
+
"src/**/*.js": [
|
|
79
|
+
"prettier --write",
|
|
80
|
+
"eslint --fix"
|
|
81
|
+
],
|
|
82
|
+
"tests/**/*.js": [
|
|
83
|
+
"prettier --write",
|
|
84
|
+
"eslint --fix"
|
|
85
|
+
],
|
|
86
|
+
"**/*.md": [
|
|
87
|
+
"prettier --write"
|
|
88
|
+
]
|
|
62
89
|
}
|
|
63
90
|
}
|
package/src/cli/args.js
CHANGED
|
@@ -1,49 +1,66 @@
|
|
|
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
|
-
case
|
|
35
|
+
case '-o':
|
|
36
|
+
case '--out':
|
|
31
37
|
opts.outDir = args[++i];
|
|
32
38
|
break;
|
|
33
39
|
|
|
34
|
-
case
|
|
35
|
-
case
|
|
40
|
+
case '-p':
|
|
41
|
+
case '--partials-dir':
|
|
36
42
|
opts.partialsDir = args[++i];
|
|
37
43
|
break;
|
|
38
44
|
|
|
39
|
-
case
|
|
45
|
+
case '--config-file':
|
|
40
46
|
opts.configFile = args[++i];
|
|
41
47
|
break;
|
|
42
48
|
|
|
43
|
-
case
|
|
44
|
-
case
|
|
49
|
+
case '-x':
|
|
50
|
+
case '--ext':
|
|
45
51
|
opts.extname = args[++i];
|
|
46
52
|
break;
|
|
53
|
+
|
|
54
|
+
case '--env-keys':
|
|
55
|
+
opts.envKeys = args[++i]
|
|
56
|
+
.split(',')
|
|
57
|
+
.map((s) => s.trim())
|
|
58
|
+
.filter(Boolean);
|
|
59
|
+
break;
|
|
60
|
+
|
|
61
|
+
case '--env-prefix':
|
|
62
|
+
opts.envPrefix = args[++i];
|
|
63
|
+
break;
|
|
47
64
|
}
|
|
48
65
|
// next argument:
|
|
49
66
|
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,16 @@
|
|
|
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 or JSON) [required]
|
|
10
|
+
-o, --out <dir> Output directory (default: dist)
|
|
11
|
+
-p, --partials-dir <dir> Partials directory
|
|
12
|
+
-x, --ext <ext> Template extension (default: .hbs)
|
|
13
|
+
--config-file <file> Project config file
|
|
14
|
+
--env-keys <keys> Comma-separated env var names to expose (default: none)
|
|
15
|
+
--env-prefix <prefix> Auto-include env vars with this prefix (e.g. JS_TMPL_)
|
|
16
|
+
-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
|
|
package/src/config/resolver.js
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
import path from
|
|
2
|
-
import process from
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import process from 'node:process';
|
|
3
3
|
|
|
4
|
-
import { DEFAULTS } from
|
|
5
|
-
import { loadProjectConfig,loadYamlOrJson } from
|
|
6
|
-
import { buildView } from
|
|
4
|
+
import { DEFAULTS } from './defaults.js';
|
|
5
|
+
import { loadProjectConfig, loadYamlOrJson } from './loader.js';
|
|
6
|
+
import { buildView, pickEnv } from './view.js';
|
|
7
7
|
|
|
8
8
|
/**
|
|
9
9
|
* Resolve valuesFile path based on valuesDir
|
|
@@ -34,9 +34,9 @@ function resolveValuesFilePath(valuesFile, valuesDir, cwd) {
|
|
|
34
34
|
* Resolve final config using:
|
|
35
35
|
* defaults < projectConfig < cliArgs
|
|
36
36
|
*
|
|
37
|
-
* @param {
|
|
38
|
-
* @param {string} cwd - Current working directory
|
|
39
|
-
* @returns {
|
|
37
|
+
* @param {import('../types.js').CliArgs} cli - CLI arguments
|
|
38
|
+
* @param {string} [cwd] - Current working directory
|
|
39
|
+
* @returns {import('../types.js').TemplateConfig} - Resolved configuration
|
|
40
40
|
*/
|
|
41
41
|
export function resolveConfig(cli, cwd = process.cwd()) {
|
|
42
42
|
const projectConfig = loadProjectConfig(cwd, cli.configFile);
|
|
@@ -47,15 +47,16 @@ export function resolveConfig(cli, cwd = process.cwd()) {
|
|
|
47
47
|
...cli,
|
|
48
48
|
};
|
|
49
49
|
|
|
50
|
+
/** @param {string} p */
|
|
50
51
|
const abs = (p) => (path.isAbsolute(p) ? p : path.join(cwd, p));
|
|
51
52
|
|
|
52
53
|
// Validate valuesFile is provided
|
|
53
54
|
if (!mergedConfig.valuesFile) {
|
|
54
55
|
throw new Error(
|
|
55
56
|
'Missing required configuration: valuesFile\n' +
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
57
|
+
'Provide via:\n' +
|
|
58
|
+
' - CLI: --values path/to/values.yaml\n' +
|
|
59
|
+
' - Config: valuesFile: "path/to/values.yaml" in js-tmpl.config.yaml',
|
|
59
60
|
);
|
|
60
61
|
}
|
|
61
62
|
|
|
@@ -63,16 +64,24 @@ export function resolveConfig(cli, cwd = process.cwd()) {
|
|
|
63
64
|
const valuesFilePath = resolveValuesFilePath(
|
|
64
65
|
mergedConfig.valuesFile,
|
|
65
66
|
mergedConfig.valuesDir,
|
|
66
|
-
cwd
|
|
67
|
+
cwd,
|
|
67
68
|
);
|
|
68
69
|
|
|
69
70
|
const values = loadYamlOrJson(valuesFilePath);
|
|
70
71
|
|
|
72
|
+
const hasEnvConfig = mergedConfig.envKeys?.length || mergedConfig.envPrefix;
|
|
73
|
+
const env = hasEnvConfig
|
|
74
|
+
? pickEnv({
|
|
75
|
+
keys: mergedConfig.envKeys || [],
|
|
76
|
+
prefix: mergedConfig.envPrefix || '',
|
|
77
|
+
})
|
|
78
|
+
: {};
|
|
79
|
+
|
|
71
80
|
return {
|
|
72
81
|
templateDir: abs(mergedConfig.templateDir),
|
|
73
|
-
partialsDir: abs(mergedConfig.partialsDir),
|
|
82
|
+
partialsDir: mergedConfig.partialsDir ? abs(mergedConfig.partialsDir) : '',
|
|
74
83
|
outDir: abs(mergedConfig.outDir),
|
|
75
84
|
extname: mergedConfig.extname,
|
|
76
|
-
view: buildView(values),
|
|
85
|
+
view: buildView(values, env),
|
|
77
86
|
};
|
|
78
87
|
}
|
package/src/config/view.js
CHANGED
|
@@ -1,6 +1,53 @@
|
|
|
1
|
-
|
|
1
|
+
/**
|
|
2
|
+
* Build the view object by merging values with environment data.
|
|
3
|
+
*
|
|
4
|
+
* **Reserved key:** `env` is always set to the provided environment object.
|
|
5
|
+
* If `values` contains a top-level `env` key, a warning is logged and
|
|
6
|
+
* the key is overwritten.
|
|
7
|
+
*
|
|
8
|
+
* @param {Record<string, unknown>} values
|
|
9
|
+
* @param {Record<string, string>} [env]
|
|
10
|
+
* @returns {Record<string, unknown>}
|
|
11
|
+
*/
|
|
12
|
+
export function buildView(values, env = {}) {
|
|
13
|
+
if ('env' in values) {
|
|
14
|
+
console.warn(
|
|
15
|
+
'Warning: "env" is a reserved key in js-tmpl and will be overwritten.\n' +
|
|
16
|
+
'Rename the "env" key in your values file to avoid this.',
|
|
17
|
+
);
|
|
18
|
+
}
|
|
2
19
|
return {
|
|
3
20
|
...values,
|
|
4
21
|
env,
|
|
5
22
|
};
|
|
6
23
|
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Pick environment variables by explicit keys and/or prefix.
|
|
27
|
+
*
|
|
28
|
+
* @param {object} options
|
|
29
|
+
* @param {string[]} options.keys - Explicit key names to include.
|
|
30
|
+
* @param {string} options.prefix - Include all vars starting with this prefix.
|
|
31
|
+
* @param {Record<string, string | undefined>} [source] - Env source (default: process.env).
|
|
32
|
+
* @returns {Record<string, string>}
|
|
33
|
+
*/
|
|
34
|
+
export function pickEnv({ keys = [], prefix = '' }, source = process.env) {
|
|
35
|
+
/** @type {Record<string, string>} */
|
|
36
|
+
const result = {};
|
|
37
|
+
|
|
38
|
+
for (const k of keys) {
|
|
39
|
+
if (k in source) {
|
|
40
|
+
result[k] = /** @type {string} */ (source[k]);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
if (prefix) {
|
|
45
|
+
for (const k of Object.keys(source)) {
|
|
46
|
+
if (k.startsWith(prefix)) {
|
|
47
|
+
result[k] = /** @type {string} */ (source[k]);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
return result;
|
|
53
|
+
}
|
|
@@ -1,12 +1,16 @@
|
|
|
1
|
-
import fs from
|
|
1
|
+
import fs from 'node:fs/promises';
|
|
2
2
|
|
|
3
|
-
import Handlebars from
|
|
3
|
+
import Handlebars from 'handlebars';
|
|
4
4
|
|
|
5
5
|
/**
|
|
6
6
|
* Render template file with Handlebars & view object.
|
|
7
|
+
* @param {string} filePath
|
|
8
|
+
* @param {Record<string, unknown>} view
|
|
9
|
+
* @param {typeof Handlebars} [hbs] - Scoped Handlebars instance; falls back to global
|
|
10
|
+
* @returns {Promise<string>}
|
|
7
11
|
*/
|
|
8
|
-
export async function renderContent(filePath, view) {
|
|
9
|
-
const raw = await fs.readFile(filePath,
|
|
10
|
-
const compile = Handlebars.compile(raw);
|
|
12
|
+
export async function renderContent(filePath, view, hbs) {
|
|
13
|
+
const raw = await fs.readFile(filePath, 'utf8');
|
|
14
|
+
const compile = (hbs || Handlebars).compile(raw);
|
|
11
15
|
return compile(view);
|
|
12
16
|
}
|
package/src/engine/partials.js
CHANGED
|
@@ -1,41 +1,117 @@
|
|
|
1
|
-
import fs from
|
|
2
|
-
import path from
|
|
1
|
+
import fs from 'node:fs/promises';
|
|
2
|
+
import path from 'node:path';
|
|
3
3
|
|
|
4
|
-
|
|
4
|
+
const VALID_SEGMENT = /^\w+$/;
|
|
5
5
|
|
|
6
6
|
/**
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
7
|
+
* Validate a partial name segment (directory or file basename).
|
|
8
|
+
* @param {string[]} segments
|
|
9
|
+
* @param {string} filePath
|
|
10
10
|
*/
|
|
11
|
-
|
|
12
|
-
|
|
11
|
+
function validateSegments(segments, filePath) {
|
|
12
|
+
if (!VALID_SEGMENT.test(segments.join(''))) {
|
|
13
|
+
throw new Error(
|
|
14
|
+
`Invalid partial name segment '${segments.join('>')}' in ${filePath} — only alphanumeric and underscore allowed`,
|
|
15
|
+
);
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Derive a partial entry from a file path.
|
|
21
|
+
*
|
|
22
|
+
* @param {string} partialsDir - Root partials directory
|
|
23
|
+
* @param {string} ext - Template extension (e.g. ".hbs")
|
|
24
|
+
* @param {string} filePath - Relative path from partialsDir
|
|
25
|
+
* @param {boolean} isFlat - If true, register by filename only; otherwise namespace by path
|
|
26
|
+
* @returns {{ name: string, source: string }}
|
|
27
|
+
*/
|
|
28
|
+
function processPartialFile(partialsDir, ext, filePath, isFlat) {
|
|
29
|
+
const abs = path.join(partialsDir, filePath);
|
|
30
|
+
|
|
31
|
+
// Flat by filename: "name.hbs" → "name"
|
|
32
|
+
if (isFlat) {
|
|
33
|
+
const name = path.basename(filePath, ext);
|
|
34
|
+
validateSegments([name], abs);
|
|
35
|
+
return { name, source: abs };
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// Namespace by path, e.g.:
|
|
39
|
+
// - "dir/name.hbs" → "dir.name"
|
|
40
|
+
// - "dir/sub/name.hbs" → "dir.sub.name"
|
|
41
|
+
// - "name.hbs" → "name" (no nesting).
|
|
42
|
+
const segments = filePath.slice(0, -ext.length).split(path.sep);
|
|
43
|
+
validateSegments(segments, abs);
|
|
44
|
+
return { name: segments.join('.'), source: abs };
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Scan partialsDir recursively and collect all partial entries.
|
|
49
|
+
* @param {string} partialsDir
|
|
50
|
+
* @param {string} ext
|
|
51
|
+
* @returns {Promise<Array<{ name: string, source: string }>>}
|
|
52
|
+
*/
|
|
53
|
+
async function scanPartialFiles(partialsDir, ext) {
|
|
54
|
+
const allFiles = await fs.readdir(partialsDir, { recursive: true });
|
|
55
|
+
|
|
56
|
+
return allFiles
|
|
57
|
+
.filter((f) => f.endsWith(ext))
|
|
58
|
+
.map((f) => {
|
|
59
|
+
const isFlat = f.startsWith('@');
|
|
60
|
+
return processPartialFile(partialsDir, ext, f, isFlat);
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Check for duplicate partial names and throw if found.
|
|
66
|
+
* @param {Array<{ name: string, source: string }>} entries
|
|
67
|
+
* @param {string} partialsDir
|
|
68
|
+
*/
|
|
69
|
+
function checkDuplicates(entries, partialsDir) {
|
|
70
|
+
/** @type {Map<string, string>} */
|
|
71
|
+
const seen = new Map();
|
|
13
72
|
|
|
14
73
|
for (const entry of entries) {
|
|
15
|
-
const
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
const key = path.basename(f, ext);
|
|
26
|
-
const name = `${group}.${key}`;
|
|
27
|
-
const content = await fs.readFile(path.join(abs, f), "utf8");
|
|
28
|
-
Handlebars.registerPartial(name, content);
|
|
29
|
-
}
|
|
30
|
-
}
|
|
31
|
-
continue;
|
|
74
|
+
const existing = seen.get(entry.name);
|
|
75
|
+
if (existing) {
|
|
76
|
+
const rel1 = path.relative(partialsDir, existing);
|
|
77
|
+
const rel2 = path.relative(partialsDir, entry.source);
|
|
78
|
+
throw new Error(
|
|
79
|
+
`Duplicate partial name '${entry.name}' — registered by both:\n` +
|
|
80
|
+
` - ${rel1}\n` +
|
|
81
|
+
` - ${rel2}\n` +
|
|
82
|
+
`Use namespaced directories to avoid collisions.`,
|
|
83
|
+
);
|
|
32
84
|
}
|
|
85
|
+
seen.set(entry.name, entry.source);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
33
88
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
89
|
+
/**
|
|
90
|
+
* Register partials from a directory onto a Handlebars instance.
|
|
91
|
+
*
|
|
92
|
+
* Naming conventions:
|
|
93
|
+
* - `name.hbs` in root → "name"
|
|
94
|
+
* - `dir/name.hbs` → "dir.name" (namespaced by directory path)
|
|
95
|
+
* - `@dir/` at root → flatten entire subtree (filename only)
|
|
96
|
+
*
|
|
97
|
+
* Throws on duplicate partial names or invalid name segments.
|
|
98
|
+
* Skips silently if partialsDir is falsy. Throws if the directory does not exist.
|
|
99
|
+
*
|
|
100
|
+
* @param {string} partialsDir - Path to partials directory
|
|
101
|
+
* @param {string} ext - Template file extension (e.g. ".hbs")
|
|
102
|
+
* @param {import('handlebars')} hbs - Handlebars instance to register partials on
|
|
103
|
+
*/
|
|
104
|
+
export async function registerPartials(partialsDir, ext, hbs) {
|
|
105
|
+
if (!partialsDir) {
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const allPartials = await scanPartialFiles(partialsDir, ext);
|
|
110
|
+
|
|
111
|
+
checkDuplicates(allPartials, partialsDir);
|
|
112
|
+
|
|
113
|
+
for (const p of allPartials) {
|
|
114
|
+
const content = await fs.readFile(p.source, 'utf8');
|
|
115
|
+
hbs.registerPartial(p.name, content);
|
|
40
116
|
}
|
|
41
117
|
}
|
|
@@ -1,9 +1,12 @@
|
|
|
1
|
-
import path from
|
|
1
|
+
import path from 'node:path';
|
|
2
2
|
|
|
3
|
-
import { getNested } from
|
|
3
|
+
import { getNested } from '../utils/object.js';
|
|
4
4
|
|
|
5
5
|
/**
|
|
6
6
|
* Render `${var}` placeholders in each segment of the relPath.
|
|
7
|
+
* @param {string} relPath
|
|
8
|
+
* @param {Record<string, unknown>} view
|
|
9
|
+
* @returns {string}
|
|
7
10
|
*/
|
|
8
11
|
export function renderPath(relPath, view) {
|
|
9
12
|
const segments = relPath.split(path.sep);
|
|
@@ -12,8 +15,8 @@ export function renderPath(relPath, view) {
|
|
|
12
15
|
//NOSONAR -- ignore S5842: Regular expression is safe here
|
|
13
16
|
seg.replace(/\$\{([^}]+)\}/g, (_, expr) => {
|
|
14
17
|
const v = getNested(view, expr.trim());
|
|
15
|
-
return String(v ??
|
|
16
|
-
})
|
|
18
|
+
return String(v ?? '');
|
|
19
|
+
}),
|
|
17
20
|
);
|
|
18
21
|
|
|
19
22
|
return path.join(...rendered);
|
|
@@ -1,18 +1,24 @@
|
|
|
1
|
-
import path from
|
|
1
|
+
import path from 'node:path';
|
|
2
2
|
|
|
3
|
-
import
|
|
4
|
-
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
3
|
+
import Handlebars from 'handlebars';
|
|
4
|
+
|
|
5
|
+
import { ensureDir, writeFileSafe } from '../utils/fs.js';
|
|
6
|
+
import { renderContent } from './contentRenderer.js';
|
|
7
|
+
import { registerPartials } from './partials.js';
|
|
8
|
+
import { renderPath } from './pathRenderer.js';
|
|
9
|
+
import { walkTemplateTree } from './treeWalker.js';
|
|
8
10
|
|
|
9
11
|
/**
|
|
10
12
|
* Main rendering orchestrator.
|
|
13
|
+
* @param {import('../types.js').TemplateConfig} cfg
|
|
14
|
+
* @param {typeof import('handlebars')} [hbs] - Optional Handlebars instance (creates an isolated one if omitted)
|
|
15
|
+
* @returns {Promise<void>}
|
|
11
16
|
*/
|
|
12
|
-
export async function renderDirectory(cfg) {
|
|
17
|
+
export async function renderDirectory(cfg, hbs) {
|
|
13
18
|
const { templateDir, partialsDir, outDir, view, extname } = cfg;
|
|
14
19
|
|
|
15
|
-
|
|
20
|
+
hbs = hbs || Handlebars.create();
|
|
21
|
+
await registerPartials(partialsDir, extname, hbs);
|
|
16
22
|
|
|
17
23
|
const files = await walkTemplateTree(templateDir, extname);
|
|
18
24
|
|
|
@@ -20,10 +26,10 @@ export async function renderDirectory(cfg) {
|
|
|
20
26
|
const relRendered = renderPath(file.relPath, view);
|
|
21
27
|
const target = path.join(
|
|
22
28
|
outDir,
|
|
23
|
-
relRendered.replace(new RegExp(`${extname}$`),
|
|
29
|
+
relRendered.replace(new RegExp(`${extname}$`), ''),
|
|
24
30
|
);
|
|
25
31
|
|
|
26
|
-
const content = await renderContent(file.absPath, view);
|
|
32
|
+
const content = await renderContent(file.absPath, view, hbs);
|
|
27
33
|
|
|
28
34
|
await ensureDir(path.dirname(target));
|
|
29
35
|
await writeFileSafe(target, content);
|
package/src/engine/treeWalker.js
CHANGED
|
@@ -1,22 +1,28 @@
|
|
|
1
|
-
import fs from
|
|
2
|
-
import path from
|
|
1
|
+
import fs from 'node:fs/promises';
|
|
2
|
+
import path from 'node:path';
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
5
|
* BFS async folder walker.
|
|
6
|
+
* @param {string} rootDir
|
|
7
|
+
* @param {string} [ext]
|
|
8
|
+
* @param {Array<string | RegExp>} [ignore]
|
|
9
|
+
* @returns {Promise<import('../types.js').TemplateFile[]>}
|
|
6
10
|
*/
|
|
7
|
-
export async function walkTemplateTree(rootDir, ext =
|
|
11
|
+
export async function walkTemplateTree(rootDir, ext = '.hbs', ignore = []) {
|
|
8
12
|
const results = [];
|
|
9
|
-
const queue = [
|
|
13
|
+
const queue = [''];
|
|
10
14
|
|
|
11
15
|
while (queue.length) {
|
|
12
|
-
const rel = queue.shift();
|
|
16
|
+
const rel = /** @type {string} */ (queue.shift());
|
|
13
17
|
const abs = path.join(rootDir, rel);
|
|
14
18
|
const stat = await fs.stat(abs);
|
|
15
19
|
|
|
16
20
|
if (stat.isDirectory()) {
|
|
17
|
-
const items = await fs.readdir(abs);
|
|
21
|
+
const items = (await fs.readdir(abs)).sort();
|
|
18
22
|
for (const name of items) {
|
|
19
|
-
if (ignore.some((i) => matchIgnore(name, i))) {
|
|
23
|
+
if (ignore.some((i) => matchIgnore(name, i))) {
|
|
24
|
+
continue;
|
|
25
|
+
}
|
|
20
26
|
queue.push(rel ? path.join(rel, name) : name);
|
|
21
27
|
}
|
|
22
28
|
} else if (path.extname(abs) === ext) {
|
|
@@ -27,6 +33,11 @@ export async function walkTemplateTree(rootDir, ext = ".hbs", ignore = []) {
|
|
|
27
33
|
return results;
|
|
28
34
|
}
|
|
29
35
|
|
|
36
|
+
/**
|
|
37
|
+
* @param {string} name
|
|
38
|
+
* @param {string | RegExp} rule
|
|
39
|
+
* @returns {boolean}
|
|
40
|
+
*/
|
|
30
41
|
function matchIgnore(name, rule) {
|
|
31
42
|
return rule instanceof RegExp ? rule.test(name) : rule === name;
|
|
32
43
|
}
|
package/src/index.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
export * from
|
|
2
|
-
export * from
|
|
1
|
+
export * from './config/resolver.js';
|
|
2
|
+
export * from './engine/renderDirectory.js';
|
package/src/types.js
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @typedef {object} CliArgs
|
|
3
|
+
* @property {string} [command]
|
|
4
|
+
* @property {string} [templateDir]
|
|
5
|
+
* @property {string} [valuesFile]
|
|
6
|
+
* @property {string} [outDir]
|
|
7
|
+
* @property {string} [partialsDir]
|
|
8
|
+
* @property {string} [configFile]
|
|
9
|
+
* @property {string} [extname]
|
|
10
|
+
* @property {string} [valuesDir]
|
|
11
|
+
* @property {string[]} [envKeys]
|
|
12
|
+
* @property {string} [envPrefix]
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* @typedef {object} TemplateConfig
|
|
17
|
+
* @property {string} templateDir
|
|
18
|
+
* @property {string} partialsDir
|
|
19
|
+
* @property {string} outDir
|
|
20
|
+
* @property {string} extname
|
|
21
|
+
* @property {Record<string, unknown>} view
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* @typedef {object} TemplateFile
|
|
26
|
+
* @property {string} absPath
|
|
27
|
+
* @property {string} relPath
|
|
28
|
+
*/
|
|
29
|
+
|
|
30
|
+
export {}; //NOSONAR
|
package/src/utils/fs.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import fs from
|
|
2
|
-
import path from
|
|
1
|
+
import fs from 'node:fs/promises';
|
|
2
|
+
import path from 'node:path';
|
|
3
3
|
|
|
4
4
|
/** Safe mkdir -p
|
|
5
5
|
* @param {string} dir Directory path to create
|
|
@@ -13,7 +13,7 @@ export async function ensureDir(dir) {
|
|
|
13
13
|
* @param {string} content Content to write
|
|
14
14
|
*/
|
|
15
15
|
export async function writeFileSafe(file, content) {
|
|
16
|
-
await fs.writeFile(file, content,
|
|
16
|
+
await fs.writeFile(file, content, 'utf8');
|
|
17
17
|
}
|
|
18
18
|
|
|
19
19
|
/** Resolve path relative to cwd
|
|
@@ -35,7 +35,8 @@ export function resolvePath(p, cwd = process.cwd()) {
|
|
|
35
35
|
* @returns {string} Absolute path.
|
|
36
36
|
*/
|
|
37
37
|
export function safeResolvePath(...segments) {
|
|
38
|
-
const isAbsolute =
|
|
38
|
+
const isAbsolute =
|
|
39
|
+
segments.length > 0 && segments[0] && path.isAbsolute(segments[0]);
|
|
39
40
|
if (isAbsolute) {
|
|
40
41
|
return path.resolve(...segments);
|
|
41
42
|
}
|
package/src/utils/object.js
CHANGED
|
@@ -1,10 +1,15 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Retrieve nested value: getNested(obj, "a.b.c")
|
|
3
|
+
* @param {Record<string, unknown>} obj
|
|
4
|
+
* @param {string} key
|
|
5
|
+
* @returns {unknown}
|
|
3
6
|
*/
|
|
4
7
|
export function getNested(obj, key) {
|
|
5
|
-
console.log("getNested", obj, key);
|
|
6
|
-
|
|
7
8
|
return key
|
|
8
|
-
.split(
|
|
9
|
-
.reduce(
|
|
9
|
+
.split('.')
|
|
10
|
+
.reduce(
|
|
11
|
+
(/** @type {unknown} */ acc, /** @type {string} */ k) =>
|
|
12
|
+
/** @type {Record<string, unknown>} */ (acc)?.[k] ?? undefined,
|
|
13
|
+
obj,
|
|
14
|
+
);
|
|
10
15
|
}
|