@constela/compiler 0.4.0 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/dist/index.d.ts +173 -5
  2. package/dist/index.js +1087 -12
  3. package/package.json +2 -2
package/dist/index.js CHANGED
@@ -27,18 +27,104 @@ import {
27
27
  createSchemaError,
28
28
  createOperationInvalidForTypeError,
29
29
  createOperationMissingFieldError,
30
- isEventHandler
30
+ createUndefinedRouteParamError,
31
+ createRouteNotDefinedError,
32
+ createUndefinedImportError,
33
+ createImportsNotDefinedError,
34
+ createInvalidDataSourceError,
35
+ createUndefinedDataSourceError,
36
+ createDataNotDefinedError,
37
+ createUndefinedDataError,
38
+ createInvalidStorageOperationError,
39
+ createInvalidStorageTypeError,
40
+ createStorageSetMissingValueError,
41
+ createInvalidClipboardOperationError,
42
+ createClipboardWriteMissingValueError,
43
+ createInvalidNavigateTargetError,
44
+ createUndefinedRefError,
45
+ isEventHandler,
46
+ DATA_SOURCE_TYPES,
47
+ DATA_TRANSFORMS,
48
+ STORAGE_OPERATIONS,
49
+ STORAGE_TYPES,
50
+ CLIPBOARD_OPERATIONS,
51
+ NAVIGATE_TARGETS
31
52
  } from "@constela/core";
32
53
  function buildPath(base, ...segments) {
33
54
  return segments.reduce((p, s) => `${p}/${s}`, base);
34
55
  }
