@constela/compiler 0.1.0 → 0.3.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 +22 -4
- package/dist/index.js +402 -60
- package/package.json +2 -2
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
|
|
44
|
+
* @param programAst - Validated AST from validate pass
|
|
40
45
|
* @returns AnalyzePassResult
|
|
41
46
|
*/
|
|
42
|
-
declare function analyzePass(
|
|
47
|
+
declare function analyzePass(programAst: Program): AnalyzePassResult;
|
|
43
48
|
|
|
44
49
|
/**
|
|
45
50
|
* Transform Pass - AST to CompiledProgram transformation
|
|
@@ -72,6 +77,8 @@ interface CompiledUpdateStep {
|
|
|
72
77
|
target: string;
|
|
73
78
|
operation: string;
|
|
74
79
|
value?: CompiledExpression;
|
|
80
|
+
index?: CompiledExpression;
|
|
81
|
+
deleteCount?: CompiledExpression;
|
|
75
82
|
}
|
|
76
83
|
interface CompiledFetchStep {
|
|
77
84
|
do: 'fetch';
|
|
@@ -107,7 +114,7 @@ interface CompiledEachNode {
|
|
|
107
114
|
key?: CompiledExpression;
|
|
108
115
|
body: CompiledNode;
|
|
109
116
|
}
|
|
110
|
-
type CompiledExpression = CompiledLitExpr | CompiledStateExpr | CompiledVarExpr | CompiledBinExpr | CompiledNotExpr;
|
|
117
|
+
type CompiledExpression = CompiledLitExpr | CompiledStateExpr | CompiledVarExpr | CompiledBinExpr | CompiledNotExpr | CompiledCondExpr | CompiledGetExpr;
|
|
111
118
|
interface CompiledLitExpr {
|
|
112
119
|
expr: 'lit';
|
|
113
120
|
value: string | number | boolean | null | unknown[];
|
|
@@ -131,6 +138,17 @@ interface CompiledNotExpr {
|
|
|
131
138
|
expr: 'not';
|
|
132
139
|
operand: CompiledExpression;
|
|
133
140
|
}
|
|
141
|
+
interface CompiledCondExpr {
|
|
142
|
+
expr: 'cond';
|
|
143
|
+
if: CompiledExpression;
|
|
144
|
+
then: CompiledExpression;
|
|
145
|
+
else: CompiledExpression;
|
|
146
|
+
}
|
|
147
|
+
interface CompiledGetExpr {
|
|
148
|
+
expr: 'get';
|
|
149
|
+
base: CompiledExpression;
|
|
150
|
+
path: string;
|
|
151
|
+
}
|
|
134
152
|
interface CompiledEventHandler {
|
|
135
153
|
event: string;
|
|
136
154
|
action: string;
|
|
@@ -195,4 +213,4 @@ type ValidatePassResult = ValidatePassSuccess | ValidatePassFailure;
|
|
|
195
213
|
*/
|
|
196
214
|
declare function validatePass(input: unknown): ValidatePassResult;
|
|
197
215
|
|
|
198
|
-
export { type AnalysisContext, type AnalyzePassFailure, type AnalyzePassResult, type AnalyzePassSuccess, type CompileFailure, type CompileResult, type CompileSuccess, type CompiledAction, type CompiledActionStep, type CompiledEachNode, type CompiledElementNode, type CompiledEventHandler, type CompiledExpression, type CompiledIfNode, type CompiledNode, type CompiledProgram, type CompiledTextNode, type ValidatePassFailure, type ValidatePassResult, type ValidatePassSuccess, analyzePass, compile, transformPass, validatePass };
|
|
216
|
+
export { type AnalysisContext, type AnalyzePassFailure, type AnalyzePassResult, type AnalyzePassSuccess, type CompileFailure, type CompileResult, type CompileSuccess, type CompiledAction, type CompiledActionStep, type CompiledEachNode, type CompiledElementNode, type CompiledEventHandler, type CompiledExpression, type CompiledFetchStep, type CompiledIfNode, type CompiledNode, type CompiledProgram, type CompiledSetStep, type CompiledTextNode, type CompiledUpdateStep, type ValidatePassFailure, type ValidatePassResult, type ValidatePassSuccess, analyzePass, compile, transformPass, validatePass };
|
package/dist/index.js
CHANGED
|
@@ -20,21 +20,31 @@ import {
|
|
|
20
20
|
createUndefinedActionError,
|
|
21
21
|
createUndefinedVarError,
|
|
22
22
|
createDuplicateActionError,
|
|
23
|
+
createComponentNotFoundError,
|
|
24
|
+
createComponentPropMissingError,
|
|
25
|
+
createComponentCycleError,
|
|
26
|
+
createUndefinedParamError,
|
|
27
|
+
createSchemaError,
|
|
28
|
+
createOperationInvalidForTypeError,
|
|
29
|
+
createOperationMissingFieldError,
|
|
23
30
|
isEventHandler
|
|
24
31
|
} from "@constela/core";
|
|
25
32
|
function buildPath(base, ...segments) {
|
|
26
33
|
return segments.reduce((p, s) => `${p}/${s}`, base);
|
|
27
34
|
}
|
|
28
|
-
function collectContext(
|
|
29
|
-
const stateNames = new Set(Object.keys(
|
|
30
|
-
const actionNames = new Set(
|
|
31
|
-
|
|
35
|
+
function collectContext(ast2) {
|
|
36
|
+
const stateNames = new Set(Object.keys(ast2.state));
|
|
37
|
+
const actionNames = new Set(ast2.actions.map((a) => a.name));
|
|
38
|
+
const componentNames = new Set(
|
|
39
|
+
ast2.components ? Object.keys(ast2.components) : []
|
|
40
|
+
);
|
|
41
|
+
return { stateNames, actionNames, componentNames };
|
|
32
42
|
}
|
|
33
|
-
function checkDuplicateActions(
|
|
43
|
+
function checkDuplicateActions(ast2) {
|
|
34
44
|
const errors = [];
|
|
35
45
|
const seenNames = /* @__PURE__ */ new Set();
|
|
36
|
-
for (let i = 0; i <
|
|
37
|
-
const action =
|
|
46
|
+
for (let i = 0; i < ast2.actions.length; i++) {
|
|
47
|
+
const action = ast2.actions[i];
|
|
38
48
|
if (action === void 0) continue;
|
|
39
49
|
if (seenNames.has(action.name)) {
|
|
40
50
|
errors.push(createDuplicateActionError(action.name, `/actions/${i}`));
|
|
@@ -43,7 +53,7 @@ function checkDuplicateActions(ast) {
|
|
|
43
53
|
}
|
|
44
54
|
return errors;
|
|
45
55
|
}
|
|
46
|
-
function validateExpression(expr, path, context, scope) {
|
|
56
|
+
function validateExpression(expr, path, context, scope, paramScope) {
|
|
47
57
|
const errors = [];
|
|
48
58
|
switch (expr.expr) {
|
|
49
59
|
case "state":
|
|
@@ -56,15 +66,28 @@ function validateExpression(expr, path, context, scope) {
|
|
|
56
66
|
errors.push(createUndefinedVarError(expr.name, path));
|
|
57
67
|
}
|
|
58
68
|
break;
|
|
69
|
+
case "param":
|
|
70
|
+
if (!paramScope || !paramScope.params.has(expr.name)) {
|
|
71
|
+
errors.push(createUndefinedParamError(expr.name, path));
|
|
72
|
+
}
|
|
73
|
+
break;
|
|
59
74
|
case "bin":
|
|
60
|
-
errors.push(...validateExpression(expr.left, buildPath(path, "left"), context, scope));
|
|
61
|
-
errors.push(...validateExpression(expr.right, buildPath(path, "right"), context, scope));
|
|
75
|
+
errors.push(...validateExpression(expr.left, buildPath(path, "left"), context, scope, paramScope));
|
|
76
|
+
errors.push(...validateExpression(expr.right, buildPath(path, "right"), context, scope, paramScope));
|
|
62
77
|
break;
|
|
63
78
|
case "not":
|
|
64
|
-
errors.push(...validateExpression(expr.operand, buildPath(path, "operand"), context, scope));
|
|
79
|
+
errors.push(...validateExpression(expr.operand, buildPath(path, "operand"), context, scope, paramScope));
|
|
65
80
|
break;
|
|
66
81
|
case "lit":
|
|
67
82
|
break;
|
|
83
|
+
case "cond":
|
|
84
|
+
errors.push(...validateExpression(expr.if, buildPath(path, "if"), context, scope, paramScope));
|
|
85
|
+
errors.push(...validateExpression(expr.then, buildPath(path, "then"), context, scope, paramScope));
|
|
86
|
+
errors.push(...validateExpression(expr.else, buildPath(path, "else"), context, scope, paramScope));
|
|
87
|
+
break;
|
|
88
|
+
case "get":
|
|
89
|
+
errors.push(...validateExpression(expr.base, buildPath(path, "base"), context, scope, paramScope));
|
|
90
|
+
break;
|
|
68
91
|
}
|
|
69
92
|
return errors;
|
|
70
93
|
}
|
|
@@ -79,16 +102,50 @@ function validateActionStep(step, path, context) {
|
|
|
79
102
|
...validateExpressionStateOnly(step.value, buildPath(path, "value"), context)
|
|
80
103
|
);
|
|
81
104
|
break;
|
|
82
|
-
case "update":
|
|
105
|
+
case "update": {
|
|
83
106
|
if (!context.stateNames.has(step.target)) {
|
|
84
107
|
errors.push(createUndefinedStateError(step.target, buildPath(path, "target")));
|
|
108
|
+
} else {
|
|
109
|
+
const stateField = ast.state[step.target];
|
|
110
|
+
if (stateField) {
|
|
111
|
+
const stateType = stateField.type;
|
|
112
|
+
const op = step.operation;
|
|
113
|
+
if (op === "toggle" && stateType !== "boolean") {
|
|
114
|
+
errors.push(createOperationInvalidForTypeError(op, stateType, buildPath(path, "operation")));
|
|
115
|
+
}
|
|
116
|
+
if (op === "merge" && stateType !== "object") {
|
|
117
|
+
errors.push(createOperationInvalidForTypeError(op, stateType, buildPath(path, "operation")));
|
|
118
|
+
}
|
|
119
|
+
if ((op === "increment" || op === "decrement") && stateType !== "number") {
|
|
120
|
+
errors.push(createOperationInvalidForTypeError(op, stateType, buildPath(path, "operation")));
|
|
121
|
+
}
|
|
122
|
+
if ((op === "push" || op === "pop" || op === "remove" || op === "replaceAt" || op === "insertAt" || op === "splice") && stateType !== "list") {
|
|
123
|
+
errors.push(createOperationInvalidForTypeError(op, stateType, buildPath(path, "operation")));
|
|
124
|
+
}
|
|
125
|
+
if (op === "merge" && !step.value) {
|
|
126
|
+
errors.push(createOperationMissingFieldError(op, "value", path));
|
|
127
|
+
}
|
|
128
|
+
if ((op === "replaceAt" || op === "insertAt") && (!step.index || !step.value)) {
|
|
129
|
+
if (!step.index) errors.push(createOperationMissingFieldError(op, "index", path));
|
|
130
|
+
if (!step.value) errors.push(createOperationMissingFieldError(op, "value", path));
|
|
131
|
+
}
|
|
132
|
+
if (op === "splice" && (!step.index || !step.deleteCount)) {
|
|
133
|
+
if (!step.index) errors.push(createOperationMissingFieldError(op, "index", path));
|
|
134
|
+
if (!step.deleteCount) errors.push(createOperationMissingFieldError(op, "deleteCount", path));
|
|
135
|
+
}
|
|
136
|
+
}
|
|
85
137
|
}
|
|
86
138
|
if (step.value) {
|
|
87
|
-
errors.push(
|
|
88
|
-
|
|
89
|
-
|
|
139
|
+
errors.push(...validateExpressionStateOnly(step.value, buildPath(path, "value"), context));
|
|
140
|
+
}
|
|
141
|
+
if (step.index) {
|
|
142
|
+
errors.push(...validateExpressionStateOnly(step.index, buildPath(path, "index"), context));
|
|
143
|
+
}
|
|
144
|
+
if (step.deleteCount) {
|
|
145
|
+
errors.push(...validateExpressionStateOnly(step.deleteCount, buildPath(path, "deleteCount"), context));
|
|
90
146
|
}
|
|
91
147
|
break;
|
|
148
|
+
}
|
|
92
149
|
case "fetch":
|
|
93
150
|
errors.push(
|
|
94
151
|
...validateExpressionStateOnly(step.url, buildPath(path, "url"), context)
|
|
@@ -141,6 +198,14 @@ function validateExpressionStateOnly(expr, path, context) {
|
|
|
141
198
|
break;
|
|
142
199
|
case "lit":
|
|
143
200
|
break;
|
|
201
|
+
case "cond":
|
|
202
|
+
errors.push(...validateExpressionStateOnly(expr.if, buildPath(path, "if"), context));
|
|
203
|
+
errors.push(...validateExpressionStateOnly(expr.then, buildPath(path, "then"), context));
|
|
204
|
+
errors.push(...validateExpressionStateOnly(expr.else, buildPath(path, "else"), context));
|
|
205
|
+
break;
|
|
206
|
+
case "get":
|
|
207
|
+
errors.push(...validateExpressionStateOnly(expr.base, buildPath(path, "base"), context));
|
|
208
|
+
break;
|
|
144
209
|
}
|
|
145
210
|
return errors;
|
|
146
211
|
}
|
|
@@ -174,11 +239,28 @@ function validateExpressionInEventPayload(expr, path, context, scope) {
|
|
|
174
239
|
break;
|
|
175
240
|
case "lit":
|
|
176
241
|
break;
|
|
242
|
+
case "cond":
|
|
243
|
+
errors.push(
|
|
244
|
+
...validateExpressionInEventPayload(expr.if, buildPath(path, "if"), context, scope)
|
|
245
|
+
);
|
|
246
|
+
errors.push(
|
|
247
|
+
...validateExpressionInEventPayload(expr.then, buildPath(path, "then"), context, scope)
|
|
248
|
+
);
|
|
249
|
+
errors.push(
|
|
250
|
+
...validateExpressionInEventPayload(expr.else, buildPath(path, "else"), context, scope)
|
|
251
|
+
);
|
|
252
|
+
break;
|
|
253
|
+
case "get":
|
|
254
|
+
errors.push(
|
|
255
|
+
...validateExpressionInEventPayload(expr.base, buildPath(path, "base"), context, scope)
|
|
256
|
+
);
|
|
257
|
+
break;
|
|
177
258
|
}
|
|
178
259
|
return errors;
|
|
179
260
|
}
|
|
180
|
-
function validateViewNode(node, path, context, scope) {
|
|
261
|
+
function validateViewNode(node, path, context, scope, options = { insideComponent: false }) {
|
|
181
262
|
const errors = [];
|
|
263
|
+
const { insideComponent, paramScope } = options;
|
|
182
264
|
switch (node.kind) {
|
|
183
265
|
case "element":
|
|
184
266
|
if (node.props) {
|
|
@@ -199,7 +281,7 @@ function validateViewNode(node, path, context, scope) {
|
|
|
199
281
|
);
|
|
200
282
|
}
|
|
201
283
|
} else {
|
|
202
|
-
errors.push(...validateExpression(propValue, propPath, context, scope));
|
|
284
|
+
errors.push(...validateExpression(propValue, propPath, context, scope, paramScope));
|
|
203
285
|
}
|
|
204
286
|
}
|
|
205
287
|
}
|
|
@@ -208,42 +290,192 @@ function validateViewNode(node, path, context, scope) {
|
|
|
208
290
|
const child = node.children[i];
|
|
209
291
|
if (child === void 0) continue;
|
|
210
292
|
errors.push(
|
|
211
|
-
...validateViewNode(child, buildPath(path, "children", i), context, scope)
|
|
293
|
+
...validateViewNode(child, buildPath(path, "children", i), context, scope, options)
|
|
212
294
|
);
|
|
213
295
|
}
|
|
214
296
|
}
|
|
215
297
|
break;
|
|
216
298
|
case "text":
|
|
217
|
-
errors.push(...validateExpression(node.value, buildPath(path, "value"), context, scope));
|
|
299
|
+
errors.push(...validateExpression(node.value, buildPath(path, "value"), context, scope, paramScope));
|
|
218
300
|
break;
|
|
219
301
|
case "if":
|
|
220
302
|
errors.push(
|
|
221
|
-
...validateExpression(node.condition, buildPath(path, "condition"), context, scope)
|
|
303
|
+
...validateExpression(node.condition, buildPath(path, "condition"), context, scope, paramScope)
|
|
222
304
|
);
|
|
223
|
-
errors.push(...validateViewNode(node.then, buildPath(path, "then"), context, scope));
|
|
305
|
+
errors.push(...validateViewNode(node.then, buildPath(path, "then"), context, scope, options));
|
|
224
306
|
if (node.else) {
|
|
225
|
-
errors.push(...validateViewNode(node.else, buildPath(path, "else"), context, scope));
|
|
307
|
+
errors.push(...validateViewNode(node.else, buildPath(path, "else"), context, scope, options));
|
|
226
308
|
}
|
|
227
309
|
break;
|
|
228
|
-
case "each":
|
|
229
|
-
errors.push(...validateExpression(node.items, buildPath(path, "items"), context, scope));
|
|
310
|
+
case "each": {
|
|
311
|
+
errors.push(...validateExpression(node.items, buildPath(path, "items"), context, scope, paramScope));
|
|
230
312
|
const bodyScope = new Set(scope);
|
|
231
313
|
bodyScope.add(node.as);
|
|
232
314
|
if (node.index) {
|
|
233
315
|
bodyScope.add(node.index);
|
|
234
316
|
}
|
|
235
317
|
if (node.key) {
|
|
236
|
-
errors.push(...validateExpression(node.key, buildPath(path, "key"), context, bodyScope));
|
|
318
|
+
errors.push(...validateExpression(node.key, buildPath(path, "key"), context, bodyScope, paramScope));
|
|
319
|
+
}
|
|
320
|
+
errors.push(...validateViewNode(node.body, buildPath(path, "body"), context, bodyScope, options));
|
|
321
|
+
break;
|
|
322
|
+
}
|
|
323
|
+
case "component": {
|
|
324
|
+
if (!context.componentNames.has(node.name)) {
|
|
325
|
+
errors.push(createComponentNotFoundError(node.name, path));
|
|
326
|
+
} else {
|
|
327
|
+
const componentDef = ast.components?.[node.name];
|
|
328
|
+
if (componentDef) {
|
|
329
|
+
errors.push(
|
|
330
|
+
...validateComponentProps(node, componentDef, path, context, scope, paramScope)
|
|
331
|
+
);
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
if (node.children) {
|
|
335
|
+
for (let i = 0; i < node.children.length; i++) {
|
|
336
|
+
const child = node.children[i];
|
|
337
|
+
if (child === void 0) continue;
|
|
338
|
+
errors.push(
|
|
339
|
+
...validateViewNode(child, buildPath(path, "children", i), context, scope, options)
|
|
340
|
+
);
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
break;
|
|
344
|
+
}
|
|
345
|
+
case "slot":
|
|
346
|
+
if (!insideComponent) {
|
|
347
|
+
errors.push(
|
|
348
|
+
createSchemaError(`Slot can only be used inside component definitions`, path)
|
|
349
|
+
);
|
|
350
|
+
}
|
|
351
|
+
break;
|
|
352
|
+
}
|
|
353
|
+
return errors;
|
|
354
|
+
}
|
|
355
|
+
function validateComponentProps(node, componentDef, path, context, scope, paramScope) {
|
|
356
|
+
const errors = [];
|
|
357
|
+
const params = componentDef.params ?? {};
|
|
358
|
+
const providedProps = node.props ?? {};
|
|
359
|
+
for (const [paramName, paramDef] of Object.entries(params)) {
|
|
360
|
+
const isRequired = paramDef.required !== false;
|
|
361
|
+
if (isRequired && !(paramName in providedProps)) {
|
|
362
|
+
errors.push(
|
|
363
|
+
createComponentPropMissingError(node.name, paramName, buildPath(path, "props"))
|
|
364
|
+
);
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
for (const [propName, propValue] of Object.entries(providedProps)) {
|
|
368
|
+
errors.push(
|
|
369
|
+
...validateExpression(propValue, buildPath(path, "props", propName), context, scope, paramScope)
|
|
370
|
+
);
|
|
371
|
+
}
|
|
372
|
+
return errors;
|
|
373
|
+
}
|
|
374
|
+
var ast;
|
|
375
|
+
function collectComponentCalls(node) {
|
|
376
|
+
const calls = /* @__PURE__ */ new Set();
|
|
377
|
+
switch (node.kind) {
|
|
378
|
+
case "component":
|
|
379
|
+
calls.add(node.name);
|
|
380
|
+
if (node.children) {
|
|
381
|
+
for (const child of node.children) {
|
|
382
|
+
for (const call of collectComponentCalls(child)) {
|
|
383
|
+
calls.add(call);
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
break;
|
|
388
|
+
case "element":
|
|
389
|
+
if (node.children) {
|
|
390
|
+
for (const child of node.children) {
|
|
391
|
+
for (const call of collectComponentCalls(child)) {
|
|
392
|
+
calls.add(call);
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
break;
|
|
397
|
+
case "if":
|
|
398
|
+
for (const call of collectComponentCalls(node.then)) {
|
|
399
|
+
calls.add(call);
|
|
400
|
+
}
|
|
401
|
+
if (node.else) {
|
|
402
|
+
for (const call of collectComponentCalls(node.else)) {
|
|
403
|
+
calls.add(call);
|
|
404
|
+
}
|
|
237
405
|
}
|
|
238
|
-
errors.push(...validateViewNode(node.body, buildPath(path, "body"), context, bodyScope));
|
|
239
406
|
break;
|
|
407
|
+
case "each":
|
|
408
|
+
for (const call of collectComponentCalls(node.body)) {
|
|
409
|
+
calls.add(call);
|
|
410
|
+
}
|
|
411
|
+
break;
|
|
412
|
+
case "text":
|
|
413
|
+
case "slot":
|
|
414
|
+
break;
|
|
415
|
+
}
|
|
416
|
+
return calls;
|
|
417
|
+
}
|
|
418
|
+
function detectComponentCycles(programAst, context) {
|
|
419
|
+
if (!programAst.components) return [];
|
|
420
|
+
const errors = [];
|
|
421
|
+
const callGraph = /* @__PURE__ */ new Map();
|
|
422
|
+
for (const [name, def] of Object.entries(programAst.components)) {
|
|
423
|
+
const calls = collectComponentCalls(def.view);
|
|
424
|
+
callGraph.set(name, calls);
|
|
425
|
+
}
|
|
426
|
+
const visited = /* @__PURE__ */ new Set();
|
|
427
|
+
const recStack = /* @__PURE__ */ new Set();
|
|
428
|
+
function dfs(name, path) {
|
|
429
|
+
visited.add(name);
|
|
430
|
+
recStack.add(name);
|
|
431
|
+
const calls = callGraph.get(name) || /* @__PURE__ */ new Set();
|
|
432
|
+
for (const callee of calls) {
|
|
433
|
+
if (!visited.has(callee)) {
|
|
434
|
+
if (dfs(callee, [...path, callee])) return true;
|
|
435
|
+
} else if (recStack.has(callee)) {
|
|
436
|
+
const cycleStart = path.indexOf(callee);
|
|
437
|
+
const cycle = cycleStart >= 0 ? [...path.slice(cycleStart), callee] : [...path, callee];
|
|
438
|
+
errors.push(createComponentCycleError(cycle, `/components/${path[0]}`));
|
|
439
|
+
return true;
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
recStack.delete(name);
|
|
443
|
+
return false;
|
|
444
|
+
}
|
|
445
|
+
for (const name of context.componentNames) {
|
|
446
|
+
if (!visited.has(name)) {
|
|
447
|
+
dfs(name, [name]);
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
return errors;
|
|
451
|
+
}
|
|
452
|
+
function validateComponents(programAst, context) {
|
|
453
|
+
const errors = [];
|
|
454
|
+
if (!programAst.components) return errors;
|
|
455
|
+
for (const [name, def] of Object.entries(programAst.components)) {
|
|
456
|
+
const paramNames = new Set(
|
|
457
|
+
def.params ? Object.keys(def.params) : []
|
|
458
|
+
);
|
|
459
|
+
const paramScope = {
|
|
460
|
+
params: paramNames,
|
|
461
|
+
componentName: name
|
|
462
|
+
};
|
|
463
|
+
errors.push(
|
|
464
|
+
...validateViewNode(
|
|
465
|
+
def.view,
|
|
466
|
+
buildPath("", "components", name, "view"),
|
|
467
|
+
context,
|
|
468
|
+
/* @__PURE__ */ new Set(),
|
|
469
|
+
{ insideComponent: true, paramScope }
|
|
470
|
+
)
|
|
471
|
+
);
|
|
240
472
|
}
|
|
241
473
|
return errors;
|
|
242
474
|
}
|
|
243
|
-
function validateActions(
|
|
475
|
+
function validateActions(programAst, context) {
|
|
244
476
|
const errors = [];
|
|
245
|
-
for (let i = 0; i <
|
|
246
|
-
const action =
|
|
477
|
+
for (let i = 0; i < programAst.actions.length; i++) {
|
|
478
|
+
const action = programAst.actions[i];
|
|
247
479
|
if (action === void 0) continue;
|
|
248
480
|
for (let j = 0; j < action.steps.length; j++) {
|
|
249
481
|
const step = action.steps[j];
|
|
@@ -255,12 +487,19 @@ function validateActions(ast, context) {
|
|
|
255
487
|
}
|
|
256
488
|
return errors;
|
|
257
489
|
}
|
|
258
|
-
function analyzePass(
|
|
259
|
-
|
|
490
|
+
function analyzePass(programAst) {
|
|
491
|
+
ast = programAst;
|
|
492
|
+
const context = collectContext(programAst);
|
|
260
493
|
const errors = [];
|
|
261
|
-
errors.push(...checkDuplicateActions(
|
|
262
|
-
errors.push(...validateActions(
|
|
263
|
-
errors.push(...
|
|
494
|
+
errors.push(...checkDuplicateActions(programAst));
|
|
495
|
+
errors.push(...validateActions(programAst, context));
|
|
496
|
+
errors.push(...detectComponentCycles(programAst, context));
|
|
497
|
+
errors.push(...validateComponents(programAst, context));
|
|
498
|
+
errors.push(
|
|
499
|
+
...validateViewNode(programAst.view, "/view", context, /* @__PURE__ */ new Set(), {
|
|
500
|
+
insideComponent: false
|
|
501
|
+
})
|
|
502
|
+
);
|
|
264
503
|
if (errors.length > 0) {
|
|
265
504
|
return {
|
|
266
505
|
ok: false,
|
|
@@ -269,14 +508,14 @@ function analyzePass(ast) {
|
|
|
269
508
|
}
|
|
270
509
|
return {
|
|
271
510
|
ok: true,
|
|
272
|
-
ast,
|
|
511
|
+
ast: programAst,
|
|
273
512
|
context
|
|
274
513
|
};
|
|
275
514
|
}
|
|
276
515
|
|
|
277
516
|
// src/passes/transform.ts
|
|
278
517
|
import { isEventHandler as isEventHandler2 } from "@constela/core";
|
|
279
|
-
function transformExpression(expr) {
|
|
518
|
+
function transformExpression(expr, ctx) {
|
|
280
519
|
switch (expr.expr) {
|
|
281
520
|
case "lit":
|
|
282
521
|
return {
|
|
@@ -302,33 +541,73 @@ function transformExpression(expr) {
|
|
|
302
541
|
return {
|
|
303
542
|
expr: "bin",
|
|
304
543
|
op: expr.op,
|
|
305
|
-
left: transformExpression(expr.left),
|
|
306
|
-
right: transformExpression(expr.right)
|
|
544
|
+
left: transformExpression(expr.left, ctx),
|
|
545
|
+
right: transformExpression(expr.right, ctx)
|
|
307
546
|
};
|
|
308
547
|
case "not":
|
|
309
548
|
return {
|
|
310
549
|
expr: "not",
|
|
311
|
-
operand: transformExpression(expr.operand)
|
|
550
|
+
operand: transformExpression(expr.operand, ctx)
|
|
551
|
+
};
|
|
552
|
+
case "param": {
|
|
553
|
+
const paramValue = ctx.currentParams?.[expr.name];
|
|
554
|
+
if (paramValue !== void 0) {
|
|
555
|
+
if (expr.path) {
|
|
556
|
+
if (paramValue.expr === "var") {
|
|
557
|
+
const existingPath = paramValue.path;
|
|
558
|
+
const resultPath = existingPath ? `${existingPath}.${expr.path}` : expr.path;
|
|
559
|
+
return {
|
|
560
|
+
expr: "var",
|
|
561
|
+
name: paramValue.name,
|
|
562
|
+
path: resultPath
|
|
563
|
+
};
|
|
564
|
+
}
|
|
565
|
+
if (paramValue.expr === "state") {
|
|
566
|
+
return {
|
|
567
|
+
expr: "var",
|
|
568
|
+
name: paramValue.name,
|
|
569
|
+
path: expr.path
|
|
570
|
+
};
|
|
571
|
+
}
|
|
572
|
+
return paramValue;
|
|
573
|
+
}
|
|
574
|
+
return paramValue;
|
|
575
|
+
}
|
|
576
|
+
return { expr: "lit", value: null };
|
|
577
|
+
}
|
|
578
|
+
case "cond":
|
|
579
|
+
return {
|
|
580
|
+
expr: "cond",
|
|
581
|
+
if: transformExpression(expr.if, ctx),
|
|
582
|
+
then: transformExpression(expr.then, ctx),
|
|
583
|
+
else: transformExpression(expr.else, ctx)
|
|
584
|
+
};
|
|
585
|
+
case "get":
|
|
586
|
+
return {
|
|
587
|
+
expr: "get",
|
|
588
|
+
base: transformExpression(expr.base, ctx),
|
|
589
|
+
path: expr.path
|
|
312
590
|
};
|
|
313
591
|
}
|
|
314
592
|
}
|
|
315
|
-
function transformEventHandler(handler) {
|
|
593
|
+
function transformEventHandler(handler, ctx) {
|
|
316
594
|
const result = {
|
|
317
595
|
event: handler.event,
|
|
318
596
|
action: handler.action
|
|
319
597
|
};
|
|
320
598
|
if (handler.payload) {
|
|
321
|
-
result.payload = transformExpression(handler.payload);
|
|
599
|
+
result.payload = transformExpression(handler.payload, ctx);
|
|
322
600
|
}
|
|
323
601
|
return result;
|
|
324
602
|
}
|
|
603
|
+
var emptyContext = { components: {} };
|
|
325
604
|
function transformActionStep(step) {
|
|
326
605
|
switch (step.do) {
|
|
327
606
|
case "set":
|
|
328
607
|
return {
|
|
329
608
|
do: "set",
|
|
330
609
|
target: step.target,
|
|
331
|
-
value: transformExpression(step.value)
|
|
610
|
+
value: transformExpression(step.value, emptyContext)
|
|
332
611
|
};
|
|
333
612
|
case "update": {
|
|
334
613
|
const updateStep = {
|
|
@@ -337,20 +616,26 @@ function transformActionStep(step) {
|
|
|
337
616
|
operation: step.operation
|
|
338
617
|
};
|
|
339
618
|
if (step.value) {
|
|
340
|
-
updateStep.value = transformExpression(step.value);
|
|
619
|
+
updateStep.value = transformExpression(step.value, emptyContext);
|
|
620
|
+
}
|
|
621
|
+
if (step.index) {
|
|
622
|
+
updateStep.index = transformExpression(step.index, emptyContext);
|
|
623
|
+
}
|
|
624
|
+
if (step.deleteCount) {
|
|
625
|
+
updateStep.deleteCount = transformExpression(step.deleteCount, emptyContext);
|
|
341
626
|
}
|
|
342
627
|
return updateStep;
|
|
343
628
|
}
|
|
344
629
|
case "fetch": {
|
|
345
630
|
const fetchStep = {
|
|
346
631
|
do: "fetch",
|
|
347
|
-
url: transformExpression(step.url)
|
|
632
|
+
url: transformExpression(step.url, emptyContext)
|
|
348
633
|
};
|
|
349
634
|
if (step.method) {
|
|
350
635
|
fetchStep.method = step.method;
|
|
351
636
|
}
|
|
352
637
|
if (step.body) {
|
|
353
|
-
fetchStep.body = transformExpression(step.body);
|
|
638
|
+
fetchStep.body = transformExpression(step.body, emptyContext);
|
|
354
639
|
}
|
|
355
640
|
if (step.result) {
|
|
356
641
|
fetchStep.result = step.result;
|
|
@@ -365,7 +650,20 @@ function transformActionStep(step) {
|
|
|
365
650
|
}
|
|
366
651
|
}
|
|
367
652
|
}
|
|
368
|
-
function
|
|
653
|
+
function flattenSlotChildren(children, ctx) {
|
|
654
|
+
const result = [];
|
|
655
|
+
for (const child of children) {
|
|
656
|
+
if (child.kind === "slot") {
|
|
657
|
+
if (ctx.currentChildren && ctx.currentChildren.length > 0) {
|
|
658
|
+
result.push(...ctx.currentChildren);
|
|
659
|
+
}
|
|
660
|
+
} else {
|
|
661
|
+
result.push(transformViewNode(child, ctx));
|
|
662
|
+
}
|
|
663
|
+
}
|
|
664
|
+
return result;
|
|
665
|
+
}
|
|
666
|
+
function transformViewNode(node, ctx) {
|
|
369
667
|
switch (node.kind) {
|
|
370
668
|
case "element": {
|
|
371
669
|
const compiledElement = {
|
|
@@ -376,48 +674,89 @@ function transformViewNode(node) {
|
|
|
376
674
|
compiledElement.props = {};
|
|
377
675
|
for (const [propName, propValue] of Object.entries(node.props)) {
|
|
378
676
|
if (isEventHandler2(propValue)) {
|
|
379
|
-
compiledElement.props[propName] = transformEventHandler(propValue);
|
|
677
|
+
compiledElement.props[propName] = transformEventHandler(propValue, ctx);
|
|
380
678
|
} else {
|
|
381
|
-
compiledElement.props[propName] = transformExpression(propValue);
|
|
679
|
+
compiledElement.props[propName] = transformExpression(propValue, ctx);
|
|
382
680
|
}
|
|
383
681
|
}
|
|
384
682
|
}
|
|
385
683
|
if (node.children && node.children.length > 0) {
|
|
386
|
-
|
|
684
|
+
const flattenedChildren = flattenSlotChildren(node.children, ctx);
|
|
685
|
+
if (flattenedChildren.length > 0) {
|
|
686
|
+
compiledElement.children = flattenedChildren;
|
|
687
|
+
}
|
|
387
688
|
}
|
|
388
689
|
return compiledElement;
|
|
389
690
|
}
|
|
390
691
|
case "text":
|
|
391
692
|
return {
|
|
392
693
|
kind: "text",
|
|
393
|
-
value: transformExpression(node.value)
|
|
694
|
+
value: transformExpression(node.value, ctx)
|
|
394
695
|
};
|
|
395
696
|
case "if": {
|
|
396
697
|
const compiledIf = {
|
|
397
698
|
kind: "if",
|
|
398
|
-
condition: transformExpression(node.condition),
|
|
399
|
-
then: transformViewNode(node.then)
|
|
699
|
+
condition: transformExpression(node.condition, ctx),
|
|
700
|
+
then: transformViewNode(node.then, ctx)
|
|
400
701
|
};
|
|
401
702
|
if (node.else) {
|
|
402
|
-
compiledIf.else = transformViewNode(node.else);
|
|
703
|
+
compiledIf.else = transformViewNode(node.else, ctx);
|
|
403
704
|
}
|
|
404
705
|
return compiledIf;
|
|
405
706
|
}
|
|
406
707
|
case "each": {
|
|
407
708
|
const compiledEach = {
|
|
408
709
|
kind: "each",
|
|
409
|
-
items: transformExpression(node.items),
|
|
710
|
+
items: transformExpression(node.items, ctx),
|
|
410
711
|
as: node.as,
|
|
411
|
-
body: transformViewNode(node.body)
|
|
712
|
+
body: transformViewNode(node.body, ctx)
|
|
412
713
|
};
|
|
413
714
|
if (node.index) {
|
|
414
715
|
compiledEach.index = node.index;
|
|
415
716
|
}
|
|
416
717
|
if (node.key) {
|
|
417
|
-
compiledEach.key = transformExpression(node.key);
|
|
718
|
+
compiledEach.key = transformExpression(node.key, ctx);
|
|
418
719
|
}
|
|
419
720
|
return compiledEach;
|
|
420
721
|
}
|
|
722
|
+
case "component": {
|
|
723
|
+
const def = ctx.components[node.name];
|
|
724
|
+
if (!def) {
|
|
725
|
+
return { kind: "element", tag: "div" };
|
|
726
|
+
}
|
|
727
|
+
const params = {};
|
|
728
|
+
if (node.props) {
|
|
729
|
+
for (const [name, expr] of Object.entries(node.props)) {
|
|
730
|
+
params[name] = transformExpression(expr, ctx);
|
|
731
|
+
}
|
|
732
|
+
}
|
|
733
|
+
const children = [];
|
|
734
|
+
if (node.children && node.children.length > 0) {
|
|
735
|
+
for (const child of node.children) {
|
|
736
|
+
children.push(transformViewNode(child, ctx));
|
|
737
|
+
}
|
|
738
|
+
}
|
|
739
|
+
const newCtx = {
|
|
740
|
+
...ctx,
|
|
741
|
+
currentParams: params,
|
|
742
|
+
currentChildren: children
|
|
743
|
+
};
|
|
744
|
+
return transformViewNode(def.view, newCtx);
|
|
745
|
+
}
|
|
746
|
+
case "slot": {
|
|
747
|
+
if (ctx.currentChildren && ctx.currentChildren.length > 0) {
|
|
748
|
+
if (ctx.currentChildren.length === 1) {
|
|
749
|
+
const child = ctx.currentChildren[0];
|
|
750
|
+
if (child) return child;
|
|
751
|
+
}
|
|
752
|
+
return {
|
|
753
|
+
kind: "element",
|
|
754
|
+
tag: "span",
|
|
755
|
+
children: ctx.currentChildren
|
|
756
|
+
};
|
|
757
|
+
}
|
|
758
|
+
return { kind: "text", value: { expr: "lit", value: "" } };
|
|
759
|
+
}
|
|
421
760
|
}
|
|
422
761
|
}
|
|
423
762
|
function transformState(state) {
|
|
@@ -440,12 +779,15 @@ function transformActions(actions) {
|
|
|
440
779
|
}
|
|
441
780
|
return compiledActions;
|
|
442
781
|
}
|
|
443
|
-
function transformPass(
|
|
782
|
+
function transformPass(ast2, _context) {
|
|
783
|
+
const ctx = {
|
|
784
|
+
components: ast2.components || {}
|
|
785
|
+
};
|
|
444
786
|
return {
|
|
445
787
|
version: "1.0",
|
|
446
|
-
state: transformState(
|
|
447
|
-
actions: transformActions(
|
|
448
|
-
view: transformViewNode(
|
|
788
|
+
state: transformState(ast2.state),
|
|
789
|
+
actions: transformActions(ast2.actions),
|
|
790
|
+
view: transformViewNode(ast2.view, ctx)
|
|
449
791
|
};
|
|
450
792
|
}
|
|
451
793
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@constela/compiler",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.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.
|
|
18
|
+
"@constela/core": "0.3.0"
|
|
19
19
|
},
|
|
20
20
|
"devDependencies": {
|
|
21
21
|
"@types/node": "^20.10.0",
|