@constela/compiler 0.1.0 → 0.2.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/dist/index.d.ts CHANGED
@@ -15,6 +15,7 @@ export { createUndefinedVarError } from '@constela/core';
15
15
  interface AnalysisContext {
16
16
  stateNames: Set<string>;
17
17
  actionNames: Set<string>;
18
+ componentNames: Set<string>;
18
19
  }
19
20
  interface AnalyzePassSuccess {
20
21
  ok: true;
@@ -32,14 +33,18 @@ type AnalyzePassResult = AnalyzePassSuccess | AnalyzePassFailure;
32
33
  *
33
34
  * - Collects state names
34
35
  * - Collects action names
36
+ * - Collects component names
35
37
  * - Validates state references
36
38
  * - Validates action references
37
39
  * - Validates variable scopes
40
+ * - Validates component references and props
41
+ * - Detects component cycles
42
+ * - Validates param references in component definitions
38
43
  *
39
- * @param ast - Validated AST from validate pass
44
+ * @param programAst - Validated AST from validate pass
40
45
  * @returns AnalyzePassResult
41
46
  */
42
- declare function analyzePass(ast: Program): AnalyzePassResult;
47
+ declare function analyzePass(programAst: Program): AnalyzePassResult;
43
48
 
44
49
  /**
45
50
  * Transform Pass - AST to CompiledProgram transformation
package/dist/index.js CHANGED
@@ -20,21 +20,29 @@ import {
20
20
  createUndefinedActionError,
21
21
  createUndefinedVarError,
22
22
  createDuplicateActionError,
23
+ createComponentNotFoundError,
24
+ createComponentPropMissingError,
25
+ createComponentCycleError,
26
+ createUndefinedParamError,
27
+ createSchemaError,
23
28
  isEventHandler
24
29
  } from "@constela/core";
25
30
  function buildPath(base, ...segments) {
26
31
  return segments.reduce((p, s) => `${p}/${s}`, base);
27
32
  }
28
- function collectContext(ast) {
29
- const stateNames = new Set(Object.keys(ast.state));
30
- const actionNames = new Set(ast.actions.map((a) => a.name));
31
- return { stateNames, actionNames };
33
+ function collectContext(ast2) {
34
+ const stateNames = new Set(Object.keys(ast2.state));
35
+ const actionNames = new Set(ast2.actions.map((a) => a.name));
36
+ const componentNames = new Set(
37
+ ast2.components ? Object.keys(ast2.components) : []
38
+ );
39
+ return { stateNames, actionNames, componentNames };
32
40
  }
33
- function checkDuplicateActions(ast) {
41
+ function checkDuplicateActions(ast2) {
34
42
  const errors = [];
35
43
  const seenNames = /* @__PURE__ */ new Set();
36
- for (let i = 0; i < ast.actions.length; i++) {
37
- const action = ast.actions[i];
44
+ for (let i = 0; i < ast2.actions.length; i++) {
45
+ const action = ast2.actions[i];
38
46
  if (action === void 0) continue;
39
47
  if (seenNames.has(action.name)) {
40
48
  errors.push(createDuplicateActionError(action.name, `/actions/${i}`));
@@ -43,7 +51,7 @@ function checkDuplicateActions(ast) {
43
51
  }
44
52
  return errors;
45
53
  }
46
- function validateExpression(expr, path, context, scope) {
54
+ function validateExpression(expr, path, context, scope, paramScope) {
47
55
  const errors = [];
48
56
  switch (expr.expr) {
49
57
  case "state":
@@ -56,12 +64,17 @@ function validateExpression(expr, path, context, scope) {
56
64
  errors.push(createUndefinedVarError(expr.name, path));
57
65
  }
58
66
  break;
67
+ case "param":
68
+ if (!paramScope || !paramScope.params.has(expr.name)) {
69
+ errors.push(createUndefinedParamError(expr.name, path));
70
+ }
71
+ break;
59
72
  case "bin":
60
- errors.push(...validateExpression(expr.left, buildPath(path, "left"), context, scope));
61
- errors.push(...validateExpression(expr.right, buildPath(path, "right"), context, scope));
73
+ errors.push(...validateExpression(expr.left, buildPath(path, "left"), context, scope, paramScope));
74
+ errors.push(...validateExpression(expr.right, buildPath(path, "right"), context, scope, paramScope));
62
75
  break;
63
76
  case "not":
64
- errors.push(...validateExpression(expr.operand, buildPath(path, "operand"), context, scope));
77
+ errors.push(...validateExpression(expr.operand, buildPath(path, "operand"), context, scope, paramScope));
65
78
  break;
66
79
  case "lit":
67
80
  break;
@@ -177,8 +190,9 @@ function validateExpressionInEventPayload(expr, path, context, scope) {
177
190
  }
178
191
  return errors;
179
192
  }
180
- function validateViewNode(node, path, context, scope) {
193
+ function validateViewNode(node, path, context, scope, options = { insideComponent: false }) {
181
194
  const errors = [];
195
+ const { insideComponent, paramScope } = options;
182
196
  switch (node.kind) {
183
197
  case "element":
184
198
  if (node.props) {
@@ -199,7 +213,7 @@ function validateViewNode(node, path, context, scope) {
199
213
  );
200
214
  }
201
215
  } else {
202
- errors.push(...validateExpression(propValue, propPath, context, scope));
216
+ errors.push(...validateExpression(propValue, propPath, context, scope, paramScope));
203
217
  }
204
218
  }
205
219
  }
@@ -208,42 +222,192 @@ function validateViewNode(node, path, context, scope) {
208
222
  const child = node.children[i];
209
223
  if (child === void 0) continue;
210
224
  errors.push(
211
- ...validateViewNode(child, buildPath(path, "children", i), context, scope)
225
+ ...validateViewNode(child, buildPath(path, "children", i), context, scope, options)
212
226
  );
213
227
  }
214
228
  }
215
229
  break;
216
230
  case "text":
217
- errors.push(...validateExpression(node.value, buildPath(path, "value"), context, scope));
231
+ errors.push(...validateExpression(node.value, buildPath(path, "value"), context, scope, paramScope));
218
232
  break;
219
233
  case "if":
220
234
  errors.push(
221
- ...validateExpression(node.condition, buildPath(path, "condition"), context, scope)
235
+ ...validateExpression(node.condition, buildPath(path, "condition"), context, scope, paramScope)
222
236
  );
223
- errors.push(...validateViewNode(node.then, buildPath(path, "then"), context, scope));
237
+ errors.push(...validateViewNode(node.then, buildPath(path, "then"), context, scope, options));
224
238
  if (node.else) {
225
- errors.push(...validateViewNode(node.else, buildPath(path, "else"), context, scope));
239
+ errors.push(...validateViewNode(node.else, buildPath(path, "else"), context, scope, options));
226
240
  }
227
241
  break;
228
- case "each":
229
- errors.push(...validateExpression(node.items, buildPath(path, "items"), context, scope));
242
+ case "each": {
243
+ errors.push(...validateExpression(node.items, buildPath(path, "items"), context, scope, paramScope));
230
244
  const bodyScope = new Set(scope);
231
245
  bodyScope.add(node.as);
232
246
  if (node.index) {
233
247
  bodyScope.add(node.index);
234
248
  }
235
249
  if (node.key) {
236
- errors.push(...validateExpression(node.key, buildPath(path, "key"), context, bodyScope));
250
+ errors.push(...validateExpression(node.key, buildPath(path, "key"), context, bodyScope, paramScope));
251
+ }
252
+ errors.push(...validateViewNode(node.body, buildPath(path, "body"), context, bodyScope, options));
253
+ break;
254
+ }
255
+ case "component": {
256
+ if (!context.componentNames.has(node.name)) {
257
+ errors.push(createComponentNotFoundError(node.name, path));
258
+ } else {
259
+ const componentDef = ast.components?.[node.name];
260
+ if (componentDef) {
261
+ errors.push(
262
+ ...validateComponentProps(node, componentDef, path, context, scope, paramScope)
263
+ );
264
+ }
265
+ }
266
+ if (node.children) {
267
+ for (let i = 0; i < node.children.length; i++) {
268
+ const child = node.children[i];
269
+ if (child === void 0) continue;
270
+ errors.push(
271
+ ...validateViewNode(child, buildPath(path, "children", i), context, scope, options)
272
+ );
273
+ }
274
+ }
275
+ break;
276
+ }
277
+ case "slot":
278
+ if (!insideComponent) {
279
+ errors.push(
280
+ createSchemaError(`Slot can only be used inside component definitions`, path)
281
+ );
282
+ }
283
+ break;
284
+ }
285
+ return errors;
286
+ }
287
+ function validateComponentProps(node, componentDef, path, context, scope, paramScope) {
288
+ const errors = [];
289
+ const params = componentDef.params ?? {};
290
+ const providedProps = node.props ?? {};
291
+ for (const [paramName, paramDef] of Object.entries(params)) {
292
+ const isRequired = paramDef.required !== false;
293
+ if (isRequired && !(paramName in providedProps)) {
294
+ errors.push(
295
+ createComponentPropMissingError(node.name, paramName, buildPath(path, "props"))
296
+ );
297
+ }
298
+ }
299
+ for (const [propName, propValue] of Object.entries(providedProps)) {
300
+ errors.push(
301
+ ...validateExpression(propValue, buildPath(path, "props", propName), context, scope, paramScope)
302
+ );
303
+ }
304
+ return errors;
305
+ }
306
+ var ast;
307
+ function collectComponentCalls(node) {
308
+ const calls = /* @__PURE__ */ new Set();
309
+ switch (node.kind) {
310
+ case "component":
311
+ calls.add(node.name);
312
+ if (node.children) {
313
+ for (const child of node.children) {
314
+ for (const call of collectComponentCalls(child)) {
315
+ calls.add(call);
316
+ }
317
+ }
318
+ }
319
+ break;
320
+ case "element":
321
+ if (node.children) {
322
+ for (const child of node.children) {
323
+ for (const call of collectComponentCalls(child)) {
324
+ calls.add(call);
325
+ }
326
+ }
327
+ }
328
+ break;
329
+ case "if":
330
+ for (const call of collectComponentCalls(node.then)) {
331
+ calls.add(call);
332
+ }
333
+ if (node.else) {
334
+ for (const call of collectComponentCalls(node.else)) {
335
+ calls.add(call);
336
+ }
337
+ }
338
+ break;
339
+ case "each":
340
+ for (const call of collectComponentCalls(node.body)) {
341
+ calls.add(call);
237
342
  }
238
- errors.push(...validateViewNode(node.body, buildPath(path, "body"), context, bodyScope));
239
343
  break;
344
+ case "text":
345
+ case "slot":
346
+ break;
347
+ }
348
+ return calls;
349
+ }
350
+ function detectComponentCycles(programAst, context) {
351
+ if (!programAst.components) return [];
352
+ const errors = [];
353
+ const callGraph = /* @__PURE__ */ new Map();
354
+ for (const [name, def] of Object.entries(programAst.components)) {
355
+ const calls = collectComponentCalls(def.view);
356
+ callGraph.set(name, calls);
357
+ }
358
+ const visited = /* @__PURE__ */ new Set();
359
+ const recStack = /* @__PURE__ */ new Set();
360
+ function dfs(name, path) {
361
+ visited.add(name);
362
+ recStack.add(name);
363
+ const calls = callGraph.get(name) || /* @__PURE__ */ new Set();
364
+ for (const callee of calls) {
365
+ if (!visited.has(callee)) {
366
+ if (dfs(callee, [...path, callee])) return true;
367
+ } else if (recStack.has(callee)) {
368
+ const cycleStart = path.indexOf(callee);
369
+ const cycle = cycleStart >= 0 ? [...path.slice(cycleStart), callee] : [...path, callee];
370
+ errors.push(createComponentCycleError(cycle, `/components/${path[0]}`));
371
+ return true;
372
+ }
373
+ }
374
+ recStack.delete(name);
375
+ return false;
376
+ }
377
+ for (const name of context.componentNames) {
378
+ if (!visited.has(name)) {
379
+ dfs(name, [name]);
380
+ }
240
381
  }
241
382
  return errors;
242
383
  }
243
- function validateActions(ast, context) {
384
+ function validateComponents(programAst, context) {
244
385
  const errors = [];
245
- for (let i = 0; i < ast.actions.length; i++) {
246
- const action = ast.actions[i];
386
+ if (!programAst.components) return errors;
387
+ for (const [name, def] of Object.entries(programAst.components)) {
388
+ const paramNames = new Set(
389
+ def.params ? Object.keys(def.params) : []
390
+ );
391
+ const paramScope = {
392
+ params: paramNames,
393
+ componentName: name
394
+ };
395
+ errors.push(
396
+ ...validateViewNode(
397
+ def.view,
398
+ buildPath("", "components", name, "view"),
399
+ context,
400
+ /* @__PURE__ */ new Set(),
401
+ { insideComponent: true, paramScope }
402
+ )
403
+ );
404
+ }
405
+ return errors;
406
+ }
407
+ function validateActions(programAst, context) {
408
+ const errors = [];
409
+ for (let i = 0; i < programAst.actions.length; i++) {
410
+ const action = programAst.actions[i];
247
411
  if (action === void 0) continue;
248
412
  for (let j = 0; j < action.steps.length; j++) {
249
413
  const step = action.steps[j];
@@ -255,12 +419,19 @@ function validateActions(ast, context) {
255
419
  }
256
420
  return errors;
257
421
  }
258
- function analyzePass(ast) {
259
- const context = collectContext(ast);
422
+ function analyzePass(programAst) {
423
+ ast = programAst;
424
+ const context = collectContext(programAst);
260
425
  const errors = [];
261
- errors.push(...checkDuplicateActions(ast));
262
- errors.push(...validateActions(ast, context));
263
- errors.push(...validateViewNode(ast.view, "/view", context, /* @__PURE__ */ new Set()));
426
+ errors.push(...checkDuplicateActions(programAst));
427
+ errors.push(...validateActions(programAst, context));
428
+ errors.push(...detectComponentCycles(programAst, context));
429
+ errors.push(...validateComponents(programAst, context));
430
+ errors.push(
431
+ ...validateViewNode(programAst.view, "/view", context, /* @__PURE__ */ new Set(), {
432
+ insideComponent: false
433
+ })
434
+ );
264
435
  if (errors.length > 0) {
265
436
  return {
266
437
  ok: false,
@@ -269,14 +440,14 @@ function analyzePass(ast) {
269
440
  }
270
441
  return {
271
442
  ok: true,
272
- ast,
443
+ ast: programAst,
273
444
  context
274
445
  };
275
446
  }
276
447
 
277
448
  // src/passes/transform.ts
278
449
  import { isEventHandler as isEventHandler2 } from "@constela/core";
279
- function transformExpression(expr) {
450
+ function transformExpression(expr, ctx) {
280
451
  switch (expr.expr) {
281
452
  case "lit":
282
453
  return {
@@ -302,33 +473,60 @@ function transformExpression(expr) {
302
473
  return {
303
474
  expr: "bin",
304
475
  op: expr.op,
305
- left: transformExpression(expr.left),
306
- right: transformExpression(expr.right)
476
+ left: transformExpression(expr.left, ctx),
477
+ right: transformExpression(expr.right, ctx)
307
478
  };
308
479
  case "not":
309
480
  return {
310
481
  expr: "not",
311
- operand: transformExpression(expr.operand)
482
+ operand: transformExpression(expr.operand, ctx)
312
483
  };
484
+ case "param": {
485
+ const paramValue = ctx.currentParams?.[expr.name];
486
+ if (paramValue !== void 0) {
487
+ if (expr.path) {
488
+ if (paramValue.expr === "var") {
489
+ const existingPath = paramValue.path;
490
+ const resultPath = existingPath ? `${existingPath}.${expr.path}` : expr.path;
491
+ return {
492
+ expr: "var",
493
+ name: paramValue.name,
494
+ path: resultPath
495
+ };
496
+ }
497
+ if (paramValue.expr === "state") {
498
+ return {
499
+ expr: "var",
500
+ name: paramValue.name,
501
+ path: expr.path
502
+ };
503
+ }
504
+ return paramValue;
505
+ }
506
+ return paramValue;
507
+ }
508
+ return { expr: "lit", value: null };
509
+ }
313
510
  }
314
511
  }
315
- function transformEventHandler(handler) {
512
+ function transformEventHandler(handler, ctx) {
316
513
  const result = {
317
514
  event: handler.event,
318
515
  action: handler.action
319
516
  };
320
517
  if (handler.payload) {
321
- result.payload = transformExpression(handler.payload);
518
+ result.payload = transformExpression(handler.payload, ctx);
322
519
  }
323
520
  return result;
324
521
  }
522
+ var emptyContext = { components: {} };
325
523
  function transformActionStep(step) {
326
524
  switch (step.do) {
327
525
  case "set":
328
526
  return {
329
527
  do: "set",
330
528
  target: step.target,
331
- value: transformExpression(step.value)
529
+ value: transformExpression(step.value, emptyContext)
332
530
  };
333
531
  case "update": {
334
532
  const updateStep = {
@@ -337,20 +535,20 @@ function transformActionStep(step) {
337
535
  operation: step.operation
338
536
  };
339
537
  if (step.value) {
340
- updateStep.value = transformExpression(step.value);
538
+ updateStep.value = transformExpression(step.value, emptyContext);
341
539
  }
342
540
  return updateStep;
343
541
  }
344
542
  case "fetch": {
345
543
  const fetchStep = {
346
544
  do: "fetch",
347
- url: transformExpression(step.url)
545
+ url: transformExpression(step.url, emptyContext)
348
546
  };
349
547
  if (step.method) {
350
548
  fetchStep.method = step.method;
351
549
  }
352
550
  if (step.body) {
353
- fetchStep.body = transformExpression(step.body);
551
+ fetchStep.body = transformExpression(step.body, emptyContext);
354
552
  }
355
553
  if (step.result) {
356
554
  fetchStep.result = step.result;
@@ -365,7 +563,20 @@ function transformActionStep(step) {
365
563
  }
366
564
  }
367
565
  }
368
- function transformViewNode(node) {
566
+ function flattenSlotChildren(children, ctx) {
567
+ const result = [];
568
+ for (const child of children) {
569
+ if (child.kind === "slot") {
570
+ if (ctx.currentChildren && ctx.currentChildren.length > 0) {
571
+ result.push(...ctx.currentChildren);
572
+ }
573
+ } else {
574
+ result.push(transformViewNode(child, ctx));
575
+ }
576
+ }
577
+ return result;
578
+ }
579
+ function transformViewNode(node, ctx) {
369
580
  switch (node.kind) {
370
581
  case "element": {
371
582
  const compiledElement = {
@@ -376,48 +587,89 @@ function transformViewNode(node) {
376
587
  compiledElement.props = {};
377
588
  for (const [propName, propValue] of Object.entries(node.props)) {
378
589
  if (isEventHandler2(propValue)) {
379
- compiledElement.props[propName] = transformEventHandler(propValue);
590
+ compiledElement.props[propName] = transformEventHandler(propValue, ctx);
380
591
  } else {
381
- compiledElement.props[propName] = transformExpression(propValue);
592
+ compiledElement.props[propName] = transformExpression(propValue, ctx);
382
593
  }
383
594
  }
384
595
  }
385
596
  if (node.children && node.children.length > 0) {
386
- compiledElement.children = node.children.map(transformViewNode);
597
+ const flattenedChildren = flattenSlotChildren(node.children, ctx);
598
+ if (flattenedChildren.length > 0) {
599
+ compiledElement.children = flattenedChildren;
600
+ }
387
601
  }
388
602
  return compiledElement;
389
603
  }
390
604
  case "text":
391
605
  return {
392
606
  kind: "text",
393
- value: transformExpression(node.value)
607
+ value: transformExpression(node.value, ctx)
394
608
  };
395
609
  case "if": {
396
610
  const compiledIf = {
397
611
  kind: "if",
398
- condition: transformExpression(node.condition),
399
- then: transformViewNode(node.then)
612
+ condition: transformExpression(node.condition, ctx),
613
+ then: transformViewNode(node.then, ctx)
400
614
  };
401
615
  if (node.else) {
402
- compiledIf.else = transformViewNode(node.else);
616
+ compiledIf.else = transformViewNode(node.else, ctx);
403
617
  }
404
618
  return compiledIf;
405
619
  }
406
620
  case "each": {
407
621
  const compiledEach = {
408
622
  kind: "each",
409
- items: transformExpression(node.items),
623
+ items: transformExpression(node.items, ctx),
410
624
  as: node.as,
411
- body: transformViewNode(node.body)
625
+ body: transformViewNode(node.body, ctx)
412
626
  };
413
627
  if (node.index) {
414
628
  compiledEach.index = node.index;
415
629
  }
416
630
  if (node.key) {
417
- compiledEach.key = transformExpression(node.key);
631
+ compiledEach.key = transformExpression(node.key, ctx);
418
632
  }
419
633
  return compiledEach;
420
634
  }
635
+ case "component": {
636
+ const def = ctx.components[node.name];
637
+ if (!def) {
638
+ return { kind: "element", tag: "div" };
639
+ }
640
+ const params = {};
641
+ if (node.props) {
642
+ for (const [name, expr] of Object.entries(node.props)) {
643
+ params[name] = transformExpression(expr, ctx);
644
+ }
645
+ }
646
+ const children = [];
647
+ if (node.children && node.children.length > 0) {
648
+ for (const child of node.children) {
649
+ children.push(transformViewNode(child, ctx));
650
+ }
651
+ }
652
+ const newCtx = {
653
+ ...ctx,
654
+ currentParams: params,
655
+ currentChildren: children
656
+ };
657
+ return transformViewNode(def.view, newCtx);
658
+ }
659
+ case "slot": {
660
+ if (ctx.currentChildren && ctx.currentChildren.length > 0) {
661
+ if (ctx.currentChildren.length === 1) {
662
+ const child = ctx.currentChildren[0];
663
+ if (child) return child;
664
+ }
665
+ return {
666
+ kind: "element",
667
+ tag: "span",
668
+ children: ctx.currentChildren
669
+ };
670
+ }
671
+ return { kind: "text", value: { expr: "lit", value: "" } };
672
+ }
421
673
  }
422
674
  }
423
675
  function transformState(state) {
@@ -440,12 +692,15 @@ function transformActions(actions) {
440
692
  }
441
693
  return compiledActions;
442
694
  }
443
- function transformPass(ast, _context) {
695
+ function transformPass(ast2, _context) {
696
+ const ctx = {
697
+ components: ast2.components || {}
698
+ };
444
699
  return {
445
700
  version: "1.0",
446
- state: transformState(ast.state),
447
- actions: transformActions(ast.actions),
448
- view: transformViewNode(ast.view)
701
+ state: transformState(ast2.state),
702
+ actions: transformActions(ast2.actions),
703
+ view: transformViewNode(ast2.view, ctx)
449
704
  };
450
705
  }
451
706
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@constela/compiler",
3
- "version": "0.1.0",
3
+ "version": "0.2.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.1.0"
18
+ "@constela/core": "0.2.0"
19
19
  },
20
20
  "devDependencies": {
21
21
  "@types/node": "^20.10.0",