rtgl 1.0.0-rc3 → 1.0.0-rc32

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 (2) hide show
  1. package/cli.js +205 -39
  2. package/package.json +7 -5
package/cli.js CHANGED
@@ -1,10 +1,12 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  import { build, check, scaffold, watch, examples } from "@rettangoli/fe/cli";
4
- import { generate, report, accept } from "@rettangoli/vt/cli";
5
- import { buildSite, watchSite, screenshotCommand, initSite } from "@rettangoli/sites/cli";
4
+ import { check as checkContracts } from "@rettangoli/check/cli";
5
+ import { build as buildBe, check as checkBe, watch as watchBe } from "@rettangoli/be/cli";
6
+ import { generate, screenshot, report, accept } from "@rettangoli/vt/cli";
7
+ import { buildSite, watchSite, initSite } from "@rettangoli/sites/cli";
6
8
  import { buildSvg } from "@rettangoli/ui/cli";
7
- import { Command } from "commander";
9
+ import { Command, InvalidArgumentError } from "commander";
8
10
  import { readFileSync, existsSync } from "fs";
9
11
  import { resolve } from "path";
10
12
  import yaml from "js-yaml";
@@ -33,6 +35,27 @@ function collectValues(value, previous = []) {
33
35
  return [...previous, value];
34
36
  }
35
37
 
38
+ function parseIntegerOption(value) {
39
+ if (!/^-?\d+$/.test(String(value))) {
40
+ throw new InvalidArgumentError(`Expected an integer but received "${value}"`);
41
+ }
42
+
43
+ const parsed = Number.parseInt(value, 10);
44
+ if (!Number.isSafeInteger(parsed)) {
45
+ throw new InvalidArgumentError(`Expected a safe integer but received "${value}"`);
46
+ }
47
+
48
+ return parsed;
49
+ }
50
+
51
+ function parsePortOption(value) {
52
+ const parsed = parseIntegerOption(value);
53
+ if (parsed < 1 || parsed > 65535) {
54
+ throw new InvalidArgumentError(`Port must be between 1 and 65535, received "${value}"`);
55
+ }
56
+ return parsed;
57
+ }
58
+
36
59
  const program = new Command();
37
60
 
38
61
  program