35
- function collectContext(ast2) {
36
- const stateNames = new Set(Object.keys(ast2.state));
37
- const actionNames = new Set(ast2.actions.map((a) => a.name));
56
+ function extractRouteParams(path) {
57
+ const params = [];
58
+ const segments = path.split("/");
59
+ for (const segment of segments) {
60
+ if (segment.startsWith(":")) {
61
+ params.push(segment.slice(1));
62
+ }
63
+ }
64
+ return params;
65
+ }
66
+ function collectRefs(node) {
67
+ const refs = /* @__PURE__ */ new Set();
68
+ switch (node.kind) {
69
+ case "element":
70
+ if (node.ref) {
71
+ refs.add(node.ref);
72
+ }
73
+ if (node.children) {
74
+ for (const child of node.children) {
75
+ for (const ref of collectRefs(child)) {
76
+ refs.add(ref);
77
+ }
78
+ }
79
+ }
80
+ break;
81
+ case "if":
82
+ for (const ref of collectRefs(node.then)) {
83
+ refs.add(ref);
84
+ }
85
+ if (node.else) {
86
+ for (const ref of collectRefs(node.else)) {
87
+ refs.add(ref);
88
+ }
89
+ }
90
+ break;
91
+ case "each":
92
+ for (const ref of collectRefs(node.body)) {
93
+ refs.add(ref);
94
+ }
95
+ break;
96
+ case "component":
97
+ if (node.children) {
98
+ for (const child of node.children) {
99
+ for (const ref of collectRefs(child)) {
100
+ refs.add(ref);
101
+ }
102
+ }
103
+ }
104
+ break;
105
+ case "text":
106
+ case "slot":
107
+ break;
108
+ }
109
+ return refs;
110
+ }
111
+ function collectContext(programAst) {
112
+ const stateNames = new Set(Object.keys(programAst.state));
113
+ const actionNames = new Set(programAst.actions.map((a) => a.name));
38
114
  const componentNames = new Set(
39
- ast2.components ? Object.keys(ast2.components) : []
115
+ programAst.components ? Object.keys(programAst.components) : []
116
+ );
117
+ const routeParams = new Set(
118
+ programAst.route ? extractRouteParams(programAst.route.path) : []
40
119
  );
41
- return { stateNames, actionNames, componentNames };
120
+ const importNames = new Set(
121
+ programAst.imports ? Object.keys(programAst.imports) : []
122
+ );
123
+ const dataNames = new Set(
124
+ programAst.data ? Object.keys(programAst.data) : []
125
+ );
126
+ const refNames = collectRefs(programAst.view);
127
+ return { stateNames, actionNames, componentNames, routeParams, importNames, dataNames, refNames };
42
128
  }
43
129
  function checkDuplicateActions(ast2) {
44
130
  const errors = [];
@@ -71,6 +157,39 @@ function validateExpression(expr, path, context, scope, paramScope) {
71
157
  errors.push(createUndefinedParamError(expr.name, path));
72
158
  }
73
159
  break;
160
+ case "route": {
161
+ if (!hasRoute) {
162
+ errors.push(createRouteNotDefinedError(path));
163
+ } else {
164
+ const source = expr.source ?? "param";
165
+ if (source === "param" && !context.routeParams.has(expr.name)) {
166
+ errors.push(createUndefinedRouteParamError(expr.name, path));
167
+ }
168
+ }
169
+ break;
170
+ }
171
+ case "import": {
172
+ if (!hasImports) {
173
+ errors.push(createImportsNotDefinedError(path));
174
+ } else if (!context.importNames.has(expr.name)) {
175
+ errors.push(createUndefinedImportError(expr.name, path));
176
+ }
177
+ break;
178
+ }
179
+ case "data": {
180
+ if (!hasData) {
181
+ errors.push(createDataNotDefinedError(path));
182
+ } else if (!context.dataNames.has(expr.name)) {
183
+ errors.push(createUndefinedDataError(expr.name, path));
184
+ }
185
+ break;
186
+ }
187
+ case "ref": {
188
+ if (!context.refNames.has(expr.name)) {
189
+ errors.push(createUndefinedRefError(expr.name, path));
190
+ }
191
+ break;
192
+ }
74
193
  case "bin":
75
194
  errors.push(...validateExpression(expr.left, buildPath(path, "left"), context, scope, paramScope));
76
195
  errors.push(...validateExpression(expr.right, buildPath(path, "right"), context, scope, paramScope));
@@ -174,6 +293,161 @@ function validateActionStep(step, path, context) {
174
293
  }
175
294
  }
176
295
  break;
296
+ case "storage": {
297
+ const storageStep = step;
298
+ if (!STORAGE_OPERATIONS.includes(storageStep.operation)) {
299
+ errors.push(createInvalidStorageOperationError(storageStep.operation, path));
300
+ }
301
+ if (!STORAGE_TYPES.includes(storageStep.storage)) {
302
+ errors.push(createInvalidStorageTypeError(storageStep.storage, path));
303
+ }
304
+ errors.push(
305
+ ...validateExpressionStateOnly(storageStep.key, buildPath(path, "key"), context)
306
+ );
307
+ if (storageStep.operation === "set" && !storageStep.value) {
308
+ errors.push(createStorageSetMissingValueError(path));
309
+ }
310
+ if (storageStep.value) {
311
+ errors.push(
312
+ ...validateExpressionStateOnly(storageStep.value, buildPath(path, "value"), context)
313
+ );
314
+ }
315
+ if (storageStep.onSuccess) {
316
+ for (let i = 0; i < storageStep.onSuccess.length; i++) {
317
+ const successStep = storageStep.onSuccess[i];
318
+ if (successStep === void 0) continue;
319
+ errors.push(
320
+ ...validateActionStep(successStep, buildPath(path, "onSuccess", i), context)
321
+ );
322
+ }
323
+ }
324
+ if (storageStep.onError) {
325
+ for (let i = 0; i < storageStep.onError.length; i++) {
326
+ const errorStep = storageStep.onError[i];
327
+ if (errorStep === void 0) continue;
328
+ errors.push(
329
+ ...validateActionStep(errorStep, buildPath(path, "onError", i), context)
330
+ );
331
+ }
332
+ }
333
+ break;
334
+ }
335
+ case "clipboard": {
336
+ const clipboardStep = step;
337
+ if (!CLIPBOARD_OPERATIONS.includes(clipboardStep.operation)) {
338
+ errors.push(createInvalidClipboardOperationError(clipboardStep.operation, path));
339
+ }
340
+ if (clipboardStep.operation === "write" && !clipboardStep.value) {
341
+ errors.push(createClipboardWriteMissingValueError(path));
342
+ }
343
+ if (clipboardStep.value) {
344
+ errors.push(
345
+ ...validateExpressionStateOnly(clipboardStep.value, buildPath(path, "value"), context)
346
+ );
347
+ }
348
+ if (clipboardStep.onSuccess) {
349
+ for (let i = 0; i < clipboardStep.onSuccess.length; i++) {
350
+ const successStep = clipboardStep.onSuccess[i];
351
+ if (successStep === void 0) continue;
352
+ errors.push(
353
+ ...validateActionStep(successStep, buildPath(path, "onSuccess", i), context)
354
+ );
355
+ }
356
+ }
357
+ if (clipboardStep.onError) {
358
+ for (let i = 0; i < clipboardStep.onError.length; i++) {
359
+ const errorStep = clipboardStep.onError[i];
360
+ if (errorStep === void 0) continue;
361
+ errors.push(
362
+ ...validateActionStep(errorStep, buildPath(path, "onError", i), context)
363
+ );
364
+ }
365
+ }
366
+ break;
367
+ }
368
+ case "navigate": {
369
+ const navigateStep = step;
370
+ errors.push(
371
+ ...validateExpressionStateOnly(navigateStep.url, buildPath(path, "url"), context)
372
+ );
373
+ if (navigateStep.target !== void 0 && !NAVIGATE_TARGETS.includes(navigateStep.target)) {
374
+ errors.push(createInvalidNavigateTargetError(navigateStep.target, path));
375
+ }
376
+ break;
377
+ }
378
+ case "import": {
379
+ const importStep = step;
380
+ if (importStep.onSuccess) {
381
+ for (let i = 0; i < importStep.onSuccess.length; i++) {
382
+ const successStep = importStep.onSuccess[i];
383
+ if (successStep === void 0) continue;
384
+ errors.push(
385
+ ...validateActionStep(successStep, buildPath(path, "onSuccess", i), context)
386
+ );
387
+ }
388
+ }
389
+ if (importStep.onError) {
390
+ for (let i = 0; i < importStep.onError.length; i++) {
391
+ const errorStep = importStep.onError[i];
392
+ if (errorStep === void 0) continue;
393
+ errors.push(
394
+ ...validateActionStep(errorStep, buildPath(path, "onError", i), context)
395
+ );
396
+ }
397
+ }
398
+ break;
399
+ }
400
+ case "call": {
401
+ const callStep = step;
402
+ errors.push(
403
+ ...validateExpressionStateOnly(callStep.target, buildPath(path, "target"), context)
404
+ );
405
+ if (callStep.args) {
406
+ for (let i = 0; i < callStep.args.length; i++) {
407
+ const arg = callStep.args[i];
408
+ if (arg === void 0) continue;
409
+ errors.push(
410
+ ...validateExpressionStateOnly(arg, buildPath(path, "args", i), context)
411
+ );
412
+ }
413
+ }
414
+ if (callStep.onSuccess) {
415
+ for (let i = 0; i < callStep.onSuccess.length; i++) {
416
+ const successStep = callStep.onSuccess[i];
417
+ if (successStep === void 0) continue;
418
+ errors.push(
419
+ ...validateActionStep(successStep, buildPath(path, "onSuccess", i), context)
420
+ );
421
+ }
422
+ }
423
+ if (callStep.onError) {
424
+ for (let i = 0; i < callStep.onError.length; i++) {
425
+ const errorStep = callStep.onError[i];
426
+ if (errorStep === void 0) continue;
427
+ errors.push(
428
+ ...validateActionStep(errorStep, buildPath(path, "onError", i), context)
429
+ );
430
+ }
431
+ }
432
+ break;
433
+ }
434
+ case "subscribe": {
435
+ const subscribeStep = step;
436
+ errors.push(
437
+ ...validateExpressionStateOnly(subscribeStep.target, buildPath(path, "target"), context)
438
+ );
439
+ if (!context.actionNames.has(subscribeStep.action)) {
440
+ errors.push(createUndefinedActionError(subscribeStep.action, buildPath(path, "action")));
441
+ }
442
+ break;
443
+ }
444
+ case "dispose": {
445
+ const disposeStep = step;
446
+ errors.push(
447
+ ...validateExpressionStateOnly(disposeStep.target, buildPath(path, "target"), context)
448
+ );
449
+ break;
450
+ }
177
451
  }
