@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 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
  [![npm version](https://img.shields.io/npm/v/@nci-gis/js-tmpl.svg)](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** - Required via `--values` flag or `valuesFile` config
80
- - ✅ **Template directory** - Must be in config or defaults to `templates/`
81
- - ✅ **Output directory** - Must be in config or defaults to `dist/`
82
- - ✅ **Partials directory** - Must be in config or defaults to `templates.partials/`
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
- "name": "{{project.name}}",
142
- "version": "{{project.version}}",
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
- ```javascript
196
- {
197
- ...valuesFromFile, // Your YAML/JSON data
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
- **Root partials** (`_name.hbs`):
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
- └── _header.hbs → {{> header}}
238
+ ├── header.hbs → {{> header}}
239
+ ├── components/
240
+ │ ├── button.hbs → {{> components.button}}
241
+ │ └── forms/
242
+ │ └── login.hbs → {{> components.forms.login}}
228
243
  ```
229
244
 
230
- **Namespaced partials** (`@group/name.hbs`):
245
+ **`@` directories** flatten their contents (filename only, no namespace):
231
246
 
232
247
  ```text
233
- templates.partials/
234
- └── @common/
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
- (input templates, data, config) → output files
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 | Default |
261
- | ------------------------ | ----------------------- | -------------------- |
262
- | `-c, --values FILE` | Values file (YAML/JSON) | **Required** |
263
- | `-t, --template-dir DIR` | Template directory | `templates` |
264
- | `-o, --out DIR` | Output directory | `dist` |
265
- | `-p, --partials-dir DIR` | Partials directory | `templates.partials` |
266
- | `-x, --ext EXT` | Template extension | `.hbs` |
267
- | `--config-file FILE` | Explicit config file | Auto-discovered |
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 comprehensive API documentation.
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
- See [examples/yaml-templates/](examples/yaml-templates/) for a complete working example demonstrating:
358
-
359
- - Dynamic file paths with `${env.NODE_ENV}`
360
- - Handlebars features (loops, conditionals)
361
- - Root and namespaced partials
362
- - Multi-format output (YAML, Markdown)
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 strict design principles:
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
- MIT © pasxd245
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.0.1-beta.2",
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 {Record<string, any>}
5
+ * @returns {import('../types.js').CliArgs}
6
6
  */
7
7
  export function parseArgs(args) {
8
- const opts = { command: "render" };
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 "render":
16
- opts.command = "render";
16
+ case '-h':
17
+ case '--help':
18
+ opts.command = 'help';
17
19
  break;
18
20
 
19
- case "-t":
20
- case "--template-dir":
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 "-c":
25
- case "--values":
30
+ case '-c':
31
+ case '--values':
26
32
  opts.valuesFile = args[++i];
27
33
  break;
28
34
 
29
- case "-o":
30
- case "--out":
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 "-p":
35
- case "--partials-dir":
44
+ case '-p':
45
+ case '--partials-dir':
36
46
  opts.partialsDir = args[++i];
37
47
  break;
38
48
 
39
- case "--config-file":
49
+ case '--config-file':
40
50
  opts.configFile = args[++i];
41
51
  break;
42
52
 
43
- case "-x":
44
- case "--ext":
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 "../config/resolver.js";
4
- import { renderDirectory } from "../engine/renderDirectory.js";
5
- import { parseArgs } from "./args.js";
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 !== "render") {
11
- console.error("Unknown command:", cli.command);
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("✔ js-tmpl completed.");
26
+ console.log('✔ js-tmpl completed.');
20
27
  }
21
28
 
22
29
  // Execute main function if this file is run directly
23
- try {
24
- await main();
25
- } catch (error) {
26
- console.error("Error:", error);
27
- process.exit(1);
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
  }
@@ -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`;
@@ -1,8 +1,10 @@
1
1
  export const DEFAULTS = {
2
- templateDir: "templates",
3
- partialsDir: "templates.partials",
4
- valuesDir: "",
5
- valuesFile: "",
6
- outDir: "dist",
7
- extname: ".hbs",
2
+ templateDir: 'templates',
3
+ partialsDir: '',
4
+ valuesDir: '',
5
+ valuesFile: '',
6
+ outDir: 'dist',
7
+ extname: '.hbs',
8
+ envKeys: [],
9
+ envPrefix: '',
8
10
  };
@@ -1,30 +1,30 @@
1
- import fs from "node:fs";
2
- import path from "node:path";
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
3
 
4
- import YAML from "js-yaml";
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 {object} Parsed values.
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
- "Check that the file exists and the path is correct."
17
+ 'Check that the file exists and the path is correct.',
18
18
  );
19
19
  }
20
20
 
21
- const raw = fs.readFileSync(filePath, "utf8");
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 {object|null} Parsed config object or null if not found.
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
- "js-tmpl.config.yaml",
45
- "js-tmpl.config.yml",
46
- "js-tmpl.config.json",
47
- path.join("config", "js-tmpl.yaml"),
48
- path.join("config", "js-tmpl.json"),
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)) {continue;}
65
+ if (!fs.existsSync(abs)) {
66
+ continue;
67
+ }
54
68
 
55
- const raw = fs.readFileSync(abs, "utf8");
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