@@ -52,6 +75,39 @@ Examples:
52
75
  `,
53
76
  );
54
77
 
78
+ program
79
+ .command("check")
80
+ .description("Run Rettangoli static contract checks")
81
+ .option("--dir <path>", "Component directory to scan (repeatable)", collectValues, [])
82
+ .option("--format <format>", "Output format: text, json, or sarif", "text")
83
+ .option("--warn-as-error", "Treat warnings as errors")
84
+ .option("--no-yahtml", "Disable YAHTML attr/prop validation")
85
+ .option("--expr", "Enable expression scope/type checks")
86
+ .option("--watch", "Watch for file changes and re-run checks")
87
+ .option("--watch-interval-ms <ms>", "Watch poll interval in milliseconds", parseIntegerOption, 800)
88
+ .addHelpText(
89
+ "after",
90
+ `
91
+
92
+ Examples:
93
+ $ rtgl check
94
+ $ rtgl check --dir src/components --format json
95
+ $ rtgl check --warn-as-error
96
+ `,
97
+ )
98
+ .action(async (options) => {
99
+ await checkContracts({
100
+ cwd: process.cwd(),
101
+ dirs: options.dir,
102
+ format: options.format,
103
+ warnAsError: !!options.warnAsError,
104
+ includeYahtml: options.yahtml !== false,
105
+ includeExpression: !!options.expr,
106
+ watch: !!options.watch,
107
+ watchIntervalMs: Number.isFinite(options.watchIntervalMs) ? options.watchIntervalMs : 800,
108
+ });
109
+ });
110
+
55
111
  const feCommand = program.command("fe").description("Frontend framework");
56
112
 
57
113
  feCommand
@@ -169,7 +225,7 @@ Examples:
169
225
  feCommand
170
226
  .command("watch")
171
227
  .description("Watch for changes")
172
- .option("-p, --port <port>", "The port to use", parseInt, 3001)
228
+ .option("-p, --port <port>", "The port to use", parsePortOption, 3001)
173
229
  .option("-s, --setup-path <path>", "Custom setup file path")
174
230
  .addHelpText(
175
231
  "after",
@@ -225,16 +281,112 @@ feCommand
225
281
  examples(options);
226
282
  });
227
283
 
284
+ const beCommand = program.command("be").description("Backend framework");
285
+
286
+ beCommand
287
+ .command("build")
288
+ .description("Build backend method registry and app entry")
289
+ .option("-s, --setup-path <path>", "Custom setup file path")
290
+ .option("-m, --middleware-dir <path>", "Custom middleware directory path")
291
+ .option("-o, --outdir <path>", "Generated output directory")
292
+ .action((options) => {
293
+ const config = readConfig();
294
+
295
+ if (!config) {
296
+ throw new Error("rettangoli.config.yaml not found");
297
+ }
298
+
299
+ if (!config.be?.dirs?.length) {
300
+ throw new Error("be.dirs not found or empty in config");
301
+ }
302
+
303
+ const missingDirs = config.be.dirs.filter(
304
+ (dir) => !existsSync(resolve(process.cwd(), dir)),
305
+ );
306
+ if (missingDirs.length > 0) {
307
+ throw new Error(`Directories do not exist: ${missingDirs.join(", ")}`);
308
+ }
309
+
310
+ options.dirs = config.be.dirs;
311
+ options.middlewareDir = options.middlewareDir || config.be.middlewareDir || "./src/middleware";
312
+ options.setup = options.setupPath || config.be.setup || "./src/setup.js";
313
+ options.outdir = options.outdir || config.be.outdir || "./.rtgl-be/generated";
314
+ options.domainErrors = config.be.domainErrors || {};
315
+
316
+ buildBe(options);
317
+ });
318
+
319
+ beCommand
320
+ .command("check")
321
+ .description("Validate backend RPC contracts")
322
+ .option("--format <format>", "Output format: text or json", "text")
323
+ .option("-m, --middleware-dir <path>", "Custom middleware directory path")
324
+ .action((options) => {
325
+ const config = readConfig();
326
+
327
+ if (!config) {
328
+ throw new Error("rettangoli.config.yaml not found");
329
+ }
330
+
331
+ if (!config.be?.dirs?.length) {
332
+ throw new Error("be.dirs not found or empty in config");
333
+ }
334
+
335
+ const missingDirs = config.be.dirs.filter(
336
+ (dir) => !existsSync(resolve(process.cwd(), dir)),
337
+ );
338
+ if (missingDirs.length > 0) {
339
+ throw new Error(`Directories do not exist: ${missingDirs.join(", ")}`);
340
+ }
341
+
342
+ options.dirs = config.be.dirs;
343
+ options.middlewareDir = options.middlewareDir || config.be.middlewareDir || "./src/middleware";
344
+
345
+ checkBe(options);
346
+ });
347
+
348
+ beCommand
349
+ .command("watch")
350
+ .description("Watch backend files and rebuild generated registry")
351
+ .option("-s, --setup-path <path>", "Custom setup file path")
352
+ .option("-m, --middleware-dir <path>", "Custom middleware directory path")
353
+ .option("-o, --outdir <path>", "Generated output directory")
354
+ .action((options) => {
355
+ const config = readConfig();
356
+
357
+ if (!config) {
358
+ throw new Error("rettangoli.config.yaml not found");
359
+ }
360
+
361
+ if (!config.be?.dirs?.length) {
362
+ throw new Error("be.dirs not found or empty in config");
363
+ }
364
+
365
+ const missingDirs = config.be.dirs.filter(
366
+ (dir) => !existsSync(resolve(process.cwd(), dir)),
367
+ );
368
+ if (missingDirs.length > 0) {
369
+ throw new Error(`Directories do not exist: ${missingDirs.join(", ")}`);
370
+ }
371
+
372
+ options.dirs = config.be.dirs;
373
+ options.middlewareDir = options.middlewareDir || config.be.middlewareDir || "./src/middleware";
374
+ options.setup = options.setupPath || config.be.setup || "./src/setup.js";
375
+ options.outdir = options.outdir || config.be.outdir || "./.rtgl-be/generated";
376
+ options.domainErrors = config.be.domainErrors || {};
377
+
378
+ watchBe(options);
379
+ });
380
+
228
381
  const vtCommand = program
229
382
  .command("vt")
230
383
  .description("Rettangoli Visual Testing");
231
384
 
232
385
  vtCommand
233
386
  .command("generate")
234
- .description("Generate visualizations")
235
- .option("--skip-screenshots", "Skip screenshot generation")
236
- .option("--concurrency <number>", "Number of parallel capture workers", parseInt)
237
- .option("--timeout <ms>", "Global capture timeout in ms", parseInt)
387
+ .description("Generate candidate HTML pages only (no screenshots)")
388
+ .option("--concurrency <number>", "Number of parallel capture workers", parseIntegerOption)
389
+ .option("--timeout <ms>", "Global capture timeout in ms", parseIntegerOption)
238
390
  .option("--wait-event <name>", "Custom event name to mark page ready (uses event wait strategy)")
239
391
  .option("--folder <path>", "Run only specs under folder prefix (repeatable)", collectValues, [])
240
392
  .option("--group <section-key>", "Run only one section key from vt.sections (repeatable)", collectValues, [])
@@ -253,10 +405,36 @@ vtCommand
253
405
  if (options.headed) {
254
406
  options.headless = false;
255
407
  }
408
+ options.captureScreenshots = false;
256
409
 
257
410
  await generate(options);
258
411
  });
259
412
 
413
+ vtCommand
414
+ .command("screenshot")
415
+ .description("Generate candidate HTML pages and capture screenshots")
416
+ .option("--concurrency <number>", "Number of parallel capture workers", parseIntegerOption)
417
+ .option("--timeout <ms>", "Global capture timeout in ms", parseIntegerOption)
418
+ .option("--wait-event <name>", "Custom event name to mark page ready (uses event wait strategy)")
419
+ .option("--folder <path>", "Run only specs under folder prefix (repeatable)", collectValues, [])
420
+ .option("--group <section-key>", "Run only one section key from vt.sections (repeatable)", collectValues, [])
421
+ .option("--item <spec-path>", "Run only one spec path relative to vt/specs (repeatable)", collectValues, [])
422
+ .option("--headed", "Run Playwright in headed mode")
423
+ .action(async (options) => {
424
+ console.log(`rtgl v${packageJson.version}`);
425
+ const config = readConfig();
426
+
427
+ if (!config) {
428
+ throw new Error("rettangoli.config.yaml not found");
429
+ }
430
+
431
+ options.vtPath = config.vt?.path || "vt";
432
+ if (options.headed) {
433
+ options.headless = false;
434
+ }
435
+ await screenshot(options);
436
+ });
437
+
260
438
  vtCommand
261
439
  .command("report")
262
440
  .description("Create reports")
@@ -315,51 +493,39 @@ sitesCommand
315
493
  sitesCommand
316
494
  .command("build")
317
495
  .description("Build the site")
318
- .option("-r, --rootDir <path>", "Path to root directory", "./")
319
- .option("-o, --outputPath <path>", "Path to destination directory", "./_site")
320
- .option("-s, --screenshots", "Capture screenshots after build", false)
496
+ .option("-r, --root-dir <path>", "Path to root directory", "./")
497
+ .option("--rootDir <path>", "Deprecated alias for --root-dir")
498
+ .option("-o, --output-path <path>", "Path to destination directory", "./_site")
499
+ .option("--outputPath <path>", "Deprecated alias for --output-path")
500
+ .option("-q, --quiet", "Suppress non-error logs")
321
501
  .action(async (options) => {
322
- console.log("Building site with options:", options);
323
502
  await buildSite({
324
503
  rootDir: options.rootDir,
325
504
  outputPath: options.outputPath,
505
+ quiet: !!options.quiet,
326
506
  });
327
- console.log("Build completed successfully!");
328
-
329
- // If screenshots option is enabled, run screenshot command
330
- if (options.screenshots) {
331
- console.log("Capturing screenshots...");
332
- await screenshotCommand({
333
- rootDir: options.rootDir,
334
- });
507
+ if (!options.quiet) {
508
+ console.log("Build completed successfully!");
335
509
  }
336
510
  });
337
511
 
338
512
  sitesCommand
339
513
  .command("watch")
340
514
  .description("Watch and rebuild site on changes")
341
- .option("-p, --port <port>", "The port to use", parseInt, 3001)
342
- .option("-r, --rootDir <path>", "Path to root directory", ".")
343
- .option("-s, --screenshots", "Enable automatic screenshot capture on page changes", false)
344
- .action(async (options) => {
345
- console.log("Starting watch mode with options:", options);
346
- watchSite({
347
- port: options.port,
348
- rootDir: options.rootDir,
349
- screenshots: options.screenshots,
350
- });
351
- });
352
-
353
- sitesCommand
354
- .command("screenshot")
355
- .description("Capture screenshots of all pages")
356
- .option("-p, --port <port>", "The port to use for temp server", parseInt, 3001)
357
- .option("-r, --rootDir <path>", "Path to root directory", ".")
515
+ .option("-p, --port <port>", "The port to use", parsePortOption, 3001)
516
+ .option("-r, --root-dir <path>", "Path to root directory", ".")
517
+ .option("--rootDir <path>", "Deprecated alias for --root-dir")
518
+ .option("-o, --output-path <path>", "Path to destination directory", "./_site")
519
+ .option("--outputPath <path>", "Deprecated alias for --output-path")
520
+ .option("--reload-mode <mode>", "Reload mode: body (hot body replacement) or full (full-page reload)", "body")
521
+ .option("-q, --quiet", "Suppress non-error logs")
358
522
  .action(async (options) => {
359
- console.log("Capturing screenshots with options:", options);
360
- await screenshotCommand({
523
+ await watchSite({
361
524
  port: options.port,
362
525
  rootDir: options.rootDir,
526
+ outputPath: options.outputPath,
527
+ reloadMode: options.reloadMode,
528
+ quiet: !!options.quiet,
363
529
  });
364
530
  });
365
531
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "rtgl",
3
- "version": "1.0.0-rc3",
3
+ "version": "1.0.0-rc32",
4
4
  "description": "CLI tool for Rettangoli - A frontend framework and development toolkit",
5
5
  "type": "module",
6
6
  "bin": {
@@ -47,9 +47,11 @@
47
47
  "dependencies": {
48
48
  "commander": "^14.0.0",
49
49
  "js-yaml": "^4.1.0",
50
- "@rettangoli/fe": "1.0.0-rc1",
51
- "@rettangoli/sites": "0.2.7",
52
- "@rettangoli/vt": "1.0.0-rc2",
53
- "@rettangoli/ui": "0.1.31"
50
+ "@rettangoli/check": "0.1.2",
51
+ "@rettangoli/be": "1.0.0-rc1",
52
+ "@rettangoli/fe": "1.0.0-rc5",
53
+ "@rettangoli/sites": "1.0.0-rc13",
54
+ "@rettangoli/vt": "1.0.0-rc16",
55
+ "@rettangoli/ui": "1.0.0-rc16"
54
56
  }
55
57
  }