@creationix/rex 0.4.1 → 0.6.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/rex-cli.ts CHANGED
@@ -1,5 +1,6 @@
1
- import { compile, parse, parseToIR, stringify } from "./rex.ts";
2
- import { evaluateSource } from "./rexc-interpreter.ts";
1
+ import { compile, parse, parseToIR, stringify, grammar } from "./rex.ts";
2
+ import { evaluateRexc, evaluateSource } from "./rexc-interpreter.ts";
3
+ import { highlightLine, highlightJSON, highlightAuto, setColorEnabled } from "./rex-repl.ts";
3
4
  import { dirname, resolve } from "node:path";
4
5
  import { readFile, writeFile } from "node:fs/promises";
5
6
 
@@ -14,6 +15,16 @@ type CliOptions = {
14
15
  dedupeValues: boolean;
15
16
  dedupeMinBytes?: number;
16
17
  help: boolean;
18
+ cat: boolean;
19
+ showSource: boolean;
20
+ showExpr: boolean;
21
+ showIR: boolean;
22
+ showRexc: boolean;
23
+ showVars: boolean;
24
+ color: boolean;
25
+ colorExplicit: boolean;
26
+ showExprExplicit: boolean;
27
+ format: "rex" | "json";
17
28
  };
18
29
 
19
30
  function parseArgs(argv: string[]): CliOptions {
@@ -24,6 +35,16 @@ function parseArgs(argv: string[]): CliOptions {
24
35
  minifyNames: false,
25
36
  dedupeValues: false,
26
37
  help: false,
38
+ cat: false,
39
+ showSource: false,
40
+ showExpr: true,
41
+ showIR: false,
42
+ showRexc: false,
43
+ showVars: false,
44
+ color: process.stdout.isTTY,
45
+ colorExplicit: false,
46
+ showExprExplicit: false,
47
+ format: "rex",
27
48
  };
28
49
  for (let index = 0; index < argv.length; index += 1) {
29
50
  const arg = argv[index];
@@ -32,6 +53,58 @@ function parseArgs(argv: string[]): CliOptions {
32
53
  options.help = true;
33
54
  continue;
34
55
  }
56
+ if (arg === "--cat") {
57
+ options.cat = true;
58
+ continue;
59
+ }
60
+ if (arg === "--show-source") {
61
+ options.showSource = true;
62
+ continue;
63
+ }
64
+ if (arg === "--show-expr") {
65
+ options.showExpr = true;
66
+ options.showExprExplicit = true;
67
+ continue;
68
+ }
69
+ if (arg === "--no-expr") {
70
+ options.showExpr = false;
71
+ options.showExprExplicit = true;
72
+ continue;
73
+ }
74
+ if (arg === "--show-ir") {
75
+ options.showIR = true;
76
+ continue;
77
+ }
78
+ if (arg === "--show-rexc") {
79
+ options.showRexc = true;
80
+ continue;
81
+ }
82
+ if (arg === "--show-vars") {
83
+ options.showVars = true;
84
+ continue;
85
+ }
86
+ if (arg === "--json") {
87
+ options.format = "json";
88
+ continue;
89
+ }
90
+ if (arg === "--format") {
91
+ const value = argv[index + 1];
92
+ if (!value) throw new Error("Missing value for --format");
93
+ if (value !== "rex" && value !== "json") throw new Error("--format must be 'rex' or 'json'");
94
+ options.format = value;
95
+ index += 1;
96
+ continue;
97
+ }
98
+ if (arg === "--color") {
99
+ options.color = true;
100
+ options.colorExplicit = true;
101
+ continue;
102
+ }
103
+ if (arg === "--no-color") {
104
+ options.color = false;
105
+ options.colorExplicit = true;
106
+ continue;
107
+ }
35
108
  if (arg === "--compile" || arg === "-c") {
36
109
  options.compile = true;
37
110
  continue;
@@ -112,9 +185,26 @@ function usage() {
112
185
  " They are concatenated in the order they appear on the command line.",
113
186
  "",
114
187
  "Output mode:",
115
- " (default) Evaluate and output result as JSON",
116
- " -c, --compile Compile to rexc bytecode",
117
- " --ir Output lowered IR as JSON",
188
+ " (default) Evaluate and output result",
189
+ " -c, --compile Show rexc only (same as --show-rexc --no-expr)",
190
+ " --ir Show IR only (same as --show-ir --no-expr)",
191
+ " --cat Print input with Rex highlighting",
192
+ " --show-source Show input source text",
193
+ " --show-expr Show expression result",
194
+ " --no-expr Hide expression result",
195
+ " --show-ir Show IR JSON",
196
+ " --show-rexc Show rexc bytecode",
197
+ " --show-vars Show variable state",
198
+ "",
199
+ "Output formatting:",
200
+ " --format <type> Output format: rex|json (default: rex)",
201
+ " --json Shortcut for --format json",
202
+ " --color Force color output",
203
+ " --no-color Disable color output",
204
+ "",
205
+ "TTY vs non-TTY:",
206
+ " TTY output prints labeled blocks when multiple outputs are selected.",
207
+ " Non-TTY output emits a single value (object if multiple outputs).",
118
208
  "",
119
209
  "Compile options:",
120
210
  " -m, --minify-names Minify local variable names",
@@ -174,12 +264,56 @@ async function resolveDomainConfig(options: CliOptions): Promise<unknown | undef
174
264
  return loadDomainConfigFromFolder(baseFolder);
175
265
  }
176
266
 
267
+ function formatJson(value: unknown, indent = 2): string {
268
+ const normalized = normalizeJsonValue(value, false);
269
+ const text = JSON.stringify(normalized, null, indent);
270
+ return text ?? "null";
271
+ }
272
+
273
+ function normalizeJsonValue(value: unknown, inArray: boolean): unknown {
274
+ if (value === undefined) return inArray ? null : undefined;
275
+ if (value === null || typeof value !== "object") return value;
276
+ if (Array.isArray(value)) {
277
+ return value.map((item) => normalizeJsonValue(item, true));
278
+ }
279
+ const out: Record<string, unknown> = {};
280
+ for (const [key, val] of Object.entries(value as Record<string, unknown>)) {
281
+ const normalized = normalizeJsonValue(val, false);
282
+ if (normalized !== undefined) out[key] = normalized;
283
+ }
284
+ return out;
285
+ }
286
+
287
+ function isRexSource(source: string): boolean {
288
+ try {
289
+ return grammar.match(source).succeeded();
290
+ } catch {
291
+ return false;
292
+ }
293
+ }
294
+
177
295
  async function main() {
178
296
  const options = parseArgs(process.argv.slice(2));
179
297
  if (options.help) {
180
298
  console.log(usage());
181
299
  return;
182
300
  }
301
+ setColorEnabled(options.color);
302
+ if (options.cat) {
303
+ const source = await resolveSource(options);
304
+ const hasRexc = options.sources.some((seg) => seg.type === "file" && seg.path.endsWith(".rexc"));
305
+ const hasRex = options.sources.some((seg) => seg.type === "file" && seg.path.endsWith(".rex"));
306
+ const hint = hasRexc && !hasRex ? "rexc" : hasRex && !hasRexc ? "rex" : undefined;
307
+ const output = (process.stdout.isTTY && options.color)
308
+ ? highlightAuto(source, hint)
309
+ : source;
310
+ if (options.out) {
311
+ await writeFile(options.out, `${output}\n`, "utf8");
312
+ return;
313
+ }
314
+ console.log(output);
315
+ return;
316
+ }
183
317
 
184
318
  // No source provided on a TTY → launch interactive REPL
185
319
  const hasSource = options.sources.length > 0 || !process.stdin.isTTY;
@@ -190,21 +324,107 @@ async function main() {
190
324
  }
191
325
 
192
326
  const source = await resolveSource(options);
327
+ const sourceIsRex = isRexSource(source);
193
328
 
194
- let output: string;
329
+ if (options.compile) {
330
+ if (!sourceIsRex) throw new Error("--compile requires Rex source");
331
+ options.showRexc = true;
332
+ if (!options.showExprExplicit) options.showExpr = false;
333
+ }
195
334
  if (options.ir) {
196
- output = JSON.stringify(parseToIR(source), null, 2);
197
- } else if (options.compile) {
335
+ if (!sourceIsRex) throw new Error("--ir requires Rex source");
336
+ options.showIR = true;
337
+ if (!options.showExprExplicit) options.showExpr = false;
338
+ }
339
+
340
+ const outputFlags = [options.showSource, options.showExpr, options.showIR, options.showRexc, options.showVars].filter(Boolean).length;
341
+ if (outputFlags === 0) {
342
+ throw new Error("No output selected. Use --show-source, --show-expr, --show-ir, --show-rexc, or --show-vars.");
343
+ }
344
+ const humanMode = process.stdout.isTTY && options.color;
345
+ type OutputKey = "source" | "ir" | "rexc" | "vars" | "result";
346
+ const outputs: Partial<Record<OutputKey, unknown>> = {};
347
+ if (options.showSource) outputs.source = source;
348
+ if (!sourceIsRex && options.showIR) {
349
+ throw new Error("--show-ir is only available for Rex source");
350
+ }
351
+
352
+ if (options.showIR) {
353
+ outputs.ir = parseToIR(source);
354
+ }
355
+
356
+ if (options.showRexc) {
198
357
  const domainConfig = await resolveDomainConfig(options);
199
- output = compile(source, {
200
- minifyNames: options.minifyNames,
201
- dedupeValues: options.dedupeValues,
202
- dedupeMinBytes: options.dedupeMinBytes,
203
- domainConfig,
204
- });
358
+ outputs.rexc = sourceIsRex
359
+ ? compile(source, {
360
+ minifyNames: options.minifyNames,
361
+ dedupeValues: options.dedupeValues,
362
+ dedupeMinBytes: options.dedupeMinBytes,
363
+ domainConfig,
364
+ })
365
+ : source;
366
+ }
367
+
368
+ if (options.showExpr || options.showVars) {
369
+ const result = sourceIsRex ? evaluateSource(source) : evaluateRexc(source);
370
+ if (options.showExpr) outputs.result = result.value;
371
+ if (options.showVars) outputs.vars = result.state.vars;
372
+ }
373
+
374
+ const order: OutputKey[] = ["source", "ir", "rexc", "vars", "result"];
375
+ const selected = order.filter((key) => outputs[key] !== undefined);
376
+
377
+ function formatValue(value: unknown, kind: OutputKey): string {
378
+ if (kind === "source") {
379
+ const raw = String(value ?? "");
380
+ if (options.format === "json") {
381
+ const json = formatJson(raw, 2);
382
+ return humanMode && options.color ? highlightJSON(json) : json;
383
+ }
384
+ return humanMode && options.color ? highlightAuto(raw) : raw;
385
+ }
386
+ if (kind === "rexc" && humanMode) {
387
+ const raw = String(value ?? "");
388
+ return options.color ? highlightAuto(raw, "rexc") : raw;
389
+ }
390
+ if (options.format === "json") {
391
+ const raw = formatJson(value, 2);
392
+ return humanMode && options.color ? highlightJSON(raw) : raw;
393
+ }
394
+ if (kind === "rexc") {
395
+ const raw = stringify(String(value ?? ""));
396
+ return humanMode && options.color ? highlightLine(raw) : raw;
397
+ }
398
+ const raw = stringify(value);
399
+ return humanMode && options.color ? highlightLine(raw) : raw;
400
+ }
401
+
402
+ let output: string;
403
+ if (humanMode) {
404
+ const header = options.color ? "\x1b[90m" : "";
405
+ const reset = options.color ? "\x1b[0m" : "";
406
+ const lines: string[] = [];
407
+ for (const key of order) {
408
+ const value = outputs[key];
409
+ if (value === undefined) continue;
410
+ lines.push(`${header}${key}:${reset} ${formatValue(value, key)}`);
411
+ }
412
+ output = lines.join("\n");
205
413
  } else {
206
- const { value } = evaluateSource(source);
207
- output = stringify(value);
414
+ let value: unknown;
415
+ if (selected.length === 1) {
416
+ const only = selected[0];
417
+ if (!only) throw new Error("No output selected.");
418
+ value = outputs[only];
419
+ } else {
420
+ const out: Record<string, unknown> = {};
421
+ for (const key of order) {
422
+ const v = outputs[key];
423
+ if (v !== undefined) out[key] = v;
424
+ }
425
+ value = out;
426
+ }
427
+ output = options.format === "json" ? formatJson(value, 2) : stringify(value);
208
428
  }
209
429
 
210
430
  if (options.out) {