178
452
  return errors;
179
453
  }
@@ -187,6 +461,39 @@ function validateExpressionStateOnly(expr, path, context) {
187
461
  break;
188
462
  case "var":
189
463
  break;
464
+ case "route": {
465
+ if (!hasRoute) {
466
+ errors.push(createRouteNotDefinedError(path));
467
+ } else {
468
+ const source = expr.source ?? "param";
469
+ if (source === "param" && !context.routeParams.has(expr.name)) {
470
+ errors.push(createUndefinedRouteParamError(expr.name, path));
471
+ }
472
+ }
473
+ break;
474
+ }
475
+ case "import": {
476
+ if (!hasImports) {
477
+ errors.push(createImportsNotDefinedError(path));
478
+ } else if (!context.importNames.has(expr.name)) {
479
+ errors.push(createUndefinedImportError(expr.name, path));
480
+ }
481
+ break;
482
+ }
483
+ case "data": {
484
+ if (!hasData) {
485
+ errors.push(createDataNotDefinedError(path));
486
+ } else if (!context.dataNames.has(expr.name)) {
487
+ errors.push(createUndefinedDataError(expr.name, path));
488
+ }
489
+ break;
490
+ }
491
+ case "ref": {
492
+ if (!context.refNames.has(expr.name)) {
493
+ errors.push(createUndefinedRefError(expr.name, path));
494
+ }
495
+ break;
496
+ }
190
497
  case "bin":
191
498
  errors.push(...validateExpressionStateOnly(expr.left, buildPath(path, "left"), context));
192
499
  errors.push(...validateExpressionStateOnly(expr.right, buildPath(path, "right"), context));
@@ -219,6 +526,39 @@ function validateExpressionInEventPayload(expr, path, context, scope) {
219
526
  break;
220
527
  case "var":
221
528
  break;
529
+ case "route": {
530
+ if (!hasRoute) {
531
+ errors.push(createRouteNotDefinedError(path));
532
+ } else {
533
+ const source = expr.source ?? "param";
534
+ if (source === "param" && !context.routeParams.has(expr.name)) {
535
+ errors.push(createUndefinedRouteParamError(expr.name, path));
536
+ }
537
+ }
538
+ break;
539
+ }
540
+ case "import": {
541
+ if (!hasImports) {
542
+ errors.push(createImportsNotDefinedError(path));
543
+ } else if (!context.importNames.has(expr.name)) {
544
+ errors.push(createUndefinedImportError(expr.name, path));
545
+ }
546
+ break;
547
+ }
548
+ case "data": {
549
+ if (!hasData) {
550
+ errors.push(createDataNotDefinedError(path));
551
+ } else if (!context.dataNames.has(expr.name)) {
552
+ errors.push(createUndefinedDataError(expr.name, path));
553
+ }
554
+ break;
555
+ }
556
+ case "ref": {
557
+ if (!context.refNames.has(expr.name)) {
558
+ errors.push(createUndefinedRefError(expr.name, path));
559
+ }
560
+ break;
561
+ }
222
562
  case "bin":
223
563
  errors.push(
224
564
  ...validateExpressionInEventPayload(expr.left, buildPath(path, "left"), context, scope)
@@ -260,7 +600,7 @@ function validateExpressionInEventPayload(expr, path, context, scope) {
260
600
  }
261
601
  function validateViewNode(node, path, context, scope, options = { insideComponent: false }) {
262
602
  const errors = [];
263
- const { insideComponent, paramScope } = options;
603
+ const { insideComponent, insideLayout, paramScope } = options;
264
604
  switch (node.kind) {
265
605
  case "element":
266
606
  if (node.props) {
@@ -343,9 +683,9 @@ function validateViewNode(node, path, context, scope, options = { insideComponen
343
683
  break;
344
684
  }
345
685
  case "slot":
346
- if (!insideComponent) {
686
+ if (!insideComponent && !insideLayout) {
347
687
  errors.push(
348
- createSchemaError(`Slot can only be used inside component definitions`, path)
688
+ createSchemaError(`Slot can only be used inside component definitions or layouts`, path)
349
689
  );
350
690
  }
351
691
  break;
@@ -372,6 +712,26 @@ function validateComponentProps(node, componentDef, path, context, scope, paramS
372
712
  return errors;
373
713
  }
374
714
  var ast;
715
+ var hasRoute;
716
+ var hasImports;
717
+ var hasData;
718
+ function validateRouteDefinition(route, context) {
719
+ const errors = [];
720
+ const emptyScope = /* @__PURE__ */ new Set();
721
+ if (route.title) {
722
+ errors.push(
723
+ ...validateExpression(route.title, "/route/title", context, emptyScope)
724
+ );
725
+ }
726
+ if (route.meta) {
727
+ for (const [key, value] of Object.entries(route.meta)) {
728
+ errors.push(
729
+ ...validateExpression(value, `/route/meta/${key}`, context, emptyScope)
730
+ );
731
+ }
732
+ }
733
+ return errors;
734
+ }
375
735
  function collectComponentCalls(node) {
376
736
  const calls = /* @__PURE__ */ new Set();
377
737
  switch (node.kind) {
@@ -472,6 +832,18 @@ function validateComponents(programAst, context) {
472
832
  }
473
833
  return errors;
474
834
  }
835
+ function validateLifecycleHooks(lifecycle, context) {
836
+ const errors = [];
837
+ if (!lifecycle) return errors;
838
+ const hooks = ["onMount", "onUnmount", "onRouteEnter", "onRouteLeave"];
839
+ for (const hook of hooks) {
840
+ const actionName = lifecycle[hook];
841
+ if (actionName && !context.actionNames.has(actionName)) {
842
+ errors.push(createUndefinedActionError(actionName, `/lifecycle/${hook}`));
843
+ }
844
+ }
845
+ return errors;
846
+ }
475
847
  function validateActions(programAst, context) {
476
848
  const errors = [];
477
849
  for (let i = 0; i < programAst.actions.length; i++) {
@@ -487,17 +859,81 @@ function validateActions(programAst, context) {
487
859
  }
488
860
  return errors;
489
861
  }
862
+ function validateDataSources(programAst, context) {
863
+ const errors = [];
864
+ if (!programAst.data) return errors;
865
+ for (const [name, source] of Object.entries(programAst.data)) {
866
+ const path = `/data/${name}`;
867
+ if (!DATA_SOURCE_TYPES.includes(source.type)) {
868
+ errors.push(createInvalidDataSourceError(name, `invalid type '${source.type}'`, path));
869
+ continue;
870
+ }
871
+ if (source.transform !== void 0) {
872
+ if (!DATA_TRANSFORMS.includes(source.transform)) {
873
+ errors.push(createInvalidDataSourceError(name, `invalid transform '${source.transform}'`, path));
874
+ }
875
+ }
876
+ switch (source.type) {
877
+ case "glob":
878
+ if (typeof source.pattern !== "string") {
879
+ errors.push(createInvalidDataSourceError(name, `glob type requires 'pattern' field`, path));
880
+ }
881
+ break;
882
+ case "file":
883
+ if (typeof source.path !== "string") {
884
+ errors.push(createInvalidDataSourceError(name, `file type requires 'path' field`, path));
885
+ }
886
+ break;
887
+ case "api":
888
+ if (typeof source.url !== "string") {
889
+ errors.push(createInvalidDataSourceError(name, `api type requires 'url' field`, path));
890
+ }
891
+ break;
892
+ }
893
+ }
894
+ return errors;
895
+ }
896
+ function validateGetStaticPaths(programAst, context) {
897
+ const errors = [];
898
+ const getStaticPaths = programAst.route?.getStaticPaths;
899
+ if (!getStaticPaths) return errors;
900
+ const path = "/route/getStaticPaths";
901
+ if (!programAst.data) {
902
+ errors.push(createDataNotDefinedError(path));
903
+ return errors;
904
+ }
905
+ if (!context.dataNames.has(getStaticPaths.source)) {
906
+ errors.push(createUndefinedDataSourceError(getStaticPaths.source, path));
907
+ }
908
+ for (const [paramName, paramExpr] of Object.entries(getStaticPaths.params)) {
909
+ errors.push(
910
+ ...validateExpressionStateOnly(paramExpr, `${path}/params/${paramName}`, context)
911
+ );
912
+ }
913
+ return errors;
914
+ }
490
915
  function analyzePass(programAst) {
491
916
  ast = programAst;
917
+ hasRoute = !!programAst.route;
918
+ hasImports = !!programAst.imports;
919
+ hasData = !!programAst.data;
920
+ const isLayout = programAst.type === "layout";
492
921
  const context = collectContext(programAst);
493
922
  const errors = [];
494
923
  errors.push(...checkDuplicateActions(programAst));
924
+ errors.push(...validateDataSources(programAst, context));
925
+ errors.push(...validateGetStaticPaths(programAst, context));
495
926
  errors.push(...validateActions(programAst, context));
927
+ errors.push(...validateLifecycleHooks(programAst.lifecycle, context));
496
928
  errors.push(...detectComponentCycles(programAst, context));
497
929
  errors.push(...validateComponents(programAst, context));
930
+ if (programAst.route) {
931
+ errors.push(...validateRouteDefinition(programAst.route, context));
932
+ }
498
933
  errors.push(
499
934
  ...validateViewNode(programAst.view, "/view", context, /* @__PURE__ */ new Set(), {
500
- insideComponent: false
935
+ insideComponent: false,
936
+ insideLayout: isLayout
501
937
  })
502
938
  );
503
939
  if (errors.length > 0) {
@@ -588,6 +1024,34 @@ function transformExpression(expr, ctx) {
588
1024
  base: transformExpression(expr.base, ctx),
589
1025
  path: expr.path
590
1026
  };
1027
+ case "route":
1028
+ return {
1029
+ expr: "route",
1030
+ name: expr.name,
1031
+ source: expr.source ?? "param"
1032
+ };
1033
+ case "import": {
1034
+ const importExpr = {
1035
+ expr: "import",
1036
+ name: expr.name
1037
+ };
1038
+ if (expr.path) {
1039
+ importExpr.path = expr.path;
1040
+ }
1041
+ return importExpr;
1042
+ }
1043
+ case "data": {
1044
+ const dataExpr = {
1045
+ expr: "import",
1046
+ name: expr.name
1047
+ };
1048
+ if (expr.path) {
1049
+ dataExpr.path = expr.path;
1050
+ }
1051
+ return dataExpr;
1052
+ }
1053
+ case "ref":
1054
+ return { expr: "ref", name: expr.name };
591
1055
  }
592
1056
  }
593
1057
  function transformEventHandler(handler, ctx) {
@@ -648,6 +1112,113 @@ function transformActionStep(step) {
648
1112
  }
649
1113
  return fetchStep;
650
1114
  }
1115
+ case "storage": {
1116
+ const storageStep = step;
1117
+ const compiledStorageStep = {
1118
+ do: "storage",
1119
+ operation: storageStep.operation,
1120
+ key: transformExpression(storageStep.key, emptyContext),
1121
+ storage: storageStep.storage
1122
+ };
1123
+ if (storageStep.value) {
1124
+ compiledStorageStep.value = transformExpression(storageStep.value, emptyContext);
1125
+ }
1126
+ if (storageStep.result) {
1127
+ compiledStorageStep.result = storageStep.result;
1128
+ }
1129
+ if (storageStep.onSuccess) {
1130
+ compiledStorageStep.onSuccess = storageStep.onSuccess.map(transformActionStep);
1131
+ }
1132
+ if (storageStep.onError) {
1133
+ compiledStorageStep.onError = storageStep.onError.map(transformActionStep);
1134
+ }
1135
+ return compiledStorageStep;
1136
+ }
1137
+ case "clipboard": {
1138
+ const clipboardStep = step;
1139
+ const compiledClipboardStep = {
1140
+ do: "clipboard",
1141
+ operation: clipboardStep.operation
1142
+ };
1143
+ if (clipboardStep.value) {
1144
+ compiledClipboardStep.value = transformExpression(clipboardStep.value, emptyContext);
1145
+ }
1146
+ if (clipboardStep.result) {
1147
+ compiledClipboardStep.result = clipboardStep.result;
1148
+ }
1149
+ if (clipboardStep.onSuccess) {
1150
+ compiledClipboardStep.onSuccess = clipboardStep.onSuccess.map(transformActionStep);
1151
+ }
1152
+ if (clipboardStep.onError) {
1153
+ compiledClipboardStep.onError = clipboardStep.onError.map(transformActionStep);
1154
+ }
1155
+ return compiledClipboardStep;
1156
+ }
1157
+ case "navigate": {
1158
+ const navigateStep = step;
1159
+ const compiledNavigateStep = {
1160
+ do: "navigate",
1161
+ url: transformExpression(navigateStep.url, emptyContext)
1162
+ };
1163
+ if (navigateStep.target) {
1164
+ compiledNavigateStep.target = navigateStep.target;
1165
+ }
1166
+ if (navigateStep.replace !== void 0) {
1167
+ compiledNavigateStep.replace = navigateStep.replace;
1168
+ }
1169
+ return compiledNavigateStep;
1170
+ }
1171
+ case "import": {
1172
+ const importStep = step;
1173
+ const compiledImportStep = {
1174
+ do: "import",
1175
+ module: importStep.module,
1176
+ result: importStep.result
1177
+ };
1178
+ if (importStep.onSuccess) {
1179
+ compiledImportStep.onSuccess = importStep.onSuccess.map(transformActionStep);
1180
+ }
1181
+ if (importStep.onError) {
1182
+ compiledImportStep.onError = importStep.onError.map(transformActionStep);
1183
+ }
1184
+ return compiledImportStep;
1185
+ }
1186
+ case "call": {
1187
+ const callStep = step;
1188
+ const compiledCallStep = {
1189
+ do: "call",
1190
+ target: transformExpression(callStep.target, emptyContext)
1191
+ };
1192
+ if (callStep.args) {
1193
+ compiledCallStep.args = callStep.args.map((arg) => transformExpression(arg, emptyContext));
1194
+ }
1195
+ if (callStep.result) {
1196
+ compiledCallStep.result = callStep.result;
1197
+ }
1198
+ if (callStep.onSuccess) {
1199
+ compiledCallStep.onSuccess = callStep.onSuccess.map(transformActionStep);
1200
+ }
1201
+ if (callStep.onError) {
1202
+ compiledCallStep.onError = callStep.onError.map(transformActionStep);
1203
+ }
1204
+ return compiledCallStep;
1205
+ }
1206
+ case "subscribe": {
1207
+ const subscribeStep = step;
1208
+ return {
1209
+ do: "subscribe",
1210
+ target: transformExpression(subscribeStep.target, emptyContext),
1211
+ event: subscribeStep.event,
1212
+ action: subscribeStep.action
1213
+ };
1214
+ }
1215
+ case "dispose": {
1216
+ const disposeStep = step;
1217
+ return {
1218
+ do: "dispose",
1219
+ target: transformExpression(disposeStep.target, emptyContext)
1220
+ };
1221
+ }
651
1222
  }
652
1223
  }
653
1224
  function flattenSlotChildren(children, ctx) {
@@ -670,6 +1241,9 @@ function transformViewNode(node, ctx) {
670
1241
  kind: "element",
671
1242
  tag: node.tag
672
1243
  };
1244
+ if (node.ref) {
1245
+ compiledElement.ref = node.ref;
1246
+ }
673
1247
  if (node.props) {
674
1248
  compiledElement.props = {};
675
1249
  for (const [propName, propValue] of Object.entries(node.props)) {
@@ -790,16 +1364,75 @@ function transformActions(actions) {
790
1364
  }
791
1365
  return compiledActions;
792
1366
  }
793
- function transformPass(ast2, _context) {
1367
+ function extractRouteParams2(path) {
1368
+ const params = [];
1369
+ const segments = path.split("/");
1370
+ for (const segment of segments) {
1371
+ if (segment.startsWith(":")) {
1372
+ params.push(segment.slice(1));
1373
+ }
1374
+ }
1375
+ return params;
1376
+ }
1377
+ function transformRouteDefinition(route, ctx) {
1378
+ const compiled = {
1379
+ path: route.path,
1380
+ params: extractRouteParams2(route.path)
1381
+ };
1382
+ if (route.title) {
1383
+ compiled.title = transformExpression(route.title, ctx);
1384
+ }
1385
+ if (route.layout) {
1386
+ compiled.layout = route.layout;
1387
+ }
1388
+ if (route.meta) {
1389
+ compiled.meta = {};
1390
+ for (const [key, value] of Object.entries(route.meta)) {
1391
+ compiled.meta[key] = transformExpression(value, ctx);
1392
+ }
1393
+ }
1394
+ return compiled;
1395
+ }
1396
+ function transformLifecycleHooks(lifecycle) {
1397
+ if (!lifecycle) return void 0;
1398
+ const hasAnyHook = lifecycle.onMount || lifecycle.onUnmount || lifecycle.onRouteEnter || lifecycle.onRouteLeave;
1399
+ if (!hasAnyHook) return void 0;
1400
+ const result = {};
1401
+ if (lifecycle.onMount) {
1402
+ result.onMount = lifecycle.onMount;
1403
+ }
1404
+ if (lifecycle.onUnmount) {
1405
+ result.onUnmount = lifecycle.onUnmount;
1406
+ }
1407
+ if (lifecycle.onRouteEnter) {
1408
+ result.onRouteEnter = lifecycle.onRouteEnter;
1409
+ }
1410
+ if (lifecycle.onRouteLeave) {
1411
+ result.onRouteLeave = lifecycle.onRouteLeave;
1412
+ }
1413
+ return result;
1414
+ }
1415
+ function transformPass(ast2, _context, importData) {
794
1416
  const ctx = {
795
1417
  components: ast2.components || {}
796
1418
  };
797
- return {
1419
+ const result = {
798
1420
  version: "1.0",
799
1421
  state: transformState(ast2.state),
800
1422
  actions: transformActions(ast2.actions),
801
1423
  view: transformViewNode(ast2.view, ctx)
802
1424
  };
1425
+ if (ast2.route) {
1426
+ result.route = transformRouteDefinition(ast2.route, ctx);
1427
+ }
1428
+ const lifecycle = transformLifecycleHooks(ast2.lifecycle);
1429
+ if (lifecycle) {
1430
+ result.lifecycle = lifecycle;
1431
+ }
1432
+ if (importData && Object.keys(importData).length > 0) {
1433
+ result.importData = importData;
1434
+ }
1435
+ return result;
803
1436
  }
804
1437
 
805
1438
  // src/compile.ts
@@ -827,10 +1460,452 @@ function compile(input) {
827
1460
 
828
1461
  // src/index.ts
829
1462
  import { createUndefinedVarError as createUndefinedVarError2 } from "@constela/core";
1463
+
1464
+ // src/passes/analyze-layout.ts
1465
+ import {
1466
+ createLayoutMissingSlotError,
1467
+ createDuplicateSlotNameError,
1468
+ createDuplicateDefaultSlotError,
1469
+ createSlotInLoopError,
1470
+ createUndefinedStateError as createUndefinedStateError2,
1471
+ createUndefinedActionError as createUndefinedActionError2,
1472
+ isEventHandler as isEventHandler3
1473
+ } from "@constela/core";
1474
+ function buildPath2(base, ...segments) {
1475
+ return segments.reduce((p, s) => `${p}/${s}`, base);
1476
+ }
1477
+ function collectContext2(layout) {
1478
+ const stateNames = new Set(layout.state ? Object.keys(layout.state) : []);
1479
+ const actionNames = new Set(layout.actions ? layout.actions.map((a) => a.name) : []);
1480
+ const componentNames = new Set(
1481
+ layout.components ? Object.keys(layout.components) : []
1482
+ );
1483
+ return { stateNames, actionNames, componentNames, routeParams: /* @__PURE__ */ new Set(), importNames: /* @__PURE__ */ new Set() };
1484
+ }
1485
+ function findSlotNodes(node, path, slots, inLoop = false) {
1486
+ if (node.kind === "slot") {
1487
+ slots.push({
1488
+ name: node.name,
1489
+ path,
1490
+ inLoop
1491
+ });
1492
+ return;
1493
+ }
1494
+ if (node.kind === "element" && node.children) {
1495
+ for (let i = 0; i < node.children.length; i++) {
1496
+ const child = node.children[i];
1497
+ if (child) {
1498
+ findSlotNodes(child, buildPath2(path, "children", i), slots, inLoop);
1499
+ }
1500
+ }
1501
+ }
1502
+ if (node.kind === "if") {
1503
+ findSlotNodes(node.then, buildPath2(path, "then"), slots, inLoop);
1504
+ if (node.else) {
1505
+ findSlotNodes(node.else, buildPath2(path, "else"), slots, inLoop);
1506
+ }
1507
+ }
1508
+ if (node.kind === "each") {
1509
+ findSlotNodes(node.body, buildPath2(path, "body"), slots, true);
1510
+ }
1511
+ if (node.kind === "component" && node.children) {
1512
+ for (let i = 0; i < node.children.length; i++) {
1513
+ const child = node.children[i];
1514
+ if (child) {
1515
+ findSlotNodes(child, buildPath2(path, "children", i), slots, inLoop);
1516
+ }
1517
+ }
1518
+ }
1519
+ }
1520
+ function validateSlots(layout) {
1521
+ const errors = [];
1522
+ const slots = [];
1523
+ const slotNames = /* @__PURE__ */ new Set();
1524
+ let hasDefaultSlot = false;
1525
+ findSlotNodes(layout.view, "/view", slots);
1526
+ if (slots.length === 0) {
1527
+ errors.push(createLayoutMissingSlotError("/view"));
1528
+ return { errors, slotNames, hasDefaultSlot };
1529
+ }
1530
+ const seenNames = /* @__PURE__ */ new Map();
1531
+ let defaultSlotPath;
1532
+ for (const slot of slots) {
1533
+ if (slot.inLoop) {
1534
+ errors.push(createSlotInLoopError(slot.path));
1535
+ continue;
1536
+ }
1537
+ if (slot.name === void 0 || slot.name === "") {
1538
+ if (defaultSlotPath !== void 0) {
1539
+ errors.push(createDuplicateDefaultSlotError(slot.path));
1540
+ } else {
1541
+ defaultSlotPath = slot.path;
1542
+ hasDefaultSlot = true;
1543
+ }
1544
+ } else {
1545
+ const existingPath = seenNames.get(slot.name);
1546
+ if (existingPath !== void 0) {
1547
+ errors.push(createDuplicateSlotNameError(slot.name, slot.path));
1548
+ } else {
1549
+ seenNames.set(slot.name, slot.path);
1550
+ slotNames.add(slot.name);
1551
+ }
1552
+ }
1553
+ }
1554
+ return { errors, slotNames, hasDefaultSlot };
1555
+ }
1556
+ function validateExpression2(expr, path, stateNames) {
1557
+ const errors = [];
1558
+ switch (expr.expr) {
1559
+ case "state":
1560
+ if (!stateNames.has(expr.name)) {
1561
+ errors.push(createUndefinedStateError2(expr.name, path));
1562
+ }
1563
+ break;
1564
+ case "bin":
1565
+ errors.push(...validateExpression2(expr.left, buildPath2(path, "left"), stateNames));
1566
+ errors.push(...validateExpression2(expr.right, buildPath2(path, "right"), stateNames));
1567
+ break;
1568
+ case "not":
1569
+ errors.push(...validateExpression2(expr.operand, buildPath2(path, "operand"), stateNames));
1570
+ break;
1571
+ case "cond":
1572
+ errors.push(...validateExpression2(expr.if, buildPath2(path, "if"), stateNames));
1573
+ errors.push(...validateExpression2(expr.then, buildPath2(path, "then"), stateNames));
1574
+ errors.push(...validateExpression2(expr.else, buildPath2(path, "else"), stateNames));
1575
+ break;
1576
+ case "get":
1577
+ errors.push(...validateExpression2(expr.base, buildPath2(path, "base"), stateNames));
1578
+ break;
1579
+ case "lit":
1580
+ case "var":
1581
+ case "param":
1582
+ case "route":
1583
+ case "import":
1584
+ break;
1585
+ }
1586
+ return errors;
1587
+ }
1588
+ function validateViewNode2(node, path, stateNames, actionNames) {
1589
+ const errors = [];
1590
+ switch (node.kind) {
1591
+ case "element":
1592
+ if (node.props) {
1593
+ for (const [propName, propValue] of Object.entries(node.props)) {
1594
+ const propPath = buildPath2(path, "props", propName);
1595
+ if (isEventHandler3(propValue)) {
1596
+ if (!actionNames.has(propValue.action)) {
1597
+ errors.push(createUndefinedActionError2(propValue.action, propPath));
1598
+ }
1599
+ if (propValue.payload) {
1600
+ errors.push(
1601
+ ...validateExpression2(propValue.payload, buildPath2(propPath, "payload"), stateNames)
1602
+ );
1603
+ }
1604
+ } else {
1605
+ errors.push(...validateExpression2(propValue, propPath, stateNames));
1606
+ }
1607
+ }
1608
+ }
1609
+ if (node.children) {
1610
+ for (let i = 0; i < node.children.length; i++) {
1611
+ const child = node.children[i];
1612
+ if (child) {
1613
+ errors.push(
1614
+ ...validateViewNode2(child, buildPath2(path, "children", i), stateNames, actionNames)
1615
+ );
1616
+ }
1617
+ }
1618
+ }
1619
+ break;
1620
+ case "text":
1621
+ errors.push(...validateExpression2(node.value, buildPath2(path, "value"), stateNames));
1622
+ break;
1623
+ case "if":
1624
+ errors.push(
1625
+ ...validateExpression2(node.condition, buildPath2(path, "condition"), stateNames)
1626
+ );
1627
+ errors.push(...validateViewNode2(node.then, buildPath2(path, "then"), stateNames, actionNames));
1628
+ if (node.else) {
1629
+ errors.push(...validateViewNode2(node.else, buildPath2(path, "else"), stateNames, actionNames));
1630
+ }
1631
+ break;
1632
+ case "each":
1633
+ errors.push(...validateExpression2(node.items, buildPath2(path, "items"), stateNames));
1634
+ errors.push(...validateViewNode2(node.body, buildPath2(path, "body"), stateNames, actionNames));
1635
+ break;
1636
+ case "slot":
1637
+ break;
1638
+ }
1639
+ return errors;
1640
+ }
1641
+ function analyzeLayoutPass(layout) {
1642
+ const baseContext = collectContext2(layout);
1643
+ const errors = [];
1644
+ const { errors: slotErrors, slotNames, hasDefaultSlot } = validateSlots(layout);
1645
+ errors.push(...slotErrors);
1646
+ if (slotErrors.length > 0) {
1647
+ return {
1648
+ ok: false,
1649
+ errors
1650
+ };
1651
+ }
1652
+ errors.push(
1653
+ ...validateViewNode2(
1654
+ layout.view,
1655
+ "/view",
1656
+ baseContext.stateNames,
1657
+ baseContext.actionNames
1658
+ )
1659
+ );
1660
+ if (errors.length > 0) {
1661
+ return {
1662
+ ok: false,
1663
+ errors
1664
+ };
1665
+ }
1666
+ return {
1667
+ ok: true,
1668
+ context: {
1669
+ ...baseContext,
1670
+ slotNames,
1671
+ hasDefaultSlot
1672
+ }
1673
+ };
1674
+ }
1675
+
1676
+ // src/passes/transform-layout.ts
1677
+ function transformState2(state) {
1678
+ if (!state) return {};
1679
+ const result = {};
1680
+ for (const [name, field] of Object.entries(state)) {
1681
+ result[name] = {
1682
+ type: field.type,
1683
+ initial: field.initial
1684
+ };
1685
+ }
1686
+ return result;
1687
+ }
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
+ };
1700
+ }
1701
+ if (step.do === "update") {
1702
+ return {
1703
+ do: "update",
1704
+ target: step.target,
1705
+ operation: step.operation
1706
+ };
1707
+ }
1708
+ return {
1709
+ do: "fetch",
1710
+ url: { expr: "lit", value: "" }
1711
+ };
1712
+ })
1713
+ }));
1714
+ }
1715
+ function transformViewNode2(node, ctx) {
1716
+ switch (node.kind) {
1717
+ case "element": {
1718
+ const result = {
1719
+ kind: "element",
1720
+ tag: node.tag
1721
+ };
1722
+ if (node.props) {
1723
+ result.props = {};
1724
+ for (const [key, value] of Object.entries(node.props)) {
1725
+ if ("event" in value) {
1726
+ result.props[key] = {
1727
+ event: value.event,
1728
+ action: value.action
1729
+ };
1730
+ } else {
1731
+ result.props[key] = value;
1732
+ }
1733
+ }
1734
+ }
1735
+ if (node.children && node.children.length > 0) {
1736
+ result.children = node.children.map(
1737
+ (child) => transformViewNode2(child, ctx)
1738
+ );
1739
+ }
1740
+ return result;
1741
+ }
1742
+ case "text":
1743
+ return {
1744
+ kind: "text",
1745
+ value: node.value
1746
+ };
1747
+ case "if": {
1748
+ const result = {
1749
+ kind: "if",
1750
+ condition: node.condition,
1751
+ then: transformViewNode2(node.then, ctx)
1752
+ };
1753
+ if (node.else) {
1754
+ result.else = transformViewNode2(node.else, ctx);
1755
+ }
1756
+ return result;
1757
+ }
1758
+ case "each":
1759
+ return {
1760
+ kind: "each",
1761
+ items: node.items,
1762
+ as: node.as,
1763
+ body: transformViewNode2(node.body, ctx)
1764
+ };
1765
+ case "slot":
1766
+ return {
1767
+ kind: "slot",
1768
+ name: node.name
1769
+ };
1770
+ case "component": {
1771
+ const def = ctx.components[node.name];
1772
+ if (def) {
1773
+ return transformViewNode2(def.view, ctx);
1774
+ }
1775
+ return { kind: "element", tag: "div" };
1776
+ }
1777
+ case "markdown":
1778
+ return {
1779
+ kind: "markdown",
1780
+ content: node.content
1781
+ };
1782
+ case "code":
1783
+ return {
1784
+ kind: "code",
1785
+ language: node.language,
1786
+ content: node.content
1787
+ };
1788
+ }
1789
+ }
1790
+ function transformLayoutPass(layout, _context) {
1791
+ const ctx = {
1792
+ components: layout.components || {}
1793
+ };
1794
+ return {
1795
+ version: "1.0",
1796
+ type: "layout",
1797
+ state: transformState2(layout.state),
1798
+ actions: transformActions2(layout.actions),
1799
+ view: transformViewNode2(layout.view, ctx),
1800
+ components: layout.components
1801
+ };
1802
+ }
1803
+ function deepCloneNode(node) {
1804
+ return JSON.parse(JSON.stringify(node));
1805
+ }
1806
+ function replaceSlots(node, defaultContent, namedContent) {
1807
+ if (node.kind === "slot") {
1808
+ const slotName = node.name;
1809
+ if (slotName && namedContent?.[slotName]) {
1810
+ return deepCloneNode(namedContent[slotName]);
1811
+ }
1812
+ return deepCloneNode(defaultContent);
1813
+ }
1814
+ if (node.kind === "element") {
1815
+ const children = node.children;
1816
+ if (children && children.length > 0) {
1817
+ const newChildren = children.map((child) => replaceSlots(child, defaultContent, namedContent));
1818
+ return {
1819
+ ...node,
1820
+ children: newChildren
1821
+ };
1822
+ }
1823
+ return node;
1824
+ }
1825
+ if (node.kind === "if") {
1826
+ const ifNode = node;
1827
+ const result = {
1828
+ ...node,
1829
+ then: replaceSlots(ifNode.then, defaultContent, namedContent)
1830
+ };
1831
+ if (ifNode.else) {
1832
+ result.else = replaceSlots(ifNode.else, defaultContent, namedContent);
1833
+ }
1834
+ return result;
1835
+ }
1836
+ if (node.kind === "each") {
1837
+ const eachNode = node;
1838
+ return {
1839
+ ...node,
1840
+ body: replaceSlots(eachNode.body, defaultContent, namedContent)
1841
+ };
1842
+ }
1843
+ return node;
1844
+ }
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;
1850
+ const composedView = replaceSlots(layoutView, page.view, namedContent);
1851
+ const mergedState = {};
1852
+ for (const [name, field] of Object.entries(page.state)) {
1853
+ mergedState[name] = field;
1854
+ }
1855
+ for (const [name, field] of Object.entries(layout.state)) {
1856
+ if (name in mergedState) {
1857
+ mergedState[`$layout.${name}`] = field;
1858
+ } else {
1859
+ mergedState[name] = field;
1860
+ }
1861
+ }
1862
+ const getActionsArray = (actions) => {
1863
+ if (Array.isArray(actions)) return actions;
1864
+ if (typeof actions === "object" && actions !== null) {
1865
+ return Object.values(actions);
1866
+ }
1867
+ return [];
1868
+ };
1869
+ const pageActions = getActionsArray(page.actions);
1870
+ const layoutActions = getActionsArray(layout.actions);
1871
+ const pageActionNames = new Set(pageActions.map((a) => a.name));
1872
+ const mergedActions = {};
1873
+ for (const action of pageActions) {
1874
+ mergedActions[action.name] = action;
1875
+ }
1876
+ for (const action of layoutActions) {
1877
+ if (pageActionNames.has(action.name)) {
1878
+ const prefixedName = `$layout.${action.name}`;
1879
+ mergedActions[prefixedName] = { ...action, name: prefixedName };
1880
+ } else {
1881
+ mergedActions[action.name] = action;
1882
+ }
1883
+ }
1884
+ const mergedComponents = {
1885
+ ...layout.components || {},
1886
+ ...page.components || {}
1887
+ };
1888
+ const result = {
1889
+ version: "1.0",
1890
+ state: mergedState,
1891
+ actions: mergedActions,
1892
+ view: composedView
1893
+ };
1894
+ if (page.route) {
1895
+ result.route = page.route;
1896
+ }
1897
+ if (Object.keys(mergedComponents).length > 0) {
1898
+ result.components = mergedComponents;
1899
+ }
1900
+ return result;
1901
+ }
830
1902
  export {
1903
+ analyzeLayoutPass,
831
1904
  analyzePass,
832
1905
  compile,
1906
+ composeLayoutWithPage,
833
1907
  createUndefinedVarError2 as createUndefinedVarError,
1908
+ transformLayoutPass,
834
1909
  transformPass,
835
1910
  validatePass
836
1911
  };