@constela/compiler 0.7.1 → 0.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +109 -215
- package/dist/index.d.ts +7 -1
- package/dist/index.js +125 -10
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# @constela/compiler
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Transforms Constela JSON programs into optimized runtime code.
|
|
4
4
|
|
|
5
5
|
## Installation
|
|
6
6
|
|
|
@@ -8,96 +8,88 @@ Compiler for the Constela UI framework - transforms AST to optimized runtime pro
|
|
|
8
8
|
npm install @constela/compiler
|
|
9
9
|
```
|
|
10
10
|
|
|
11
|
-
##
|
|
11
|
+
## Usage
|
|
12
12
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
1. **Validate Pass** - Schema and syntax validation
|
|
16
|
-
2. **Analyze Pass** - Semantic analysis and context collection
|
|
17
|
-
3. **Transform Pass** - AST-to-Program transformation
|
|
18
|
-
|
|
19
|
-
## API Reference
|
|
20
|
-
|
|
21
|
-
### compile
|
|
22
|
-
|
|
23
|
-
Main compilation function that orchestrates the entire pipeline.
|
|
24
|
-
|
|
25
|
-
```typescript
|
|
26
|
-
import { compile } from '@constela/compiler';
|
|
27
|
-
|
|
28
|
-
const result = compile(input);
|
|
29
|
-
|
|
30
|
-
if (result.ok) {
|
|
31
|
-
// Success
|
|
32
|
-
console.log(result.program);
|
|
33
|
-
} else {
|
|
34
|
-
// Failure - array of errors
|
|
35
|
-
console.error(result.errors);
|
|
36
|
-
}
|
|
13
|
+
```bash
|
|
14
|
+
constela compile app.json --out dist/app.compiled.json
|
|
37
15
|
```
|
|
38
16
|
|
|
39
|
-
|
|
40
|
-
- `input: unknown` - Raw program input (typically parsed JSON)
|
|
41
|
-
|
|
42
|
-
**Returns:** `CompileResult`
|
|
43
|
-
- Success: `{ ok: true, program: CompiledProgram }`
|
|
44
|
-
- Failure: `{ ok: false, errors: ConstelaError[] }`
|
|
45
|
-
|
|
46
|
-
### Individual Passes
|
|
47
|
-
|
|
48
|
-
#### validatePass
|
|
49
|
-
|
|
50
|
-
```typescript
|
|
51
|
-
import { validatePass } from '@constela/compiler';
|
|
17
|
+
## JSON Input
|
|
52
18
|
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
19
|
+
```json
|
|
20
|
+
{
|
|
21
|
+
"version": "1.0",
|
|
22
|
+
"state": {
|
|
23
|
+
"count": { "type": "number", "initial": 0 }
|
|
24
|
+
},
|
|
25
|
+
"actions": [
|
|
26
|
+
{
|
|
27
|
+
"name": "increment",
|
|
28
|
+
"steps": [{ "do": "update", "target": "count", "operation": "increment" }]
|
|
29
|
+
}
|
|
30
|
+
],
|
|
31
|
+
"view": {
|
|
32
|
+
"kind": "element",
|
|
33
|
+
"tag": "button",
|
|
34
|
+
"props": { "onClick": { "event": "click", "action": "increment" } },
|
|
35
|
+
"children": [{ "kind": "text", "value": { "expr": "state", "name": "count" } }]
|
|
36
|
+
}
|
|
56
37
|
}
|
|
57
38
|
```
|
|
58
39
|
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
```typescript
|
|
62
|
-
import { analyzePass } from '@constela/compiler';
|
|
63
|
-
|
|
64
|
-
const result = analyzePass(validatedProgram);
|
|
65
|
-
if (result.ok) {
|
|
66
|
-
// result.context contains analysis results
|
|
67
|
-
console.log(result.context.stateNames);
|
|
68
|
-
console.log(result.context.actionNames);
|
|
69
|
-
}
|
|
70
|
-
```
|
|
40
|
+
## Compiler Pipeline
|
|
71
41
|
|
|
72
|
-
|
|
73
|
-
- `stateNames: Set<string>` - State field identifiers
|
|
74
|
-
- `actionNames: Set<string>` - Action names
|
|
75
|
-
- `componentNames: Set<string>` - Component identifiers
|
|
76
|
-
- `routeParams: Set<string>` - Route parameter names
|
|
77
|
-
- `importNames: Set<string>` - External import names
|
|
78
|
-
- `dataNames: Set<string>` - Data source names
|
|
79
|
-
- `refNames: Set<string>` - DOM element reference names
|
|
42
|
+
The compiler transforms JSON programs through three passes:
|
|
80
43
|
|
|
81
|
-
|
|
44
|
+
1. **Validate** - JSON Schema validation
|
|
45
|
+
2. **Analyze** - Semantic analysis (state, actions, components, routes)
|
|
46
|
+
3. **Transform** - AST to optimized runtime program
|
|
82
47
|
|
|
83
|
-
|
|
84
|
-
import { transformPass } from '@constela/compiler';
|
|
48
|
+
## CompiledProgram Structure
|
|
85
49
|
|
|
86
|
-
|
|
50
|
+
```json
|
|
51
|
+
{
|
|
52
|
+
"version": "1.0",
|
|
53
|
+
"route": {
|
|
54
|
+
"path": "/users/:id",
|
|
55
|
+
"params": ["id"],
|
|
56
|
+
"title": { ... },
|
|
57
|
+
"layout": "MainLayout"
|
|
58
|
+
},
|
|
59
|
+
"lifecycle": {
|
|
60
|
+
"onMount": "loadData",
|
|
61
|
+
"onUnmount": "cleanup"
|
|
62
|
+
},
|
|
63
|
+
"state": {
|
|
64
|
+
"count": { "type": "number", "initial": 0 }
|
|
65
|
+
},
|
|
66
|
+
"actions": {
|
|
67
|
+
"increment": {
|
|
68
|
+
"name": "increment",
|
|
69
|
+
"steps": [{ "do": "update", "target": "count", "operation": "increment" }]
|
|
70
|
+
}
|
|
71
|
+
},
|
|
72
|
+
"view": { ... }
|
|
73
|
+
}
|
|
87
74
|
```
|
|
88
75
|
|
|
89
76
|
## Layout Compilation
|
|
90
77
|
|
|
91
|
-
Layouts
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
78
|
+
Layouts are compiled separately with slot validation:
|
|
79
|
+
|
|
80
|
+
```json
|
|
81
|
+
{
|
|
82
|
+
"version": "1.0",
|
|
83
|
+
"type": "layout",
|
|
84
|
+
"view": {
|
|
85
|
+
"kind": "element",
|
|
86
|
+
"tag": "div",
|
|
87
|
+
"children": [
|
|
88
|
+
{ "kind": "component", "name": "Header" },
|
|
89
|
+
{ "kind": "slot" },
|
|
90
|
+
{ "kind": "component", "name": "Footer" }
|
|
91
|
+
]
|
|
92
|
+
}
|
|
101
93
|
}
|
|
102
94
|
```
|
|
103
95
|
|
|
@@ -107,170 +99,72 @@ if (result.ok) {
|
|
|
107
99
|
- No duplicate default slots
|
|
108
100
|
- Slots not inside loops
|
|
109
101
|
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
```typescript
|
|
113
|
-
import { transformLayoutPass } from '@constela/compiler';
|
|
114
|
-
|
|
115
|
-
const compiledLayout = transformLayoutPass(layoutProgram, layoutContext);
|
|
116
|
-
```
|
|
117
|
-
|
|
118
|
-
### composeLayoutWithPage
|
|
119
|
-
|
|
120
|
-
Composes a compiled layout with a page program.
|
|
121
|
-
|
|
122
|
-
```typescript
|
|
123
|
-
import { composeLayoutWithPage } from '@constela/compiler';
|
|
124
|
-
|
|
125
|
-
const composedProgram = composeLayoutWithPage(compiledLayout, compiledPage);
|
|
126
|
-
```
|
|
127
|
-
|
|
128
|
-
**Composition Process:**
|
|
129
|
-
- Merges state from both layout and page
|
|
130
|
-
- Merges actions from both
|
|
131
|
-
- Replaces slot nodes with page content
|
|
132
|
-
- Named slots match by name
|
|
102
|
+
## Error Handling
|
|
133
103
|
|
|
134
|
-
|
|
104
|
+
Structured errors with JSON Pointer paths:
|
|
135
105
|
|
|
136
|
-
```
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
params: string[];
|
|
142
|
-
title?: CompiledExpression;
|
|
143
|
-
layout?: string;
|
|
144
|
-
layoutParams?: Record<string, CompiledExpression>;
|
|
145
|
-
meta?: Record<string, CompiledExpression>;
|
|
146
|
-
};
|
|
147
|
-
lifecycle?: {
|
|
148
|
-
onMount?: string;
|
|
149
|
-
onUnmount?: string;
|
|
150
|
-
onRouteEnter?: string;
|
|
151
|
-
onRouteLeave?: string;
|
|
152
|
-
};
|
|
153
|
-
state: Record<string, {
|
|
154
|
-
type: 'number' | 'string' | 'list' | 'boolean' | 'object';
|
|
155
|
-
initial: unknown;
|
|
156
|
-
}>;
|
|
157
|
-
actions: Record<string, CompiledAction>;
|
|
158
|
-
view: CompiledNode;
|
|
159
|
-
importData?: Record<string, unknown>;
|
|
106
|
+
```json
|
|
107
|
+
{
|
|
108
|
+
"code": "UNDEFINED_STATE",
|
|
109
|
+
"message": "State \"count\" is not defined",
|
|
110
|
+
"path": "/view/children/0/props/onClick"
|
|
160
111
|
}
|
|
161
112
|
```
|
|
162
113
|
|
|
163
|
-
|
|
114
|
+
**Error Codes:**
|
|
115
|
+
- `SCHEMA_INVALID` - JSON Schema validation error
|
|
116
|
+
- `UNDEFINED_STATE` - Reference to undefined state
|
|
117
|
+
- `UNDEFINED_ACTION` - Reference to undefined action
|
|
118
|
+
- `DUPLICATE_ACTION` - Duplicate action name
|
|
119
|
+
- `VAR_UNDEFINED` - Undefined variable reference
|
|
120
|
+
- `COMPONENT_NOT_FOUND` - Undefined component
|
|
121
|
+
- `COMPONENT_PROP_MISSING` - Missing required prop
|
|
122
|
+
- `COMPONENT_CYCLE` - Circular component reference
|
|
123
|
+
- `OPERATION_INVALID_FOR_TYPE` - Invalid operation for state type
|
|
124
|
+
- `LAYOUT_MISSING_SLOT` - Layout missing slot node
|
|
125
|
+
- `LAYOUT_NOT_FOUND` - Referenced layout not found
|
|
164
126
|
|
|
165
|
-
|
|
127
|
+
## Internal API
|
|
166
128
|
|
|
167
|
-
|
|
129
|
+
> For framework developers only. End users should use the CLI.
|
|
168
130
|
|
|
169
|
-
|
|
170
|
-
type CompiledExpression =
|
|
171
|
-
| CompiledLitExpr
|
|
172
|
-
| CompiledStateExpr
|
|
173
|
-
| CompiledVarExpr
|
|
174
|
-
| CompiledBinExpr
|
|
175
|
-
| CompiledNotExpr
|
|
176
|
-
| CompiledCondExpr
|
|
177
|
-
| CompiledGetExpr
|
|
178
|
-
| CompiledRouteExpr
|
|
179
|
-
| CompiledImportExpr
|
|
180
|
-
| CompiledDataExpr
|
|
181
|
-
| CompiledRefExpr
|
|
182
|
-
| CompiledIndexExpr
|
|
183
|
-
| CompiledParamExpr;
|
|
184
|
-
```
|
|
185
|
-
|
|
186
|
-
### CompiledNode (7 types)
|
|
131
|
+
### compile
|
|
187
132
|
|
|
188
133
|
```typescript
|
|
189
|
-
|
|
190
|
-
| CompiledElementNode
|
|
191
|
-
| CompiledTextNode
|
|
192
|
-
| CompiledIfNode
|
|
193
|
-
| CompiledEachNode
|
|
194
|
-
| CompiledMarkdownNode
|
|
195
|
-
| CompiledCodeNode
|
|
196
|
-
| CompiledSlotNode;
|
|
197
|
-
```
|
|
134
|
+
import { compile } from '@constela/compiler';
|
|
198
135
|
|
|
199
|
-
|
|
136
|
+
const result = compile(jsonInput);
|
|
200
137
|
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
steps: CompiledStep[];
|
|
138
|
+
if (result.ok) {
|
|
139
|
+
console.log(result.program);
|
|
140
|
+
} else {
|
|
141
|
+
console.error(result.errors);
|
|
206
142
|
}
|
|
207
143
|
```
|
|
208
144
|
|
|
209
|
-
###
|
|
145
|
+
### Individual Passes
|
|
210
146
|
|
|
211
147
|
```typescript
|
|
212
|
-
|
|
213
|
-
| CompiledSetStep
|
|
214
|
-
| CompiledUpdateStep
|
|
215
|
-
| CompiledFetchStep
|
|
216
|
-
| CompiledStorageStep
|
|
217
|
-
| CompiledClipboardStep
|
|
218
|
-
| CompiledNavigateStep
|
|
219
|
-
| CompiledImportStep
|
|
220
|
-
| CompiledCallStep
|
|
221
|
-
| CompiledSubscribeStep
|
|
222
|
-
| CompiledDisposeStep
|
|
223
|
-
| CompiledDomStep
|
|
224
|
-
| CompiledIfStep;
|
|
225
|
-
```
|
|
148
|
+
import { validatePass, analyzePass, transformPass } from '@constela/compiler';
|
|
226
149
|
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
The compiler collects multiple errors during the analyze pass:
|
|
150
|
+
// Step 1: Validate
|
|
151
|
+
const validated = validatePass(input);
|
|
230
152
|
|
|
231
|
-
|
|
232
|
-
const
|
|
153
|
+
// Step 2: Analyze
|
|
154
|
+
const analyzed = analyzePass(validated.program);
|
|
233
155
|
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
console.log(`[${error.code}] ${error.message}`);
|
|
237
|
-
console.log(` at ${error.path}`);
|
|
238
|
-
}
|
|
239
|
-
}
|
|
156
|
+
// Step 3: Transform
|
|
157
|
+
const compiled = transformPass(analyzed.program, analyzed.context);
|
|
240
158
|
```
|
|
241
159
|
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
## Example
|
|
160
|
+
### Layout Compilation
|
|
245
161
|
|
|
246
162
|
```typescript
|
|
247
|
-
import {
|
|
163
|
+
import { analyzeLayoutPass, transformLayoutPass, composeLayoutWithPage } from '@constela/compiler';
|
|
248
164
|
|
|
249
|
-
const
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
count: { type: 'number', initial: 0 }
|
|
253
|
-
},
|
|
254
|
-
actions: [
|
|
255
|
-
{
|
|
256
|
-
name: 'increment',
|
|
257
|
-
steps: [{ do: 'update', target: 'count', operation: 'increment' }]
|
|
258
|
-
}
|
|
259
|
-
],
|
|
260
|
-
view: {
|
|
261
|
-
kind: 'element',
|
|
262
|
-
tag: 'button',
|
|
263
|
-
props: { onClick: { event: 'click', action: 'increment' } },
|
|
264
|
-
children: [{ kind: 'text', value: { expr: 'state', name: 'count' } }]
|
|
265
|
-
}
|
|
266
|
-
};
|
|
267
|
-
|
|
268
|
-
const result = compile(program);
|
|
269
|
-
|
|
270
|
-
if (result.ok) {
|
|
271
|
-
// result.program is ready for runtime
|
|
272
|
-
console.log(result.program.actions.increment);
|
|
273
|
-
}
|
|
165
|
+
const layoutResult = analyzeLayoutPass(layoutProgram);
|
|
166
|
+
const compiledLayout = transformLayoutPass(layoutProgram, layoutResult.context);
|
|
167
|
+
const composedProgram = composeLayoutWithPage(compiledLayout, compiledPage);
|
|
274
168
|
```
|
|
275
169
|
|
|
276
170
|
## License
|
package/dist/index.d.ts
CHANGED
|
@@ -20,6 +20,7 @@ interface AnalysisContext {
|
|
|
20
20
|
importNames: Set<string>;
|
|
21
21
|
dataNames: Set<string>;
|
|
22
22
|
refNames: Set<string>;
|
|
23
|
+
styleNames: Set<string>;
|
|
23
24
|
}
|
|
24
25
|
interface AnalyzePassSuccess {
|
|
25
26
|
ok: true;
|
|
@@ -234,7 +235,7 @@ interface CompiledSlotNode {
|
|
|
234
235
|
kind: 'slot';
|
|
235
236
|
name?: string;
|
|
236
237
|
}
|
|
237
|
-
type CompiledExpression = CompiledLitExpr | CompiledStateExpr | CompiledVarExpr | CompiledBinExpr | CompiledNotExpr | CompiledCondExpr | CompiledGetExpr | CompiledRouteExpr | CompiledImportExpr | CompiledDataExpr | CompiledRefExpr | CompiledIndexExpr | CompiledParamExpr;
|
|
238
|
+
type CompiledExpression = CompiledLitExpr | CompiledStateExpr | CompiledVarExpr | CompiledBinExpr | CompiledNotExpr | CompiledCondExpr | CompiledGetExpr | CompiledRouteExpr | CompiledImportExpr | CompiledDataExpr | CompiledRefExpr | CompiledIndexExpr | CompiledParamExpr | CompiledStyleExpr;
|
|
238
239
|
interface CompiledLitExpr {
|
|
239
240
|
expr: 'lit';
|
|
240
241
|
value: string | number | boolean | null | unknown[];
|
|
@@ -295,6 +296,11 @@ interface CompiledParamExpr {
|
|
|
295
296
|
name: string;
|
|
296
297
|
path?: string;
|
|
297
298
|
}
|
|
299
|
+
interface CompiledStyleExpr {
|
|
300
|
+
expr: 'style';
|
|
301
|
+
name: string;
|
|
302
|
+
variants?: Record<string, CompiledExpression>;
|
|
303
|
+
}
|
|
298
304
|
interface CompiledEventHandler {
|
|
299
305
|
event: string;
|
|
300
306
|
action: string;
|
package/dist/index.js
CHANGED
|
@@ -42,6 +42,9 @@ import {
|
|
|
42
42
|
createClipboardWriteMissingValueError,
|
|
43
43
|
createInvalidNavigateTargetError,
|
|
44
44
|
createUndefinedRefError,
|
|
45
|
+
createUndefinedStyleError,
|
|
46
|
+
createUndefinedVariantError,
|
|
47
|
+
findSimilarNames,
|
|
45
48
|
isEventHandler,
|
|
46
49
|
DATA_SOURCE_TYPES,
|
|
47
50
|
DATA_TRANSFORMS,
|
|
@@ -53,6 +56,15 @@ import {
|
|
|
53
56
|
function buildPath(base, ...segments) {
|
|
54
57
|
return segments.reduce((p, s) => `${p}/${s}`, base);
|
|
55
58
|
}
|
|
59
|
+
function createErrorOptionsWithSuggestion(name, availableNames) {
|
|
60
|
+
const availableNamesArray = Array.from(availableNames);
|
|
61
|
+
const similarNames = findSimilarNames(name, availableNames);
|
|
62
|
+
const suggestion = similarNames.length > 0 ? `Did you mean '${similarNames[0]}'?` : void 0;
|
|
63
|
+
return {
|
|
64
|
+
suggestion,
|
|
65
|
+
context: { availableNames: availableNamesArray }
|
|
66
|
+
};
|
|
67
|
+
}
|
|
56
68
|
function extractRouteParams(path) {
|
|
57
69
|
const params = [];
|
|
58
70
|
const segments = path.split("/");
|
|
@@ -124,7 +136,10 @@ function collectContext(programAst) {
|
|
|
124
136
|
programAst.data ? Object.keys(programAst.data) : []
|
|
125
137
|
);
|
|
126
138
|
const refNames = collectRefs(programAst.view);
|
|
127
|
-
|
|
139
|
+
const styleNames = new Set(
|
|
140
|
+
programAst.styles ? Object.keys(programAst.styles) : []
|
|
141
|
+
);
|
|
142
|
+
return { stateNames, actionNames, componentNames, routeParams, importNames, dataNames, refNames, styleNames };
|
|
128
143
|
}
|
|
129
144
|
function checkDuplicateActions(ast2) {
|
|
130
145
|
const errors = [];
|
|
@@ -144,7 +159,8 @@ function validateExpression(expr, path, context, scope, paramScope) {
|
|
|
144
159
|
switch (expr.expr) {
|
|
145
160
|
case "state":
|
|
146
161
|
if (!context.stateNames.has(expr.name)) {
|
|
147
|
-
|
|
162
|
+
const errorOptions = createErrorOptionsWithSuggestion(expr.name, context.stateNames);
|
|
163
|
+
errors.push(createUndefinedStateError(expr.name, path, errorOptions));
|
|
148
164
|
}
|
|
149
165
|
break;
|
|
150
166
|
case "var":
|
|
@@ -207,6 +223,32 @@ function validateExpression(expr, path, context, scope, paramScope) {
|
|
|
207
223
|
case "get":
|
|
208
224
|
errors.push(...validateExpression(expr.base, buildPath(path, "base"), context, scope, paramScope));
|
|
209
225
|
break;
|
|
226
|
+
case "style":
|
|
227
|
+
errors.push(...validateStyleExpression(expr, path, context, scope, paramScope));
|
|
228
|
+
break;
|
|
229
|
+
}
|
|
230
|
+
return errors;
|
|
231
|
+
}
|
|
232
|
+
function validateStyleExpression(expr, path, context, scope, paramScope) {
|
|
233
|
+
const errors = [];
|
|
234
|
+
if (!context.styleNames.has(expr.name)) {
|
|
235
|
+
const errorOptions = createErrorOptionsWithSuggestion(expr.name, context.styleNames);
|
|
236
|
+
errors.push(createUndefinedStyleError(expr.name, path, errorOptions));
|
|
237
|
+
return errors;
|
|
238
|
+
}
|
|
239
|
+
if (expr.variants) {
|
|
240
|
+
const stylePreset = ast.styles?.[expr.name];
|
|
241
|
+
const availableVariants = new Set(
|
|
242
|
+
stylePreset?.variants ? Object.keys(stylePreset.variants) : []
|
|
243
|
+
);
|
|
244
|
+
for (const [variantKey, variantValue] of Object.entries(expr.variants)) {
|
|
245
|
+
const variantPath = buildPath(path, "variants", variantKey);
|
|
246
|
+
if (!availableVariants.has(variantKey)) {
|
|
247
|
+
const errorOptions = createErrorOptionsWithSuggestion(variantKey, availableVariants);
|
|
248
|
+
errors.push(createUndefinedVariantError(variantKey, expr.name, variantPath, errorOptions));
|
|
249
|
+
}
|
|
250
|
+
errors.push(...validateExpression(variantValue, variantPath, context, scope, paramScope));
|
|
251
|
+
}
|
|
210
252
|
}
|
|
211
253
|
return errors;
|
|
212
254
|
}
|
|
@@ -215,7 +257,8 @@ function validateActionStep(step, path, context) {
|
|
|
215
257
|
switch (step.do) {
|
|
216
258
|
case "set":
|
|
217
259
|
if (!context.stateNames.has(step.target)) {
|
|
218
|
-
|
|
260
|
+
const errorOptions = createErrorOptionsWithSuggestion(step.target, context.stateNames);
|
|
261
|
+
errors.push(createUndefinedStateError(step.target, buildPath(path, "target"), errorOptions));
|
|
219
262
|
}
|
|
220
263
|
errors.push(
|
|
221
264
|
...validateExpressionStateOnly(step.value, buildPath(path, "value"), context)
|
|
@@ -223,7 +266,8 @@ function validateActionStep(step, path, context) {
|
|
|
223
266
|
break;
|
|
224
267
|
case "update": {
|
|
225
268
|
if (!context.stateNames.has(step.target)) {
|
|
226
|
-
|
|
269
|
+
const errorOptions = createErrorOptionsWithSuggestion(step.target, context.stateNames);
|
|
270
|
+
errors.push(createUndefinedStateError(step.target, buildPath(path, "target"), errorOptions));
|
|
227
271
|
} else {
|
|
228
272
|
const stateField = ast.state[step.target];
|
|
229
273
|
if (stateField) {
|
|
@@ -437,7 +481,8 @@ function validateActionStep(step, path, context) {
|
|
|
437
481
|
...validateExpressionStateOnly(subscribeStep.target, buildPath(path, "target"), context)
|
|
438
482
|
);
|
|
439
483
|
if (!context.actionNames.has(subscribeStep.action)) {
|
|
440
|
-
|
|
484
|
+
const errorOptions = createErrorOptionsWithSuggestion(subscribeStep.action, context.actionNames);
|
|
485
|
+
errors.push(createUndefinedActionError(subscribeStep.action, buildPath(path, "action"), errorOptions));
|
|
441
486
|
}
|
|
442
487
|
break;
|
|
443
488
|
}
|
|
@@ -456,7 +501,8 @@ function validateExpressionStateOnly(expr, path, context) {
|
|
|
456
501
|
switch (expr.expr) {
|
|
457
502
|
case "state":
|
|
458
503
|
if (!context.stateNames.has(expr.name)) {
|
|
459
|
-
|
|
504
|
+
const errorOptions = createErrorOptionsWithSuggestion(expr.name, context.stateNames);
|
|
505
|
+
errors.push(createUndefinedStateError(expr.name, path, errorOptions));
|
|
460
506
|
}
|
|
461
507
|
break;
|
|
462
508
|
case "var":
|
|
@@ -513,6 +559,32 @@ function validateExpressionStateOnly(expr, path, context) {
|
|
|
513
559
|
case "get":
|
|
514
560
|
errors.push(...validateExpressionStateOnly(expr.base, buildPath(path, "base"), context));
|
|
515
561
|
break;
|
|
562
|
+
case "style":
|
|
563
|
+
errors.push(...validateStyleExpressionStateOnly(expr, path, context));
|
|
564
|
+
break;
|
|
565
|
+
}
|
|
566
|
+
return errors;
|
|
567
|
+
}
|
|
568
|
+
function validateStyleExpressionStateOnly(expr, path, context) {
|
|
569
|
+
const errors = [];
|
|
570
|
+
if (!context.styleNames.has(expr.name)) {
|
|
571
|
+
const errorOptions = createErrorOptionsWithSuggestion(expr.name, context.styleNames);
|
|
572
|
+
errors.push(createUndefinedStyleError(expr.name, path, errorOptions));
|
|
573
|
+
return errors;
|
|
574
|
+
}
|
|
575
|
+
if (expr.variants) {
|
|
576
|
+
const stylePreset = ast.styles?.[expr.name];
|
|
577
|
+
const availableVariants = new Set(
|
|
578
|
+
stylePreset?.variants ? Object.keys(stylePreset.variants) : []
|
|
579
|
+
);
|
|
580
|
+
for (const [variantKey, variantValue] of Object.entries(expr.variants)) {
|
|
581
|
+
const variantPath = buildPath(path, "variants", variantKey);
|
|
582
|
+
if (!availableVariants.has(variantKey)) {
|
|
583
|
+
const errorOptions = createErrorOptionsWithSuggestion(variantKey, availableVariants);
|
|
584
|
+
errors.push(createUndefinedVariantError(variantKey, expr.name, variantPath, errorOptions));
|
|
585
|
+
}
|
|
586
|
+
errors.push(...validateExpressionStateOnly(variantValue, variantPath, context));
|
|
587
|
+
}
|
|
516
588
|
}
|
|
517
589
|
return errors;
|
|
518
590
|
}
|
|
@@ -521,7 +593,8 @@ function validateExpressionInEventPayload(expr, path, context, scope) {
|
|
|
521
593
|
switch (expr.expr) {
|
|
522
594
|
case "state":
|
|
523
595
|
if (!context.stateNames.has(expr.name)) {
|
|
524
|
-
|
|
596
|
+
const errorOptions = createErrorOptionsWithSuggestion(expr.name, context.stateNames);
|
|
597
|
+
errors.push(createUndefinedStateError(expr.name, path, errorOptions));
|
|
525
598
|
}
|
|
526
599
|
break;
|
|
527
600
|
case "var":
|
|
@@ -595,6 +668,32 @@ function validateExpressionInEventPayload(expr, path, context, scope) {
|
|
|
595
668
|
...validateExpressionInEventPayload(expr.base, buildPath(path, "base"), context, scope)
|
|
596
669
|
);
|
|
597
670
|
break;
|
|
671
|
+
case "style":
|
|
672
|
+
errors.push(...validateStyleExpressionInEventPayload(expr, path, context, scope));
|
|
673
|
+
break;
|
|
674
|
+
}
|
|
675
|
+
return errors;
|
|
676
|
+
}
|
|
677
|
+
function validateStyleExpressionInEventPayload(expr, path, context, scope) {
|
|
678
|
+
const errors = [];
|
|
679
|
+
if (!context.styleNames.has(expr.name)) {
|
|
680
|
+
const errorOptions = createErrorOptionsWithSuggestion(expr.name, context.styleNames);
|
|
681
|
+
errors.push(createUndefinedStyleError(expr.name, path, errorOptions));
|
|
682
|
+
return errors;
|
|
683
|
+
}
|
|
684
|
+
if (expr.variants) {
|
|
685
|
+
const stylePreset = ast.styles?.[expr.name];
|
|
686
|
+
const availableVariants = new Set(
|
|
687
|
+
stylePreset?.variants ? Object.keys(stylePreset.variants) : []
|
|
688
|
+
);
|
|
689
|
+
for (const [variantKey, variantValue] of Object.entries(expr.variants)) {
|
|
690
|
+
const variantPath = buildPath(path, "variants", variantKey);
|
|
691
|
+
if (!availableVariants.has(variantKey)) {
|
|
692
|
+
const errorOptions = createErrorOptionsWithSuggestion(variantKey, availableVariants);
|
|
693
|
+
errors.push(createUndefinedVariantError(variantKey, expr.name, variantPath, errorOptions));
|
|
694
|
+
}
|
|
695
|
+
errors.push(...validateExpressionInEventPayload(variantValue, variantPath, context, scope));
|
|
696
|
+
}
|
|
598
697
|
}
|
|
599
698
|
return errors;
|
|
600
699
|
}
|
|
@@ -608,7 +707,8 @@ function validateViewNode(node, path, context, scope, options = { insideComponen
|
|
|
608
707
|
const propPath = buildPath(path, "props", propName);
|
|
609
708
|
if (isEventHandler(propValue)) {
|
|
610
709
|
if (!context.actionNames.has(propValue.action)) {
|
|
611
|
-
|
|
710
|
+
const errorOptions = createErrorOptionsWithSuggestion(propValue.action, context.actionNames);
|
|
711
|
+
errors.push(createUndefinedActionError(propValue.action, propPath, errorOptions));
|
|
612
712
|
}
|
|
613
713
|
if (propValue.payload) {
|
|
614
714
|
errors.push(
|
|
@@ -662,7 +762,8 @@ function validateViewNode(node, path, context, scope, options = { insideComponen
|
|
|
662
762
|
}
|
|
663
763
|
case "component": {
|
|
664
764
|
if (!context.componentNames.has(node.name)) {
|
|
665
|
-
|
|
765
|
+
const errorOptions = createErrorOptionsWithSuggestion(node.name, context.componentNames);
|
|
766
|
+
errors.push(createComponentNotFoundError(node.name, path, errorOptions));
|
|
666
767
|
} else {
|
|
667
768
|
const componentDef = ast.components?.[node.name];
|
|
668
769
|
if (componentDef) {
|
|
@@ -839,7 +940,8 @@ function validateLifecycleHooks(lifecycle, context) {
|
|
|
839
940
|
for (const hook of hooks) {
|
|
840
941
|
const actionName = lifecycle[hook];
|
|
841
942
|
if (actionName && !context.actionNames.has(actionName)) {
|
|
842
|
-
|
|
943
|
+
const errorOptions = createErrorOptionsWithSuggestion(actionName, context.actionNames);
|
|
944
|
+
errors.push(createUndefinedActionError(actionName, `/lifecycle/${hook}`, errorOptions));
|
|
843
945
|
}
|
|
844
946
|
}
|
|
845
947
|
return errors;
|
|
@@ -1063,6 +1165,19 @@ function transformExpression(expr, ctx) {
|
|
|
1063
1165
|
base: transformExpression(expr.base, ctx),
|
|
1064
1166
|
key: transformExpression(expr.key, ctx)
|
|
1065
1167
|
};
|
|
1168
|
+
case "style": {
|
|
1169
|
+
const styleExpr = {
|
|
1170
|
+
expr: "style",
|
|
1171
|
+
name: expr.name
|
|
1172
|
+
};
|
|
1173
|
+
if (expr.variants) {
|
|
1174
|
+
styleExpr.variants = {};
|
|
1175
|
+
for (const [key, value] of Object.entries(expr.variants)) {
|
|
1176
|
+
styleExpr.variants[key] = transformExpression(value, ctx);
|
|
1177
|
+
}
|
|
1178
|
+
}
|
|
1179
|
+
return styleExpr;
|
|
1180
|
+
}
|
|
1066
1181
|
}
|
|
1067
1182
|
}
|
|
1068
1183
|
function transformEventHandler(handler, ctx) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@constela/compiler",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.8.0",
|
|
4
4
|
"description": "Compiler for Constela UI framework - AST to Program transformation",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
"dist"
|
|
16
16
|
],
|
|
17
17
|
"dependencies": {
|
|
18
|
-
"@constela/core": "0.
|
|
18
|
+
"@constela/core": "0.8.0"
|
|
19
19
|
},
|
|
20
20
|
"devDependencies": {
|
|
21
21
|
"@types/node": "^20.10.0",
|