@constela/cli 0.3.30 → 0.4.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.
Files changed (3) hide show
  1. package/README.md +183 -0
  2. package/dist/index.js +1019 -28
  3. package/package.json +4 -4
package/README.md CHANGED
@@ -30,6 +30,10 @@ constela compile <input> [options]
30
30
  **Options:**
31
31
  - `-o, --out <path>` - Output file path
32
32
  - `--pretty` - Pretty-print JSON output
33
+ - `--json` - Output results as JSON (for tooling/AI integration)
34
+ - `-w, --watch` - Watch input file and recompile on changes
35
+ - `-v, --verbose` - Show detailed compilation progress
36
+ - `--debug` - Show internal debug information
33
37
 
34
38
  **Examples:**
35
39
 
@@ -42,6 +46,114 @@ constela compile app.json --out dist/app.compiled.json
42
46
 
43
47
  # Pretty-print output
44
48
  constela compile app.json --pretty
49
+
50
+ # JSON output for AI tools
51
+ constela compile app.json --json
52
+
53
+ # Watch mode for development
54
+ constela compile app.json --watch
55
+
56
+ # Verbose output with timing
57
+ constela compile app.json --verbose
58
+ # Output:
59
+ # [1/3] Validating schema... OK (2ms)
60
+ # [2/3] Analyzing semantics... OK (1ms)
61
+ # [3/3] Transforming AST... OK (0ms)
62
+ # Compilation successful (5ms total)
63
+
64
+ # Debug information
65
+ constela compile app.json --debug
66
+ # Output:
67
+ # [DEBUG] Input file: app.json (1234 bytes)
68
+ # [DEBUG] Parse time: 1ms
69
+ # [DEBUG] Validate pass: 15 nodes validated (2ms)
70
+ # ...
71
+ ```
72
+
73
+ ### constela validate
74
+
75
+ Validates Constela JSON files without full compilation (faster feedback).
76
+
77
+ ```bash
78
+ constela validate [input] [options]
79
+ ```
80
+
81
+ **Arguments:**
82
+ - `[input]` - Input DSL file (JSON) or directory with `--all`
83
+
84
+ **Options:**
85
+ - `-a, --all` - Validate all JSON files in directory recursively
86
+ - `--json` - Output results as JSON
87
+
88
+ **Examples:**
89
+
90
+ ```bash
91
+ # Validate single file
92
+ constela validate app.json
93
+
94
+ # Validate all JSON files in directory
95
+ constela validate --all src/routes/
96
+
97
+ # JSON output for tooling
98
+ constela validate app.json --json
99
+ ```
100
+
101
+ **Error Output with Suggestions:**
102
+
103
+ ```
104
+ Error [UNDEFINED_STATE] at /view/children/0/value/name
105
+
106
+ Undefined state reference: 'count'
107
+
108
+ Did you mean 'counter'?
109
+ ```
110
+
111
+ ### constela inspect
112
+
113
+ Inspects Constela program structure without compilation.
114
+
115
+ ```bash
116
+ constela inspect <input> [options]
117
+ ```
118
+
119
+ **Arguments:**
120
+ - `<input>` - Input DSL file (JSON)
121
+
122
+ **Options:**
123
+ - `--state` - Show only state information
124
+ - `--actions` - Show only actions information
125
+ - `--components` - Show only components information
126
+ - `--view` - Show only view tree
127
+ - `--json` - Output as JSON
128
+
129
+ **Examples:**
130
+
131
+ ```bash
132
+ # Show all program structure
133
+ constela inspect app.json
134
+
135
+ # Show only state
136
+ constela inspect app.json --state
137
+
138
+ # JSON output
139
+ constela inspect app.json --json
140
+ ```
141
+
142
+ **Output:**
143
+
144
+ ```
145
+ State (2 fields):
146
+ count: number = 0
147
+ items: list = []
148
+
149
+ Actions (2):
150
+ increment: update count +1
151
+ addItem: push to items
152
+
153
+ View Tree:
154
+ element<div>
155
+ text: state.count
156
+ element<button> onClick=increment
45
157
  ```
46
158
 
47
159
  ### constela dev
@@ -156,6 +268,77 @@ export default {
156
268
  } satisfies ConstelaConfig;
157
269
  ```
158
270
 
271
+ ## Debugging Guide
272
+
273
+ ### Using Debug Mode
274
+
275
+ Add `--debug` to any compile command to see internal processing:
276
+
277
+ ```bash
278
+ constela compile app.json --debug
279
+ ```
280
+
281
+ **Debug output includes:**
282
+ - File size and parse time
283
+ - Number of nodes validated
284
+ - Analysis pass details
285
+ - Transform timings
286
+
287
+ ### Common Issues
288
+
289
+ #### UNDEFINED_STATE Error
290
+
291
+ ```
292
+ Error [UNDEFINED_STATE] at /view/children/0/value/name
293
+ Undefined state reference: 'count'
294
+ Did you mean 'counter'?
295
+ ```
296
+
297
+ **Solution:** Check your state name spelling. The error shows suggested corrections.
298
+
299
+ #### SCHEMA_INVALID Error
300
+
301
+ ```
302
+ Error [SCHEMA_INVALID] at /view/kind
303
+ Invalid value: 'button'. Expected one of: element, text, if, each, component, slot, markdown, code
304
+ ```
305
+
306
+ **Solution:** Use correct node kind. `kind: "element"` with `tag: "button"` for buttons.
307
+
308
+ #### Component Not Found
309
+
310
+ ```
311
+ Error [COMPONENT_NOT_FOUND] at /view/children/0/name
312
+ Component 'Header' is not defined
313
+ ```
314
+
315
+ **Solution:** Define the component in the `components` section or check import paths.
316
+
317
+ ### Inspecting Program Structure
318
+
319
+ Use `inspect` to understand your program without compilation:
320
+
321
+ ```bash
322
+ # See state structure
323
+ constela inspect app.json --state
324
+
325
+ # See view tree
326
+ constela inspect app.json --view
327
+
328
+ # Get JSON for tooling
329
+ constela inspect app.json --json | jq '.state'
330
+ ```
331
+
332
+ ### Browser DevTools
333
+
334
+ For runtime debugging:
335
+
336
+ 1. Open DevTools → Sources tab
337
+ 2. Find `@constela/runtime` in the source tree
338
+ 3. Set breakpoints in action handlers
339
+
340
+ State changes are logged to console in development mode.
341
+
159
342
  ## Exit Codes
