@nci-gis/js-tmpl 0.1.0 → 0.1.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 CHANGED
@@ -51,7 +51,8 @@ See [Design Principles](docs/PRINCIPLES.md) - Core philosophy guiding all decisi
51
51
  ## Features
52
52
 
53
53
  - 🎯 **Dynamic File Paths** - Use `${var}` placeholders in paths and filenames
54
- - 🧩 **Handlebars Templates** - Full Handlebars feature set (loops, conditionals, helpers)
54
+ - 🧩 **Handlebars Templates** - Full Handlebars feature set (loops, conditionals, partials)
55
+ - 🛠️ **Custom Helpers** - `registerHelpers` on a scoped instance, validated and atomic
55
56
  - 📦 **Partial System** - Reusable components with root and namespaced partials
56
57
  - ⚙️ **Flexible Configuration** - CLI args > project config > defaults
57
58
  - 🌲 **BFS Tree Walking** - Async, non-blocking template discovery
@@ -167,6 +168,27 @@ const config = resolveConfig({
167
168
  await renderDirectory(config);
168
169
  ```
169
170
 
171
+ **With custom helpers:**
172
+
173
+ ```javascript
174
+ import Handlebars from 'handlebars';
175
+ import {
176
+ registerHelpers,
177
+ renderDirectory,
178
+ resolveConfig,
179
+ } from '@nci-gis/js-tmpl';
180
+
181
+ const hbs = Handlebars.create();
182
+ registerHelpers(hbs, { upper: (s) => s.toUpperCase() });
183
+
184
+ await renderDirectory(resolveConfig({ valuesFile: './values.yaml' }), hbs);
185
+ ```
186
+
187
+ Helpers must be pure functions. Strict mode still applies to plain
188
+ `{{var}}` lookups around them; see
189
+ [Strict templates](docs/API.md#strict-templates) for what is not checked
190
+ inside helper arguments.
191
+
170
192
  ### 4. Get output
171
193
 
172
194
  ```text
@@ -284,6 +306,12 @@ js-tmpl render [options]
284
306
  | `--config-file FILE` | Explicit config file | Auto-discovered |
285
307
  | `--env-keys KEYS` | Comma-separated env var names to expose | None |
286
308
  | `--env-prefix PREFIX` | Auto-include env vars with this prefix | None |
309
+ | `--verbose` | Print stack traces on error | Off |
310
+ | `-h, --help` | Show usage | |
311
+
312
+ The CLI is strict: an unknown option, an option without its value, a
313
+ repeated option, or an unexpected argument is an error. Exit codes: `0`
314
+ success, `1` render or configuration error, `2` usage error.
287
315
 
288
316
  Both `--values` and `--values-dir` are optional (VP-8, VP-6). If neither is
289
317
  supplied, `view` is `{ env: {...} }` only. Missing `{{var}}` in a template
@@ -320,6 +348,8 @@ See [docs/API.md](docs/API.md) for the complete API reference — parameters, re
320
348
  - [examples/value-partials/](examples/value-partials/) — composing `view`
321
349
  from multiple structured files via `--values-dir` (directory-as-namespace,
322
350
  no merge, `@`-flatten escape).
351
+ - [examples/helpers/](examples/helpers/) — registering pure custom helpers on
352
+ a scoped Handlebars instance with `registerHelpers`.
323
353
 
324
354
  ## Testing
325
355
 
package/bin/js-tmpl.js ADDED
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env node
2
+
3
+ // Installed CLI entry. A Node file, not a shell wrapper: npm links it from
4
+ // node_modules/.bin on every OS, and imports resolve from this file's real
5
+ // location, not from the symlink.
6
+ import { run } from '../src/cli/main.js';
7
+
8
+ process.exitCode = await run(process.argv.slice(2));
package/package.json CHANGED
@@ -1,10 +1,10 @@
1
1
  {
2
2
  "name": "@nci-gis/js-tmpl",
3
- "version": "0.1.0",
3
+ "version": "0.1.1",
4
4
  "description": "The pure JavaScript templating engine that uses handlebars.",
5
5
  "type": "module",
6
6
  "bin": {
7
- "js-tmpl": "bin/js-tmpl"
7
+ "js-tmpl": "bin/js-tmpl.js"
8
8
  },
9
9
  "exports": {
10
10
  ".": {
@@ -13,9 +13,9 @@
13
13
  },
14
14
  "scripts": {
15
15
  "prepare": "husky",
16
- "test": "node --test $(find tests -name '*.test.js')",
17
- "test:watch": "node --test --watch $(find tests -name '*.test.js')",
18
- "test:coverage": "node --experimental-test-coverage --test $(find tests -name '*.test.js')",
16
+ "test": "node --test",
17
+ "test:watch": "node --test --watch",
18
+ "test:coverage": "node --test --experimental-test-coverage --test-coverage-exclude=\"tests/**\" --test-coverage-exclude=\"examples/**\" --test-coverage-lines=99 --test-coverage-branches=99 --test-coverage-functions=99",
19
19
  "format": "pnpm format:code && pnpm format:md",
20
20
  "format:check": "pnpm format:code:check && pnpm format:md:check",
21
21
  "format:code": "prettier --write src/ tests/",
@@ -31,7 +31,10 @@
31
31
  "tool": "node src/cli/main.js",
32
32
  "docs:check-links": "remark --frail --quiet *.md docs/*.md tests/README.md examples/**/*.md",
33
33
  "docs:check-exports": "node scripts/check-doc-exports.js",
34
- "docs:check": "pnpm docs:check-links && pnpm docs:check-exports"
34
+ "docs:check": "pnpm docs:check-links && pnpm docs:check-exports",
35
+ "examples:update": "node scripts/update-golden.js",
36
+ "pack:check": "node scripts/check-pack.js",
37
+ "verify": "pnpm lint && pnpm format:check && pnpm docs:check && pnpm pack:check && pnpm test:coverage"
35
38
  },
36
39
  "keywords": [
37
40
  "js",
@@ -61,7 +64,6 @@
61
64
  },
62
65
  "packageManager": "pnpm@10.22.0",
63
66
  "dependencies": {
64
- "config": "^4.1.1",
65
67
  "handlebars": "^4.7.8",
66
68
  "js-yaml": "^4.1.1"
67
69
  },
package/src/cli/args.js CHANGED
@@ -1,73 +1,146 @@
1
+ import { parseArgs as parseNodeArgs } from 'node:util';
2
+
1
3
  /**
2
- * Parse CLI arguments.
3
- *
4
- * @param {string[]} args
5
- * @returns {import('../types.js').CliArgs}
4
+ * The command line was malformed: unknown option, missing value, stray
5
+ * argument. The CLI exits with code 2 for these, distinct from render or
6
+ * config failures (code 1).
6
7
  */
7
- export function parseArgs(args) {
8
- /** @type {import('../types.js').CliArgs} */
9
- const opts = { command: 'render' };
10
-
11
- let i = 0;
12
- while (i < args.length) {
13
- const a = args[i];
14
-
15
- switch (a) {
16
- case '-h':
17
- case '--help':
18
- opts.command = 'help';
19
- break;
8
+ export class UsageError extends Error {
9
+ /** @param {string} message */
10
+ constructor(message) {
11
+ super(message);
12
+ this.name = 'UsageError';
13
+ }
14
+ }
20
15
 
21
- case 'render':
22
- opts.command = 'render';
23
- break;
16
+ /** Option name → CliArgs field. Short aliases live in OPTIONS. */
17
+ const FIELDS = {
18
+ 'template-dir': 'templateDir',
19
+ values: 'valuesFile',
20
+ 'values-dir': 'valuesDir',
21
+ out: 'outDir',
22
+ 'partials-dir': 'partialsDir',
23
+ 'config-file': 'configFile',
24
+ ext: 'extname',
25
+ 'env-prefix': 'envPrefix',
26
+ };
24
27
 
25
- case '-t':
26
- case '--template-dir':
27
- opts.templateDir = args[++i];
28
- break;
28
+ /** @type {import('node:util').ParseArgsConfig['options']} */
29
+ const OPTIONS = {
30
+ help: { type: 'boolean', short: 'h' },
31
+ verbose: { type: 'boolean' },
32
+ 'template-dir': { type: 'string', short: 't' },
33
+ values: { type: 'string', short: 'c' },
34
+ 'values-dir': { type: 'string' },
35
+ out: { type: 'string', short: 'o' },
36
+ 'partials-dir': { type: 'string', short: 'p' },
37
+ 'config-file': { type: 'string' },
38
+ ext: { type: 'string', short: 'x' },
39
+ 'env-keys': { type: 'string' },
40
+ 'env-prefix': { type: 'string' },
41
+ };
29
42
 
30
- case '-c':
31
- case '--values':
32
- opts.valuesFile = args[++i];
33
- break;
43
+ const COMMANDS = new Set(['render']);
34
44
 
35
- case '--values-dir':
36
- opts.valuesDir = args[++i];
37
- break;
45
+ /**
46
+ * Run node:util parseArgs, turning its errors into UsageError with the
47
+ * first sentence of Node's message (the rest is generic advice). An option
48
+ * followed by another option (`-o --values x`) is reported as missing its
49
+ * value rather than taking `--values` as the output dir.
50
+ *
51
+ * @param {string[]} args
52
+ */
53
+ function tokenize(args) {
54
+ try {
55
+ return parseNodeArgs({
56
+ args,
57
+ options: OPTIONS,
58
+ strict: true,
59
+ allowPositionals: true,
60
+ tokens: true,
61
+ });
62
+ } catch (error) {
63
+ const err = /** @type {Error & { code?: string }} */ (error);
64
+ if (err.code?.startsWith('ERR_PARSE_ARGS_')) {
65
+ const first = err.message.split(/\.(?:\s|$)/)[0];
66
+ const ambiguous = first.match(/^Option '([^']+)' argument is ambiguous/);
67
+ throw new UsageError(
68
+ ambiguous ? `Option '${ambiguous[1]}' is missing its value` : first,
69
+ );
70
+ }
71
+ throw err;
72
+ }
73
+ }
38
74
 
39
- case '-o':
40
- case '--out':
41
- opts.outDir = args[++i];
42
- break;
75
+ /**
76
+ * Throw if any option appears more than once (no silent last-wins).
77
+ *
78
+ * @param {Array<{ kind: string, name?: string, rawName?: string }>} tokens
79
+ */
80
+ function assertNoRepeats(tokens) {
81
+ /** @type {Map<string, string>} */
82
+ const seen = new Map();
83
+ for (const t of tokens) {
84
+ if (t.kind !== 'option' || !t.name) {
85
+ continue;
86
+ }
87
+ if (seen.has(t.name)) {
88
+ throw new UsageError(
89
+ `Option '--${t.name}' given more than once (${seen.get(t.name)}, ${t.rawName})`,
90
+ );
91
+ }
92
+ seen.set(t.name, t.rawName ?? `--${t.name}`);
93
+ }
94
+ }
43
95
 
44
- case '-p':
45
- case '--partials-dir':
46
- opts.partialsDir = args[++i];
47
- break;
96
+ /**
97
+ * Parse CLI arguments (without the node binary and script path).
98
+ *
99
+ * Strict: unknown options, options missing their value, repeated options,
100
+ * and positional arguments other than one `render` command throw
101
+ * `UsageError`. Only options actually given appear in the result, so unset
102
+ * options never override config-file or default values.
103
+ *
104
+ * @param {string[]} args - e.g. `process.argv.slice(2)`
105
+ * @returns {import('../types.js').CliArgs}
106
+ * @throws {UsageError}
107
+ */
108
+ export function parseArgs(args) {
109
+ const { values, positionals, tokens } = tokenize(args);
110
+ assertNoRepeats(/** @type {any[]} */ (tokens));
48
111
 
49
- case '--config-file':
50
- opts.configFile = args[++i];
51
- break;
112
+ const [command = 'render', ...extra] = positionals;
113
+ if (!COMMANDS.has(command)) {
114
+ throw new UsageError(`Unknown command '${command}'`);
115
+ }
116
+ if (extra.length > 0) {
117
+ throw new UsageError(`Unexpected argument '${extra[0]}'`);
118
+ }
52
119
 
53
- case '-x':
54
- case '--ext':
55
- opts.extname = args[++i];
56
- break;
120
+ /** @type {import('../types.js').CliArgs} */
121
+ const opts = { command: values.help ? 'help' : command };
122
+ if (values.verbose) {
123
+ opts.verbose = true;
124
+ }
57
125
 
58
- case '--env-keys':
59
- opts.envKeys = args[++i]
60
- .split(',')
61
- .map((s) => s.trim())
62
- .filter(Boolean);
63
- break;
126
+ for (const [option, field] of Object.entries(FIELDS)) {
127
+ const value = values[option];
128
+ if (typeof value === 'string') {
129
+ /** @type {Record<string, unknown>} */ (opts)[field] = value;
130
+ }
131
+ }
64
132
 
65
- case '--env-prefix':
66
- opts.envPrefix = args[++i];
67
- break;
133
+ if (typeof values['env-keys'] === 'string') {
134
+ const keys = values['env-keys']
135
+ .split(',')
136
+ .map((s) => s.trim())
137
+ .filter(Boolean);
138
+ if (keys.length === 0) {
139
+ throw new UsageError(
140
+ "Option '--env-keys' needs at least one variable name",
141
+ );
68
142
  }
69
- // next argument:
70
- i++;
143
+ opts.envKeys = keys;
71
144
  }
72
145
 
73
146
  return opts;
package/src/cli/main.js CHANGED
@@ -2,40 +2,61 @@
2
2
 
3
3
  import { resolveConfig } from '../config/resolver.js';
4
4
  import { renderDirectory } from '../engine/renderDirectory.js';
5
- import { parseArgs } from './args.js';
5
+ import { parseArgs, UsageError } from './args.js';
6
6
  import { USAGE } from './usage.js';
7
7
 
8
- /** @returns {Promise<void>} */
9
- export async function main() {
10
- const cli = parseArgs(process.argv);
8
+ /**
9
+ * Parse arguments and run the command. Throws on any failure; `run` turns
10
+ * that into output and an exit code.
11
+ *
12
+ * @param {string[]} argv - Arguments without the node binary and script path
13
+ * @returns {Promise<void>}
14
+ */
15
+ export async function main(argv) {
16
+ const cli = parseArgs(argv);
11
17
 
12
18
  if (cli.command === 'help') {
13
19
  console.log(USAGE);
14
20
  return;
15
21
  }
16
22
 
17
- if (cli.command !== 'render') {
18
- console.error('Unknown command:', cli.command);
19
- process.exit(1);
20
- }
21
-
22
23
  const cfg = resolveConfig(cli);
23
-
24
24
  await renderDirectory(cfg);
25
-
26
25
  console.log('✔ js-tmpl completed.');
27
26
  }
28
27
 
29
- // Execute main function if this file is run directly
28
+ /**
29
+ * CLI error boundary. Prints `js-tmpl: <message>` (plus the stack with
30
+ * `--verbose`) and returns the exit code: 0 ok, 1 render/config error,
31
+ * 2 usage error.
32
+ *
33
+ * @param {string[]} argv - Arguments without the node binary and script path
34
+ * @returns {Promise<number>}
35
+ */
36
+ export async function run(argv) {
37
+ try {
38
+ await main(argv);
39
+ return 0;
40
+ } catch (error) {
41
+ const err = /** @type {Error} */ (error);
42
+ console.error(`js-tmpl: ${err.message}`);
43
+ if (err instanceof UsageError) {
44
+ console.error("Run 'js-tmpl --help' for usage.");
45
+ return 2;
46
+ }
47
+ if (argv.includes('--verbose')) {
48
+ console.error(err.stack);
49
+ }
50
+ return 1;
51
+ }
52
+ }
53
+
54
+ // Direct execution (`node src/cli/main.js …`). The installed CLI enters
55
+ // through bin/js-tmpl.js instead, which calls `run` itself.
30
56
  const isDirectRun =
31
57
  process.argv[1] &&
32
58
  import.meta.url.endsWith(process.argv[1].replaceAll('\\', '/'));
33
59
 
34
60
  if (isDirectRun) {
35
- try {
36
- await main();
37
- } catch (error) {
38
- console.error('Error:', error);
39
- process.exit(1);
40
- }
61
+ process.exitCode = await run(process.argv.slice(2));
41
62
  }
package/src/cli/usage.js CHANGED
@@ -14,4 +14,10 @@ Options:
14
14
  --config-file <file> Project config file
15
15
  --env-keys <keys> Comma-separated env var names to expose (default: none)
16
16
  --env-prefix <prefix> Auto-include env vars with this prefix (e.g. JS_TMPL_)
17
- -h, --help Show this help message`;
17
+ --verbose Print stack traces on error
18
+ -h, --help Show this help message
19
+
20
+ Exit codes:
21
+ 0 success
22
+ 1 render or configuration error
23
+ 2 usage error (unknown option, missing value, unexpected argument)`;
@@ -0,0 +1,93 @@
1
+ /**
2
+ * Bare-identifier rule for helper names: `{{name}}` must parse without
3
+ * bracket notation. Hyphens are allowed (`date-format` is idiomatic).
4
+ */
5
+ const HELPER_NAME_RE = /^[a-zA-Z_$][\w$-]*$/;
6
+
7
+ /**
8
+ * Describe a value's type for error messages (`null` and arrays included).
9
+ *
10
+ * @param {unknown} value
11
+ * @returns {string}
12
+ */
13
+ function describeType(value) {
14
+ if (value === null) {
15
+ return 'null';
16
+ }
17
+ if (Array.isArray(value)) {
18
+ return 'array';
19
+ }
20
+ return typeof value;
21
+ }
22
+
23
+ /**
24
+ * Throw if a single helper entry is invalid or collides on `hbs`.
25
+ *
26
+ * @param {string} name
27
+ * @param {unknown} fn
28
+ * @param {typeof import('handlebars')} hbs
29
+ */
30
+ function assertValidHelper(name, fn, hbs) {
31
+ if (!HELPER_NAME_RE.test(name)) {
32
+ throw new Error(
33
+ `Invalid helper name '${name}' — must start with a letter, underscore, or\n` +
34
+ 'dollar sign, and contain only letters, digits, underscores, dollars, or hyphens.\n' +
35
+ 'For exotic names, use hbs.registerHelper() directly.',
36
+ );
37
+ }
38
+ if (typeof fn !== 'function') {
39
+ throw new Error(
40
+ `Helper '${name}' must be a function, got ${describeType(fn)}.\n` +
41
+ 'Each value in helpersMap must be a callable function.',
42
+ );
43
+ }
44
+ if (Object.hasOwn(hbs.helpers, name)) {
45
+ throw new Error(
46
+ `Helper '${name}' is already registered on this Handlebars instance.\n` +
47
+ 'To intentionally override a built-in, use hbs.registerHelper() directly.',
48
+ );
49
+ }
50
+ }
51
+
52
+ /**
53
+ * Register custom helpers on a scoped Handlebars instance.
54
+ *
55
+ * Validates every entry before registering any (atomic): a map with one
56
+ * invalid entry registers nothing. Names must be bare identifiers
57
+ * (`/^[a-zA-Z_$][\w$-]*$/`), values must be functions, and a name already
58
+ * registered on `hbs` (built-ins included) throws. Skips silently if
59
+ * `helpersMap` is falsy or empty.
60
+ *
61
+ * Helpers must be pure: same arguments, same result. js-tmpl cannot enforce
62
+ * this; a helper reading the clock, randomness, env, or disk makes output
63
+ * non-deterministic.
64
+ *
65
+ * @param {typeof import('handlebars')} hbs - Handlebars instance to register on
66
+ * @param {Record<string, import('handlebars').HelperDelegate>} [helpersMap] - Helper name → function
67
+ * @returns {void}
68
+ */
69
+ export function registerHelpers(hbs, helpersMap) {
70
+ if (!hbs || typeof hbs.registerHelper !== 'function') {
71
+ throw new Error(
72
+ 'registerHelpers requires a Handlebars instance as its first argument.\n' +
73
+ 'Create one with Handlebars.create() and pass it to renderDirectory too.',
74
+ );
75
+ }
76
+ if (!helpersMap) {
77
+ return;
78
+ }
79
+ if (typeof helpersMap !== 'object' || Array.isArray(helpersMap)) {
80
+ throw new Error(
81
+ `helpersMap must be an object of name → function, got ${describeType(helpersMap)}.\n` +
82
+ 'Example: registerHelpers(hbs, { upper: (s) => s.toUpperCase() })',
83
+ );
84
+ }
85
+
86
+ const entries = Object.entries(helpersMap);
87
+ for (const [name, fn] of entries) {
88
+ assertValidHelper(name, fn, hbs);
89
+ }
90
+ for (const [name, fn] of entries) {
91
+ hbs.registerHelper(name, fn);
92
+ }
93
+ }
@@ -8,6 +8,63 @@ import { registerPartials } from './partials.js';
8
8
  import { renderPath } from './pathRenderer.js';
9
9
  import { walkTemplateTree } from './treeWalker.js';
10
10
 
11
+ /**
12
+ * Throw if `target` is not strictly inside `outDir` — a `${var}` value such
13
+ * as `../x` must never write outside the output directory.
14
+ *
15
+ * @param {string} target
16
+ * @param {string} outDir
17
+ * @param {string} relPath - Template path, for the error message
18
+ * @param {string} rendered - Rendered path, for the error message
19
+ */
20
+ function assertInsideOutDir(target, outDir, relPath, rendered) {
21
+ const rel = path.relative(path.resolve(outDir), path.resolve(target));
22
+ const escapes =
23
+ rel === '' || rel.split(path.sep)[0] === '..' || path.isAbsolute(rel);
24
+ if (escapes) {
25
+ const vars = [...relPath.matchAll(/\$\{([^}]+)\}/g)].map((m) => m[1]);
26
+ throw new Error(
27
+ `Template '${relPath}' renders to '${rendered}', which is outside outDir '${outDir}'.\n` +
28
+ (vars.length ? `Check the values of: ${vars.join(', ')}. ` : '') +
29
+ "Path values must not contain '..' segments.",
30
+ );
31
+ }
32
+ }
33
+
34
+ /**
35
+ * Map every template to its output path before anything is rendered or
36
+ * written, so an escaping path or two templates sharing a target fail
37
+ * before the first file is touched.
38
+ *
39
+ * @param {Array<{ relPath: string, absPath: string }>} files
40
+ * @param {import('../types.js').TemplateConfig} cfg
41
+ * @returns {Array<{ file: { relPath: string, absPath: string }, target: string }>}
42
+ */
43
+ function planTargets(files, cfg) {
44
+ const { outDir, view, extname } = cfg;
45
+ /** @type {Map<string, string>} */
46
+ const owners = new Map();
47
+
48
+ return files.map((file) => {
49
+ const rendered = renderPath(file.relPath, view).replace(
50
+ new RegExp(`${extname}$`),
51
+ '',
52
+ );
53
+ const target = path.join(outDir, rendered);
54
+ assertInsideOutDir(target, outDir, file.relPath, rendered);
55
+
56
+ const owner = owners.get(target);
57
+ if (owner) {
58
+ throw new Error(
59
+ `Templates '${owner}' and '${file.relPath}' both render to '${rendered}'.\n` +
60
+ 'Each output file must come from exactly one template; check the path values.',
61
+ );
62
+ }
63
+ owners.set(target, file.relPath);
64
+ return { file, target };
65
+ });
66
+ }
67
+
11
68
  /**
12
69
  * Main rendering orchestrator.
13
70
  * @param {import('../types.js').TemplateConfig} cfg
@@ -15,20 +72,14 @@ import { walkTemplateTree } from './treeWalker.js';
15
72
  * @returns {Promise<void>}
16
73
  */
17
74
  export async function renderDirectory(cfg, hbs) {
18
- const { templateDir, partialsDir, outDir, view, extname } = cfg;
75
+ const { templateDir, partialsDir, view, extname } = cfg;
19
76
 
20
77
  hbs = hbs || Handlebars.create();
21
78
  await registerPartials(partialsDir, extname, hbs);
22
79
 
23
80
  const files = await walkTemplateTree(templateDir, { ext: extname, view });
24
81
 
25
- for (const file of files) {
26
- const relRendered = renderPath(file.relPath, view);
27
- const target = path.join(
28
- outDir,
29
- relRendered.replace(new RegExp(`${extname}$`), ''),
30
- );
31
-
82
+ for (const { file, target } of planTargets(files, cfg)) {
32
83
  const content = await renderContent(file.absPath, view, hbs, file.relPath);
33
84
 
34
85
  await ensureDir(path.dirname(target));
package/src/index.js CHANGED
@@ -1,2 +1,3 @@
1
1
  export * from './config/resolver.js';
2
+ export * from './engine/helpers.js';
2
3
  export * from './engine/renderDirectory.js';
package/src/types.js CHANGED
@@ -10,6 +10,7 @@
10
10
  * @property {string} [valuesDir]
11
11
  * @property {string[]} [envKeys]
12
12
  * @property {string} [envPrefix]
13
+ * @property {boolean} [verbose] - CLI only: print stack traces on error
13
14
  */
14
15
 
15
16
  /**
package/bin/js-tmpl DELETED
@@ -1,3 +0,0 @@
1
- #!/usr/bin/env bash
2
- # CLI entry point for js-tmpl
3
- node "$(dirname "$0")/../src/cli/main.js" "$@"