@constela/compiler 0.7.0 → 0.8.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/README.md ADDED
@@ -0,0 +1,172 @@
1
+ # @constela/compiler
2
+
3
+ Transforms Constela JSON programs into optimized runtime code.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ npm install @constela/compiler
9
+ ```
10
+
11
+ ## Usage
12
+
13
+ ```bash
14
+ constela compile app.json --out dist/app.compiled.json
15
+ ```
16
+
17
+ ## JSON Input
18
+
19
+ ```json
20
+ {
21
+ "version": "1.0",
22
+ "state": {
23
+ "count": { "type": "number", "initial": 0 }
24
+ },
25
+ "actions": [
26
+ {
27
+ "name": "increment",
28
+ "steps": [{ "do": "update", "target": "count", "operation": "increment" }]
29
+ }
30
+ ],
31
+ "view": {
32
+ "kind": "element",
33
+ "tag": "button",
34
+ "props": { "onClick": { "event": "click", "action": "increment" } },
35
+ "children": [{ "kind": "text", "value": { "expr": "state", "name": "count" } }]
36
+ }
37
+ }
38
+ ```
39
+
40
+ ## Compiler Pipeline
41
+
42
+ The compiler transforms JSON programs through three passes:
43
+
44
+ 1. **Validate** - JSON Schema validation
45
+ 2. **Analyze** - Semantic analysis (state, actions, components, routes)
46
+ 3. **Transform** - AST to optimized runtime program
47
+
48
+ ## CompiledProgram Structure
49
+
50
+ ```json
51
+ {
52
+ "version": "1.0",
53
+ "route": {
54
+ "path": "/users/:id",
55
+ "params": ["id"],
56
+ "title": { ... },
57
+ "layout": "MainLayout"
58
+ },
59
+ "lifecycle": {
60
+ "onMount": "loadData",
61
+ "onUnmount": "cleanup"
62
+ },
63
+ "state": {
64
+ "count": { "type": "number", "initial": 0 }
65
+ },
66
+ "actions": {
67
+ "increment": {
68
+ "name": "increment",
69
+ "steps": [{ "do": "update", "target": "count", "operation": "increment" }]
70
+ }
71
+ },
72
+ "view": { ... }
73
+ }
74
+ ```
75
+
76
+ ## Layout Compilation
77
+
78
+ Layouts are compiled separately with slot validation:
79
+
80
+ ```json
81
+ {
82
+ "version": "1.0",
83
+ "type": "layout",
84
+ "view": {
85
+ "kind": "element",
86
+ "tag": "div",
87
+ "children": [
88
+ { "kind": "component", "name": "Header" },
89
+ { "kind": "slot" },
90
+ { "kind": "component", "name": "Footer" }
91
+ ]
92
+ }
93
+ }
94
+ ```
95
+
96
+ **Layout Validations:**
97
+ - At least one slot exists
98
+ - No duplicate named slots
99
+ - No duplicate default slots
100
+ - Slots not inside loops
101
+
102
+ ## Error Handling
103
+
104
+ Structured errors with JSON Pointer paths:
105
+
106
+ ```json
107
+ {
108
+ "code": "UNDEFINED_STATE",
109
+ "message": "State \"count\" is not defined",
110
+ "path": "/view/children/0/props/onClick"
111
+ }
112
+ ```
113
+
114
+ **Error Codes:**
115
+ - `SCHEMA_INVALID` - JSON Schema validation error
116
+ - `UNDEFINED_STATE` - Reference to undefined state
117
+ - `UNDEFINED_ACTION` - Reference to undefined action
118
+ - `DUPLICATE_ACTION` - Duplicate action name
119
+ - `VAR_UNDEFINED` - Undefined variable reference
120
+ - `COMPONENT_NOT_FOUND` - Undefined component
121
+ - `COMPONENT_PROP_MISSING` - Missing required prop
122
+ - `COMPONENT_CYCLE` - Circular component reference
123
+ - `OPERATION_INVALID_FOR_TYPE` - Invalid operation for state type
124
+ - `LAYOUT_MISSING_SLOT` - Layout missing slot node
125
+ - `LAYOUT_NOT_FOUND` - Referenced layout not found
126
+
127
+ ## Internal API
128
+
129
+ > For framework developers only. End users should use the CLI.
130
+
131
+ ### compile
132
+
133
+ ```typescript
134
+ import { compile } from '@constela/compiler';
135
+
136
+ const result = compile(jsonInput);
137
+
138
+ if (result.ok) {
139
+ console.log(result.program);
140
+ } else {
141
+ console.error(result.errors);
142
+ }
143
+ ```
144
+
145
+ ### Individual Passes
146
+
147
+ ```typescript
148
+ import { validatePass, analyzePass, transformPass } from '@constela/compiler';
149
+
150
+ // Step 1: Validate
151
+ const validated = validatePass(input);
152
+
153
+ // Step 2: Analyze
154
+ const analyzed = analyzePass(validated.program);
155
+
156
+ // Step 3: Transform
157
+ const compiled = transformPass(analyzed.program, analyzed.context);
158
+ ```
159
+
160
+ ### Layout Compilation
161
+
162
+ ```typescript
163
+ import { analyzeLayoutPass, transformLayoutPass, composeLayoutWithPage } from '@constela/compiler';
164
+
165
+ const layoutResult = analyzeLayoutPass(layoutProgram);
166
+ const compiledLayout = transformLayoutPass(layoutProgram, layoutResult.context);
167
+ const composedProgram = composeLayoutWithPage(compiledLayout, compiledPage);
168
+ ```
169
+
170
+ ## License
171
+
172
+ MIT
package/dist/index.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { Program, ConstelaError, LayoutProgram, ComponentDef, ViewNode } from '@constela/core';
1
+ import { Program, ConstelaError, Expression, LayoutProgram, ComponentDef, ViewNode } from '@constela/core';
2
2
  export { createUndefinedVarError } from '@constela/core';
3
3
 
4
4
  /**
@@ -20,6 +20,7 @@ interface AnalysisContext {
20
20
  importNames: Set<string>;
21
21
  dataNames: Set<string>;
22
22
  refNames: Set<string>;
23
+ styleNames: Set<string>;
23
24
  }
24
25
  interface AnalyzePassSuccess {
25
26
  ok: true;
@@ -63,6 +64,7 @@ interface CompiledRouteDefinition {
63
64
  params: string[];
64
65
  title?: CompiledExpression;
65
66
  layout?: string;
67
+ layoutParams?: Record<string, Expression>;
66
68
  meta?: Record<string, CompiledExpression>;
67
69
  }
68
70
  interface CompiledLifecycleHooks {
@@ -87,7 +89,7 @@ interface CompiledAction {
87
89
  name: string;
88
90
  steps: CompiledActionStep[];
89
91
  }
90
- type CompiledActionStep = CompiledSetStep | CompiledUpdateStep | CompiledFetchStep | CompiledStorageStep | CompiledClipboardStep | CompiledNavigateStep | CompiledImportStep | CompiledCallStep | CompiledSubscribeStep | CompiledDisposeStep;
92
+ type CompiledActionStep = CompiledSetStep | CompiledUpdateStep | CompiledFetchStep | CompiledStorageStep | CompiledClipboardStep | CompiledNavigateStep | CompiledImportStep | CompiledCallStep | CompiledSubscribeStep | CompiledDisposeStep | CompiledDomStep | CompiledIfStep;
91
93
  interface CompiledSetStep {
92
94
  do: 'set';
93
95
  target: string;
@@ -178,7 +180,23 @@ interface CompiledDisposeStep {
178
180
  do: 'dispose';
179
181
  target: CompiledExpression;
180
182
  }
181
- type CompiledNode = CompiledElementNode | CompiledTextNode | CompiledIfNode | CompiledEachNode | CompiledMarkdownNode | CompiledCodeNode;
183
+ /**
184
+ * Compiled DOM manipulation step
185
+ */
186
+ interface CompiledDomStep {
187
+ do: 'dom';
188
+ operation: 'addClass' | 'removeClass' | 'toggleClass' | 'setAttribute' | 'removeAttribute';
189
+ selector: CompiledExpression;
190
+ value?: CompiledExpression;
191
+ attribute?: string;
192
+ }
193
+ interface CompiledIfStep {
194
+ do: 'if';
195
+ condition: CompiledExpression;
196
+ then: CompiledActionStep[];
197
+ else?: CompiledActionStep[];
198
+ }
199
+ type CompiledNode = CompiledElementNode | CompiledTextNode | CompiledIfNode | CompiledEachNode | CompiledMarkdownNode | CompiledCodeNode | CompiledSlotNode;
182
200
  interface CompiledElementNode {
183
201
  kind: 'element';
184
202
  tag: string;
@@ -213,7 +231,11 @@ interface CompiledCodeNode {
213
231
  language: CompiledExpression;
214
232
  content: CompiledExpression;
215
233
  }
216
- type CompiledExpression = CompiledLitExpr | CompiledStateExpr | CompiledVarExpr | CompiledBinExpr | CompiledNotExpr | CompiledCondExpr | CompiledGetExpr | CompiledRouteExpr | CompiledImportExpr | CompiledRefExpr;
234
+ interface CompiledSlotNode {
235
+ kind: 'slot';
236
+ name?: string;
237
+ }
238
+ type CompiledExpression = CompiledLitExpr | CompiledStateExpr | CompiledVarExpr | CompiledBinExpr | CompiledNotExpr | CompiledCondExpr | CompiledGetExpr | CompiledRouteExpr | CompiledImportExpr | CompiledDataExpr | CompiledRefExpr | CompiledIndexExpr | CompiledParamExpr | CompiledStyleExpr;
217
239
  interface CompiledLitExpr {
218
240
  expr: 'lit';
219
241
  value: string | number | boolean | null | unknown[];
@@ -221,6 +243,7 @@ interface CompiledLitExpr {
221
243
  interface CompiledStateExpr {
222
244
  expr: 'state';
223
245
  name: string;
246
+ path?: string;
224
247
  }
225
248
  interface CompiledVarExpr {
226
249
  expr: 'var';
@@ -258,6 +281,26 @@ interface CompiledImportExpr {
258
281
  name: string;
259
282
  path?: string;
260
283
  }
284
+ interface CompiledDataExpr {
285
+ expr: 'data';
286
+ name: string;
287
+ path?: string;
288
+ }
289
+ interface CompiledIndexExpr {
290
+ expr: 'index';
291
+ base: CompiledExpression;
292
+ key: CompiledExpression;
293
+ }
294
+ interface CompiledParamExpr {
295
+ expr: 'param';
296
+ name: string;
297
+ path?: string;
298
+ }
299
+ interface CompiledStyleExpr {
300
+ expr: 'style';
301
+ name: string;
302
+ variants?: Record<string, CompiledExpression>;
303
+ }
261
304
  interface CompiledEventHandler {
262
305
  event: string;
263
306
  action: string;
@@ -380,6 +423,7 @@ interface CompiledLayoutProgram {
380
423
  actions: CompiledAction[];
381
424
  view: CompiledNode;
382
425
  components?: Record<string, ComponentDef> | undefined;
426
+ importData?: Record<string, unknown>;
383
427
  }
384
428
  /**
385
429
  * Transforms a layout program into a compiled layout
@@ -388,6 +432,6 @@ declare function transformLayoutPass(layout: LayoutProgram, _context: LayoutAnal
388
432
  /**
389
433
  * Composes a layout with a page, inserting page content into slots
390
434
  */
391
- declare function composeLayoutWithPage(layout: CompiledProgram, page: CompiledProgram, slots?: Record<string, ViewNode>): CompiledProgram;
435
+ declare function composeLayoutWithPage(layout: CompiledProgram, page: CompiledProgram, layoutParams?: Record<string, Expression>, slots?: Record<string, ViewNode>): CompiledProgram;
392
436
 
393
- export { type AnalysisContext, type AnalyzePassFailure, type AnalyzePassResult, type AnalyzePassSuccess, type CompileFailure, type CompileResult, type CompileSuccess, type CompiledAction, type CompiledActionStep, type CompiledBinExpr, type CompiledCallStep, type CompiledClipboardStep, type CompiledCodeNode, type CompiledCondExpr, type CompiledDisposeStep, type CompiledEachNode, type CompiledElementNode, type CompiledEventHandler, type CompiledExpression, type CompiledFetchStep, type CompiledGetExpr, type CompiledIfNode, type CompiledImportExpr, type CompiledImportStep, type CompiledLayoutProgram, type CompiledLifecycleHooks, type CompiledLitExpr, type CompiledMarkdownNode, type CompiledNavigateStep, type CompiledNode, type CompiledNotExpr, type CompiledProgram, type CompiledRefExpr, type CompiledRouteDefinition, type CompiledRouteExpr, type CompiledSetStep, type CompiledStateExpr, type CompiledStorageStep, type CompiledSubscribeStep, type CompiledTextNode, type CompiledUpdateStep, type CompiledVarExpr, type LayoutAnalysisContext, type LayoutAnalysisFailure, type LayoutAnalysisResult, type LayoutAnalysisSuccess, type ValidatePassFailure, type ValidatePassResult, type ValidatePassSuccess, analyzeLayoutPass, analyzePass, compile, composeLayoutWithPage, transformLayoutPass, transformPass, validatePass };
437
+ export { type AnalysisContext, type AnalyzePassFailure, type AnalyzePassResult, type AnalyzePassSuccess, type CompileFailure, type CompileResult, type CompileSuccess, type CompiledAction, type CompiledActionStep, type CompiledBinExpr, type CompiledCallStep, type CompiledClipboardStep, type CompiledCodeNode, type CompiledCondExpr, type CompiledDataExpr, type CompiledDisposeStep, type CompiledDomStep, type CompiledEachNode, type CompiledElementNode, type CompiledEventHandler, type CompiledExpression, type CompiledFetchStep, type CompiledGetExpr, type CompiledIfNode, type CompiledIfStep, type CompiledImportExpr, type CompiledImportStep, type CompiledIndexExpr, type CompiledLayoutProgram, type CompiledLifecycleHooks, type CompiledLitExpr, type CompiledMarkdownNode, type CompiledNavigateStep, type CompiledNode, type CompiledNotExpr, type CompiledProgram, type CompiledRefExpr, type CompiledRouteDefinition, type CompiledRouteExpr, type CompiledSetStep, type CompiledSlotNode, type CompiledStateExpr, type CompiledStorageStep, type CompiledSubscribeStep, type CompiledTextNode, type CompiledUpdateStep, type CompiledVarExpr, type LayoutAnalysisContext, type LayoutAnalysisFailure, type LayoutAnalysisResult, type LayoutAnalysisSuccess, type ValidatePassFailure, type ValidatePassResult, type ValidatePassSuccess, analyzeLayoutPass, analyzePass, compile, composeLayoutWithPage, transformLayoutPass, transformPass, validatePass };
package/dist/index.js CHANGED
@@ -42,6 +42,9 @@ import {
42
42
  createClipboardWriteMissingValueError,
43
43
  createInvalidNavigateTargetError,
44
44
  createUndefinedRefError,
45
+ createUndefinedStyleError,
46
+ createUndefinedVariantError,
47
+ findSimilarNames,
45
48
  isEventHandler,
46
49
  DATA_SOURCE_TYPES,
47
50
  DATA_TRANSFORMS,
@@ -53,6 +56,15 @@ import {
53
56
  function buildPath(base, ...segments) {
54
57
  return segments.reduce((p, s) => `${p}/${s}`, base);
55
58
  }
59
+ function createErrorOptionsWithSuggestion(name, availableNames) {
60
+ const availableNamesArray = Array.from(availableNames);
61
+ const similarNames = findSimilarNames(name, availableNames);
62
+ const suggestion = similarNames.length > 0 ? `Did you mean '${similarNames[0]}'?` : void 0;
63
+ return {
64
+ suggestion,
65
+ context: { availableNames: availableNamesArray }
66
+ };
67
+ }
56
68
  function extractRouteParams(path) {
57
69
  const params = [];
58
70
  const segments = path.split("/");
@@ -124,7 +136,10 @@ function collectContext(programAst) {
124
136
  programAst.data ? Object.keys(programAst.data) : []
125
137
  );
126
138
  const refNames = collectRefs(programAst.view);
127
- return { stateNames, actionNames, componentNames, routeParams, importNames, dataNames, refNames };
139
+ const styleNames = new Set(
140
+ programAst.styles ? Object.keys(programAst.styles) : []
141
+ );
142
+ return { stateNames, actionNames, componentNames, routeParams, importNames, dataNames, refNames, styleNames };
128
143
  }
129
144
  function checkDuplicateActions(ast2) {
130
145
  const errors = [];
@@ -144,7 +159,8 @@ function validateExpression(expr, path, context, scope, paramScope) {
144
159
  switch (expr.expr) {
145
160
  case "state":
146
161
  if (!context.stateNames.has(expr.name)) {
147
- errors.push(createUndefinedStateError(expr.name, path));
162
+ const errorOptions = createErrorOptionsWithSuggestion(expr.name, context.stateNames);
163
+ errors.push(createUndefinedStateError(expr.name, path, errorOptions));
148
164
  }
149
165
  break;
150
166
  case "var":
@@ -207,6 +223,32 @@ function validateExpression(expr, path, context, scope, paramScope) {
207
223
  case "get":
208
224
  errors.push(...validateExpression(expr.base, buildPath(path, "base"), context, scope, paramScope));
209
225
  break;
226
+ case "style":
227
+ errors.push(...validateStyleExpression(expr, path, context, scope, paramScope));
228
+ break;
229
+ }
230
+ return errors;
231
+ }
232
+ function validateStyleExpression(expr, path, context, scope, paramScope) {
233
+ const errors = [];
234
+ if (!context.styleNames.has(expr.name)) {
235
+ const errorOptions = createErrorOptionsWithSuggestion(expr.name, context.styleNames);
236
+ errors.push(createUndefinedStyleError(expr.name, path, errorOptions));
237
+ return errors;
238
+ }
239
+ if (expr.variants) {
240
+ const stylePreset = ast.styles?.[expr.name];
241
+ const availableVariants = new Set(
242
+ stylePreset?.variants ? Object.keys(stylePreset.variants) : []
243
+ );
244
+ for (const [variantKey, variantValue] of Object.entries(expr.variants)) {
245
+ const variantPath = buildPath(path, "variants", variantKey);
246
+ if (!availableVariants.has(variantKey)) {
247
+ const errorOptions = createErrorOptionsWithSuggestion(variantKey, availableVariants);
248
+ errors.push(createUndefinedVariantError(variantKey, expr.name, variantPath, errorOptions));
249
+ }
250
+ errors.push(...validateExpression(variantValue, variantPath, context, scope, paramScope));
251
+ }
210
252
  }
211
253
  return errors;
212
254
  }
@@ -215,7 +257,8 @@ function validateActionStep(step, path, context) {
215
257
  switch (step.do) {
216
258
  case "set":
217
259
  if (!context.stateNames.has(step.target)) {
218
- errors.push(createUndefinedStateError(step.target, buildPath(path, "target")));
260
+ const errorOptions = createErrorOptionsWithSuggestion(step.target, context.stateNames);
261
+ errors.push(createUndefinedStateError(step.target, buildPath(path, "target"), errorOptions));
219
262
  }
220
263
  errors.push(
221
264
  ...validateExpressionStateOnly(step.value, buildPath(path, "value"), context)
@@ -223,7 +266,8 @@ function validateActionStep(step, path, context) {
223
266
  break;
224
267
  case "update": {
225
268
  if (!context.stateNames.has(step.target)) {
226
- errors.push(createUndefinedStateError(step.target, buildPath(path, "target")));
269
+ const errorOptions = createErrorOptionsWithSuggestion(step.target, context.stateNames);
270
+ errors.push(createUndefinedStateError(step.target, buildPath(path, "target"), errorOptions));
227
271
  } else {
228
272
  const stateField = ast.state[step.target];
229
273
  if (stateField) {
@@ -437,7 +481,8 @@ function validateActionStep(step, path, context) {
437
481
  ...validateExpressionStateOnly(subscribeStep.target, buildPath(path, "target"), context)
438
482
  );
439
483
  if (!context.actionNames.has(subscribeStep.action)) {
440
- errors.push(createUndefinedActionError(subscribeStep.action, buildPath(path, "action")));
484
+ const errorOptions = createErrorOptionsWithSuggestion(subscribeStep.action, context.actionNames);
485
+ errors.push(createUndefinedActionError(subscribeStep.action, buildPath(path, "action"), errorOptions));
441
486
  }
442
487
  break;
443
488
  }
@@ -456,7 +501,8 @@ function validateExpressionStateOnly(expr, path, context) {
456
501
  switch (expr.expr) {
457
502
  case "state":
458
503
  if (!context.stateNames.has(expr.name)) {
459
- errors.push(createUndefinedStateError(expr.name, path));
504
+ const errorOptions = createErrorOptionsWithSuggestion(expr.name, context.stateNames);
505
+ errors.push(createUndefinedStateError(expr.name, path, errorOptions));
460
506
  }
461
507
  break;
462
508
  case "var":
@@ -513,6 +559,32 @@ function validateExpressionStateOnly(expr, path, context) {
513
559
  case "get":
514
560
  errors.push(...validateExpressionStateOnly(expr.base, buildPath(path, "base"), context));
515
561
  break;
562
+ case "style":
563
+ errors.push(...validateStyleExpressionStateOnly(expr, path, context));
564
+ break;
565
+ }
566
+ return errors;
567
+ }
568
+ function validateStyleExpressionStateOnly(expr, path, context) {
569
+ const errors = [];
570
+ if (!context.styleNames.has(expr.name)) {
571
+ const errorOptions = createErrorOptionsWithSuggestion(expr.name, context.styleNames);
572
+ errors.push(createUndefinedStyleError(expr.name, path, errorOptions));
573
+ return errors;
574
+ }
575
+ if (expr.variants) {
576
+ const stylePreset = ast.styles?.[expr.name];
577
+ const availableVariants = new Set(
578
+ stylePreset?.variants ? Object.keys(stylePreset.variants) : []
579
+ );
580
+ for (const [variantKey, variantValue] of Object.entries(expr.variants)) {
581
+ const variantPath = buildPath(path, "variants", variantKey);
582
+ if (!availableVariants.has(variantKey)) {
583
+ const errorOptions = createErrorOptionsWithSuggestion(variantKey, availableVariants);
584
+ errors.push(createUndefinedVariantError(variantKey, expr.name, variantPath, errorOptions));
585
+ }
586
+ errors.push(...validateExpressionStateOnly(variantValue, variantPath, context));
587
+ }
516
588
  }
517
589
  return errors;
518
590
  }
@@ -521,7 +593,8 @@ function validateExpressionInEventPayload(expr, path, context, scope) {
521
593
  switch (expr.expr) {
522
594
  case "state":
523
595
  if (!context.stateNames.has(expr.name)) {
524
- errors.push(createUndefinedStateError(expr.name, path));
596
+ const errorOptions = createErrorOptionsWithSuggestion(expr.name, context.stateNames);
597
+ errors.push(createUndefinedStateError(expr.name, path, errorOptions));
525
598
  }
526
599
  break;
527
600
  case "var":
@@ -595,6 +668,32 @@ function validateExpressionInEventPayload(expr, path, context, scope) {
595
668
  ...validateExpressionInEventPayload(expr.base, buildPath(path, "base"), context, scope)
596
669
  );
597
670
  break;
671
+ case "style":
672
+ errors.push(...validateStyleExpressionInEventPayload(expr, path, context, scope));
673
+ break;
674
+ }
675
+ return errors;
676
+ }
677
+ function validateStyleExpressionInEventPayload(expr, path, context, scope) {
678
+ const errors = [];
679
+ if (!context.styleNames.has(expr.name)) {
680
+ const errorOptions = createErrorOptionsWithSuggestion(expr.name, context.styleNames);
681
+ errors.push(createUndefinedStyleError(expr.name, path, errorOptions));
682
+ return errors;
683
+ }
684
+ if (expr.variants) {
685
+ const stylePreset = ast.styles?.[expr.name];
686
+ const availableVariants = new Set(
687
+ stylePreset?.variants ? Object.keys(stylePreset.variants) : []
688
+ );
689
+ for (const [variantKey, variantValue] of Object.entries(expr.variants)) {
690
+ const variantPath = buildPath(path, "variants", variantKey);
691
+ if (!availableVariants.has(variantKey)) {
692
+ const errorOptions = createErrorOptionsWithSuggestion(variantKey, availableVariants);
693
+ errors.push(createUndefinedVariantError(variantKey, expr.name, variantPath, errorOptions));
694
+ }
695
+ errors.push(...validateExpressionInEventPayload(variantValue, variantPath, context, scope));
696
+ }
598
697
  }
599
698
  return errors;
600
699
  }
@@ -608,7 +707,8 @@ function validateViewNode(node, path, context, scope, options = { insideComponen
608
707
  const propPath = buildPath(path, "props", propName);
609
708
  if (isEventHandler(propValue)) {
610
709
  if (!context.actionNames.has(propValue.action)) {
611
- errors.push(createUndefinedActionError(propValue.action, propPath));
710
+ const errorOptions = createErrorOptionsWithSuggestion(propValue.action, context.actionNames);
711
+ errors.push(createUndefinedActionError(propValue.action, propPath, errorOptions));
612
712
  }
613
713
  if (propValue.payload) {
614
714
  errors.push(
@@ -662,7 +762,8 @@ function validateViewNode(node, path, context, scope, options = { insideComponen
662
762
  }
663
763
  case "component": {
664
764
  if (!context.componentNames.has(node.name)) {
665
- errors.push(createComponentNotFoundError(node.name, path));
765
+ const errorOptions = createErrorOptionsWithSuggestion(node.name, context.componentNames);
766
+ errors.push(createComponentNotFoundError(node.name, path, errorOptions));
666
767
  } else {
667
768
  const componentDef = ast.components?.[node.name];
668
769
  if (componentDef) {
@@ -839,7 +940,8 @@ function validateLifecycleHooks(lifecycle, context) {
839
940
  for (const hook of hooks) {
840
941
  const actionName = lifecycle[hook];
841
942
  if (actionName && !context.actionNames.has(actionName)) {
842
- errors.push(createUndefinedActionError(actionName, `/lifecycle/${hook}`));
943
+ const errorOptions = createErrorOptionsWithSuggestion(actionName, context.actionNames);
944
+ errors.push(createUndefinedActionError(actionName, `/lifecycle/${hook}`, errorOptions));
843
945
  }
844
946
  }
845
947
  return errors;
@@ -958,11 +1060,16 @@ function transformExpression(expr, ctx) {
958
1060
  expr: "lit",
959
1061
  value: expr.value
960
1062
  };
961
- case "state":
962
- return {
1063
+ case "state": {
1064
+ const stateExpr = {
963
1065
  expr: "state",
964
1066
  name: expr.name
965
1067
  };
1068
+ if (expr.path) {
1069
+ stateExpr.path = expr.path;
1070
+ }
1071
+ return stateExpr;
1072
+ }
966
1073
  case "var": {
967
1074
  const varExpr = {
968
1075
  expr: "var",
@@ -1052,6 +1159,25 @@ function transformExpression(expr, ctx) {
1052
1159
  }
1053
1160
  case "ref":
1054
1161
  return { expr: "ref", name: expr.name };
1162
+ case "index":
1163
+ return {
1164
+ expr: "index",
1165
+ base: transformExpression(expr.base, ctx),
1166
+ key: transformExpression(expr.key, ctx)
1167
+ };
1168
+ case "style": {
1169
+ const styleExpr = {
1170
+ expr: "style",
1171
+ name: expr.name
1172
+ };
1173
+ if (expr.variants) {
1174
+ styleExpr.variants = {};
1175
+ for (const [key, value] of Object.entries(expr.variants)) {
1176
+ styleExpr.variants[key] = transformExpression(value, ctx);
1177
+ }
1178
+ }
1179
+ return styleExpr;
1180
+ }
1055
1181
  }
1056
1182
  }
1057
1183
  function transformEventHandler(handler, ctx) {
@@ -1219,6 +1345,16 @@ function transformActionStep(step) {
1219
1345
  target: transformExpression(disposeStep.target, emptyContext)
1220
1346
  };
1221
1347
  }
1348
+ case "dom": {
1349
+ const domStep = step;
1350
+ return {
1351
+ do: "dom",
1352
+ operation: domStep.operation,
1353
+ selector: transformExpression(domStep.selector, emptyContext),
1354
+ ...domStep.value && { value: transformExpression(domStep.value, emptyContext) },
1355
+ ...domStep.attribute && { attribute: domStep.attribute }
1356
+ };
1357
+ }
1222
1358
  }
1223
1359
  }
1224
1360
  function flattenSlotChildren(children, ctx) {
@@ -1385,6 +1521,9 @@ function transformRouteDefinition(route, ctx) {
1385
1521
  if (route.layout) {
1386
1522
  compiled.layout = route.layout;
1387
1523
  }
1524
+ if (route.layoutParams) {
1525
+ compiled.layoutParams = route.layoutParams;
1526
+ }
1388
1527
  if (route.meta) {
1389
1528
  compiled.meta = {};
1390
1529
  for (const [key, value] of Object.entries(route.meta)) {
@@ -1685,31 +1824,254 @@ function transformState2(state) {
1685
1824
  }
1686
1825
  return result;
1687
1826
  }
1688
- function transformActions2(actions) {
1689
- if (!actions) return [];
1690
- return actions.map((action) => ({
1691
- name: action.name,
1692
- steps: action.steps.map((step) => {
1693
- if (step.do === "set") {
1694
- return {
1695
- do: "set",
1696
- target: step.target,
1697
- value: { expr: "lit", value: null }
1698
- // Simplified for now
1699
- };
1827
+ function transformExpression2(expr) {
1828
+ switch (expr.expr) {
1829
+ case "lit":
1830
+ return { expr: "lit", value: expr.value };
1831
+ case "state":
1832
+ return { expr: "state", name: expr.name };
1833
+ case "var": {
1834
+ const varExpr = { expr: "var", name: expr.name };
1835
+ if (expr.path) {
1836
+ varExpr.path = expr.path;
1700
1837
  }
1701
- if (step.do === "update") {
1702
- return {
1703
- do: "update",
1704
- target: step.target,
1705
- operation: step.operation
1706
- };
1838
+ return varExpr;
1839
+ }
1840
+ case "bin":
1841
+ return {
1842
+ expr: "bin",
1843
+ op: expr.op,
1844
+ left: transformExpression2(expr.left),
1845
+ right: transformExpression2(expr.right)
1846
+ };
1847
+ case "not":
1848
+ return {
1849
+ expr: "not",
1850
+ operand: transformExpression2(expr.operand)
1851
+ };
1852
+ case "cond":
1853
+ return {
1854
+ expr: "cond",
1855
+ if: transformExpression2(expr.if),
1856
+ then: transformExpression2(expr.then),
1857
+ else: transformExpression2(expr.else)
1858
+ };
1859
+ case "get":
1860
+ return {
1861
+ expr: "get",
1862
+ base: transformExpression2(expr.base),
1863
+ path: expr.path
1864
+ };
1865
+ case "route":
1866
+ return {
1867
+ expr: "route",
1868
+ name: expr.name,
1869
+ source: expr.source ?? "param"
1870
+ };
1871
+ case "import": {
1872
+ const importExpr = { expr: "import", name: expr.name };
1873
+ if (expr.path) {
1874
+ importExpr.path = expr.path;
1875
+ }
1876
+ return importExpr;
1877
+ }
1878
+ case "data": {
1879
+ const dataExpr = { expr: "import", name: expr.name };
1880
+ if (expr.path) {
1881
+ dataExpr.path = expr.path;
1707
1882
  }
1883
+ return dataExpr;
1884
+ }
1885
+ case "param": {
1886
+ const paramExpr = { expr: "param", name: expr.name };
1887
+ if (expr.path) {
1888
+ paramExpr.path = expr.path;
1889
+ }
1890
+ return paramExpr;
1891
+ }
1892
+ case "ref":
1893
+ return { expr: "ref", name: expr.name };
1894
+ default:
1895
+ return { expr: "lit", value: null };
1896
+ }
1897
+ }
1898
+ function transformActionStep2(step) {
1899
+ switch (step.do) {
1900
+ case "set":
1708
1901
  return {
1902
+ do: "set",
1903
+ target: step.target,
1904
+ value: transformExpression2(step.value)
1905
+ };
1906
+ case "update": {
1907
+ const updateStep = {
1908
+ do: "update",
1909
+ target: step.target,
1910
+ operation: step.operation
1911
+ };
1912
+ if (step.value) {
1913
+ updateStep.value = transformExpression2(step.value);
1914
+ }
1915
+ if (step.index) {
1916
+ updateStep.index = transformExpression2(step.index);
1917
+ }
1918
+ if (step.deleteCount) {
1919
+ updateStep.deleteCount = transformExpression2(step.deleteCount);
1920
+ }
1921
+ return updateStep;
1922
+ }
1923
+ case "fetch": {
1924
+ const fetchStep = {
1709
1925
  do: "fetch",
1710
- url: { expr: "lit", value: "" }
1926
+ url: transformExpression2(step.url)
1711
1927
  };
1712
- })
1928
+ if (step.method) {
1929
+ fetchStep.method = step.method;
1930
+ }
1931
+ if (step.body) {
1932
+ fetchStep.body = transformExpression2(step.body);
1933
+ }
1934
+ if (step.result) {
1935
+ fetchStep.result = step.result;
1936
+ }
1937
+ if (step.onSuccess) {
1938
+ fetchStep.onSuccess = step.onSuccess.map(transformActionStep2);
1939
+ }
1940
+ if (step.onError) {
1941
+ fetchStep.onError = step.onError.map(transformActionStep2);
1942
+ }
1943
+ return fetchStep;
1944
+ }
1945
+ case "storage": {
1946
+ const storageStep = step;
1947
+ const compiledStorageStep = {
1948
+ do: "storage",
1949
+ operation: storageStep.operation,
1950
+ key: transformExpression2(storageStep.key),
1951
+ storage: storageStep.storage
1952
+ };
1953
+ if (storageStep.value) {
1954
+ compiledStorageStep.value = transformExpression2(storageStep.value);
1955
+ }
1956
+ if (storageStep.result) {
1957
+ compiledStorageStep.result = storageStep.result;
1958
+ }
1959
+ if (storageStep.onSuccess) {
1960
+ compiledStorageStep.onSuccess = storageStep.onSuccess.map(transformActionStep2);
1961
+ }
1962
+ if (storageStep.onError) {
1963
+ compiledStorageStep.onError = storageStep.onError.map(transformActionStep2);
1964
+ }
1965
+ return compiledStorageStep;
1966
+ }
1967
+ case "clipboard": {
1968
+ const clipboardStep = step;
1969
+ const compiledClipboardStep = {
1970
+ do: "clipboard",
1971
+ operation: clipboardStep.operation
1972
+ };
1973
+ if (clipboardStep.value) {
1974
+ compiledClipboardStep.value = transformExpression2(clipboardStep.value);
1975
+ }
1976
+ if (clipboardStep.result) {
1977
+ compiledClipboardStep.result = clipboardStep.result;
1978
+ }
1979
+ if (clipboardStep.onSuccess) {
1980
+ compiledClipboardStep.onSuccess = clipboardStep.onSuccess.map(transformActionStep2);
1981
+ }
1982
+ if (clipboardStep.onError) {
1983
+ compiledClipboardStep.onError = clipboardStep.onError.map(transformActionStep2);
1984
+ }
1985
+ return compiledClipboardStep;
1986
+ }
1987
+ case "navigate": {
1988
+ const navigateStep = step;
1989
+ const compiledNavigateStep = {
1990
+ do: "navigate",
1991
+ url: transformExpression2(navigateStep.url)
1992
+ };
1993
+ if (navigateStep.target) {
1994
+ compiledNavigateStep.target = navigateStep.target;
1995
+ }
1996
+ if (navigateStep.replace !== void 0) {
1997
+ compiledNavigateStep.replace = navigateStep.replace;
1998
+ }
1999
+ return compiledNavigateStep;
2000
+ }
2001
+ case "import": {
2002
+ const importStep = step;
2003
+ const compiledImportStep = {
2004
+ do: "import",
2005
+ module: importStep.module,
2006
+ result: importStep.result
2007
+ };
2008
+ if (importStep.onSuccess) {
2009
+ compiledImportStep.onSuccess = importStep.onSuccess.map(transformActionStep2);
2010
+ }
2011
+ if (importStep.onError) {
2012
+ compiledImportStep.onError = importStep.onError.map(transformActionStep2);
2013
+ }
2014
+ return compiledImportStep;
2015
+ }
2016
+ case "call": {
2017
+ const callStep = step;
2018
+ const compiledCallStep = {
2019
+ do: "call",
2020
+ target: transformExpression2(callStep.target)
2021
+ };
2022
+ if (callStep.args) {
2023
+ compiledCallStep.args = callStep.args.map((arg) => transformExpression2(arg));
2024
+ }
2025
+ if (callStep.result) {
2026
+ compiledCallStep.result = callStep.result;
2027
+ }
2028
+ if (callStep.onSuccess) {
2029
+ compiledCallStep.onSuccess = callStep.onSuccess.map(transformActionStep2);
2030
+ }
2031
+ if (callStep.onError) {
2032
+ compiledCallStep.onError = callStep.onError.map(transformActionStep2);
2033
+ }
2034
+ return compiledCallStep;
2035
+ }
2036
+ case "subscribe": {
2037
+ const subscribeStep = step;
2038
+ return {
2039
+ do: "subscribe",
2040
+ target: transformExpression2(subscribeStep.target),
2041
+ event: subscribeStep.event,
2042
+ action: subscribeStep.action
2043
+ };
2044
+ }
2045
+ case "dispose": {
2046
+ const disposeStep = step;
2047
+ return {
2048
+ do: "dispose",
2049
+ target: transformExpression2(disposeStep.target)
2050
+ };
2051
+ }
2052
+ case "dom": {
2053
+ const domStep = step;
2054
+ return {
2055
+ do: "dom",
2056
+ operation: domStep.operation,
2057
+ selector: transformExpression2(domStep.selector),
2058
+ ...domStep.value && { value: transformExpression2(domStep.value) },
2059
+ ...domStep.attribute && { attribute: domStep.attribute }
2060
+ };
2061
+ }
2062
+ default:
2063
+ return {
2064
+ do: "set",
2065
+ target: "_unknown",
2066
+ value: { expr: "lit", value: null }
2067
+ };
2068
+ }
2069
+ }
2070
+ function transformActions2(actions) {
2071
+ if (!actions) return [];
2072
+ return actions.map((action) => ({
2073
+ name: action.name,
2074
+ steps: action.steps.map(transformActionStep2)
1713
2075
  }));
1714
2076
  }
1715
2077
  function transformViewNode2(node, ctx) {
@@ -1791,7 +2153,7 @@ function transformLayoutPass(layout, _context) {
1791
2153
  const ctx = {
1792
2154
  components: layout.components || {}
1793
2155
  };
1794
- return {
2156
+ const result = {
1795
2157
  version: "1.0",
1796
2158
  type: "layout",
1797
2159
  state: transformState2(layout.state),
@@ -1799,17 +2161,161 @@ function transformLayoutPass(layout, _context) {
1799
2161
  view: transformViewNode2(layout.view, ctx),
1800
2162
  components: layout.components
1801
2163
  };
2164
+ if (layout.importData && Object.keys(layout.importData).length > 0) {
2165
+ result.importData = layout.importData;
2166
+ }
2167
+ return result;
1802
2168
  }
1803
2169
  function deepCloneNode(node) {
1804
2170
  return JSON.parse(JSON.stringify(node));
1805
2171
  }
2172
+ function isParamExpression(value) {
2173
+ return typeof value === "object" && value !== null && value.expr === "param" && typeof value.name === "string";
2174
+ }
2175
+ function resolveParamExpression(paramExpr, layoutParams) {
2176
+ const resolvedValue = layoutParams[paramExpr.name];
2177
+ if (!resolvedValue) {
2178
+ return { expr: "lit", value: null };
2179
+ }
2180
+ if (paramExpr.path) {
2181
+ return {
2182
+ expr: "get",
2183
+ base: resolvedValue,
2184
+ path: paramExpr.path
2185
+ };
2186
+ }
2187
+ return resolvedValue;
2188
+ }
2189
+ function resolveExpressionValue(value, layoutParams) {
2190
+ if (!value || typeof value !== "object") {
2191
+ return value;
2192
+ }
2193
+ if (isParamExpression(value)) {
2194
+ return resolveParamExpression(value, layoutParams);
2195
+ }
2196
+ if (Array.isArray(value)) {
2197
+ return value.map((item) => resolveExpressionValue(item, layoutParams));
2198
+ }
2199
+ const obj = value;
2200
+ const result = {};
2201
+ for (const [key, val] of Object.entries(obj)) {
2202
+ result[key] = resolveExpressionValue(val, layoutParams);
2203
+ }
2204
+ return result;
2205
+ }
2206
+ function resolvePropsParams(props, layoutParams) {
2207
+ const result = {};
2208
+ for (const [key, value] of Object.entries(props)) {
2209
+ result[key] = resolveExpressionValue(value, layoutParams);
2210
+ }
2211
+ return result;
2212
+ }
2213
+ function resolveParamExpressions(node, layoutParams) {
2214
+ switch (node.kind) {
2215
+ case "element": {
2216
+ const elementNode = node;
2217
+ const result = {
2218
+ kind: "element",
2219
+ tag: elementNode.tag
2220
+ };
2221
+ if (elementNode.props) {
2222
+ result.props = resolvePropsParams(
2223
+ elementNode.props,
2224
+ layoutParams
2225
+ );
2226
+ }
2227
+ if (elementNode.children && elementNode.children.length > 0) {
2228
+ result.children = elementNode.children.map(
2229
+ (child) => resolveParamExpressions(child, layoutParams)
2230
+ );
2231
+ }
2232
+ return result;
2233
+ }
2234
+ case "text": {
2235
+ const textNode = node;
2236
+ return {
2237
+ kind: "text",
2238
+ value: resolveExpressionValue(textNode.value, layoutParams)
2239
+ };
2240
+ }
2241
+ case "if": {
2242
+ const ifNode = node;
2243
+ const result = {
2244
+ kind: "if",
2245
+ condition: resolveExpressionValue(ifNode.condition, layoutParams),
2246
+ then: resolveParamExpressions(ifNode.then, layoutParams)
2247
+ };
2248
+ if (ifNode.else) {
2249
+ result.else = resolveParamExpressions(
2250
+ ifNode.else,
2251
+ layoutParams
2252
+ );
2253
+ }
2254
+ return result;
2255
+ }
2256
+ case "each": {
2257
+ const eachNode = node;
2258
+ return {
2259
+ kind: "each",
2260
+ items: resolveExpressionValue(eachNode.items, layoutParams),
2261
+ as: eachNode.as,
2262
+ body: resolveParamExpressions(eachNode.body, layoutParams)
2263
+ };
2264
+ }
2265
+ default:
2266
+ return node;
2267
+ }
2268
+ }
2269
+ function processNamedSlotsOnly(node, namedContent) {
2270
+ if (node.kind === "slot") {
2271
+ const slotName = node.name;
2272
+ if (slotName && namedContent[slotName]) {
2273
+ return deepCloneNode(namedContent[slotName]);
2274
+ }
2275
+ return node;
2276
+ }
2277
+ if (node.kind === "element") {
2278
+ const children = node.children;
2279
+ if (children && children.length > 0) {
2280
+ const newChildren = children.map((child) => processNamedSlotsOnly(child, namedContent));
2281
+ return {
2282
+ ...node,
2283
+ children: newChildren
2284
+ };
2285
+ }
2286
+ return node;
2287
+ }
2288
+ if (node.kind === "if") {
2289
+ const ifNode = node;
2290
+ const result = {
2291
+ ...node,
2292
+ then: processNamedSlotsOnly(ifNode.then, namedContent)
2293
+ };
2294
+ if (ifNode.else) {
2295
+ result.else = processNamedSlotsOnly(ifNode.else, namedContent);
2296
+ }
2297
+ return result;
2298
+ }
2299
+ if (node.kind === "each") {
2300
+ const eachNode = node;
2301
+ return {
2302
+ ...node,
2303
+ body: processNamedSlotsOnly(eachNode.body, namedContent)
2304
+ };
2305
+ }
2306
+ return node;
2307
+ }
1806
2308
  function replaceSlots(node, defaultContent, namedContent) {
1807
2309
  if (node.kind === "slot") {
1808
2310
  const slotName = node.name;
1809
2311
  if (slotName && namedContent?.[slotName]) {
1810
2312
  return deepCloneNode(namedContent[slotName]);
1811
2313
  }
1812
- return deepCloneNode(defaultContent);
2314
+ const clonedDefault = deepCloneNode(defaultContent);
2315
+ if (namedContent && Object.keys(namedContent).length > 0) {
2316
+ return processNamedSlotsOnly(clonedDefault, namedContent);
2317
+ }
2318
+ return clonedDefault;
1813
2319
  }
1814
2320
  if (node.kind === "element") {
1815
2321
  const children = node.children;
@@ -1842,11 +2348,31 @@ function replaceSlots(node, defaultContent, namedContent) {
1842
2348
  }
1843
2349
  return node;
1844
2350
  }
1845
- function composeLayoutWithPage(layout, page, slots) {
1846
- const layoutView = deepCloneNode(layout.view);
1847
- const namedContent = slots ? Object.fromEntries(
1848
- Object.entries(slots).map(([name, node]) => [name, node])
1849
- ) : void 0;
2351
+ function extractMdxSlotsFromImportData(importData) {
2352
+ if (!importData) return void 0;
2353
+ for (const [, dataSource] of Object.entries(importData)) {
2354
+ if (!Array.isArray(dataSource)) continue;
2355
+ for (const item of dataSource) {
2356
+ if (typeof item === "object" && item !== null && "content" in item && typeof item.content === "object") {
2357
+ const content = item.content;
2358
+ return { "mdx-content": content };
2359
+ }
2360
+ }
2361
+ }
2362
+ return void 0;
2363
+ }
2364
+ function composeLayoutWithPage(layout, page, layoutParams, slots) {
2365
+ let layoutView = deepCloneNode(layout.view);
2366
+ const resolvedParams = layoutParams ?? {};
2367
+ layoutView = resolveParamExpressions(layoutView, resolvedParams);
2368
+ let namedContent;
2369
+ if (slots) {
2370
+ namedContent = Object.fromEntries(
2371
+ Object.entries(slots).map(([name, node]) => [name, node])
2372
+ );
2373
+ } else {
2374
+ namedContent = extractMdxSlotsFromImportData(page.importData);
2375
+ }
1850
2376
  const composedView = replaceSlots(layoutView, page.view, namedContent);
1851
2377
  const mergedState = {};
1852
2378
  for (const [name, field] of Object.entries(page.state)) {
@@ -1897,6 +2423,16 @@ function composeLayoutWithPage(layout, page, slots) {
1897
2423
  if (Object.keys(mergedComponents).length > 0) {
1898
2424
  result.components = mergedComponents;
1899
2425
  }
2426
+ const mergedImportData = {
2427
+ ...layout.importData || {},
2428
+ ...page.importData || {}
2429
+ };
2430
+ if (Object.keys(mergedImportData).length > 0) {
2431
+ result.importData = mergedImportData;
2432
+ }
2433
+ if (page.lifecycle) {
2434
+ result.lifecycle = page.lifecycle;
2435
+ }
1900
2436
  return result;
1901
2437
  }
1902
2438
  export {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@constela/compiler",
3
- "version": "0.7.0",
3
+ "version": "0.8.0",
4
4
  "description": "Compiler for Constela UI framework - AST to Program transformation",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -15,7 +15,7 @@
15
15
  "dist"
16
16
  ],
17
17
  "dependencies": {
18
- "@constela/core": "0.7.0"
18
+ "@constela/core": "0.8.0"
19
19
  },
20
20
  "devDependencies": {
21
21
  "@types/node": "^20.10.0",