@rhinostone/swig 2.1.0 → 2.3.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.
Files changed (101) hide show
  1. package/HISTORY.md +14 -0
  2. package/ROADMAP.md +11 -0
  3. package/bin/swig.js +272 -87
  4. package/dist/swig.js +120 -41
  5. package/dist/swig.min.js +2 -2
  6. package/dist/swig.min.js.map +1 -1
  7. package/lib/filters.js +23 -20
  8. package/lib/swig.js +13 -2
  9. package/lib/tags/import.js +42 -21
  10. package/lib/tags/include.js +4 -0
  11. package/package.json +3 -4
  12. package/.changes/unreleased/.gitkeep +0 -0
  13. package/.changes/v0.1.2.md +0 -6
  14. package/.changes/v0.1.3.md +0 -6
  15. package/.changes/v0.1.5.md +0 -11
  16. package/.changes/v0.1.6.md +0 -6
  17. package/.changes/v0.1.7.md +0 -7
  18. package/.changes/v0.1.8.md +0 -7
  19. package/.changes/v0.1.9.md +0 -7
  20. package/.changes/v0.10.0.md +0 -11
  21. package/.changes/v0.11.0.md +0 -15
  22. package/.changes/v0.11.1.md +0 -6
  23. package/.changes/v0.11.2.md +0 -6
  24. package/.changes/v0.12.0.md +0 -7
  25. package/.changes/v0.12.1.md +0 -11
  26. package/.changes/v0.13.0.md +0 -8
  27. package/.changes/v0.13.1.md +0 -8
  28. package/.changes/v0.13.2.md +0 -9
  29. package/.changes/v0.13.3.md +0 -5
  30. package/.changes/v0.13.4.md +0 -5
  31. package/.changes/v0.13.5.md +0 -4
  32. package/.changes/v0.14.0.md +0 -6
  33. package/.changes/v0.2.0.md +0 -7
  34. package/.changes/v0.2.1.md +0 -6
  35. package/.changes/v0.2.2.md +0 -6
  36. package/.changes/v0.2.3.md +0 -10
  37. package/.changes/v0.3.0.md +0 -6
  38. package/.changes/v0.4.0.md +0 -11
  39. package/.changes/v0.5.0.md +0 -11
  40. package/.changes/v0.6.0.md +0 -12
  41. package/.changes/v0.6.1.md +0 -6
  42. package/.changes/v0.7.0.md +0 -7
  43. package/.changes/v0.8.0.md +0 -18
  44. package/.changes/v0.9.0.md +0 -13
  45. package/.changes/v0.9.1.md +0 -6
  46. package/.changes/v0.9.2.md +0 -6
  47. package/.changes/v0.9.3.md +0 -6
  48. package/.changes/v0.9.4.md +0 -10
  49. package/.changes/v1.0.0-pre1.md +0 -22
  50. package/.changes/v1.0.0-pre2.md +0 -18
  51. package/.changes/v1.0.0-pre3.md +0 -7
  52. package/.changes/v1.0.0-rc1.md +0 -12
  53. package/.changes/v1.0.0-rc2.md +0 -10
  54. package/.changes/v1.0.0-rc3.md +0 -7
  55. package/.changes/v1.0.0.md +0 -7
  56. package/.changes/v1.1.0.md +0 -6
  57. package/.changes/v1.2.0.md +0 -10
  58. package/.changes/v1.2.1.md +0 -4
  59. package/.changes/v1.2.2.md +0 -4
  60. package/.changes/v1.3.0.md +0 -14
  61. package/.changes/v1.3.2.md +0 -6
  62. package/.changes/v1.4.0.md +0 -13
  63. package/.changes/v1.4.1.md +0 -5
  64. package/.changes/v1.4.2.md +0 -10
  65. package/.changes/v1.4.3.md +0 -4
  66. package/.changes/v1.4.4.md +0 -6
  67. package/.changes/v1.4.5.md +0 -10
  68. package/.changes/v1.4.6.md +0 -6
  69. package/.changes/v1.4.7.md +0 -8
  70. package/.changes/v1.5.0.md +0 -4
  71. package/.changes/v1.6.0.md +0 -4
  72. package/.changes/v2.0.0-alpha.1.md +0 -4
  73. package/.changes/v2.0.0-alpha.2.md +0 -4
  74. package/.changes/v2.0.0-alpha.4.md +0 -10
  75. package/.changes/v2.0.0-alpha.5.md +0 -16
  76. package/.changes/v2.0.0-alpha.6.md +0 -4
  77. package/.changes/v2.0.0-alpha.7.md +0 -4
  78. package/.changes/v2.0.0-alpha.8.md +0 -4
  79. package/.changes/v2.0.0.md +0 -6
  80. package/.changes/v2.0.1.md +0 -8
  81. package/.changes/v2.1.0.md +0 -8
  82. package/.playwright-mcp/console-2026-04-22T15-34-20-480Z.log +0 -2
  83. package/.playwright-mcp/console-2026-04-22T15-35-04-265Z.log +0 -11
  84. package/.playwright-mcp/console-2026-04-22T15-37-26-953Z.log +0 -1
  85. package/.playwright-mcp/console-2026-04-22T15-51-15-160Z.log +0 -148
  86. package/.playwright-mcp/console-2026-04-22T15-51-33-405Z.log +0 -74
  87. package/.playwright-mcp/console-2026-04-22T15-51-53-922Z.log +0 -74
  88. package/.playwright-mcp/console-2026-04-22T15-53-10-736Z.log +0 -74
  89. package/.playwright-mcp/console-2026-04-22T15-53-40-091Z.log +0 -883
  90. package/.playwright-mcp/console-2026-04-22T16-12-02-541Z.log +0 -74
  91. package/.playwright-mcp/console-2026-04-22T16-33-44-982Z.log +0 -13973
  92. package/.playwright-mcp/page-2026-04-22T15-34-24-524Z.yml +0 -1
  93. package/.playwright-mcp/page-2026-04-22T15-35-04-346Z.yml +0 -0
  94. package/.playwright-mcp/page-2026-04-22T15-37-27-039Z.yml +0 -5
  95. package/.playwright-mcp/page-2026-04-22T15-51-15-600Z.yml +0 -76
  96. package/.playwright-mcp/page-2026-04-22T15-51-33-605Z.yml +0 -0
  97. package/.playwright-mcp/page-2026-04-22T15-51-54-206Z.yml +0 -676
  98. package/.playwright-mcp/page-2026-04-22T15-53-11-277Z.yml +0 -632
  99. package/.playwright-mcp/page-2026-04-22T15-53-40-297Z.yml +0 -0
  100. package/.playwright-mcp/page-2026-04-22T16-12-02-855Z.yml +0 -0
  101. package/.playwright-mcp/page-2026-04-22T16-33-45-281Z.yml +0 -0
