rtgl 1.0.0-rc7 → 1.0.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 (2) hide show
  1. package/cli.js +187 -9
  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";
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";
5
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")
@@ -334,7 +512,7 @@ sitesCommand
334
512
  sitesCommand
335
513
  .command("watch")
336
514
  .description("Watch and rebuild site on changes")
337
- .option("-p, --port <port>", "The port to use", parseInt, 3001)
515
+ .option("-p, --port <port>", "The port to use", parsePortOption, 3001)
338
516
  .option("-r, --root-dir <path>", "Path to root directory", ".")
339
517
  .option("--rootDir <path>", "Deprecated alias for --root-dir")
340
518
  .option("-o, --output-path <path>", "Path to destination directory", "./_site")
@@ -342,7 +520,7 @@ sitesCommand
342
520
  .option("--reload-mode <mode>", "Reload mode: body (hot body replacement) or full (full-page reload)", "body")
343
521
  .option("-q, --quiet", "Suppress non-error logs")
344
522
  .action(async (options) => {
345
- watchSite({
523
+ await watchSite({
346
524
  port: options.port,
347
525
  rootDir: options.rootDir,
348
526
  outputPath: options.outputPath,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "rtgl",
3
- "version": "1.0.0-rc7",
3
+ "version": "1.0.0",
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-rc3",
51
- "@rettangoli/sites": "1.0.0-rc1",
52
- "@rettangoli/vt": "1.0.0-rc3",
53
- "@rettangoli/ui": "1.0.0-rc1"
50
+ "@rettangoli/check": "workspace:0.1.2",
51
+ "@rettangoli/be": "workspace:1.0.0-rc1",
52
+ "@rettangoli/fe": "workspace:1.0.0",
53
+ "@rettangoli/sites": "workspace:1.0.0-rc13",
54
+ "@rettangoli/vt": "workspace:1.0.0-rc16",
55
+ "@rettangoli/ui": "workspace:1.0.0"
54
56
  }
55
57
  }