160
343
 
161
344
  | Code | Description |
package/dist/index.js CHANGED
@@ -4,9 +4,213 @@
4
4
  import { Command } from "commander";
5
5
 
6
6
  // src/commands/compile.ts
7
- import { compile } from "@constela/compiler";
8
- import { readFileSync, writeFileSync, mkdirSync, statSync } from "fs";
7
+ import { validatePass, analyzePass, transformPass } from "@constela/compiler";
8
+ import { ConstelaError } from "@constela/core";
9
+ import { readFileSync, writeFileSync, mkdirSync, statSync, watch as fsWatch } from "fs";
9
10
  import { dirname, basename, join } from "path";
11
+ function shouldUseColors() {
12
+ if (process.env["NO_COLOR"] !== void 0) {
13
+ return false;
14
+ }
15
+ if (process.env["FORCE_COLOR"] === "1") {
16
+ return true;
17
+ }
18
+ return process.stdout.isTTY === true;
19
+ }
20
+ var colors = {
21
+ red: (s) => `\x1B[31m${s}\x1B[0m`,
22
+ green: (s) => `\x1B[32m${s}\x1B[0m`,
23
+ yellow: (s) => `\x1B[33m${s}\x1B[0m`,
24
+ reset: "\x1B[0m"
25
+ };
26
+ function getColors() {
27
+ if (shouldUseColors()) {
28
+ return colors;
29
+ }
30
+ return {
31
+ red: (s) => s,
32
+ green: (s) => s,
33
+ yellow: (s) => s,
34
+ reset: ""
35
+ };
36
+ }
37
+ function countViewNodes(node) {
38
+ let count = 1;
39
+ switch (node.kind) {
40
+ case "element":
41
+ if (node.children) {
42
+ for (const child of node.children) {
43
+ count += countViewNodes(child);
44
+ }
45
+ }
46
+ break;
47
+ case "if":
48
+ count += countViewNodes(node.then);
49
+ if (node.else) {
50
+ count += countViewNodes(node.else);
51
+ }
52
+ break;
53
+ case "each":
54
+ count += countViewNodes(node.template);
55
+ break;
56
+ // text, markdown, code, slot nodes have no children
57
+ default:
58
+ break;
59
+ }
60
+ return count;
61
+ }
62
+ function createVerboseLogger(enabled, c) {
63
+ if (!enabled) {
64
+ return {
65
+ phaseStart: () => {
66
+ },
67
+ phaseOk: () => {
68
+ },
69
+ phaseFail: () => {
70
+ },
71
+ detail: () => {
72
+ },
73
+ summary: () => {
74
+ }
75
+ };
76
+ }
77
+ return {
78
+ phaseStart(phase, total, message) {
79
+ process.stdout.write(`[${phase}/${total}] ${message}...`);
80
+ },
81
+ phaseOk(phase, total, message, durationMs) {
82
+ console.log(`[${phase}/${total}] ${message}... ${c.green("OK")} (${durationMs}ms)`);
83
+ },
84
+ phaseFail(phase, total, message) {
85
+ console.log(`[${phase}/${total}] ${message}... ${c.red("FAILED")}`);
86
+ },
87
+ detail(message) {
88
+ console.log(` ${message}`);
89
+ },
90
+ summary(states, actions, viewNodes, totalMs) {
91
+ console.log("");
92
+ console.log("Summary:");
93
+ console.log(` States: ${states}`);
94
+ console.log(` Actions: ${actions}`);
95
+ console.log(` View nodes: ${viewNodes}`);
96
+ console.log("");
97
+ console.log(`${c.green("Compilation successful")} (${totalMs}ms total)`);
98
+ }
99
+ };
100
+ }
101
+ function createDebugLogger(enabled) {
102
+ if (!enabled) {
103
+ return {
104
+ inputFile: () => {
105
+ },
106
+ parseTime: () => {
107
+ },
108
+ validatePass: () => {
109
+ },
110
+ analyzePass: () => {
111
+ },
112
+ transformPass: () => {
113
+ },
114
+ getInfo: () => void 0
115
+ };
116
+ }
117
+ const info = {
118
+ inputFile: "",
119
+ inputSize: 0,
120
+ parseTime: 0,
121
+ validateTime: 0,
122
+ analyzeTime: 0,
123
+ transformTime: 0,
124
+ nodesValidated: 0,
125
+ stateCount: 0,
126
+ actionCount: 0,
127
+ viewNodeCount: 0,
128
+ outputSize: 0
129
+ };
130
+ return {
131
+ inputFile(filename, size) {
132
+ info.inputFile = filename;
133
+ info.inputSize = size;
134
+ console.log(`[DEBUG] Input file: ${filename} (${size} bytes)`);
135
+ },
136
+ parseTime(ms) {
137
+ info.parseTime = ms;
138
+ console.log(`[DEBUG] Parse time: ${ms}ms`);
139
+ },
140
+ validatePass(nodes, ms) {
141
+ info.nodesValidated = nodes;
142
+ info.validateTime = ms;
143
+ console.log(`[DEBUG] Validate pass: ${nodes} nodes validated (${ms}ms)`);
144
+ },
145
+ analyzePass(states, actions, views, ms) {
146
+ info.stateCount = states;
147
+ info.actionCount = actions;
148
+ info.viewNodeCount = views;
149
+ info.analyzeTime = ms;
150
+ console.log(`[DEBUG] Analyze pass: ${states} states, ${actions} actions, ${views} views (${ms}ms)`);
151
+ },
152
+ transformPass(outputSize, ms) {
153
+ info.outputSize = outputSize;
154
+ info.transformTime = ms;
155
+ console.log(`[DEBUG] Transform pass: output size ${outputSize} bytes (${ms}ms)`);
156
+ },
157
+ getInfo() {
158
+ return info;
159
+ }
160
+ };
161
+ }
162
+ function buildDebugInfo(inputFile, inputSize, parseTime, validateTime, analyzeTime, transformTime, nodesValidated, stateCount, actionCount, viewNodeCount, outputSize) {
163
+ return {
164
+ inputFile,
165
+ inputSize,
166
+ parseTime,
167
+ validateTime,
168
+ analyzeTime,
169
+ transformTime,
170
+ nodesValidated,
171
+ stateCount,
172
+ actionCount,
173
+ viewNodeCount,
174
+ outputSize
175
+ };
176
+ }
177
+ function countAstNodes(ast) {
178
+ let count = 1;
179
+ count += Object.keys(ast.state).length;
180
+ for (const action of ast.actions) {
181
+ count += 1;
182
+ count += action.steps.length;
183
+ }
184
+ count += countViewNodesFromAst(ast.view);
185
+ if (ast.components) {
186
+ for (const key of Object.keys(ast.components)) {
187
+ const comp = ast.components[key];
188
+ if (comp) {
189
+ count += 1;
190
+ count += countViewNodesFromAst(comp.view);
191
+ }
192
+ }
193
+ }
194
+ return count;
195
+ }
196
+ function countViewNodesFromAst(node) {
197
+ let count = 1;
198
+ if ("children" in node && Array.isArray(node.children)) {
199
+ for (const child of node.children) {
200
+ count += countViewNodesFromAst(child);
201
+ }
202
+ }
203
+ if ("then" in node && node.then) {
204
+ count += countViewNodesFromAst(node.then);
205
+ }
206
+ if ("else" in node && node.else) {
207
+ count += countViewNodesFromAst(node.else);
208
+ }
209
+ if ("template" in node && node.template) {
210
+ count += countViewNodesFromAst(node.template);
211
+ }
212
+ return count;
213
+ }
10
214
  function getOutputPath(inputPath, options) {
11
215
  if (options.out) {
12
216
  return options.out;
@@ -19,64 +223,849 @@ function getOutputPath(inputPath, options) {
19
223
  }
20
224
  return join(dir, `${base}.compiled.json`);
21
225
  }
22
- async function compileCommand(inputPath, options) {
226
+ function formatColoredError(error, c) {
227
+ const lines = [];
228
+ const pathInfo = error.path ? ` at ${error.path}` : "";
229
+ lines.push(`Error [${c.red(error.code)}]: ${error.message}${pathInfo}`);
230
+ if (error.suggestion) {
231
+ lines.push(` ${c.yellow(`Did you mean: ${error.suggestion}`)}`);
232
+ }
233
+ return lines.join("\n");
234
+ }
235
+ function errorToJson(error) {
236
+ const result = {
237
+ code: error.code,
238
+ message: error.message
239
+ };
240
+ if (error.path !== void 0) {
241
+ result.path = error.path;
242
+ }
243
+ if (error.suggestion !== void 0) {
244
+ result.suggestion = error.suggestion;
245
+ }
246
+ if (error.context !== void 0) {
247
+ result.context = error.context;
248
+ }
249
+ return result;
250
+ }
251
+ function outputJson(output) {
252
+ console.log(JSON.stringify(output));
253
+ }
254
+ function performCompilation(inputPath, options, c) {
255
+ const startTime = performance.now();
256
+ const verbose = createVerboseLogger(options.verbose === true, c);
257
+ const isJsonMode = options.json === true;
258
+ const debug = createDebugLogger(options.debug === true && !isJsonMode);
259
+ let inputSize;
23
260
  try {
24
261
  const stat = statSync(inputPath);
25
262
  if (!stat.isFile()) {
26
- console.error(`Error: Input path is not a regular file: ${inputPath}`);
27
- process.exit(1);
263
+ return {
264
+ ok: false,
265
+ errors: [new ConstelaError("SCHEMA_INVALID", `Input path is not a regular file: ${inputPath}`)],
266
+ duration: Math.round(performance.now() - startTime)
267
+ };
28
268
  }
269
+ inputSize = stat.size;
29
270
  } catch (err) {
30
271
  const error = err;
31
- if (error.code === "ENOENT") {
32
- console.error(`Error: File not found: ${inputPath}`);
33
- } else {
34
- console.error(`Error: Could not access file: ${inputPath} - ${error.message}`);
35
- }
36
- process.exit(1);
272
+ const message = error.code === "ENOENT" ? `File not found: ${inputPath}` : `Could not access file: ${inputPath} - ${error.message}`;
273
+ return {
274
+ ok: false,
275
+ errors: [new ConstelaError("SCHEMA_INVALID", message)],
276
+ duration: Math.round(performance.now() - startTime)
277
+ };
37
278
  }
279
+ debug.inputFile(basename(inputPath), inputSize);
38
280
  let fileContent;
39
281
  try {
40
282
  fileContent = readFileSync(inputPath, "utf-8");
41
283
  } catch (err) {
42
284
  const error = err;
43
- console.error(`Error: Could not read file: ${inputPath} - ${error.message}`);
44
- process.exit(1);
285
+ return {
286
+ ok: false,
287
+ errors: [new ConstelaError("SCHEMA_INVALID", `Could not read file: ${inputPath} - ${error.message}`)],
288
+ duration: Math.round(performance.now() - startTime)
289
+ };
45
290
  }
291
+ const parseStart = performance.now();
46
292
  let inputJson;
47
293
  try {
48
294
  inputJson = JSON.parse(fileContent);
49
295
  } catch (err) {
50
296
  const error = err;
51
- console.error(`Error: Invalid JSON: ${error.message}`);
52
- process.exit(1);
297
+ return {
298
+ ok: false,
299
+ errors: [new ConstelaError("SCHEMA_INVALID", `Invalid JSON: ${error.message}`)],
300
+ duration: Math.round(performance.now() - startTime)
301
+ };
53
302
  }
54
- const result = compile(inputJson);
55
- if (!result.ok) {
56
- for (const error of result.errors) {
57
- const pathInfo = error.path ? ` at ${error.path}` : "";
58
- console.error(`Error [${error.code}]: ${error.message}${pathInfo}`);
59
- }
60
- process.exit(1);
303
+ const parseDuration = Math.round(performance.now() - parseStart);
304
+ debug.parseTime(parseDuration);
305
+ const phase1Start = performance.now();
306
+ const validateResult = validatePass(inputJson);
307
+ const phase1Duration = Math.round(performance.now() - phase1Start);
308
+ if (!validateResult.ok) {
309
+ verbose.phaseFail(1, 3, "Validating schema");
310
+ return {
311
+ ok: false,
312
+ errors: [validateResult.error],
313
+ duration: Math.round(performance.now() - startTime)
314
+ };
61
315
  }
316
+ verbose.phaseOk(1, 3, "Validating schema", phase1Duration);
317
+ const ast = validateResult.ast;
318
+ const nodesValidated = countAstNodes(ast);
319
+ debug.validatePass(nodesValidated, phase1Duration);
320
+ const phase2Start = performance.now();
321
+ const stateNames = Object.keys(ast.state);
322
+ const actionNames = ast.actions.map((a) => a.name);
323
+ verbose.detail(`Collecting state names: ${stateNames.join(", ")}`);
324
+ verbose.detail(`Collecting action names: ${actionNames.join(", ")}`);
325
+ verbose.detail("Validating view tree");
326
+ const analyzeResult = analyzePass(ast);
327
+ const phase2Duration = Math.round(performance.now() - phase2Start);
328
+ if (!analyzeResult.ok) {
329
+ verbose.phaseFail(2, 3, "Analyzing semantics");
330
+ return {
331
+ ok: false,
332
+ errors: analyzeResult.errors,
333
+ duration: Math.round(performance.now() - startTime)
334
+ };
335
+ }
336
+ verbose.phaseOk(2, 3, "Analyzing semantics", phase2Duration);
337
+ const phase3Start = performance.now();
338
+ const program2 = transformPass(analyzeResult.ast, analyzeResult.context);
339
+ const phase3Duration = Math.round(performance.now() - phase3Start);
340
+ verbose.phaseOk(3, 3, "Transforming AST", phase3Duration);
341
+ const viewNodeCount = countViewNodes(program2.view);
342
+ debug.analyzePass(stateNames.length, actionNames.length, viewNodeCount, phase2Duration);
62
343
  const outputPath = getOutputPath(inputPath, options);
63
344
  const outputDir = dirname(outputPath);
64
345
  try {
65
346
  mkdirSync(outputDir, { recursive: true });
66
347
  } catch (err) {
67
348
  const error = err;
68
- console.error(`Error: Could not create output directory: ${outputDir} - ${error.message}`);
69
- process.exit(1);
349
+ return {
350
+ ok: false,
351
+ errors: [new ConstelaError("SCHEMA_INVALID", `Could not create output directory: ${outputDir} - ${error.message}`)],
352
+ duration: Math.round(performance.now() - startTime)
353
+ };
70
354
  }
71
- const outputContent = options.pretty ? JSON.stringify(result.program, null, 2) : JSON.stringify(result.program);
355
+ const outputContent = options.pretty ? JSON.stringify(program2, null, 2) : JSON.stringify(program2);
72
356
  try {
73
357
  writeFileSync(outputPath, outputContent, "utf-8");
74
358
  } catch (err) {
75
359
  const error = err;
76
- console.error(`Error: Could not write output file: ${outputPath} - ${error.message}`);
360
+ return {
361
+ ok: false,
362
+ errors: [new ConstelaError("SCHEMA_INVALID", `Could not write output file: ${outputPath} - ${error.message}`)],
363
+ duration: Math.round(performance.now() - startTime)
364
+ };
365
+ }
366
+ const outputSize = outputContent.length;
367
+ debug.transformPass(outputSize, phase3Duration);
368
+ const totalDuration = Math.round(performance.now() - startTime);
369
+ verbose.summary(stateNames.length, actionNames.length, viewNodeCount, totalDuration);
370
+ const result = {
371
+ ok: true,
372
+ outputPath,
373
+ duration: totalDuration
374
+ };
375
+ if (options.debug) {
376
+ result.debugInfo = buildDebugInfo(
377
+ basename(inputPath),
378
+ inputSize,
379
+ parseDuration,
380
+ phase1Duration,
381
+ phase2Duration,
382
+ phase3Duration,
383
+ nodesValidated,
384
+ stateNames.length,
385
+ actionNames.length,
386
+ viewNodeCount,
387
+ outputSize
388
+ );
389
+ }
390
+ return result;
391
+ }
392
+ function outputResult(inputPath, result, options, c) {
393
+ const isJsonMode = options.json === true;
394
+ if (result.ok) {
395
+ if (isJsonMode) {
396
+ const output = {
397
+ success: true,
398
+ inputFile: basename(inputPath),
399
+ outputFile: basename(result.outputPath),
400
+ diagnostics: { duration: result.duration }
401
+ };
402
+ if (result.debugInfo) {
403
+ output.debug = result.debugInfo;
404
+ }
405
+ outputJson(output);
406
+ } else if (!options.verbose) {
407
+ console.log(`${c.green("\u2713")} Compiled ${inputPath} \u2192 ${result.outputPath}`);
408
+ }
409
+ } else {
410
+ if (isJsonMode) {
411
+ const output = {
412
+ success: false,
413
+ errors: result.errors.map(errorToJson),
414
+ diagnostics: { duration: result.duration }
415
+ };
416
+ outputJson(output);
417
+ } else {
418
+ for (const error of result.errors) {
419
+ console.error(formatColoredError(error, c));
420
+ }
421
+ }
422
+ }
423
+ }
424
+ async function compileCommand(inputPath, options) {
425
+ const isJsonMode = options.json === true;
426
+ const c = isJsonMode ? { red: (s) => s, green: (s) => s, yellow: (s) => s, reset: "" } : getColors();
427
+ const result = performCompilation(inputPath, options, c);
428
+ outputResult(inputPath, result, options, c);
429
+ if (!options.watch) {
430
+ if (!result.ok) {
431
+ process.exit(1);
432
+ }
433
+ return;
434
+ }
435
+ console.log("[Watching for changes...]");
436
+ const watcher = fsWatch(inputPath, (eventType) => {
437
+ if (eventType === "change") {
438
+ console.log(`[File changed: ${inputPath}]`);
439
+ const watchResult = performCompilation(inputPath, options, c);
440
+ outputResult(inputPath, watchResult, options, c);
441
+ console.log("[Watching for changes...]");
442
+ }
443
+ });
444
+ process.on("SIGINT", () => {
445
+ watcher.close();
446
+ console.log("\n[Watch mode stopped]");
447
+ process.exit(0);
448
+ });
449
+ await new Promise(() => {
450
+ });
451
+ }
452
+
453
+ // src/commands/validate.ts
454
+ import { validatePass as validatePass2, analyzePass as analyzePass2 } from "@constela/compiler";
455
+ import { ConstelaError as ConstelaError2 } from "@constela/core";
456
+ import { readFileSync as readFileSync2, readdirSync, statSync as statSync2 } from "fs";
457
+ import { join as join2, basename as basename2 } from "path";
458
+ function shouldUseColors2() {
459
+ if (process.env["NO_COLOR"] !== void 0) {
460
+ return false;
461
+ }
462
+ if (process.env["FORCE_COLOR"] === "1") {
463
+ return true;
464
+ }
465
+ return process.stdout.isTTY === true;
466
+ }
467
+ var colors2 = {
468
+ red: (s) => `\x1B[31m${s}\x1B[0m`,
469
+ green: (s) => `\x1B[32m${s}\x1B[0m`,
470
+ yellow: (s) => `\x1B[33m${s}\x1B[0m`,
471
+ reset: "\x1B[0m"
472
+ };
473
+ function getColors2() {
474
+ if (shouldUseColors2()) {
475
+ return colors2;
476
+ }
477
+ return {
478
+ red: (s) => s,
479
+ green: (s) => s,
480
+ yellow: (s) => s,
481
+ reset: ""
482
+ };
483
+ }
484
+ function formatColoredError2(error, c) {
485
+ const lines = [];
486
+ const pathInfo = error.path ? ` at ${error.path}` : "";
487
+ lines.push(`Error [${c.red(error.code)}]: ${error.message}${pathInfo}`);
488
+ if (error.suggestion) {
489
+ lines.push(` ${c.yellow(`Suggestion: ${error.suggestion}`)}`);
490
+ }
491
+ return lines.join("\n");
492
+ }
493
+ function errorToJson2(error) {
494
+ const result = {
495
+ code: error.code,
496
+ message: error.message
497
+ };
498
+ if (error.path !== void 0) {
499
+ result.path = error.path;
500
+ }
501
+ if (error.suggestion !== void 0) {
502
+ result.suggestion = error.suggestion;
503
+ }
504
+ if (error.context !== void 0) {
505
+ result.context = error.context;
506
+ }
507
+ return result;
508
+ }
509
+ function outputJson2(output) {
510
+ console.log(JSON.stringify(output));
511
+ }
512
+ function findJsonFiles(dir) {
513
+ const files = [];
514
+ const entries = readdirSync(dir);
515
+ for (const entry of entries) {
516
+ const fullPath = join2(dir, entry);
517
+ const stat = statSync2(fullPath);
518
+ if (stat.isDirectory()) {
519
+ files.push(...findJsonFiles(fullPath));
520
+ } else if (stat.isFile() && entry.endsWith(".json")) {
521
+ files.push(fullPath);
522
+ }
523
+ }
524
+ return files;
525
+ }
526
+ function validateSingleFile(filePath) {
527
+ let fileContent;
528
+ try {
529
+ fileContent = readFileSync2(filePath, "utf-8");
530
+ } catch (err) {
531
+ const error = err;
532
+ if (error.code === "ENOENT") {
533
+ return {
534
+ valid: false,
535
+ errors: [new ConstelaError2("SCHEMA_INVALID", `File not found: ${filePath}`)]
536
+ };
537
+ }
538
+ return {
539
+ valid: false,
540
+ errors: [new ConstelaError2("SCHEMA_INVALID", `Could not read file: ${filePath} - ${error.message}`)]
541
+ };
542
+ }
543
+ let inputJson;
544
+ try {
545
+ inputJson = JSON.parse(fileContent);
546
+ } catch (err) {
547
+ const error = err;
548
+ return {
549
+ valid: false,
550
+ errors: [new ConstelaError2("SCHEMA_INVALID", `Invalid JSON: ${error.message}`)]
551
+ };
552
+ }
553
+ const validateResult = validatePass2(inputJson);
554
+ if (!validateResult.ok) {
555
+ return {
556
+ valid: false,
557
+ errors: [validateResult.error]
558
+ };
559
+ }
560
+ const analyzeResult = analyzePass2(validateResult.ast);
561
+ if (!analyzeResult.ok) {
562
+ return {
563
+ valid: false,
564
+ errors: analyzeResult.errors
565
+ };
566
+ }
567
+ return {
568
+ valid: true,
569
+ errors: []
570
+ };
571
+ }
572
+ async function validateCommand(input, options) {
573
+ const startTime = performance.now();
574
+ const isJsonMode = options.json === true;
575
+ const isAllMode = options.all === true;
576
+ const c = isJsonMode ? { red: (s) => s, green: (s) => s, yellow: (s) => s, reset: "" } : getColors2();
577
+ function exitWithResult(success, output) {
578
+ if (isJsonMode && output) {
579
+ outputJson2(output);
580
+ }
581
+ process.exit(success ? 0 : 1);
582
+ }
583
+ if (isAllMode) {
584
+ const dirPath = input ?? ".";
585
+ try {
586
+ const stat = statSync2(dirPath);
587
+ if (!stat.isDirectory()) {
588
+ const duration3 = Math.round(performance.now() - startTime);
589
+ if (isJsonMode) {
590
+ exitWithResult(false, {
591
+ success: false,
592
+ errors: [{ code: "SCHEMA_INVALID", message: `Not a directory: ${dirPath}` }],
593
+ diagnostics: { duration: duration3 }
594
+ });
595
+ } else {
596
+ console.error(`Error: Not a directory: ${dirPath}`);
597
+ process.exit(1);
598
+ }
599
+ }
600
+ } catch (err) {
601
+ const duration3 = Math.round(performance.now() - startTime);
602
+ if (isJsonMode) {
603
+ exitWithResult(false, {
604
+ success: false,
605
+ errors: [{ code: "SCHEMA_INVALID", message: `Directory not found: ${dirPath}` }],
606
+ diagnostics: { duration: duration3 }
607
+ });
608
+ } else {
609
+ console.error(`Error: Directory not found: ${dirPath}`);
610
+ process.exit(1);
611
+ }
612
+ }
613
+ const jsonFiles = findJsonFiles(dirPath);
614
+ const validatedCount = jsonFiles.length;
615
+ const fileErrors = [];
616
+ const validFiles = [];
617
+ for (const filePath of jsonFiles) {
618
+ const result2 = validateSingleFile(filePath);
619
+ const fileName2 = basename2(filePath);
620
+ if (result2.valid) {
621
+ validFiles.push(fileName2);
622
+ } else {
623
+ fileErrors.push({
624
+ file: fileName2,
625
+ errors: result2.errors.map(errorToJson2)
626
+ });
627
+ if (!isJsonMode) {
628
+ console.error(`
629
+ ${c.red("Error")} in ${fileName2}:`);
630
+ for (const error of result2.errors) {
631
+ console.error(formatColoredError2(error, c));
632
+ }
633
+ }
634
+ }
635
+ }
636
+ const duration2 = Math.round(performance.now() - startTime);
637
+ const hasErrors = fileErrors.length > 0;
638
+ if (isJsonMode) {
639
+ if (hasErrors) {
640
+ const output = {
641
+ success: false,
642
+ files: fileErrors,
643
+ validatedCount,
644
+ diagnostics: { duration: duration2 }
645
+ };
646
+ exitWithResult(false, output);
647
+ } else {
648
+ const output = {
649
+ success: true,
650
+ files: validFiles,
651
+ validatedCount,
652
+ diagnostics: { duration: duration2 }
653
+ };
654
+ exitWithResult(true, output);
655
+ }
656
+ } else {
657
+ if (hasErrors) {
658
+ console.log(`
659
+ Validated ${validatedCount} files with errors`);
660
+ process.exit(1);
661
+ } else {
662
+ console.log(`${c.green("OK")} Validated ${validatedCount} files successfully`);
663
+ process.exit(0);
664
+ }
665
+ }
666
+ }
667
+ if (!input) {
668
+ const duration2 = Math.round(performance.now() - startTime);
669
+ if (isJsonMode) {
670
+ exitWithResult(false, {
671
+ success: false,
672
+ errors: [{ code: "SCHEMA_INVALID", message: "No input file specified" }],
673
+ diagnostics: { duration: duration2 }
674
+ });
675
+ } else {
676
+ console.error("Error: No input file specified");
677
+ process.exit(1);
678
+ }
679
+ }
680
+ const result = validateSingleFile(input);
681
+ const duration = Math.round(performance.now() - startTime);
682
+ const fileName = basename2(input);
683
+ if (result.valid) {
684
+ if (isJsonMode) {
685
+ const output = {
686
+ success: true,
687
+ file: fileName,
688
+ validatedCount: 1,
689
+ diagnostics: { duration }
690
+ };
691
+ exitWithResult(true, output);
692
+ } else {
693
+ console.log(`${c.green("OK")} ${input} is valid`);
694
+ process.exit(0);
695
+ }
696
+ } else {
697
+ if (isJsonMode) {
698
+ const output = {
699
+ success: false,
700
+ file: fileName,
701
+ errors: result.errors.map(errorToJson2),
702
+ diagnostics: { duration }
703
+ };
704
+ exitWithResult(false, output);
705
+ } else {
706
+ for (const error of result.errors) {
707
+ console.error(formatColoredError2(error, c));
708
+ }
709
+ process.exit(1);
710
+ }
711
+ }
712
+ }
713
+
714
+ // src/commands/inspect.ts
715
+ import { readFileSync as readFileSync3 } from "fs";
716
+ function shouldUseColors3() {
717
+ if (process.env["NO_COLOR"] !== void 0) {
718
+ return false;
719
+ }
720
+ if (process.env["FORCE_COLOR"] === "1") {
721
+ return true;
722
+ }
723
+ return process.stdout.isTTY === true;
724
+ }
725
+ var colors3 = {
726
+ red: (s) => `\x1B[31m${s}\x1B[0m`,
727
+ green: (s) => `\x1B[32m${s}\x1B[0m`,
728
+ yellow: (s) => `\x1B[33m${s}\x1B[0m`,
729
+ cyan: (s) => `\x1B[36m${s}\x1B[0m`,
730
+ dim: (s) => `\x1B[2m${s}\x1B[0m`,
731
+ reset: "\x1B[0m"
732
+ };
733
+ function getColors3() {
734
+ if (shouldUseColors3()) {
735
+ return colors3;
736
+ }
737
+ return {
738
+ red: (s) => s,
739
+ green: (s) => s,
740
+ yellow: (s) => s,
741
+ cyan: (s) => s,
742
+ dim: (s) => s,
743
+ reset: ""
744
+ };
745
+ }
746
+ function summarizeStep(step) {
747
+ switch (step.do) {
748
+ case "set":
749
+ return `set ${step.target}`;
750
+ case "update":
751
+ return `${step.operation} ${step.target}`;
752
+ case "fetch":
753
+ return "fetch";
754
+ case "storage":
755
+ return `storage ${step.operation}`;
756
+ case "clipboard":
757
+ return `clipboard ${step.operation}`;
758
+ case "navigate":
759
+ return "navigate";
760
+ case "import":
761
+ return `import ${step.module}`;
762
+ case "call":
763
+ return "call";
764
+ case "subscribe":
765
+ return `subscribe ${step.event}`;
766
+ case "dispose":
767
+ return "dispose";
768
+ case "dom":
769
+ return `dom ${step.operation}`;
770
+ default:
771
+ return "unknown";
772
+ }
773
+ }
774
+ function summarizeAction(action) {
775
+ if (action.steps.length === 0) {
776
+ return "(empty)";
777
+ }
778
+ if (action.steps.length === 1) {
779
+ return summarizeStep(action.steps[0]);
780
+ }
781
+ return `${summarizeStep(action.steps[0])} + ${action.steps.length - 1} more`;
782
+ }
783
+ function viewNodeToInfo(node) {
784
+ const info = { kind: node.kind };
785
+ switch (node.kind) {
786
+ case "element": {
787
+ info.tag = node.tag;
788
+ if (node.props) {
789
+ for (const [key, value] of Object.entries(node.props)) {
790
+ if (key === "onClick" && typeof value === "object" && value !== null && "action" in value) {
791
+ info.event = `onClick=${value.action}`;
792
+ break;
793
+ }
794
+ }
795
+ }
796
+ if (node.children && node.children.length > 0) {
797
+ info.children = node.children.map(viewNodeToInfo);
798
+ }
799
+ break;
800
+ }
801
+ case "text": {
802
+ const val = node.value;
803
+ if (val.expr === "state") {
804
+ info.text = `state.${val.name}`;
805
+ } else if (val.expr === "lit") {
806
+ info.text = String(val.value);
807
+ } else if (val.expr === "param") {
808
+ info.text = `param.${val.name}`;
809
+ } else {
810
+ info.text = `(${val.expr})`;
811
+ }
812
+ break;
813
+ }
814
+ case "if": {
815
+ info.children = [viewNodeToInfo(node.then)];
816
+ if (node.else) {
817
+ info.children.push(viewNodeToInfo(node.else));
818
+ }
819
+ break;
820
+ }
821
+ case "each": {
822
+ info.children = [viewNodeToInfo(node.body)];
823
+ break;
824
+ }
825
+ case "component": {
826
+ info.tag = node.name;
827
+ if (node.children && node.children.length > 0) {
828
+ info.children = node.children.map(viewNodeToInfo);
829
+ }
830
+ break;
831
+ }
832
+ default:
833
+ break;
834
+ }
835
+ return info;
836
+ }
837
+ function formatStateSection(state, c) {
838
+ const lines = [];
839
+ const fieldCount = Object.keys(state).length;
840
+ lines.push(`${c.cyan("State")} (${fieldCount} fields):`);
841
+ for (const [name, field] of Object.entries(state)) {
842
+ const initialStr = JSON.stringify(field.initial);
843
+ lines.push(` ${name}: ${c.yellow(field.type)} = ${c.dim(initialStr)}`);
844
+ }
845
+ return lines;
846
+ }
847
+ function formatActionsSection(actions, c) {
848
+ const lines = [];
849
+ lines.push(`${c.cyan("Actions")} (${actions.length}):`);
850
+ for (const action of actions) {
851
+ const summary = summarizeAction(action);
852
+ lines.push(` ${action.name}: ${c.dim(summary)}`);
853
+ }
854
+ return lines;
855
+ }
856
+ function normalizeComponents(components) {
857
+ if (Array.isArray(components)) {
858
+ return components.map((comp) => ({
859
+ name: comp.name,
860
+ params: Array.isArray(comp.params) ? comp.params : Object.entries(comp.params ?? {}).map(([pName, pDef]) => ({
861
+ name: pName,
862
+ type: pDef.type
863
+ }))
864
+ }));
865
+ }
866
+ return Object.entries(components).map(
867
+ ([name, def]) => ({
868
+ name,
869
+ params: Array.isArray(def.params) ? def.params : Object.entries(def.params ?? {}).map(([pName, pDef]) => ({
870
+ name: pName,
871
+ type: pDef.type
872
+ }))
873
+ })
874
+ );
875
+ }
876
+ function formatComponentsSection(components, c) {
877
+ const lines = [];
878
+ const normalized = normalizeComponents(components);
879
+ const componentCount = normalized.length;
880
+ lines.push(`${c.cyan("Components")} (${componentCount}):`);
881
+ for (const comp of normalized) {
882
+ const paramList = comp.params.map((p) => `${p.name}: ${p.type}`).join(", ");
883
+ lines.push(` ${comp.name}: params(${c.dim(paramList)})`);
884
+ }
885
+ return lines;
886
+ }
887
+ function formatStylesSection(styles, c) {
888
+ const lines = [];
889
+ const styleCount = Object.keys(styles).length;
890
+ lines.push(`${c.cyan("Styles")} (${styleCount}):`);
891
+ for (const [name, preset] of Object.entries(styles)) {
892
+ const variantCount = preset.variants ? Object.keys(preset.variants).length : 0;
893
+ const desc = variantCount > 0 ? `base + ${variantCount} variants` : "base only";
894
+ lines.push(` ${name}: ${c.dim(desc)}`);
895
+ }
896
+ return lines;
897
+ }
898
+ function formatViewTree(node, c, indent = 0) {
899
+ const lines = [];
900
+ const prefix = " ".repeat(indent);
901
+ switch (node.kind) {
902
+ case "element": {
903
+ let line = `${prefix}element<${c.green(node.tag)}>`;
904
+ if (node.props) {
905
+ for (const [key, value] of Object.entries(node.props)) {
906
+ if (key === "onClick" && typeof value === "object" && value !== null && "action" in value) {
907
+ line += ` ${c.dim(`onClick=${value.action}`)}`;
908
+ }
909
+ }
910
+ }
911
+ lines.push(line);
912
+ if (node.children) {
913
+ for (const child of node.children) {
914
+ lines.push(...formatViewTree(child, c, indent + 1));
915
+ }
916
+ }
917
+ break;
918
+ }
919
+ case "text": {
920
+ const val = node.value;
921
+ let textDesc;
922
+ if (val.expr === "state") {
923
+ textDesc = `state.${val.name}`;
924
+ } else if (val.expr === "lit") {
925
+ textDesc = JSON.stringify(val.value);
926
+ } else if (val.expr === "param") {
927
+ textDesc = `param.${val.name}`;
928
+ } else {
929
+ textDesc = `(${val.expr})`;
930
+ }
931
+ lines.push(`${prefix}text: ${c.dim(textDesc)}`);
932
+ break;
933
+ }
934
+ case "if": {
935
+ lines.push(`${prefix}if:`);
936
+ lines.push(`${prefix} then:`);
937
+ lines.push(...formatViewTree(node.then, c, indent + 2));
938
+ if (node.else) {
939
+ lines.push(`${prefix} else:`);
940
+ lines.push(...formatViewTree(node.else, c, indent + 2));
941
+ }
942
+ break;
943
+ }
944
+ case "each": {
945
+ lines.push(`${prefix}each (as ${node.as}):`);
946
+ lines.push(...formatViewTree(node.body, c, indent + 1));
947
+ break;
948
+ }
949
+ case "component": {
950
+ lines.push(`${prefix}component<${c.green(node.name)}>`);
951
+ if (node.children) {
952
+ for (const child of node.children) {
953
+ lines.push(...formatViewTree(child, c, indent + 1));
954
+ }
955
+ }
956
+ break;
957
+ }
958
+ case "slot": {
959
+ const slotName = node.name ? `(${node.name})` : "";
960
+ lines.push(`${prefix}slot${slotName}`);
961
+ break;
962
+ }
963
+ case "markdown": {
964
+ lines.push(`${prefix}markdown`);
965
+ break;
966
+ }
967
+ case "code": {
968
+ lines.push(`${prefix}code`);
969
+ break;
970
+ }
971
+ }
972
+ return lines;
973
+ }
974
+ function formatViewSection(view, c) {
975
+ const lines = [];
976
+ lines.push(`${c.cyan("View Tree")}:`);
977
+ lines.push(...formatViewTree(view, c, 1));
978
+ return lines;
979
+ }
980
+ async function inspectCommand(input, options) {
981
+ const isJsonMode = options.json === true;
982
+ const c = isJsonMode ? {
983
+ red: (s) => s,
984
+ green: (s) => s,
985
+ yellow: (s) => s,
986
+ cyan: (s) => s,
987
+ dim: (s) => s,
988
+ reset: ""
989
+ } : getColors3();
990
+ const showAll = !options.state && !options.actions && !options.components && !options.view;
991
+ const showState = showAll || options.state === true;
992
+ const showActions = showAll || options.actions === true;
993
+ const showComponents = showAll || options.components === true;
994
+ const showView = showAll || options.view === true;
995
+ const showStyles = showAll;
996
+ let fileContent;
997
+ try {
998
+ fileContent = readFileSync3(input, "utf-8");
999
+ } catch (err) {
1000
+ const error = err;
1001
+ if (error.code === "ENOENT") {
1002
+ console.error(`Error: File not found: ${input}`);
1003
+ } else {
1004
+ console.error(`Error: Could not read file: ${input} - ${error.message}`);
1005
+ }
77
1006
  process.exit(1);
78
1007
  }
79
- console.log(`Compiled ${inputPath} \u2192 ${outputPath}`);
1008
+ let program2;
1009
+ try {
1010
+ program2 = JSON.parse(fileContent);
1011
+ } catch (err) {
1012
+ const error = err;
1013
+ console.error(`Error: Invalid JSON syntax: ${error.message}`);
1014
+ process.exit(1);
1015
+ }
1016
+ if (isJsonMode) {
1017
+ const output = {};
1018
+ if (showState && program2.state) {
1019
+ output.state = {};
1020
+ for (const [name, field] of Object.entries(program2.state)) {
1021
+ output.state[name] = {
1022
+ type: field.type,
1023
+ initial: field.initial
1024
+ };
1025
+ }
1026
+ }
1027
+ if (showActions && program2.actions) {
1028
+ output.actions = program2.actions.map((action) => ({
1029
+ name: action.name,
1030
+ stepSummary: summarizeAction(action)
1031
+ }));
1032
+ }
1033
+ if (showComponents && program2.components) {
1034
+ output.components = normalizeComponents(program2.components);
1035
+ }
1036
+ if (showStyles && program2.styles) {
1037
+ output.styles = program2.styles;
1038
+ }
1039
+ if (showView && program2.view) {
1040
+ output.viewTree = viewNodeToInfo(program2.view);
1041
+ }
1042
+ console.log(JSON.stringify(output));
1043
+ } else {
1044
+ const sections = [];
1045
+ if (showState && program2.state) {
1046
+ sections.push(formatStateSection(program2.state, c));
1047
+ }
1048
+ if (showActions && program2.actions) {
1049
+ sections.push(formatActionsSection(program2.actions, c));
1050
+ }
1051
+ if (showComponents && program2.components) {
1052
+ sections.push(formatComponentsSection(program2.components, c));
1053
+ }
1054
+ if (showStyles && program2.styles) {
1055
+ sections.push(formatStylesSection(program2.styles, c));
1056
+ }
1057
+ if (showView && program2.view) {
1058
+ sections.push(formatViewSection(program2.view, c));
1059
+ }
1060
+ for (let i = 0; i < sections.length; i++) {
1061
+ for (const line of sections[i]) {
1062
+ console.log(line);
1063
+ }
1064
+ if (i < sections.length - 1) {
1065
+ console.log("");
1066
+ }
1067
+ }
1068
+ }
80
1069
  }
81
1070
 
82
1071
  // src/commands/dev.ts
@@ -185,7 +1174,9 @@ async function startCommand(options) {
185
1174
  // src/index.ts
186
1175
  var program = new Command();
187
1176
  program.name("constela").description("Constela UI framework CLI").version("0.1.0");
188
- program.command("compile <input>").description("Compile a Constela DSL file").option("-o, --out <path>", "Output file path").option("--pretty", "Pretty-print JSON output").action(compileCommand);
1177
+ program.command("compile <input>").description("Compile a Constela DSL file").option("-o, --out <path>", "Output file path").option("--pretty", "Pretty-print JSON output").option("--json", "Output results as JSON").option("-w, --watch", "Watch input file for changes and recompile").option("-v, --verbose", "Show detailed progress during compilation").option("--debug", "Show internal debug information").action(compileCommand);
1178
+ program.command("validate [input]").description("Validate Constela JSON files without compilation").option("-a, --all", "Validate all JSON files in directory recursively").option("--json", "Output results as JSON").action(validateCommand);
1179
+ program.command("inspect <input>").description("Inspect Constela program structure").option("--state", "Show only state").option("--actions", "Show only actions").option("--components", "Show only components").option("--view", "Show only view tree").option("--json", "Output as JSON").action(inspectCommand);
189
1180
  program.command("dev").description("Start development server").option("-p, --port <number>", "Port number (default: 3000)").option("--host <string>", "Host address").option("--routesDir <path>", "Routes directory").option("--publicDir <path>", "Public directory").option("--layoutsDir <path>", "Layouts directory").action(devCommand);
190
1181
  program.command("build").description("Build for production").option("-o, --outDir <path>", "Output directory (default: dist)").option("--routesDir <path>", "Routes directory").option("--publicDir <path>", "Public directory").option("--layoutsDir <path>", "Layouts directory").action(buildCommand);
191
1182
  program.command("start").description("Start production server").option("-p, --port <number>", "Port number (default: 3000)").action(startCommand);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@constela/cli",
3
- "version": "0.3.30",
3
+ "version": "0.4.1",
4
4
  "description": "CLI tools for Constela UI framework",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -19,9 +19,9 @@
19
19
  ],
20
20
  "dependencies": {
21
21
  "commander": "^12.0.0",
22
- "@constela/core": "0.7.0",
23
- "@constela/compiler": "0.7.1",
24
- "@constela/start": "1.2.23"
22
+ "@constela/core": "0.8.0",
23
+ "@constela/start": "1.2.25",
24
+ "@constela/compiler": "0.8.0"
25
25
  },
26
26
  "devDependencies": {
27
27
  "@types/node": "^20.10.0",