package/HISTORY.md CHANGED
@@ -1,3 +1,17 @@
1
+ [2.3.0](https://github.com/gina-io/swig/tree/v2.3.0) / 2026-05-14
2
+ -----------------------------------------------------------------
3
+
4
+ * **Changed** Drop `yargs` and `terser` from production `dependencies`. CLI argument parsing now uses a small built-in zero-dependency parser; `terser` (used only by `swig compile --minify`) is loaded lazily and moves to `devDependencies`. A library install of `@rhinostone/swig` now pulls in only `@rhinostone/swig-core`.
5
+
6
+ [2.2.0](https://github.com/gina-io/swig/tree/v2.2.0) / 2026-05-11
7
+ -----------------------------------------------------------------
8
+
9
+ * **Added** `renderFile(path, locals, cb)` and `compileFile(path, options, cb)` automatically route to the async-codegen path when the configured loader signals async support via `loader.async === true`. The async path defers template resolution from parse time to render time via a new `_swig.getTemplate(path, options)` runtime helper that returns `Promise<TemplateFn>` — `extends`, `include`, `import`, and `from` emit deferred IR shapes from the frontend (`IRExtendsDeferred`, `IRIncludeDeferred`, `IRImportDeferred`, `IRFromImportDeferred`) and the shared backend wraps the compiled body in an `AsyncFunction`. Block overrides thread through the inheritance chain via a sixth `_blocks` positional argument on the wrapped template function; macro imports pick up exports via the new `Promise<{output, exports}>` template-fn return shape. Both `@rhinostone/swig` and `@rhinostone/swig-twig` flavors — parity across the two surfaces. Static template targets (string literals in `extends` / `include` / `import` / `from`) work end-to-end against async loaders; dynamic targets (`{% extends parent_var %}`, `{% include user_template %}`) are not yet supported on the async path and surface a clear runtime error with the unresolved expression visible in the message — full dynamic-target support is tracked as a follow-up. The sync render path is unchanged — loaders without `loader.async === true` continue to use the established sync `_swig.compileFile(...)` resolution, including the built-in `loaders.fs` and `loaders.memory` which remain dual-mode. End-to-end coverage at `tests/async/render-file-cb-dispatch.test.js` (native, 11 cases) and `tests/swig-twig/async/render-file-cb-dispatch.test.js` (Twig, 11 cases) covers static extends chains, includes, macro imports, mixed graphs, dynamic include paths surfacing the runtime error, the `ignoreMissing` flag, `with-context` isolation, bare-name and aliased `from` import, and error propagation.
10
+
11
+ * **Changed** `renderFileAsync(path, locals, cb)` and `compileFileAsync(path, options, cb)` on both `@rhinostone/swig` and `@rhinostone/swig-twig` are soft-deprecated via JSDoc only — no runtime warning, no `console.warn`. Use `renderFile(path, locals, cb)` / `compileFile(path, options, cb)` with an async loader (`loader.async === true`) instead; the dispatch is automatic. The legacy pre-walker entry points remain fully functional in 2.x and will be removed in 3.0.
12
+
13
+ * **Changed** Improved the performance of the `escape` / `e` filter in both `@rhinostone/swig` and `@rhinostone/swig-twig`. The HTML default branch now runs a two-pass form — an entity-aware first pass that preserves already-escaped sequences (`&amp;`, `&lt;`, `&gt;`, `&quot;`, `&#39;`) followed by a single character-class regex `[<>"']` with a lookup function for the rest — instead of five sequential single-character regex passes. A scalar fast-path also skips `iterateFilter.apply` when input is null, undefined, or a non-object. Output is byte-identical to the previous behavior on every input, including the entity-preservation semantics swig has shipped since the upstream fork. Measured against `benchmarks/render.js` (medians of 5 runs, autoescape on, Node 25), simple-var-output goes from ~2.46M to 3.86M ops/s (+57%, flipping the verdict from `nunjucks 1.32x faster` to `swig 1.18x faster`); filter chain +37%; for-loop (5 items) +54%; if/else branch +71%; nested for+if+filter +56%. The `case 'js'` branch and array-iteration fallback through `iterateFilter` are unchanged. All 1443 tests pass including the 9 CVE regressions.
14
+
1
15
  [2.1.0](https://github.com/gina-io/swig/tree/v2.1.0) / 2026-05-10
2
16
  -----------------------------------------------------------------
3
17
 
package/ROADMAP.md CHANGED
@@ -14,6 +14,7 @@ _No near-term scheduled items. See [Future (post-2.0)](#future-post-20) for upco
14
14
 
15
15
  | Status | Item |
16
16
  | --- | --- |
17
+ | Planned | Async parse path for dynamic targets — full support for `{% extends parent_var %}`, `{% include user_template %}`, and runtime-resolved `import` / `from` paths on the async-codegen branch. Static-target async dispatch shipped in 2.2.0; dynamic-target support is on hold pending consumer demand. |
17
18
  | Planned | Ship Jinja2 and Django frontends as additional `@rhinostone/swig-*` packages. On demand — when there's concrete user demand. |
18
19
  | Planned | Test framework migration. Replace mocha 1.x + expect.js with `node:test` + `node:assert/strict`, swap mocha-phantomjs for a modern browser-test harness, swap blanket for `c8`. (The Node engines bump is upstream-driven by gina and is being treated as done.) |
19
20
 
@@ -21,6 +22,16 @@ _No near-term scheduled items. See [Future (post-2.0)](#future-post-20) for upco
21
22
 
22
23
  ## Completed
23
24
 
25
+ ### v2.3.0 (May 2026)
26
+
27
+ - Reduced `@rhinostone/swig`'s production dependency footprint to a single package. `yargs` and `terser` were CLI-only — the library entry point never loaded them — but sat in production `dependencies`, so every library install pulled in their full dependency trees. The CLI's argument parsing is now handled by a small built-in zero-dependency parser; `terser` (used only by `swig compile --minify`) is loaded lazily and ships as a `devDependency`, with `--minify` printing an install hint instead of crashing if it is absent. A library install of `@rhinostone/swig` now pulls in only `@rhinostone/swig-core`. No change to the CLI surface or rendering behavior.
28
+
29
+ ### v2.2.0 (May 2026)
30
+
31
+ - `renderFile(path, locals, cb)` and `compileFile(path, options, cb)` now automatically route to the async-codegen path when the configured loader signals async support via `loader.async === true`. The async path defers template resolution from parse time to render time via a new `_swig.getTemplate(path, options)` runtime helper that returns `Promise<TemplateFn>`; `extends`, `include`, `import`, and `from` emit deferred IR shapes and the shared backend wraps the compiled body in an `AsyncFunction`. Block overrides thread through the inheritance chain via a sixth `_blocks` positional argument; macro imports pick up exports via a `Promise<{output, exports}>` template-fn return shape. Both `@rhinostone/swig` and `@rhinostone/swig-twig` flavors — parity across the two surfaces. Static template targets (string literals in `extends` / `include` / `import` / `from`) work end-to-end against async loaders; dynamic targets surface a clear runtime error and are tracked as a follow-up. The sync render path is unchanged — loaders without `loader.async === true` continue to use the established sync `_swig.compileFile(...)` resolution, including the built-in `loaders.fs` and `loaders.memory` which remain dual-mode.
32
+ - `renderFileAsync(path, locals, cb)` and `compileFileAsync(path, options, cb)` on both `@rhinostone/swig` and `@rhinostone/swig-twig` are soft-deprecated via JSDoc only — no runtime warning. Use `renderFile` / `compileFile` with an async loader (`loader.async === true`) instead; the dispatch is automatic. The legacy pre-walker entry points remain fully functional in 2.x and will be removed in 3.0.
33
+ - Performance improvement to the `escape` / `e` filter in both flavors. The HTML default branch switched from a five-replace chain to an entity-preserving two-pass form (entity-aware first pass that preserves already-escaped sequences, followed by a single character-class regex with a lookup function). A scalar fast-path skips the array/object iteration when input is null, undefined, or a non-object. Output is byte-identical to the previous behavior on every input. Measured against `benchmarks/render.js` (medians of 5 runs, autoescape on, Node 25): simple-var-output `+57%` (flipping the bench verdict from `nunjucks 1.32x faster` to `swig 1.18x faster`); filter chain `+37%`; for-loop (5 items) `+54%`; if/else branch `+71%`; nested for+if+filter `+56%`.
34
+
24
35
  ### v2.1.0 (May 2026)
25
36
 
26
37
  - Async loader support via `renderFileAsync(path, locals, cb)` and `compileFileAsync(path, options, cb)` on `@rhinostone/swig` and `@rhinostone/swig-twig`. The implementation pre-walks the template dependency graph through the user loader's cb-shape arm, builds an in-memory map keyed by resolved path, then runs the existing sync render against an in-memory wrapper for the duration of the call. Supports `extends`, `include`, `import`, and Twig `from import` with string-literal paths; dynamic paths surface a `Pre-walked map missing path` error at render time. Existing sync `renderFile` / `compileFile` consumers are unaffected.
package/bin/swig.js CHANGED
@@ -2,109 +2,73 @@
2
2
  /*jslint es5: true */
3
3
 
4
4
  var swig = require('../index'),
5
- yargs = require('yargs'),
6
5
  fs = require('fs'),
7
6
  path = require('path'),
8
7
  filters = require('../lib/filters'),
9
- utils = require('../lib/utils'),
10
- terser = require('terser');
11
-
12
- var command,
13
- wrapstart = 'var tpl = ',
14
- wrapend = ';',
15
- argv = yargs
16
- .usage('\n Usage:\n' +
17
- ' $0 compile [files] [options]\n' +
18
- ' $0 compile --recursive <dir> [options]\n' +
19
- ' $0 run [files] [options]\n' +
20
- ' $0 render [files] [options]\n'
21
- )
22
- .describe({
23
- v: 'Show the Swig version number.',
24
- o: 'Output location.',
25
- h: 'Show this help screen.',
26
- j: 'Variable context as a JSON file.',
27
- c: 'Variable context as a CommonJS-style file. Used only if option `j` is not provided.',
28
- m: 'Minify compiled functions with terser',
29
- r: 'Recursively compile every template in <dir> into a single AOT bundle module.',
30
- 'ext': 'Comma-separated list of file extensions to include when using --recursive (e.g. ".html,.swig"). Defaults to no filter.',
31
- 'filters': 'Custom filters as a CommonJS-style file',
32
- 'tags': 'Custom tags as a CommonJS-style file',
33
- 'options': 'Customize Swig\'s Options from a CommonJS-style file',
34
- 'wrap-start': 'Template wrapper beginning for "compile".',
35
- 'wrap-end': 'Template wrapper end for "compile".',
36
- 'method-name': 'Method name to set template to and run from.'
37
- })
38
- .alias('v', 'version')
39
- .alias('o', 'output')
40
- .default('o', 'stdout')
41
- .alias('h', 'help')
42
- .alias('j', 'json')
43
- .alias('c', 'context')
44
- .alias('m', 'minify')
45
- .alias('r', 'recursive')
46
- .default('wrap-start', wrapstart)
47
- .default('wrap-end', wrapend)
48
- .default('method-name', 'tpl')
49
- .check(function (argv) {
50
- if (argv.v) {
51
- return true;
52
- }
53
-
54
- if (!argv._.length) {
55
- throw new Error('');
56
- }
8
+ utils = require('../lib/utils');
57
9
 
58
- command = argv._.shift();
59
- if (command !== 'compile' && command !== 'render' && command !== 'run') {
60
- throw new Error('Unrecognized command "' + command + '". Use -h for help.');
61
- }
10
+ var wrapstart = 'var tpl = ',
11
+ wrapend = ';';
62
12
 
63
- if (argv['method-name'] !== 'tpl' && argv['wrap-start'] !== wrapstart) {
64
- throw new Error('Cannot use arguments "--method-name" and "--wrap-start" together.');
65
- }
66
-
67
- if (argv['method-name'] !== 'tpl') {
68
- argv['wrap-start'] = 'var ' + argv['method-name'] + ' = ';
69
- }
70
-
71
- if (argv.r) {
72
- if (command !== 'compile') {
73
- throw new Error('--recursive can only be used with "compile".');
74
- }
75
- if (argv._.length) {
76
- throw new Error('--recursive does not accept positional file arguments; pass a single directory via --recursive <dir>.');
77
- }
78
- if (argv['method-name'] !== 'tpl') {
79
- throw new Error('--recursive cannot be combined with --method-name; the bundle exports a map of templates, not a single named function.');
80
- }
81
- if (argv['wrap-start'] !== wrapstart || argv['wrap-end'] !== wrapend) {
82
- throw new Error('--recursive cannot be combined with --wrap-start / --wrap-end; the bundle wrapper is fixed.');
83
- }
84
- }
85
-
86
- if (argv.ext && !argv.r) {
87
- throw new Error('--ext is only meaningful with --recursive.');
88
- }
89
-
90
- return true;
91
- })
92
- .argv,
13
+ /**
14
+ * CLI flag table. Each entry is keyed by the canonical name the rest of this
15
+ * file reads. <code>alias</code> is the long-form name folded onto that key,
16
+ * <code>boolean</code> marks flags that never consume the following token,
17
+ * and <code>default</code> seeds a value when the flag is absent.
18
+ *
19
+ * @private
20
+ */
21
+ var FLAGS = {
22
+ 'v': { alias: 'version', boolean: true },
23
+ 'o': { alias: 'output', default: 'stdout' },
24
+ 'h': { alias: 'help', boolean: true },
25
+ 'j': { alias: 'json' },
26
+ 'c': { alias: 'context' },
27
+ 'm': { alias: 'minify', boolean: true },
28
+ 'r': { alias: 'recursive' },
29
+ 'ext': {},
30
+ 'filters': {},
31
+ 'tags': {},
32
+ 'options': {},
33
+ 'wrap-start': { default: wrapstart },
34
+ 'wrap-end': { default: wrapend },
35
+ 'method-name': { default: 'tpl' }
36
+ };
37
+
38
+ var argv = parseArgs(process.argv.slice(2)),
39
+ command,
93
40
  ctx = {},
94
41
  out = function (file, str) {
95
42
  console.log(str);
96
43
  },
97
44
  efn = function () {},
98
- anonymous,
99
- files,
100
45
  fn;
101
46
 
47
+ // Show this help screen.
48
+ if (argv.h) {
49
+ console.log(usage());
50
+ process.exit(0);
51
+ }
52
+
102
53
  // What version?
103
54
  if (argv.v) {
104
55
  console.log(require('../package').version);
105
56
  process.exit(0);
106
57
  }
107
58
 
59
+ // Validate the parsed arguments and resolve the subcommand. On any invalid
60
+ // combination, print the message (when there is one) plus the usage screen
61
+ // and exit non-zero — the behaviour the former yargs `.check()` gate had.
62
+ try {
63
+ command = validate(argv);
64
+ } catch (e) {
65
+ if (e.message) {
66
+ console.error(e.message);
67
+ }
68
+ console.error(usage());
69
+ process.exit(1);
70
+ }
71
+
108
72
  // Pull in any context data provided
109
73
  if (argv.j) {
110
74
  ctx = JSON.parse(fs.readFileSync(argv.j, 'utf8'));
@@ -158,7 +122,7 @@ case 'compile':
158
122
  r = argv['wrap-start'] + r + argv['wrap-end'];
159
123
 
160
124
  if (argv.m) {
161
- r = terser.minify_sync(r).code;
125
+ r = loadTerser().minify_sync(r).code;
162
126
  }
163
127
 
164
128
  out(file, r);
@@ -191,6 +155,227 @@ if (argv.r) {
191
155
  });
192
156
  }
193
157
 
158
+ /**
159
+ * Parse a <code>process.argv</code> tail into a flag / positional map.
160
+ * Supports <code>--name</code>, <code>--name=value</code>,
161
+ * <code>--name value</code>, <code>-x</code>, <code>-x value</code>,
162
+ * <code>-x=value</code>, and a <code>--</code> terminator after which every
163
+ * token is positional. Long-form aliases are folded onto their canonical
164
+ * {@link FLAGS} key; boolean flags never consume the following token. Bare
165
+ * tokens collect into <code>argv._</code>. Unknown flags are tolerated and
166
+ * kept verbatim, matching the former non-strict yargs behaviour.
167
+ *
168
+ * @param {string[]} args <code>process.argv.slice(2)</code>.
169
+ * @return {object} <code>{ _: [positionals], &lt;flag&gt;: value }</code>.
170
+ * @private
171
+ */
172
+ function parseArgs(args) {
173
+ var argv = { _: [] },
174
+ i,
175
+ token,
176
+ body,
177
+ eq,
178
+ name,
179
+ canonical,
180
+ value,
181
+ key;
182
+
183
+ for (i = 0; i < args.length; i += 1) {
184
+ token = args[i];
185
+
186
+ if (token === '--') {
187
+ argv._ = argv._.concat(args.slice(i + 1));
188
+ break;
189
+ }
190
+
191
+ if (token.slice(0, 2) === '--') {
192
+ body = token.slice(2);
193
+ } else if (token.charAt(0) === '-' && token.length > 1) {
194
+ body = token.slice(1);
195
+ } else {
196
+ argv._.push(token);
197
+ continue;
198
+ }
199
+
200
+ eq = body.indexOf('=');
201
+ if (eq !== -1) {
202
+ name = body.slice(0, eq);
203
+ value = body.slice(eq + 1);
204
+ } else {
205
+ name = body;
206
+ value = undefined;
207
+ }
208
+
209
+ canonical = aliasToCanonical(name);
210
+
211
+ if (value === undefined) {
212
+ if (FLAGS[canonical] && FLAGS[canonical].boolean) {
213
+ value = true;
214
+ } else if (i + 1 < args.length && !isFlagToken(args[i + 1])) {
215
+ i += 1;
216
+ value = args[i];
217
+ } else {
218
+ value = true;
219
+ }
220
+ }
221
+
222
+ argv[canonical] = value;
223
+ }
224
+
225
+ // Seed defaults for any flag the caller did not pass.
226
+ for (key in FLAGS) {
227
+ if (FLAGS.hasOwnProperty(key) && FLAGS[key].hasOwnProperty('default') && !argv.hasOwnProperty(key)) {
228
+ argv[key] = FLAGS[key].default;
229
+ }
230
+ }
231
+
232
+ return argv;
233
+ }
234
+
235
+ /**
236
+ * Resolve a flag name to its canonical {@link FLAGS} key. A name that is
237
+ * already canonical, or that matches no known alias, is returned unchanged.
238
+ *
239
+ * @param {string} name Flag name as written on the command line.
240
+ * @return {string} Canonical key.
241
+ * @private
242
+ */
243
+ function aliasToCanonical(name) {
244
+ var key;
245
+ if (FLAGS.hasOwnProperty(name)) {
246
+ return name;
247
+ }
248
+ for (key in FLAGS) {
249
+ if (FLAGS.hasOwnProperty(key) && FLAGS[key].alias === name) {
250
+ return key;
251
+ }
252
+ }
253
+ return name;
254
+ }
255
+
256
+ /**
257
+ * Is <var>token</var> a flag rather than a value? Used to decide whether a
258
+ * value-taking flag should consume the next token.
259
+ *
260
+ * @param {string} token Candidate token.
261
+ * @return {boolean} True when the token looks like a flag.
262
+ * @private
263
+ */
264
+ function isFlagToken(token) {
265
+ return token.charAt(0) === '-' && token.length > 1;
266
+ }
267
+
268
+ /**
269
+ * Validate the parsed arguments and resolve the subcommand. Mirrors the gate
270
+ * the former yargs <code>.check()</code> chain enforced — same checks, same
271
+ * messages, same <code>--method-name</code> &rarr; <code>--wrap-start</code>
272
+ * rewrite. Throws on any invalid combination; the caller prints the message
273
+ * plus the usage screen and exits non-zero.
274
+ *
275
+ * @param {object} argv Parsed argv from {@link parseArgs}.
276
+ * @return {string} The resolved subcommand: compile, render, or run.
277
+ * @private
278
+ */
279
+ function validate(argv) {
280
+ var cmd;
281
+
282
+ if (!argv._.length) {
283
+ throw new Error('');
284
+ }
285
+
286
+ cmd = argv._.shift();
287
+ if (cmd !== 'compile' && cmd !== 'render' && cmd !== 'run') {
288
+ throw new Error('Unrecognized command "' + cmd + '". Use -h for help.');
289
+ }
290
+
291
+ if (argv['method-name'] !== 'tpl' && argv['wrap-start'] !== wrapstart) {
292
+ throw new Error('Cannot use arguments "--method-name" and "--wrap-start" together.');
293
+ }
294
+
295
+ if (argv['method-name'] !== 'tpl') {
296
+ argv['wrap-start'] = 'var ' + argv['method-name'] + ' = ';
297
+ }
298
+
299
+ if (argv.r) {
300
+ if (cmd !== 'compile') {
301
+ throw new Error('--recursive can only be used with "compile".');
302
+ }
303
+ if (argv._.length) {
304
+ throw new Error('--recursive does not accept positional file arguments; pass a single directory via --recursive <dir>.');
305
+ }
306
+ if (argv['method-name'] !== 'tpl') {
307
+ throw new Error('--recursive cannot be combined with --method-name; the bundle exports a map of templates, not a single named function.');
308
+ }
309
+ if (argv['wrap-start'] !== wrapstart || argv['wrap-end'] !== wrapend) {
310
+ throw new Error('--recursive cannot be combined with --wrap-start / --wrap-end; the bundle wrapper is fixed.');
311
+ }
312
+ }
313
+
314
+ if (argv.ext && !argv.r) {
315
+ throw new Error('--ext is only meaningful with --recursive.');
316
+ }
317
+
318
+ return cmd;
319
+ }
320
+
321
+ /**
322
+ * Build the CLI usage screen — the command synopsis plus the option table.
323
+ *
324
+ * @return {string} The full usage text.
325
+ * @private
326
+ */
327
+ function usage() {
328
+ return [
329
+ '',
330
+ ' Usage:',
331
+ ' swig compile [files] [options]',
332
+ ' swig compile --recursive <dir> [options]',
333
+ ' swig run [files] [options]',
334
+ ' swig render [files] [options]',
335
+ '',
336
+ ' Options:',
337
+ ' -v, --version Show the Swig version number.',
338
+ ' -o, --output Output location.',
339
+ ' -h, --help Show this help screen.',
340
+ ' -j, --json Variable context as a JSON file.',
341
+ ' -c, --context Variable context as a CommonJS-style file. Used only if option `j` is not provided.',
342
+ ' -m, --minify Minify compiled functions with terser',
343
+ ' -r, --recursive Recursively compile every template in <dir> into a single AOT bundle module.',
344
+ ' --ext Comma-separated list of file extensions to include when using --recursive (e.g. ".html,.swig"). Defaults to no filter.',
345
+ ' --filters Custom filters as a CommonJS-style file',
346
+ ' --tags Custom tags as a CommonJS-style file',
347
+ ' --options Customize Swig\'s Options from a CommonJS-style file',
348
+ ' --wrap-start Template wrapper beginning for "compile".',
349
+ ' --wrap-end Template wrapper end for "compile".',
350
+ ' --method-name Method name to set template to and run from.',
351
+ ''
352
+ ].join('\n');
353
+ }
354
+
355
+ /**
356
+ * Lazily load the optional <code>terser</code> package, used only by the
357
+ * <code>--minify</code> flag. terser is a CLI-only dependency — the library
358
+ * entry point never needs it — so it ships as a devDependency rather than a
359
+ * runtime one, and a plain <code>npm install @rhinostone/swig</code> does not
360
+ * pull it in. Print a friendly install hint and exit non-zero if a CLI user
361
+ * reaches <code>--minify</code> without it.
362
+ *
363
+ * @return {object} The terser module.
364
+ * @private
365
+ */
366
+ function loadTerser() {
367
+ try {
368
+ return require('terser');
369
+ } catch (e) {
370
+ if (e.code !== 'MODULE_NOT_FOUND') {
371
+ throw e;
372
+ }
373
+ console.error('The --minify flag needs the "terser" package, which is not installed.');
374
+ console.error('Install it with: npm install terser');
375
+ process.exit(1);
376
+ }
377
+ }
378
+
194
379
  /**
195
380
  * Walk a directory recursively, returning every regular-file path found.
196
381
  * Skips dotfile entries and dot-directories so platform metadata such as
@@ -268,7 +453,7 @@ function bundleRecursive(dir) {
268
453
  output = 'module.exports = {\n' + parts.join(',\n') + '\n};\n';
269
454
 
270
455
  if (argv.m) {
271
- output = terser.minify_sync(output).code;
456
+ output = loadTerser().minify_sync(output).code;
272
457
  }
273
458
 
274
459
  if (argv.o === 'stdout') {