@constela/compiler 0.1.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/LICENSE +21 -0
- package/dist/index.d.ts +198 -0
- package/dist/index.js +483 -0
- package/package.json +37 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Constela Contributors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
import { Program, ConstelaError } from '@constela/core';
|
|
2
|
+
export { createUndefinedVarError } from '@constela/core';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Analyze Pass - Static analysis for semantic validation
|
|
6
|
+
*
|
|
7
|
+
* This pass performs semantic analysis on the validated AST:
|
|
8
|
+
* - Collects state and action names
|
|
9
|
+
* - Validates state references
|
|
10
|
+
* - Validates action references
|
|
11
|
+
* - Validates variable scopes in each loops
|
|
12
|
+
* - Detects duplicate action names
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
interface AnalysisContext {
|
|
16
|
+
stateNames: Set<string>;
|
|
17
|
+
actionNames: Set<string>;
|
|
18
|
+
}
|
|
19
|
+
interface AnalyzePassSuccess {
|
|
20
|
+
ok: true;
|
|
21
|
+
ast: Program;
|
|
22
|
+
context: AnalysisContext;
|
|
23
|
+
}
|
|
24
|
+
interface AnalyzePassFailure {
|
|
25
|
+
ok: false;
|
|
26
|
+
errors: ConstelaError[];
|
|
27
|
+
}
|
|
28
|
+
type AnalyzePassResult = AnalyzePassSuccess | AnalyzePassFailure;
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Performs static analysis on the validated AST
|
|
32
|
+
*
|
|
33
|
+
* - Collects state names
|
|
34
|
+
* - Collects action names
|
|
35
|
+
* - Validates state references
|
|
36
|
+
* - Validates action references
|
|
37
|
+
* - Validates variable scopes
|
|
38
|
+
*
|
|
39
|
+
* @param ast - Validated AST from validate pass
|
|
40
|
+
* @returns AnalyzePassResult
|
|
41
|
+
*/
|
|
42
|
+
declare function analyzePass(ast: Program): AnalyzePassResult;
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Transform Pass - AST to CompiledProgram transformation
|
|
46
|
+
*
|
|
47
|
+
* This pass transforms the validated and analyzed AST into a CompiledProgram
|
|
48
|
+
* that is optimized for runtime execution.
|
|
49
|
+
*/
|
|
50
|
+
|
|
51
|
+
interface CompiledProgram {
|
|
52
|
+
version: '1.0';
|
|
53
|
+
state: Record<string, {
|
|
54
|
+
type: string;
|
|
55
|
+
initial: unknown;
|
|
56
|
+
}>;
|
|
57
|
+
actions: Record<string, CompiledAction>;
|
|
58
|
+
view: CompiledNode;
|
|
59
|
+
}
|
|
60
|
+
interface CompiledAction {
|
|
61
|
+
name: string;
|
|
62
|
+
steps: CompiledActionStep[];
|
|
63
|
+
}
|
|
64
|
+
type CompiledActionStep = CompiledSetStep | CompiledUpdateStep | CompiledFetchStep;
|
|
65
|
+
interface CompiledSetStep {
|
|
66
|
+
do: 'set';
|
|
67
|
+
target: string;
|
|
68
|
+
value: CompiledExpression;
|
|
69
|
+
}
|
|
70
|
+
interface CompiledUpdateStep {
|
|
71
|
+
do: 'update';
|
|
72
|
+
target: string;
|
|
73
|
+
operation: string;
|
|
74
|
+
value?: CompiledExpression;
|
|
75
|
+
}
|
|
76
|
+
interface CompiledFetchStep {
|
|
77
|
+
do: 'fetch';
|
|
78
|
+
url: CompiledExpression;
|
|
79
|
+
method?: string;
|
|
80
|
+
body?: CompiledExpression;
|
|
81
|
+
result?: string;
|
|
82
|
+
onSuccess?: CompiledActionStep[];
|
|
83
|
+
onError?: CompiledActionStep[];
|
|
84
|
+
}
|
|
85
|
+
type CompiledNode = CompiledElementNode | CompiledTextNode | CompiledIfNode | CompiledEachNode;
|
|
86
|
+
interface CompiledElementNode {
|
|
87
|
+
kind: 'element';
|
|
88
|
+
tag: string;
|
|
89
|
+
props?: Record<string, CompiledExpression | CompiledEventHandler>;
|
|
90
|
+
children?: CompiledNode[];
|
|
91
|
+
}
|
|
92
|
+
interface CompiledTextNode {
|
|
93
|
+
kind: 'text';
|
|
94
|
+
value: CompiledExpression;
|
|
95
|
+
}
|
|
96
|
+
interface CompiledIfNode {
|
|
97
|
+
kind: 'if';
|
|
98
|
+
condition: CompiledExpression;
|
|
99
|
+
then: CompiledNode;
|
|
100
|
+
else?: CompiledNode;
|
|
101
|
+
}
|
|
102
|
+
interface CompiledEachNode {
|
|
103
|
+
kind: 'each';
|
|
104
|
+
items: CompiledExpression;
|
|
105
|
+
as: string;
|
|
106
|
+
index?: string;
|
|
107
|
+
key?: CompiledExpression;
|
|
108
|
+
body: CompiledNode;
|
|
109
|
+
}
|
|
110
|
+
type CompiledExpression = CompiledLitExpr | CompiledStateExpr | CompiledVarExpr | CompiledBinExpr | CompiledNotExpr;
|
|
111
|
+
interface CompiledLitExpr {
|
|
112
|
+
expr: 'lit';
|
|
113
|
+
value: string | number | boolean | null | unknown[];
|
|
114
|
+
}
|
|
115
|
+
interface CompiledStateExpr {
|
|
116
|
+
expr: 'state';
|
|
117
|
+
name: string;
|
|
118
|
+
}
|
|
119
|
+
interface CompiledVarExpr {
|
|
120
|
+
expr: 'var';
|
|
121
|
+
name: string;
|
|
122
|
+
path?: string;
|
|
123
|
+
}
|
|
124
|
+
interface CompiledBinExpr {
|
|
125
|
+
expr: 'bin';
|
|
126
|
+
op: string;
|
|
127
|
+
left: CompiledExpression;
|
|
128
|
+
right: CompiledExpression;
|
|
129
|
+
}
|
|
130
|
+
interface CompiledNotExpr {
|
|
131
|
+
expr: 'not';
|
|
132
|
+
operand: CompiledExpression;
|
|
133
|
+
}
|
|
134
|
+
interface CompiledEventHandler {
|
|
135
|
+
event: string;
|
|
136
|
+
action: string;
|
|
137
|
+
payload?: CompiledExpression;
|
|
138
|
+
}
|
|
139
|
+
/**
|
|
140
|
+
* Transforms the validated and analyzed AST into a CompiledProgram
|
|
141
|
+
*
|
|
142
|
+
* @param ast - Validated AST from validate pass
|
|
143
|
+
* @param _context - Analysis context from analyze pass (unused in current implementation)
|
|
144
|
+
* @returns CompiledProgram
|
|
145
|
+
*/
|
|
146
|
+
declare function transformPass(ast: Program, _context: AnalysisContext): CompiledProgram;
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Main compile function for @constela/compiler
|
|
150
|
+
*
|
|
151
|
+
* This module provides the main compile function that orchestrates
|
|
152
|
+
* the three compilation passes: validate -> analyze -> transform.
|
|
153
|
+
*/
|
|
154
|
+
|
|
155
|
+
interface CompileSuccess {
|
|
156
|
+
ok: true;
|
|
157
|
+
program: CompiledProgram;
|
|
158
|
+
}
|
|
159
|
+
interface CompileFailure {
|
|
160
|
+
ok: false;
|
|
161
|
+
errors: ConstelaError[];
|
|
162
|
+
}
|
|
163
|
+
type CompileResult = CompileSuccess | CompileFailure;
|
|
164
|
+
/**
|
|
165
|
+
* Compiles a Constela AST into a CompiledProgram
|
|
166
|
+
*
|
|
167
|
+
* Pipeline: validate -> analyze -> transform
|
|
168
|
+
*
|
|
169
|
+
* @param input - Raw AST input to compile
|
|
170
|
+
* @returns CompileResult with either compiled program or errors
|
|
171
|
+
*/
|
|
172
|
+
declare function compile(input: unknown): CompileResult;
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* Validate Pass - Schema validation using @constela/core
|
|
176
|
+
*
|
|
177
|
+
* This pass validates the raw input against the Constela AST schema
|
|
178
|
+
* and performs basic semantic validation.
|
|
179
|
+
*/
|
|
180
|
+
|
|
181
|
+
interface ValidatePassSuccess {
|
|
182
|
+
ok: true;
|
|
183
|
+
ast: Program;
|
|
184
|
+
}
|
|
185
|
+
interface ValidatePassFailure {
|
|
186
|
+
ok: false;
|
|
187
|
+
error: ConstelaError;
|
|
188
|
+
}
|
|
189
|
+
type ValidatePassResult = ValidatePassSuccess | ValidatePassFailure;
|
|
190
|
+
/**
|
|
191
|
+
* Validates the AST using @constela/core validateAst
|
|
192
|
+
*
|
|
193
|
+
* @param input - Raw input to validate
|
|
194
|
+
* @returns ValidatePassResult
|
|
195
|
+
*/
|
|
196
|
+
declare function validatePass(input: unknown): ValidatePassResult;
|
|
197
|
+
|
|
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 };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,483 @@
|
|
|
1
|
+
// src/passes/validate.ts
|
|
2
|
+
import { validateAst } from "@constela/core";
|
|
3
|
+
function validatePass(input) {
|
|
4
|
+
const result = validateAst(input);
|
|
5
|
+
if (result.ok) {
|
|
6
|
+
return {
|
|
7
|
+
ok: true,
|
|
8
|
+
ast: result.ast
|
|
9
|
+
};
|
|
10
|
+
}
|
|
11
|
+
return {
|
|
12
|
+
ok: false,
|
|
13
|
+
error: result.error
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
// src/passes/analyze.ts
|
|
18
|
+
import {
|
|
19
|
+
createUndefinedStateError,
|
|
20
|
+
createUndefinedActionError,
|
|
21
|
+
createUndefinedVarError,
|
|
22
|
+
createDuplicateActionError,
|
|
23
|
+
isEventHandler
|
|
24
|
+
} from "@constela/core";
|
|
25
|
+
function buildPath(base, ...segments) {
|
|
26
|
+
return segments.reduce((p, s) => `${p}/${s}`, base);
|
|
27
|
+
}
|
|
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 };
|
|
32
|
+
}
|
|
33
|
+
function checkDuplicateActions(ast) {
|
|
34
|
+
const errors = [];
|
|
35
|
+
const seenNames = /* @__PURE__ */ new Set();
|
|
36
|
+
for (let i = 0; i < ast.actions.length; i++) {
|
|
37
|
+
const action = ast.actions[i];
|
|
38
|
+
if (action === void 0) continue;
|
|
39
|
+
if (seenNames.has(action.name)) {
|
|
40
|
+
errors.push(createDuplicateActionError(action.name, `/actions/${i}`));
|
|
41
|
+
}
|
|
42
|
+
seenNames.add(action.name);
|
|
43
|
+
}
|
|
44
|
+
return errors;
|
|
45
|
+
}
|
|
46
|
+
function validateExpression(expr, path, context, scope) {
|
|
47
|
+
const errors = [];
|
|
48
|
+
switch (expr.expr) {
|
|
49
|
+
case "state":
|
|
50
|
+
if (!context.stateNames.has(expr.name)) {
|
|
51
|
+
errors.push(createUndefinedStateError(expr.name, path));
|
|
52
|
+
}
|
|
53
|
+
break;
|
|
54
|
+
case "var":
|
|
55
|
+
if (!scope.has(expr.name)) {
|
|
56
|
+
errors.push(createUndefinedVarError(expr.name, path));
|
|
57
|
+
}
|
|
58
|
+
break;
|
|
59
|
+
case "bin":
|
|
60
|
+
errors.push(...validateExpression(expr.left, buildPath(path, "left"), context, scope));
|
|
61
|
+
errors.push(...validateExpression(expr.right, buildPath(path, "right"), context, scope));
|
|
62
|
+
break;
|
|
63
|
+
case "not":
|
|
64
|
+
errors.push(...validateExpression(expr.operand, buildPath(path, "operand"), context, scope));
|
|
65
|
+
break;
|
|
66
|
+
case "lit":
|
|
67
|
+
break;
|
|
68
|
+
}
|
|
69
|
+
return errors;
|
|
70
|
+
}
|
|
71
|
+
function validateActionStep(step, path, context) {
|
|
72
|
+
const errors = [];
|
|
73
|
+
switch (step.do) {
|
|
74
|
+
case "set":
|
|
75
|
+
if (!context.stateNames.has(step.target)) {
|
|
76
|
+
errors.push(createUndefinedStateError(step.target, buildPath(path, "target")));
|
|
77
|
+
}
|
|
78
|
+
errors.push(
|
|
79
|
+
...validateExpressionStateOnly(step.value, buildPath(path, "value"), context)
|
|
80
|
+
);
|
|
81
|
+
break;
|
|
82
|
+
case "update":
|
|
83
|
+
if (!context.stateNames.has(step.target)) {
|
|
84
|
+
errors.push(createUndefinedStateError(step.target, buildPath(path, "target")));
|
|
85
|
+
}
|
|
86
|
+
if (step.value) {
|
|
87
|
+
errors.push(
|
|
88
|
+
...validateExpressionStateOnly(step.value, buildPath(path, "value"), context)
|
|
89
|
+
);
|
|
90
|
+
}
|
|
91
|
+
break;
|
|
92
|
+
case "fetch":
|
|
93
|
+
errors.push(
|
|
94
|
+
...validateExpressionStateOnly(step.url, buildPath(path, "url"), context)
|
|
95
|
+
);
|
|
96
|
+
if (step.body) {
|
|
97
|
+
errors.push(
|
|
98
|
+
...validateExpressionStateOnly(step.body, buildPath(path, "body"), context)
|
|
99
|
+
);
|
|
100
|
+
}
|
|
101
|
+
if (step.onSuccess) {
|
|
102
|
+
for (let i = 0; i < step.onSuccess.length; i++) {
|
|
103
|
+
const successStep = step.onSuccess[i];
|
|
104
|
+
if (successStep === void 0) continue;
|
|
105
|
+
errors.push(
|
|
106
|
+
...validateActionStep(successStep, buildPath(path, "onSuccess", i), context)
|
|
107
|
+
);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
if (step.onError) {
|
|
111
|
+
for (let i = 0; i < step.onError.length; i++) {
|
|
112
|
+
const errorStep = step.onError[i];
|
|
113
|
+
if (errorStep === void 0) continue;
|
|
114
|
+
errors.push(
|
|
115
|
+
...validateActionStep(errorStep, buildPath(path, "onError", i), context)
|
|
116
|
+
);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
break;
|
|
120
|
+
}
|
|
121
|
+
return errors;
|
|
122
|
+
}
|
|
123
|
+
function validateExpressionStateOnly(expr, path, context) {
|
|
124
|
+
const errors = [];
|
|
125
|
+
switch (expr.expr) {
|
|
126
|
+
case "state":
|
|
127
|
+
if (!context.stateNames.has(expr.name)) {
|
|
128
|
+
errors.push(createUndefinedStateError(expr.name, path));
|
|
129
|
+
}
|
|
130
|
+
break;
|
|
131
|
+
case "var":
|
|
132
|
+
break;
|
|
133
|
+
case "bin":
|
|
134
|
+
errors.push(...validateExpressionStateOnly(expr.left, buildPath(path, "left"), context));
|
|
135
|
+
errors.push(...validateExpressionStateOnly(expr.right, buildPath(path, "right"), context));
|
|
136
|
+
break;
|
|
137
|
+
case "not":
|
|
138
|
+
errors.push(
|
|
139
|
+
...validateExpressionStateOnly(expr.operand, buildPath(path, "operand"), context)
|
|
140
|
+
);
|
|
141
|
+
break;
|
|
142
|
+
case "lit":
|
|
143
|
+
break;
|
|
144
|
+
}
|
|
145
|
+
return errors;
|
|
146
|
+
}
|
|
147
|
+
function validateExpressionInEventPayload(expr, path, context, scope) {
|
|
148
|
+
const errors = [];
|
|
149
|
+
switch (expr.expr) {
|
|
150
|
+
case "state":
|
|
151
|
+
if (!context.stateNames.has(expr.name)) {
|
|
152
|
+
errors.push(createUndefinedStateError(expr.name, path));
|
|
153
|
+
}
|
|
154
|
+
break;
|
|
155
|
+
case "var":
|
|
156
|
+
break;
|
|
157
|
+
case "bin":
|
|
158
|
+
errors.push(
|
|
159
|
+
...validateExpressionInEventPayload(expr.left, buildPath(path, "left"), context, scope)
|
|
160
|
+
);
|
|
161
|
+
errors.push(
|
|
162
|
+
...validateExpressionInEventPayload(expr.right, buildPath(path, "right"), context, scope)
|
|
163
|
+
);
|
|
164
|
+
break;
|
|
165
|
+
case "not":
|
|
166
|
+
errors.push(
|
|
167
|
+
...validateExpressionInEventPayload(
|
|
168
|
+
expr.operand,
|
|
169
|
+
buildPath(path, "operand"),
|
|
170
|
+
context,
|
|
171
|
+
scope
|
|
172
|
+
)
|
|
173
|
+
);
|
|
174
|
+
break;
|
|
175
|
+
case "lit":
|
|
176
|
+
break;
|
|
177
|
+
}
|
|
178
|
+
return errors;
|
|
179
|
+
}
|
|
180
|
+
function validateViewNode(node, path, context, scope) {
|
|
181
|
+
const errors = [];
|
|
182
|
+
switch (node.kind) {
|
|
183
|
+
case "element":
|
|
184
|
+
if (node.props) {
|
|
185
|
+
for (const [propName, propValue] of Object.entries(node.props)) {
|
|
186
|
+
const propPath = buildPath(path, "props", propName);
|
|
187
|
+
if (isEventHandler(propValue)) {
|
|
188
|
+
if (!context.actionNames.has(propValue.action)) {
|
|
189
|
+
errors.push(createUndefinedActionError(propValue.action, propPath));
|
|
190
|
+
}
|
|
191
|
+
if (propValue.payload) {
|
|
192
|
+
errors.push(
|
|
193
|
+
...validateExpressionInEventPayload(
|
|
194
|
+
propValue.payload,
|
|
195
|
+
buildPath(propPath, "payload"),
|
|
196
|
+
context,
|
|
197
|
+
scope
|
|
198
|
+
)
|
|
199
|
+
);
|
|
200
|
+
}
|
|
201
|
+
} else {
|
|
202
|
+
errors.push(...validateExpression(propValue, propPath, context, scope));
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
if (node.children) {
|
|
207
|
+
for (let i = 0; i < node.children.length; i++) {
|
|
208
|
+
const child = node.children[i];
|
|
209
|
+
if (child === void 0) continue;
|
|
210
|
+
errors.push(
|
|
211
|
+
...validateViewNode(child, buildPath(path, "children", i), context, scope)
|
|
212
|
+
);
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
break;
|
|
216
|
+
case "text":
|
|
217
|
+
errors.push(...validateExpression(node.value, buildPath(path, "value"), context, scope));
|
|
218
|
+
break;
|
|
219
|
+
case "if":
|
|
220
|
+
errors.push(
|
|
221
|
+
...validateExpression(node.condition, buildPath(path, "condition"), context, scope)
|
|
222
|
+
);
|
|
223
|
+
errors.push(...validateViewNode(node.then, buildPath(path, "then"), context, scope));
|
|
224
|
+
if (node.else) {
|
|
225
|
+
errors.push(...validateViewNode(node.else, buildPath(path, "else"), context, scope));
|
|
226
|
+
}
|
|
227
|
+
break;
|
|
228
|
+
case "each":
|
|
229
|
+
errors.push(...validateExpression(node.items, buildPath(path, "items"), context, scope));
|
|
230
|
+
const bodyScope = new Set(scope);
|
|
231
|
+
bodyScope.add(node.as);
|
|
232
|
+
if (node.index) {
|
|
233
|
+
bodyScope.add(node.index);
|
|
234
|
+
}
|
|
235
|
+
if (node.key) {
|
|
236
|
+
errors.push(...validateExpression(node.key, buildPath(path, "key"), context, bodyScope));
|
|
237
|
+
}
|
|
238
|
+
errors.push(...validateViewNode(node.body, buildPath(path, "body"), context, bodyScope));
|
|
239
|
+
break;
|
|
240
|
+
}
|
|
241
|
+
return errors;
|
|
242
|
+
}
|
|
243
|
+
function validateActions(ast, context) {
|
|
244
|
+
const errors = [];
|
|
245
|
+
for (let i = 0; i < ast.actions.length; i++) {
|
|
246
|
+
const action = ast.actions[i];
|
|
247
|
+
if (action === void 0) continue;
|
|
248
|
+
for (let j = 0; j < action.steps.length; j++) {
|
|
249
|
+
const step = action.steps[j];
|
|
250
|
+
if (step === void 0) continue;
|
|
251
|
+
errors.push(
|
|
252
|
+
...validateActionStep(step, buildPath("", "actions", i, "steps", j), context)
|
|
253
|
+
);
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
return errors;
|
|
257
|
+
}
|
|
258
|
+
function analyzePass(ast) {
|
|
259
|
+
const context = collectContext(ast);
|
|
260
|
+
const errors = [];
|
|
261
|
+
errors.push(...checkDuplicateActions(ast));
|
|
262
|
+
errors.push(...validateActions(ast, context));
|
|
263
|
+
errors.push(...validateViewNode(ast.view, "/view", context, /* @__PURE__ */ new Set()));
|
|
264
|
+
if (errors.length > 0) {
|
|
265
|
+
return {
|
|
266
|
+
ok: false,
|
|
267
|
+
errors
|
|
268
|
+
};
|
|
269
|
+
}
|
|
270
|
+
return {
|
|
271
|
+
ok: true,
|
|
272
|
+
ast,
|
|
273
|
+
context
|
|
274
|
+
};
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
// src/passes/transform.ts
|
|
278
|
+
import { isEventHandler as isEventHandler2 } from "@constela/core";
|
|
279
|
+
function transformExpression(expr) {
|
|
280
|
+
switch (expr.expr) {
|
|
281
|
+
case "lit":
|
|
282
|
+
return {
|
|
283
|
+
expr: "lit",
|
|
284
|
+
value: expr.value
|
|
285
|
+
};
|
|
286
|
+
case "state":
|
|
287
|
+
return {
|
|
288
|
+
expr: "state",
|
|
289
|
+
name: expr.name
|
|
290
|
+
};
|
|
291
|
+
case "var": {
|
|
292
|
+
const varExpr = {
|
|
293
|
+
expr: "var",
|
|
294
|
+
name: expr.name
|
|
295
|
+
};
|
|
296
|
+
if (expr.path) {
|
|
297
|
+
varExpr.path = expr.path;
|
|
298
|
+
}
|
|
299
|
+
return varExpr;
|
|
300
|
+
}
|
|
301
|
+
case "bin":
|
|
302
|
+
return {
|
|
303
|
+
expr: "bin",
|
|
304
|
+
op: expr.op,
|
|
305
|
+
left: transformExpression(expr.left),
|
|
306
|
+
right: transformExpression(expr.right)
|
|
307
|
+
};
|
|
308
|
+
case "not":
|
|
309
|
+
return {
|
|
310
|
+
expr: "not",
|
|
311
|
+
operand: transformExpression(expr.operand)
|
|
312
|
+
};
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
function transformEventHandler(handler) {
|
|
316
|
+
const result = {
|
|
317
|
+
event: handler.event,
|
|
318
|
+
action: handler.action
|
|
319
|
+
};
|
|
320
|
+
if (handler.payload) {
|
|
321
|
+
result.payload = transformExpression(handler.payload);
|
|
322
|
+
}
|
|
323
|
+
return result;
|
|
324
|
+
}
|
|
325
|
+
function transformActionStep(step) {
|
|
326
|
+
switch (step.do) {
|
|
327
|
+
case "set":
|
|
328
|
+
return {
|
|
329
|
+
do: "set",
|
|
330
|
+
target: step.target,
|
|
331
|
+
value: transformExpression(step.value)
|
|
332
|
+
};
|
|
333
|
+
case "update": {
|
|
334
|
+
const updateStep = {
|
|
335
|
+
do: "update",
|
|
336
|
+
target: step.target,
|
|
337
|
+
operation: step.operation
|
|
338
|
+
};
|
|
339
|
+
if (step.value) {
|
|
340
|
+
updateStep.value = transformExpression(step.value);
|
|
341
|
+
}
|
|
342
|
+
return updateStep;
|
|
343
|
+
}
|
|
344
|
+
case "fetch": {
|
|
345
|
+
const fetchStep = {
|
|
346
|
+
do: "fetch",
|
|
347
|
+
url: transformExpression(step.url)
|
|
348
|
+
};
|
|
349
|
+
if (step.method) {
|
|
350
|
+
fetchStep.method = step.method;
|
|
351
|
+
}
|
|
352
|
+
if (step.body) {
|
|
353
|
+
fetchStep.body = transformExpression(step.body);
|
|
354
|
+
}
|
|
355
|
+
if (step.result) {
|
|
356
|
+
fetchStep.result = step.result;
|
|
357
|
+
}
|
|
358
|
+
if (step.onSuccess) {
|
|
359
|
+
fetchStep.onSuccess = step.onSuccess.map(transformActionStep);
|
|
360
|
+
}
|
|
361
|
+
if (step.onError) {
|
|
362
|
+
fetchStep.onError = step.onError.map(transformActionStep);
|
|
363
|
+
}
|
|
364
|
+
return fetchStep;
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
function transformViewNode(node) {
|
|
369
|
+
switch (node.kind) {
|
|
370
|
+
case "element": {
|
|
371
|
+
const compiledElement = {
|
|
372
|
+
kind: "element",
|
|
373
|
+
tag: node.tag
|
|
374
|
+
};
|
|
375
|
+
if (node.props) {
|
|
376
|
+
compiledElement.props = {};
|
|
377
|
+
for (const [propName, propValue] of Object.entries(node.props)) {
|
|
378
|
+
if (isEventHandler2(propValue)) {
|
|
379
|
+
compiledElement.props[propName] = transformEventHandler(propValue);
|
|
380
|
+
} else {
|
|
381
|
+
compiledElement.props[propName] = transformExpression(propValue);
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
if (node.children && node.children.length > 0) {
|
|
386
|
+
compiledElement.children = node.children.map(transformViewNode);
|
|
387
|
+
}
|
|
388
|
+
return compiledElement;
|
|
389
|
+
}
|
|
390
|
+
case "text":
|
|
391
|
+
return {
|
|
392
|
+
kind: "text",
|
|
393
|
+
value: transformExpression(node.value)
|
|
394
|
+
};
|
|
395
|
+
case "if": {
|
|
396
|
+
const compiledIf = {
|
|
397
|
+
kind: "if",
|
|
398
|
+
condition: transformExpression(node.condition),
|
|
399
|
+
then: transformViewNode(node.then)
|
|
400
|
+
};
|
|
401
|
+
if (node.else) {
|
|
402
|
+
compiledIf.else = transformViewNode(node.else);
|
|
403
|
+
}
|
|
404
|
+
return compiledIf;
|
|
405
|
+
}
|
|
406
|
+
case "each": {
|
|
407
|
+
const compiledEach = {
|
|
408
|
+
kind: "each",
|
|
409
|
+
items: transformExpression(node.items),
|
|
410
|
+
as: node.as,
|
|
411
|
+
body: transformViewNode(node.body)
|
|
412
|
+
};
|
|
413
|
+
if (node.index) {
|
|
414
|
+
compiledEach.index = node.index;
|
|
415
|
+
}
|
|
416
|
+
if (node.key) {
|
|
417
|
+
compiledEach.key = transformExpression(node.key);
|
|
418
|
+
}
|
|
419
|
+
return compiledEach;
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
function transformState(state) {
|
|
424
|
+
const compiledState = {};
|
|
425
|
+
for (const [name, field] of Object.entries(state)) {
|
|
426
|
+
compiledState[name] = {
|
|
427
|
+
type: field.type,
|
|
428
|
+
initial: field.initial
|
|
429
|
+
};
|
|
430
|
+
}
|
|
431
|
+
return compiledState;
|
|
432
|
+
}
|
|
433
|
+
function transformActions(actions) {
|
|
434
|
+
const compiledActions = {};
|
|
435
|
+
for (const action of actions) {
|
|
436
|
+
compiledActions[action.name] = {
|
|
437
|
+
name: action.name,
|
|
438
|
+
steps: action.steps.map(transformActionStep)
|
|
439
|
+
};
|
|
440
|
+
}
|
|
441
|
+
return compiledActions;
|
|
442
|
+
}
|
|
443
|
+
function transformPass(ast, _context) {
|
|
444
|
+
return {
|
|
445
|
+
version: "1.0",
|
|
446
|
+
state: transformState(ast.state),
|
|
447
|
+
actions: transformActions(ast.actions),
|
|
448
|
+
view: transformViewNode(ast.view)
|
|
449
|
+
};
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
// src/compile.ts
|
|
453
|
+
function compile(input) {
|
|
454
|
+
const validateResult = validatePass(input);
|
|
455
|
+
if (!validateResult.ok) {
|
|
456
|
+
return {
|
|
457
|
+
ok: false,
|
|
458
|
+
errors: [validateResult.error]
|
|
459
|
+
};
|
|
460
|
+
}
|
|
461
|
+
const analyzeResult = analyzePass(validateResult.ast);
|
|
462
|
+
if (!analyzeResult.ok) {
|
|
463
|
+
return {
|
|
464
|
+
ok: false,
|
|
465
|
+
errors: analyzeResult.errors
|
|
466
|
+
};
|
|
467
|
+
}
|
|
468
|
+
const program = transformPass(analyzeResult.ast, analyzeResult.context);
|
|
469
|
+
return {
|
|
470
|
+
ok: true,
|
|
471
|
+
program
|
|
472
|
+
};
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
// src/index.ts
|
|
476
|
+
import { createUndefinedVarError as createUndefinedVarError2 } from "@constela/core";
|
|
477
|
+
export {
|
|
478
|
+
analyzePass,
|
|
479
|
+
compile,
|
|
480
|
+
createUndefinedVarError2 as createUndefinedVarError,
|
|
481
|
+
transformPass,
|
|
482
|
+
validatePass
|
|
483
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@constela/compiler",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Compiler for Constela UI framework - AST to Program transformation",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./dist/index.js",
|
|
7
|
+
"types": "./dist/index.d.ts",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"types": "./dist/index.d.ts",
|
|
11
|
+
"import": "./dist/index.js"
|
|
12
|
+
}
|
|
13
|
+
},
|
|
14
|
+
"files": [
|
|
15
|
+
"dist"
|
|
16
|
+
],
|
|
17
|
+
"dependencies": {
|
|
18
|
+
"@constela/core": "0.1.0"
|
|
19
|
+
},
|
|
20
|
+
"devDependencies": {
|
|
21
|
+
"@types/node": "^20.10.0",
|
|
22
|
+
"tsup": "^8.0.0",
|
|
23
|
+
"typescript": "^5.3.0",
|
|
24
|
+
"vitest": "^2.0.0"
|
|
25
|
+
},
|
|
26
|
+
"engines": {
|
|
27
|
+
"node": ">=20.0.0"
|
|
28
|
+
},
|
|
29
|
+
"license": "MIT",
|
|
30
|
+
"scripts": {
|
|
31
|
+
"build": "tsup src/index.ts --format esm --dts --clean",
|
|
32
|
+
"type-check": "tsc --noEmit",
|
|
33
|
+
"test": "vitest run",
|
|
34
|
+
"test:watch": "vitest",
|
|
35
|
+
"clean": "rm -rf dist"
|
|
36
|
+
}
|
|
37
|
+
}
|