@constela/compiler 0.4.0 → 0.5.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 +127 -5
- package/dist/index.js +889 -8
- package/package.json +2 -2
package/dist/index.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Program, ConstelaError } from '@constela/core';
|
|
1
|
+
import { Program, ConstelaError, LayoutProgram, ComponentDef, ViewNode } from '@constela/core';
|
|
2
2
|
export { createUndefinedVarError } from '@constela/core';
|
|
3
3
|
|
|
4
4
|
/**
|
|
@@ -16,6 +16,9 @@ interface AnalysisContext {
|
|
|
16
16
|
stateNames: Set<string>;
|
|
17
17
|
actionNames: Set<string>;
|
|
18
18
|
componentNames: Set<string>;
|
|
19
|
+
routeParams: Set<string>;
|
|
20
|
+
importNames: Set<string>;
|
|
21
|
+
dataNames: Set<string>;
|
|
19
22
|
}
|
|
20
23
|
interface AnalyzePassSuccess {
|
|
21
24
|
ok: true;
|
|
@@ -40,6 +43,7 @@ type AnalyzePassResult = AnalyzePassSuccess | AnalyzePassFailure;
|
|
|
40
43
|
* - Validates component references and props
|
|
41
44
|
* - Detects component cycles
|
|
42
45
|
* - Validates param references in component definitions
|
|
46
|
+
* - Validates data sources and getStaticPaths
|
|
43
47
|
*
|
|
44
48
|
* @param programAst - Validated AST from validate pass
|
|
45
49
|
* @returns AnalyzePassResult
|
|
@@ -53,20 +57,36 @@ declare function analyzePass(programAst: Program): AnalyzePassResult;
|
|
|
53
57
|
* that is optimized for runtime execution.
|
|
54
58
|
*/
|
|
55
59
|
|
|
60
|
+
interface CompiledRouteDefinition {
|
|
61
|
+
path: string;
|
|
62
|
+
params: string[];
|
|
63
|
+
title?: CompiledExpression;
|
|
64
|
+
layout?: string;
|
|
65
|
+
meta?: Record<string, CompiledExpression>;
|
|
66
|
+
}
|
|
67
|
+
interface CompiledLifecycleHooks {
|
|
68
|
+
onMount?: string;
|
|
69
|
+
onUnmount?: string;
|
|
70
|
+
onRouteEnter?: string;
|
|
71
|
+
onRouteLeave?: string;
|
|
72
|
+
}
|
|
56
73
|
interface CompiledProgram {
|
|
57
74
|
version: '1.0';
|
|
75
|
+
route?: CompiledRouteDefinition;
|
|
76
|
+
lifecycle?: CompiledLifecycleHooks;
|
|
58
77
|
state: Record<string, {
|
|
59
78
|
type: string;
|
|
60
79
|
initial: unknown;
|
|
61
80
|
}>;
|
|
62
81
|
actions: Record<string, CompiledAction>;
|
|
63
82
|
view: CompiledNode;
|
|
83
|
+
importData?: Record<string, unknown>;
|
|
64
84
|
}
|
|
65
85
|
interface CompiledAction {
|
|
66
86
|
name: string;
|
|
67
87
|
steps: CompiledActionStep[];
|
|
68
88
|
}
|
|
69
|
-
type CompiledActionStep = CompiledSetStep | CompiledUpdateStep | CompiledFetchStep;
|
|
89
|
+
type CompiledActionStep = CompiledSetStep | CompiledUpdateStep | CompiledFetchStep | CompiledStorageStep | CompiledClipboardStep | CompiledNavigateStep;
|
|
70
90
|
interface CompiledSetStep {
|
|
71
91
|
do: 'set';
|
|
72
92
|
target: string;
|
|
@@ -89,6 +109,30 @@ interface CompiledFetchStep {
|
|
|
89
109
|
onSuccess?: CompiledActionStep[];
|
|
90
110
|
onError?: CompiledActionStep[];
|
|
91
111
|
}
|
|
112
|
+
interface CompiledStorageStep {
|
|
113
|
+
do: 'storage';
|
|
114
|
+
operation: 'get' | 'set' | 'remove';
|
|
115
|
+
key: CompiledExpression;
|
|
116
|
+
value?: CompiledExpression;
|
|
117
|
+
storage: 'local' | 'session';
|
|
118
|
+
result?: string;
|
|
119
|
+
onSuccess?: CompiledActionStep[];
|
|
120
|
+
onError?: CompiledActionStep[];
|
|
121
|
+
}
|
|
122
|
+
interface CompiledClipboardStep {
|
|
123
|
+
do: 'clipboard';
|
|
124
|
+
operation: 'write' | 'read';
|
|
125
|
+
value?: CompiledExpression;
|
|
126
|
+
result?: string;
|
|
127
|
+
onSuccess?: CompiledActionStep[];
|
|
128
|
+
onError?: CompiledActionStep[];
|
|
129
|
+
}
|
|
130
|
+
interface CompiledNavigateStep {
|
|
131
|
+
do: 'navigate';
|
|
132
|
+
url: CompiledExpression;
|
|
133
|
+
target?: '_self' | '_blank';
|
|
134
|
+
replace?: boolean;
|
|
135
|
+
}
|
|
92
136
|
type CompiledNode = CompiledElementNode | CompiledTextNode | CompiledIfNode | CompiledEachNode | CompiledMarkdownNode | CompiledCodeNode;
|
|
93
137
|
interface CompiledElementNode {
|
|
94
138
|
kind: 'element';
|
|
@@ -123,7 +167,7 @@ interface CompiledCodeNode {
|
|
|
123
167
|
language: CompiledExpression;
|
|
124
168
|
content: CompiledExpression;
|
|
125
169
|
}
|
|
126
|
-
type CompiledExpression = CompiledLitExpr | CompiledStateExpr | CompiledVarExpr | CompiledBinExpr | CompiledNotExpr | CompiledCondExpr | CompiledGetExpr;
|
|
170
|
+
type CompiledExpression = CompiledLitExpr | CompiledStateExpr | CompiledVarExpr | CompiledBinExpr | CompiledNotExpr | CompiledCondExpr | CompiledGetExpr | CompiledRouteExpr | CompiledImportExpr;
|
|
127
171
|
interface CompiledLitExpr {
|
|
128
172
|
expr: 'lit';
|
|
129
173
|
value: string | number | boolean | null | unknown[];
|
|
@@ -158,6 +202,16 @@ interface CompiledGetExpr {
|
|
|
158
202
|
base: CompiledExpression;
|
|
159
203
|
path: string;
|
|
160
204
|
}
|
|
205
|
+
interface CompiledRouteExpr {
|
|
206
|
+
expr: 'route';
|
|
207
|
+
name: string;
|
|
208
|
+
source: 'param' | 'query' | 'path';
|
|
209
|
+
}
|
|
210
|
+
interface CompiledImportExpr {
|
|
211
|
+
expr: 'import';
|
|
212
|
+
name: string;
|
|
213
|
+
path?: string;
|
|
214
|
+
}
|
|
161
215
|
interface CompiledEventHandler {
|
|
162
216
|
event: string;
|
|
163
217
|
action: string;
|
|
@@ -168,9 +222,10 @@ interface CompiledEventHandler {
|
|
|
168
222
|
*
|
|
169
223
|
* @param ast - Validated AST from validate pass
|
|
170
224
|
* @param _context - Analysis context from analyze pass (unused in current implementation)
|
|
225
|
+
* @param importData - Optional resolved import data to include in the compiled program
|
|
171
226
|
* @returns CompiledProgram
|
|
172
227
|
*/
|
|
173
|
-
declare function transformPass(ast: Program, _context: AnalysisContext): CompiledProgram;
|
|
228
|
+
declare function transformPass(ast: Program, _context: AnalysisContext, importData?: Record<string, unknown>): CompiledProgram;
|
|
174
229
|
|
|
175
230
|
/**
|
|
176
231
|
* Main compile function for @constela/compiler
|
|
@@ -222,4 +277,71 @@ type ValidatePassResult = ValidatePassSuccess | ValidatePassFailure;
|
|
|
222
277
|
*/
|
|
223
278
|
declare function validatePass(input: unknown): ValidatePassResult;
|
|
224
279
|
|
|
225
|
-
|
|
280
|
+
/**
|
|
281
|
+
* Layout Analysis Pass - Semantic validation for layout programs
|
|
282
|
+
*
|
|
283
|
+
* This pass performs semantic analysis on layout programs:
|
|
284
|
+
* - Validates that at least one slot node exists
|
|
285
|
+
* - Detects duplicate named slots
|
|
286
|
+
* - Validates state and action references within layouts
|
|
287
|
+
* - Warns/errors for slots inside loops
|
|
288
|
+
*/
|
|
289
|
+
|
|
290
|
+
interface LayoutAnalysisContext {
|
|
291
|
+
stateNames: Set<string>;
|
|
292
|
+
actionNames: Set<string>;
|
|
293
|
+
componentNames: Set<string>;
|
|
294
|
+
routeParams: Set<string>;
|
|
295
|
+
importNames: Set<string>;
|
|
296
|
+
slotNames: Set<string>;
|
|
297
|
+
hasDefaultSlot: boolean;
|
|
298
|
+
}
|
|
299
|
+
interface LayoutAnalysisSuccess {
|
|
300
|
+
ok: true;
|
|
301
|
+
context: LayoutAnalysisContext;
|
|
302
|
+
}
|
|
303
|
+
interface LayoutAnalysisFailure {
|
|
304
|
+
ok: false;
|
|
305
|
+
errors: ConstelaError[];
|
|
306
|
+
}
|
|
307
|
+
type LayoutAnalysisResult = LayoutAnalysisSuccess | LayoutAnalysisFailure;
|
|
308
|
+
/**
|
|
309
|
+
* Performs static analysis on a layout program
|
|
310
|
+
*
|
|
311
|
+
* - Validates at least one slot exists
|
|
312
|
+
* - Detects duplicate slot names
|
|
313
|
+
* - Validates state references
|
|
314
|
+
* - Validates action references
|
|
315
|
+
*
|
|
316
|
+
* @param layout - Layout program to analyze
|
|
317
|
+
* @returns LayoutAnalysisResult
|
|
318
|
+
*/
|
|
319
|
+
declare function analyzeLayoutPass(layout: LayoutProgram): LayoutAnalysisResult;
|
|
320
|
+
|
|
321
|
+
/**
|
|
322
|
+
* Layout Transform Pass - Layout to CompiledProgram transformation and composition
|
|
323
|
+
*
|
|
324
|
+
* This pass transforms layout programs and composes them with page programs.
|
|
325
|
+
*/
|
|
326
|
+
|
|
327
|
+
interface CompiledLayoutProgram {
|
|
328
|
+
version: '1.0';
|
|
329
|
+
type: 'layout';
|
|
330
|
+
state: Record<string, {
|
|
331
|
+
type: string;
|
|
332
|
+
initial: unknown;
|
|
333
|
+
}>;
|
|
334
|
+
actions: CompiledAction[];
|
|
335
|
+
view: CompiledNode;
|
|
336
|
+
components?: Record<string, ComponentDef> | undefined;
|
|
337
|
+
}
|
|
338
|
+
/**
|
|
339
|
+
* Transforms a layout program into a compiled layout
|
|
340
|
+
*/
|
|
341
|
+
declare function transformLayoutPass(layout: LayoutProgram, _context: LayoutAnalysisContext): CompiledLayoutProgram;
|
|
342
|
+
/**
|
|
343
|
+
* Composes a layout with a page, inserting page content into slots
|
|
344
|
+
*/
|
|
345
|
+
declare function composeLayoutWithPage(layout: CompiledProgram, page: CompiledProgram, slots?: Record<string, ViewNode>): CompiledProgram;
|
|
346
|
+
|
|
347
|
+
export { type AnalysisContext, type AnalyzePassFailure, type AnalyzePassResult, type AnalyzePassSuccess, type CompileFailure, type CompileResult, type CompileSuccess, type CompiledAction, type CompiledActionStep, type CompiledClipboardStep, type CompiledCodeNode, type CompiledEachNode, type CompiledElementNode, type CompiledEventHandler, type CompiledExpression, type CompiledFetchStep, type CompiledIfNode, type CompiledImportExpr, type CompiledLayoutProgram, type CompiledLifecycleHooks, type CompiledMarkdownNode, type CompiledNavigateStep, type CompiledNode, type CompiledProgram, type CompiledRouteDefinition, type CompiledRouteExpr, type CompiledSetStep, type CompiledStorageStep, type CompiledTextNode, type CompiledUpdateStep, type LayoutAnalysisContext, type LayoutAnalysisFailure, type LayoutAnalysisResult, type LayoutAnalysisSuccess, type ValidatePassFailure, type ValidatePassResult, type ValidatePassSuccess, analyzeLayoutPass, analyzePass, compile, composeLayoutWithPage, transformLayoutPass, transformPass, validatePass };
|
package/dist/index.js
CHANGED
|
@@ -27,18 +27,57 @@ import {
|
|
|
27
27
|
createSchemaError,
|
|
28
28
|
createOperationInvalidForTypeError,
|
|
29
29
|
createOperationMissingFieldError,
|
|
30
|
-
|
|
30
|
+
createUndefinedRouteParamError,
|
|
31
|
+
createRouteNotDefinedError,
|
|
32
|
+
createUndefinedImportError,
|
|
33
|
+
createImportsNotDefinedError,
|
|
34
|
+
createInvalidDataSourceError,
|
|
35
|
+
createUndefinedDataSourceError,
|
|
36
|
+
createDataNotDefinedError,
|
|
37
|
+
createUndefinedDataError,
|
|
38
|
+
createInvalidStorageOperationError,
|
|
39
|
+
createInvalidStorageTypeError,
|
|
40
|
+
createStorageSetMissingValueError,
|
|
41
|
+
createInvalidClipboardOperationError,
|
|
42
|
+
createClipboardWriteMissingValueError,
|
|
43
|
+
createInvalidNavigateTargetError,
|
|
44
|
+
isEventHandler,
|
|
45
|
+
DATA_SOURCE_TYPES,
|
|
46
|
+
DATA_TRANSFORMS,
|
|
47
|
+
STORAGE_OPERATIONS,
|
|
48
|
+
STORAGE_TYPES,
|
|
49
|
+
CLIPBOARD_OPERATIONS,
|
|
50
|
+
NAVIGATE_TARGETS
|
|
31
51
|
} from "@constela/core";
|
|
32
52
|
function buildPath(base, ...segments) {
|
|
33
53
|
return segments.reduce((p, s) => `${p}/${s}`, base);
|
|
34
54
|
}
|
|
55
|
+
function extractRouteParams(path) {
|
|
56
|
+
const params = [];
|
|
57
|
+
const segments = path.split("/");
|
|
58
|
+
for (const segment of segments) {
|
|
59
|
+
if (segment.startsWith(":")) {
|
|
60
|
+
params.push(segment.slice(1));
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
return params;
|
|
64
|
+
}
|
|
35
65
|
function collectContext(ast2) {
|
|
36
66
|
const stateNames = new Set(Object.keys(ast2.state));
|
|
37
67
|
const actionNames = new Set(ast2.actions.map((a) => a.name));
|
|
38
68
|
const componentNames = new Set(
|
|
39
69
|
ast2.components ? Object.keys(ast2.components) : []
|
|
40
70
|
);
|
|
41
|
-
|
|
71
|
+
const routeParams = new Set(
|
|
72
|
+
ast2.route ? extractRouteParams(ast2.route.path) : []
|
|
73
|
+
);
|
|
74
|
+
const importNames = new Set(
|
|
75
|
+
ast2.imports ? Object.keys(ast2.imports) : []
|
|
76
|
+
);
|
|
77
|
+
const dataNames = new Set(
|
|
78
|
+
ast2.data ? Object.keys(ast2.data) : []
|
|
79
|
+
);
|
|
80
|
+
return { stateNames, actionNames, componentNames, routeParams, importNames, dataNames };
|
|
42
81
|
}
|
|
43
82
|
function checkDuplicateActions(ast2) {
|
|
44
83
|
const errors = [];
|
|
@@ -71,6 +110,33 @@ function validateExpression(expr, path, context, scope, paramScope) {
|
|
|
71
110
|
errors.push(createUndefinedParamError(expr.name, path));
|
|
72
111
|
}
|
|
73
112
|
break;
|
|
113
|
+
case "route": {
|
|
114
|
+
if (!hasRoute) {
|
|
115
|
+
errors.push(createRouteNotDefinedError(path));
|
|
116
|
+
} else {
|
|
117
|
+
const source = expr.source ?? "param";
|
|
118
|
+
if (source === "param" && !context.routeParams.has(expr.name)) {
|
|
119
|
+
errors.push(createUndefinedRouteParamError(expr.name, path));
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
break;
|
|
123
|
+
}
|
|
124
|
+
case "import": {
|
|
125
|
+
if (!hasImports) {
|
|
126
|
+
errors.push(createImportsNotDefinedError(path));
|
|
127
|
+
} else if (!context.importNames.has(expr.name)) {
|
|
128
|
+
errors.push(createUndefinedImportError(expr.name, path));
|
|
129
|
+
}
|
|
130
|
+
break;
|
|
131
|
+
}
|
|
132
|
+
case "data": {
|
|
133
|
+
if (!hasData) {
|
|
134
|
+
errors.push(createDataNotDefinedError(path));
|
|
135
|
+
} else if (!context.dataNames.has(expr.name)) {
|
|
136
|
+
errors.push(createUndefinedDataError(expr.name, path));
|
|
137
|
+
}
|
|
138
|
+
break;
|
|
139
|
+
}
|
|
74
140
|
case "bin":
|
|
75
141
|
errors.push(...validateExpression(expr.left, buildPath(path, "left"), context, scope, paramScope));
|
|
76
142
|
errors.push(...validateExpression(expr.right, buildPath(path, "right"), context, scope, paramScope));
|
|
@@ -174,6 +240,88 @@ function validateActionStep(step, path, context) {
|
|
|
174
240
|
}
|
|
175
241
|
}
|
|
176
242
|
break;
|
|
243
|
+
case "storage": {
|
|
244
|
+
const storageStep = step;
|
|
245
|
+
if (!STORAGE_OPERATIONS.includes(storageStep.operation)) {
|
|
246
|
+
errors.push(createInvalidStorageOperationError(storageStep.operation, path));
|
|
247
|
+
}
|
|
248
|
+
if (!STORAGE_TYPES.includes(storageStep.storage)) {
|
|
249
|
+
errors.push(createInvalidStorageTypeError(storageStep.storage, path));
|
|
250
|
+
}
|
|
251
|
+
errors.push(
|
|
252
|
+
...validateExpressionStateOnly(storageStep.key, buildPath(path, "key"), context)
|
|
253
|
+
);
|
|
254
|
+
if (storageStep.operation === "set" && !storageStep.value) {
|
|
255
|
+
errors.push(createStorageSetMissingValueError(path));
|
|
256
|
+
}
|
|
257
|
+
if (storageStep.value) {
|
|
258
|
+
errors.push(
|
|
259
|
+
...validateExpressionStateOnly(storageStep.value, buildPath(path, "value"), context)
|
|
260
|
+
);
|
|
261
|
+
}
|
|
262
|
+
if (storageStep.onSuccess) {
|
|
263
|
+
for (let i = 0; i < storageStep.onSuccess.length; i++) {
|
|
264
|
+
const successStep = storageStep.onSuccess[i];
|
|
265
|
+
if (successStep === void 0) continue;
|
|
266
|
+
errors.push(
|
|
267
|
+
...validateActionStep(successStep, buildPath(path, "onSuccess", i), context)
|
|
268
|
+
);
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
if (storageStep.onError) {
|
|
272
|
+
for (let i = 0; i < storageStep.onError.length; i++) {
|
|
273
|
+
const errorStep = storageStep.onError[i];
|
|
274
|
+
if (errorStep === void 0) continue;
|
|
275
|
+
errors.push(
|
|
276
|
+
...validateActionStep(errorStep, buildPath(path, "onError", i), context)
|
|
277
|
+
);
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
break;
|
|
281
|
+
}
|
|
282
|
+
case "clipboard": {
|
|
283
|
+
const clipboardStep = step;
|
|
284
|
+
if (!CLIPBOARD_OPERATIONS.includes(clipboardStep.operation)) {
|
|
285
|
+
errors.push(createInvalidClipboardOperationError(clipboardStep.operation, path));
|
|
286
|
+
}
|
|
287
|
+
if (clipboardStep.operation === "write" && !clipboardStep.value) {
|
|
288
|
+
errors.push(createClipboardWriteMissingValueError(path));
|
|
289
|
+
}
|
|
290
|
+
if (clipboardStep.value) {
|
|
291
|
+
errors.push(
|
|
292
|
+
...validateExpressionStateOnly(clipboardStep.value, buildPath(path, "value"), context)
|
|
293
|
+
);
|
|
294
|
+
}
|
|
295
|
+
if (clipboardStep.onSuccess) {
|
|
296
|
+
for (let i = 0; i < clipboardStep.onSuccess.length; i++) {
|
|
297
|
+
const successStep = clipboardStep.onSuccess[i];
|
|
298
|
+
if (successStep === void 0) continue;
|
|
299
|
+
errors.push(
|
|
300
|
+
...validateActionStep(successStep, buildPath(path, "onSuccess", i), context)
|
|
301
|
+
);
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
if (clipboardStep.onError) {
|
|
305
|
+
for (let i = 0; i < clipboardStep.onError.length; i++) {
|
|
306
|
+
const errorStep = clipboardStep.onError[i];
|
|
307
|
+
if (errorStep === void 0) continue;
|
|
308
|
+
errors.push(
|
|
309
|
+
...validateActionStep(errorStep, buildPath(path, "onError", i), context)
|
|
310
|
+
);
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
break;
|
|
314
|
+
}
|
|
315
|
+
case "navigate": {
|
|
316
|
+
const navigateStep = step;
|
|
317
|
+
errors.push(
|
|
318
|
+
...validateExpressionStateOnly(navigateStep.url, buildPath(path, "url"), context)
|
|
319
|
+
);
|
|
320
|
+
if (navigateStep.target !== void 0 && !NAVIGATE_TARGETS.includes(navigateStep.target)) {
|
|
321
|
+
errors.push(createInvalidNavigateTargetError(navigateStep.target, path));
|
|
322
|
+
}
|
|
323
|
+
break;
|
|
324
|
+
}
|
|
177
325
|
}
|
|
178
326
|
return errors;
|
|
179
327
|
}
|
|
@@ -187,6 +335,33 @@ function validateExpressionStateOnly(expr, path, context) {
|
|
|
187
335
|
break;
|
|
188
336
|
case "var":
|
|
189
337
|
break;
|
|
338
|
+
case "route": {
|
|
339
|
+
if (!hasRoute) {
|
|
340
|
+
errors.push(createRouteNotDefinedError(path));
|
|
341
|
+
} else {
|
|
342
|
+
const source = expr.source ?? "param";
|
|
343
|
+
if (source === "param" && !context.routeParams.has(expr.name)) {
|
|
344
|
+
errors.push(createUndefinedRouteParamError(expr.name, path));
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
break;
|
|
348
|
+
}
|
|
349
|
+
case "import": {
|
|
350
|
+
if (!hasImports) {
|
|
351
|
+
errors.push(createImportsNotDefinedError(path));
|
|
352
|
+
} else if (!context.importNames.has(expr.name)) {
|
|
353
|
+
errors.push(createUndefinedImportError(expr.name, path));
|
|
354
|
+
}
|
|
355
|
+
break;
|
|
356
|
+
}
|
|
357
|
+
case "data": {
|
|
358
|
+
if (!hasData) {
|
|
359
|
+
errors.push(createDataNotDefinedError(path));
|
|
360
|
+
} else if (!context.dataNames.has(expr.name)) {
|
|
361
|
+
errors.push(createUndefinedDataError(expr.name, path));
|
|
362
|
+
}
|
|
363
|
+
break;
|
|
364
|
+
}
|
|
190
365
|
case "bin":
|
|
191
366
|
errors.push(...validateExpressionStateOnly(expr.left, buildPath(path, "left"), context));
|
|
192
367
|
errors.push(...validateExpressionStateOnly(expr.right, buildPath(path, "right"), context));
|
|
@@ -219,6 +394,33 @@ function validateExpressionInEventPayload(expr, path, context, scope) {
|
|
|
219
394
|
break;
|
|
220
395
|
case "var":
|
|
221
396
|
break;
|
|
397
|
+
case "route": {
|
|
398
|
+
if (!hasRoute) {
|
|
399
|
+
errors.push(createRouteNotDefinedError(path));
|
|
400
|
+
} else {
|
|
401
|
+
const source = expr.source ?? "param";
|
|
402
|
+
if (source === "param" && !context.routeParams.has(expr.name)) {
|
|
403
|
+
errors.push(createUndefinedRouteParamError(expr.name, path));
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
break;
|
|
407
|
+
}
|
|
408
|
+
case "import": {
|
|
409
|
+
if (!hasImports) {
|
|
410
|
+
errors.push(createImportsNotDefinedError(path));
|
|
411
|
+
} else if (!context.importNames.has(expr.name)) {
|
|
412
|
+
errors.push(createUndefinedImportError(expr.name, path));
|
|
413
|
+
}
|
|
414
|
+
break;
|
|
415
|
+
}
|
|
416
|
+
case "data": {
|
|
417
|
+
if (!hasData) {
|
|
418
|
+
errors.push(createDataNotDefinedError(path));
|
|
419
|
+
} else if (!context.dataNames.has(expr.name)) {
|
|
420
|
+
errors.push(createUndefinedDataError(expr.name, path));
|
|
421
|
+
}
|
|
422
|
+
break;
|
|
423
|
+
}
|
|
222
424
|
case "bin":
|
|
223
425
|
errors.push(
|
|
224
426
|
...validateExpressionInEventPayload(expr.left, buildPath(path, "left"), context, scope)
|
|
@@ -260,7 +462,7 @@ function validateExpressionInEventPayload(expr, path, context, scope) {
|
|
|
260
462
|
}
|
|
261
463
|
function validateViewNode(node, path, context, scope, options = { insideComponent: false }) {
|
|
262
464
|
const errors = [];
|
|
263
|
-
const { insideComponent, paramScope } = options;
|
|
465
|
+
const { insideComponent, insideLayout, paramScope } = options;
|
|
264
466
|
switch (node.kind) {
|
|
265
467
|
case "element":
|
|
266
468
|
if (node.props) {
|
|
@@ -343,9 +545,9 @@ function validateViewNode(node, path, context, scope, options = { insideComponen
|
|
|
343
545
|
break;
|
|
344
546
|
}
|
|
345
547
|
case "slot":
|
|
346
|
-
if (!insideComponent) {
|
|
548
|
+
if (!insideComponent && !insideLayout) {
|
|
347
549
|
errors.push(
|
|
348
|
-
createSchemaError(`Slot can only be used inside component definitions`, path)
|
|
550
|
+
createSchemaError(`Slot can only be used inside component definitions or layouts`, path)
|
|
349
551
|
);
|
|
350
552
|
}
|
|
351
553
|
break;
|
|
@@ -372,6 +574,26 @@ function validateComponentProps(node, componentDef, path, context, scope, paramS
|
|
|
372
574
|
return errors;
|
|
373
575
|
}
|
|
374
576
|
var ast;
|
|
577
|
+
var hasRoute;
|
|
578
|
+
var hasImports;
|
|
579
|
+
var hasData;
|
|
580
|
+
function validateRouteDefinition(route, context) {
|
|
581
|
+
const errors = [];
|
|
582
|
+
const emptyScope = /* @__PURE__ */ new Set();
|
|
583
|
+
if (route.title) {
|
|
584
|
+
errors.push(
|
|
585
|
+
...validateExpression(route.title, "/route/title", context, emptyScope)
|
|
586
|
+
);
|
|
587
|
+
}
|
|
588
|
+
if (route.meta) {
|
|
589
|
+
for (const [key, value] of Object.entries(route.meta)) {
|
|
590
|
+
errors.push(
|
|
591
|
+
...validateExpression(value, `/route/meta/${key}`, context, emptyScope)
|
|
592
|
+
);
|
|
593
|
+
}
|
|
594
|
+
}
|
|
595
|
+
return errors;
|
|
596
|
+
}
|
|
375
597
|
function collectComponentCalls(node) {
|
|
376
598
|
const calls = /* @__PURE__ */ new Set();
|
|
377
599
|
switch (node.kind) {
|
|
@@ -472,6 +694,18 @@ function validateComponents(programAst, context) {
|
|
|
472
694
|
}
|
|
473
695
|
return errors;
|
|
474
696
|
}
|
|
697
|
+
function validateLifecycleHooks(lifecycle, context) {
|
|
698
|
+
const errors = [];
|
|
699
|
+
if (!lifecycle) return errors;
|
|
700
|
+
const hooks = ["onMount", "onUnmount", "onRouteEnter", "onRouteLeave"];
|
|
701
|
+
for (const hook of hooks) {
|
|
702
|
+
const actionName = lifecycle[hook];
|
|
703
|
+
if (actionName && !context.actionNames.has(actionName)) {
|
|
704
|
+
errors.push(createUndefinedActionError(actionName, `/lifecycle/${hook}`));
|
|
705
|
+
}
|
|
706
|
+
}
|
|
707
|
+
return errors;
|
|
708
|
+
}
|
|
475
709
|
function validateActions(programAst, context) {
|
|
476
710
|
const errors = [];
|
|
477
711
|
for (let i = 0; i < programAst.actions.length; i++) {
|
|
@@ -487,17 +721,81 @@ function validateActions(programAst, context) {
|
|
|
487
721
|
}
|
|
488
722
|
return errors;
|
|
489
723
|
}
|
|
724
|
+
function validateDataSources(programAst, context) {
|
|
725
|
+
const errors = [];
|
|
726
|
+
if (!programAst.data) return errors;
|
|
727
|
+
for (const [name, source] of Object.entries(programAst.data)) {
|
|
728
|
+
const path = `/data/${name}`;
|
|
729
|
+
if (!DATA_SOURCE_TYPES.includes(source.type)) {
|
|
730
|
+
errors.push(createInvalidDataSourceError(name, `invalid type '${source.type}'`, path));
|
|
731
|
+
continue;
|
|
732
|
+
}
|
|
733
|
+
if (source.transform !== void 0) {
|
|
734
|
+
if (!DATA_TRANSFORMS.includes(source.transform)) {
|
|
735
|
+
errors.push(createInvalidDataSourceError(name, `invalid transform '${source.transform}'`, path));
|
|
736
|
+
}
|
|
737
|
+
}
|
|
738
|
+
switch (source.type) {
|
|
739
|
+
case "glob":
|
|
740
|
+
if (typeof source.pattern !== "string") {
|
|
741
|
+
errors.push(createInvalidDataSourceError(name, `glob type requires 'pattern' field`, path));
|
|
742
|
+
}
|
|
743
|
+
break;
|
|
744
|
+
case "file":
|
|
745
|
+
if (typeof source.path !== "string") {
|
|
746
|
+
errors.push(createInvalidDataSourceError(name, `file type requires 'path' field`, path));
|
|
747
|
+
}
|
|
748
|
+
break;
|
|
749
|
+
case "api":
|
|
750
|
+
if (typeof source.url !== "string") {
|
|
751
|
+
errors.push(createInvalidDataSourceError(name, `api type requires 'url' field`, path));
|
|
752
|
+
}
|
|
753
|
+
break;
|
|
754
|
+
}
|
|
755
|
+
}
|
|
756
|
+
return errors;
|
|
757
|
+
}
|
|
758
|
+
function validateGetStaticPaths(programAst, context) {
|
|
759
|
+
const errors = [];
|
|
760
|
+
const getStaticPaths = programAst.route?.getStaticPaths;
|
|
761
|
+
if (!getStaticPaths) return errors;
|
|
762
|
+
const path = "/route/getStaticPaths";
|
|
763
|
+
if (!programAst.data) {
|
|
764
|
+
errors.push(createDataNotDefinedError(path));
|
|
765
|
+
return errors;
|
|
766
|
+
}
|
|
767
|
+
if (!context.dataNames.has(getStaticPaths.source)) {
|
|
768
|
+
errors.push(createUndefinedDataSourceError(getStaticPaths.source, path));
|
|
769
|
+
}
|
|
770
|
+
for (const [paramName, paramExpr] of Object.entries(getStaticPaths.params)) {
|
|
771
|
+
errors.push(
|
|
772
|
+
...validateExpressionStateOnly(paramExpr, `${path}/params/${paramName}`, context)
|
|
773
|
+
);
|
|
774
|
+
}
|
|
775
|
+
return errors;
|
|
776
|
+
}
|
|
490
777
|
function analyzePass(programAst) {
|
|
491
778
|
ast = programAst;
|
|
779
|
+
hasRoute = !!programAst.route;
|
|
780
|
+
hasImports = !!programAst.imports;
|
|
781
|
+
hasData = !!programAst.data;
|
|
782
|
+
const isLayout = programAst.type === "layout";
|
|
492
783
|
const context = collectContext(programAst);
|
|
493
784
|
const errors = [];
|
|
494
785
|
errors.push(...checkDuplicateActions(programAst));
|
|
786
|
+
errors.push(...validateDataSources(programAst, context));
|
|
787
|
+
errors.push(...validateGetStaticPaths(programAst, context));
|
|
495
788
|
errors.push(...validateActions(programAst, context));
|
|
789
|
+
errors.push(...validateLifecycleHooks(programAst.lifecycle, context));
|
|
496
790
|
errors.push(...detectComponentCycles(programAst, context));
|
|
497
791
|
errors.push(...validateComponents(programAst, context));
|
|
792
|
+
if (programAst.route) {
|
|
793
|
+
errors.push(...validateRouteDefinition(programAst.route, context));
|
|
794
|
+
}
|
|
498
795
|
errors.push(
|
|
499
796
|
...validateViewNode(programAst.view, "/view", context, /* @__PURE__ */ new Set(), {
|
|
500
|
-
insideComponent: false
|
|
797
|
+
insideComponent: false,
|
|
798
|
+
insideLayout: isLayout
|
|
501
799
|
})
|
|
502
800
|
);
|
|
503
801
|
if (errors.length > 0) {
|
|
@@ -588,6 +886,32 @@ function transformExpression(expr, ctx) {
|
|
|
588
886
|
base: transformExpression(expr.base, ctx),
|
|
589
887
|
path: expr.path
|
|
590
888
|
};
|
|
889
|
+
case "route":
|
|
890
|
+
return {
|
|
891
|
+
expr: "route",
|
|
892
|
+
name: expr.name,
|
|
893
|
+
source: expr.source ?? "param"
|
|
894
|
+
};
|
|
895
|
+
case "import": {
|
|
896
|
+
const importExpr = {
|
|
897
|
+
expr: "import",
|
|
898
|
+
name: expr.name
|
|
899
|
+
};
|
|
900
|
+
if (expr.path) {
|
|
901
|
+
importExpr.path = expr.path;
|
|
902
|
+
}
|
|
903
|
+
return importExpr;
|
|
904
|
+
}
|
|
905
|
+
case "data": {
|
|
906
|
+
const dataExpr = {
|
|
907
|
+
expr: "import",
|
|
908
|
+
name: expr.name
|
|
909
|
+
};
|
|
910
|
+
if (expr.path) {
|
|
911
|
+
dataExpr.path = expr.path;
|
|
912
|
+
}
|
|
913
|
+
return dataExpr;
|
|
914
|
+
}
|
|
591
915
|
}
|
|
592
916
|
}
|
|
593
917
|
function transformEventHandler(handler, ctx) {
|
|
@@ -648,6 +972,62 @@ function transformActionStep(step) {
|
|
|
648
972
|
}
|
|
649
973
|
return fetchStep;
|
|
650
974
|
}
|
|
975
|
+
case "storage": {
|
|
976
|
+
const storageStep = step;
|
|
977
|
+
const compiledStorageStep = {
|
|
978
|
+
do: "storage",
|
|
979
|
+
operation: storageStep.operation,
|
|
980
|
+
key: transformExpression(storageStep.key, emptyContext),
|
|
981
|
+
storage: storageStep.storage
|
|
982
|
+
};
|
|
983
|
+
if (storageStep.value) {
|
|
984
|
+
compiledStorageStep.value = transformExpression(storageStep.value, emptyContext);
|
|
985
|
+
}
|
|
986
|
+
if (storageStep.result) {
|
|
987
|
+
compiledStorageStep.result = storageStep.result;
|
|
988
|
+
}
|
|
989
|
+
if (storageStep.onSuccess) {
|
|
990
|
+
compiledStorageStep.onSuccess = storageStep.onSuccess.map(transformActionStep);
|
|
991
|
+
}
|
|
992
|
+
if (storageStep.onError) {
|
|
993
|
+
compiledStorageStep.onError = storageStep.onError.map(transformActionStep);
|
|
994
|
+
}
|
|
995
|
+
return compiledStorageStep;
|
|
996
|
+
}
|
|
997
|
+
case "clipboard": {
|
|
998
|
+
const clipboardStep = step;
|
|
999
|
+
const compiledClipboardStep = {
|
|
1000
|
+
do: "clipboard",
|
|
1001
|
+
operation: clipboardStep.operation
|
|
1002
|
+
};
|
|
1003
|
+
if (clipboardStep.value) {
|
|
1004
|
+
compiledClipboardStep.value = transformExpression(clipboardStep.value, emptyContext);
|
|
1005
|
+
}
|
|
1006
|
+
if (clipboardStep.result) {
|
|
1007
|
+
compiledClipboardStep.result = clipboardStep.result;
|
|
1008
|
+
}
|
|
1009
|
+
if (clipboardStep.onSuccess) {
|
|
1010
|
+
compiledClipboardStep.onSuccess = clipboardStep.onSuccess.map(transformActionStep);
|
|
1011
|
+
}
|
|
1012
|
+
if (clipboardStep.onError) {
|
|
1013
|
+
compiledClipboardStep.onError = clipboardStep.onError.map(transformActionStep);
|
|
1014
|
+
}
|
|
1015
|
+
return compiledClipboardStep;
|
|
1016
|
+
}
|
|
1017
|
+
case "navigate": {
|
|
1018
|
+
const navigateStep = step;
|
|
1019
|
+
const compiledNavigateStep = {
|
|
1020
|
+
do: "navigate",
|
|
1021
|
+
url: transformExpression(navigateStep.url, emptyContext)
|
|
1022
|
+
};
|
|
1023
|
+
if (navigateStep.target) {
|
|
1024
|
+
compiledNavigateStep.target = navigateStep.target;
|
|
1025
|
+
}
|
|
1026
|
+
if (navigateStep.replace !== void 0) {
|
|
1027
|
+
compiledNavigateStep.replace = navigateStep.replace;
|
|
1028
|
+
}
|
|
1029
|
+
return compiledNavigateStep;
|
|
1030
|
+
}
|
|
651
1031
|
}
|
|
652
1032
|
}
|
|
653
1033
|
function flattenSlotChildren(children, ctx) {
|
|
@@ -790,16 +1170,75 @@ function transformActions(actions) {
|
|
|
790
1170
|
}
|
|
791
1171
|
return compiledActions;
|
|
792
1172
|
}
|
|
793
|
-
function
|
|
1173
|
+
function extractRouteParams2(path) {
|
|
1174
|
+
const params = [];
|
|
1175
|
+
const segments = path.split("/");
|
|
1176
|
+
for (const segment of segments) {
|
|
1177
|
+
if (segment.startsWith(":")) {
|
|
1178
|
+
params.push(segment.slice(1));
|
|
1179
|
+
}
|
|
1180
|
+
}
|
|
1181
|
+
return params;
|
|
1182
|
+
}
|
|
1183
|
+
function transformRouteDefinition(route, ctx) {
|
|
1184
|
+
const compiled = {
|
|
1185
|
+
path: route.path,
|
|
1186
|
+
params: extractRouteParams2(route.path)
|
|
1187
|
+
};
|
|
1188
|
+
if (route.title) {
|
|
1189
|
+
compiled.title = transformExpression(route.title, ctx);
|
|
1190
|
+
}
|
|
1191
|
+
if (route.layout) {
|
|
1192
|
+
compiled.layout = route.layout;
|
|
1193
|
+
}
|
|
1194
|
+
if (route.meta) {
|
|
1195
|
+
compiled.meta = {};
|
|
1196
|
+
for (const [key, value] of Object.entries(route.meta)) {
|
|
1197
|
+
compiled.meta[key] = transformExpression(value, ctx);
|
|
1198
|
+
}
|
|
1199
|
+
}
|
|
1200
|
+
return compiled;
|
|
1201
|
+
}
|
|
1202
|
+
function transformLifecycleHooks(lifecycle) {
|
|
1203
|
+
if (!lifecycle) return void 0;
|
|
1204
|
+
const hasAnyHook = lifecycle.onMount || lifecycle.onUnmount || lifecycle.onRouteEnter || lifecycle.onRouteLeave;
|
|
1205
|
+
if (!hasAnyHook) return void 0;
|
|
1206
|
+
const result = {};
|
|
1207
|
+
if (lifecycle.onMount) {
|
|
1208
|
+
result.onMount = lifecycle.onMount;
|
|
1209
|
+
}
|
|
1210
|
+
if (lifecycle.onUnmount) {
|
|
1211
|
+
result.onUnmount = lifecycle.onUnmount;
|
|
1212
|
+
}
|
|
1213
|
+
if (lifecycle.onRouteEnter) {
|
|
1214
|
+
result.onRouteEnter = lifecycle.onRouteEnter;
|
|
1215
|
+
}
|
|
1216
|
+
if (lifecycle.onRouteLeave) {
|
|
1217
|
+
result.onRouteLeave = lifecycle.onRouteLeave;
|
|
1218
|
+
}
|
|
1219
|
+
return result;
|
|
1220
|
+
}
|
|
1221
|
+
function transformPass(ast2, _context, importData) {
|
|
794
1222
|
const ctx = {
|
|
795
1223
|
components: ast2.components || {}
|
|
796
1224
|
};
|
|
797
|
-
|
|
1225
|
+
const result = {
|
|
798
1226
|
version: "1.0",
|
|
799
1227
|
state: transformState(ast2.state),
|
|
800
1228
|
actions: transformActions(ast2.actions),
|
|
801
1229
|
view: transformViewNode(ast2.view, ctx)
|
|
802
1230
|
};
|
|
1231
|
+
if (ast2.route) {
|
|
1232
|
+
result.route = transformRouteDefinition(ast2.route, ctx);
|
|
1233
|
+
}
|
|
1234
|
+
const lifecycle = transformLifecycleHooks(ast2.lifecycle);
|
|
1235
|
+
if (lifecycle) {
|
|
1236
|
+
result.lifecycle = lifecycle;
|
|
1237
|
+
}
|
|
1238
|
+
if (importData && Object.keys(importData).length > 0) {
|
|
1239
|
+
result.importData = importData;
|
|
1240
|
+
}
|
|
1241
|
+
return result;
|
|
803
1242
|
}
|
|
804
1243
|
|
|
805
1244
|
// src/compile.ts
|
|
@@ -827,10 +1266,452 @@ function compile(input) {
|
|
|
827
1266
|
|
|
828
1267
|
// src/index.ts
|
|
829
1268
|
import { createUndefinedVarError as createUndefinedVarError2 } from "@constela/core";
|
|
1269
|
+
|
|
1270
|
+
// src/passes/analyze-layout.ts
|
|
1271
|
+
import {
|
|
1272
|
+
createLayoutMissingSlotError,
|
|
1273
|
+
createDuplicateSlotNameError,
|
|
1274
|
+
createDuplicateDefaultSlotError,
|
|
1275
|
+
createSlotInLoopError,
|
|
1276
|
+
createUndefinedStateError as createUndefinedStateError2,
|
|
1277
|
+
createUndefinedActionError as createUndefinedActionError2,
|
|
1278
|
+
isEventHandler as isEventHandler3
|
|
1279
|
+
} from "@constela/core";
|
|
1280
|
+
function buildPath2(base, ...segments) {
|
|
1281
|
+
return segments.reduce((p, s) => `${p}/${s}`, base);
|
|
1282
|
+
}
|
|
1283
|
+
function collectContext2(layout) {
|
|
1284
|
+
const stateNames = new Set(layout.state ? Object.keys(layout.state) : []);
|
|
1285
|
+
const actionNames = new Set(layout.actions ? layout.actions.map((a) => a.name) : []);
|
|
1286
|
+
const componentNames = new Set(
|
|
1287
|
+
layout.components ? Object.keys(layout.components) : []
|
|
1288
|
+
);
|
|
1289
|
+
return { stateNames, actionNames, componentNames, routeParams: /* @__PURE__ */ new Set(), importNames: /* @__PURE__ */ new Set() };
|
|
1290
|
+
}
|
|
1291
|
+
function findSlotNodes(node, path, slots, inLoop = false) {
|
|
1292
|
+
if (node.kind === "slot") {
|
|
1293
|
+
slots.push({
|
|
1294
|
+
name: node.name,
|
|
1295
|
+
path,
|
|
1296
|
+
inLoop
|
|
1297
|
+
});
|
|
1298
|
+
return;
|
|
1299
|
+
}
|
|
1300
|
+
if (node.kind === "element" && node.children) {
|
|
1301
|
+
for (let i = 0; i < node.children.length; i++) {
|
|
1302
|
+
const child = node.children[i];
|
|
1303
|
+
if (child) {
|
|
1304
|
+
findSlotNodes(child, buildPath2(path, "children", i), slots, inLoop);
|
|
1305
|
+
}
|
|
1306
|
+
}
|
|
1307
|
+
}
|
|
1308
|
+
if (node.kind === "if") {
|
|
1309
|
+
findSlotNodes(node.then, buildPath2(path, "then"), slots, inLoop);
|
|
1310
|
+
if (node.else) {
|
|
1311
|
+
findSlotNodes(node.else, buildPath2(path, "else"), slots, inLoop);
|
|
1312
|
+
}
|
|
1313
|
+
}
|
|
1314
|
+
if (node.kind === "each") {
|
|
1315
|
+
findSlotNodes(node.body, buildPath2(path, "body"), slots, true);
|
|
1316
|
+
}
|
|
1317
|
+
if (node.kind === "component" && node.children) {
|
|
1318
|
+
for (let i = 0; i < node.children.length; i++) {
|
|
1319
|
+
const child = node.children[i];
|
|
1320
|
+
if (child) {
|
|
1321
|
+
findSlotNodes(child, buildPath2(path, "children", i), slots, inLoop);
|
|
1322
|
+
}
|
|
1323
|
+
}
|
|
1324
|
+
}
|
|
1325
|
+
}
|
|
1326
|
+
function validateSlots(layout) {
|
|
1327
|
+
const errors = [];
|
|
1328
|
+
const slots = [];
|
|
1329
|
+
const slotNames = /* @__PURE__ */ new Set();
|
|
1330
|
+
let hasDefaultSlot = false;
|
|
1331
|
+
findSlotNodes(layout.view, "/view", slots);
|
|
1332
|
+
if (slots.length === 0) {
|
|
1333
|
+
errors.push(createLayoutMissingSlotError("/view"));
|
|
1334
|
+
return { errors, slotNames, hasDefaultSlot };
|
|
1335
|
+
}
|
|
1336
|
+
const seenNames = /* @__PURE__ */ new Map();
|
|
1337
|
+
let defaultSlotPath;
|
|
1338
|
+
for (const slot of slots) {
|
|
1339
|
+
if (slot.inLoop) {
|
|
1340
|
+
errors.push(createSlotInLoopError(slot.path));
|
|
1341
|
+
continue;
|
|
1342
|
+
}
|
|
1343
|
+
if (slot.name === void 0 || slot.name === "") {
|
|
1344
|
+
if (defaultSlotPath !== void 0) {
|
|
1345
|
+
errors.push(createDuplicateDefaultSlotError(slot.path));
|
|
1346
|
+
} else {
|
|
1347
|
+
defaultSlotPath = slot.path;
|
|
1348
|
+
hasDefaultSlot = true;
|
|
1349
|
+
}
|
|
1350
|
+
} else {
|
|
1351
|
+
const existingPath = seenNames.get(slot.name);
|
|
1352
|
+
if (existingPath !== void 0) {
|
|
1353
|
+
errors.push(createDuplicateSlotNameError(slot.name, slot.path));
|
|
1354
|
+
} else {
|
|
1355
|
+
seenNames.set(slot.name, slot.path);
|
|
1356
|
+
slotNames.add(slot.name);
|
|
1357
|
+
}
|
|
1358
|
+
}
|
|
1359
|
+
}
|
|
1360
|
+
return { errors, slotNames, hasDefaultSlot };
|
|
1361
|
+
}
|
|
1362
|
+
function validateExpression2(expr, path, stateNames) {
|
|
1363
|
+
const errors = [];
|
|
1364
|
+
switch (expr.expr) {
|
|
1365
|
+
case "state":
|
|
1366
|
+
if (!stateNames.has(expr.name)) {
|
|
1367
|
+
errors.push(createUndefinedStateError2(expr.name, path));
|
|
1368
|
+
}
|
|
1369
|
+
break;
|
|
1370
|
+
case "bin":
|
|
1371
|
+
errors.push(...validateExpression2(expr.left, buildPath2(path, "left"), stateNames));
|
|
1372
|
+
errors.push(...validateExpression2(expr.right, buildPath2(path, "right"), stateNames));
|
|
1373
|
+
break;
|
|
1374
|
+
case "not":
|
|
1375
|
+
errors.push(...validateExpression2(expr.operand, buildPath2(path, "operand"), stateNames));
|
|
1376
|
+
break;
|
|
1377
|
+
case "cond":
|
|
1378
|
+
errors.push(...validateExpression2(expr.if, buildPath2(path, "if"), stateNames));
|
|
1379
|
+
errors.push(...validateExpression2(expr.then, buildPath2(path, "then"), stateNames));
|
|
1380
|
+
errors.push(...validateExpression2(expr.else, buildPath2(path, "else"), stateNames));
|
|
1381
|
+
break;
|
|
1382
|
+
case "get":
|
|
1383
|
+
errors.push(...validateExpression2(expr.base, buildPath2(path, "base"), stateNames));
|
|
1384
|
+
break;
|
|
1385
|
+
case "lit":
|
|
1386
|
+
case "var":
|
|
1387
|
+
case "param":
|
|
1388
|
+
case "route":
|
|
1389
|
+
case "import":
|
|
1390
|
+
break;
|
|
1391
|
+
}
|
|
1392
|
+
return errors;
|
|
1393
|
+
}
|
|
1394
|
+
function validateViewNode2(node, path, stateNames, actionNames) {
|
|
1395
|
+
const errors = [];
|
|
1396
|
+
switch (node.kind) {
|
|
1397
|
+
case "element":
|
|
1398
|
+
if (node.props) {
|
|
1399
|
+
for (const [propName, propValue] of Object.entries(node.props)) {
|
|
1400
|
+
const propPath = buildPath2(path, "props", propName);
|
|
1401
|
+
if (isEventHandler3(propValue)) {
|
|
1402
|
+
if (!actionNames.has(propValue.action)) {
|
|
1403
|
+
errors.push(createUndefinedActionError2(propValue.action, propPath));
|
|
1404
|
+
}
|
|
1405
|
+
if (propValue.payload) {
|
|
1406
|
+
errors.push(
|
|
1407
|
+
...validateExpression2(propValue.payload, buildPath2(propPath, "payload"), stateNames)
|
|
1408
|
+
);
|
|
1409
|
+
}
|
|
1410
|
+
} else {
|
|
1411
|
+
errors.push(...validateExpression2(propValue, propPath, stateNames));
|
|
1412
|
+
}
|
|
1413
|
+
}
|
|
1414
|
+
}
|
|
1415
|
+
if (node.children) {
|
|
1416
|
+
for (let i = 0; i < node.children.length; i++) {
|
|
1417
|
+
const child = node.children[i];
|
|
1418
|
+
if (child) {
|
|
1419
|
+
errors.push(
|
|
1420
|
+
...validateViewNode2(child, buildPath2(path, "children", i), stateNames, actionNames)
|
|
1421
|
+
);
|
|
1422
|
+
}
|
|
1423
|
+
}
|
|
1424
|
+
}
|
|
1425
|
+
break;
|
|
1426
|
+
case "text":
|
|
1427
|
+
errors.push(...validateExpression2(node.value, buildPath2(path, "value"), stateNames));
|
|
1428
|
+
break;
|
|
1429
|
+
case "if":
|
|
1430
|
+
errors.push(
|
|
1431
|
+
...validateExpression2(node.condition, buildPath2(path, "condition"), stateNames)
|
|
1432
|
+
);
|
|
1433
|
+
errors.push(...validateViewNode2(node.then, buildPath2(path, "then"), stateNames, actionNames));
|
|
1434
|
+
if (node.else) {
|
|
1435
|
+
errors.push(...validateViewNode2(node.else, buildPath2(path, "else"), stateNames, actionNames));
|
|
1436
|
+
}
|
|
1437
|
+
break;
|
|
1438
|
+
case "each":
|
|
1439
|
+
errors.push(...validateExpression2(node.items, buildPath2(path, "items"), stateNames));
|
|
1440
|
+
errors.push(...validateViewNode2(node.body, buildPath2(path, "body"), stateNames, actionNames));
|
|
1441
|
+
break;
|
|
1442
|
+
case "slot":
|
|
1443
|
+
break;
|
|
1444
|
+
}
|
|
1445
|
+
return errors;
|
|
1446
|
+
}
|
|
1447
|
+
function analyzeLayoutPass(layout) {
|
|
1448
|
+
const baseContext = collectContext2(layout);
|
|
1449
|
+
const errors = [];
|
|
1450
|
+
const { errors: slotErrors, slotNames, hasDefaultSlot } = validateSlots(layout);
|
|
1451
|
+
errors.push(...slotErrors);
|
|
1452
|
+
if (slotErrors.length > 0) {
|
|
1453
|
+
return {
|
|
1454
|
+
ok: false,
|
|
1455
|
+
errors
|
|
1456
|
+
};
|
|
1457
|
+
}
|
|
1458
|
+
errors.push(
|
|
1459
|
+
...validateViewNode2(
|
|
1460
|
+
layout.view,
|
|
1461
|
+
"/view",
|
|
1462
|
+
baseContext.stateNames,
|
|
1463
|
+
baseContext.actionNames
|
|
1464
|
+
)
|
|
1465
|
+
);
|
|
1466
|
+
if (errors.length > 0) {
|
|
1467
|
+
return {
|
|
1468
|
+
ok: false,
|
|
1469
|
+
errors
|
|
1470
|
+
};
|
|
1471
|
+
}
|
|
1472
|
+
return {
|
|
1473
|
+
ok: true,
|
|
1474
|
+
context: {
|
|
1475
|
+
...baseContext,
|
|
1476
|
+
slotNames,
|
|
1477
|
+
hasDefaultSlot
|
|
1478
|
+
}
|
|
1479
|
+
};
|
|
1480
|
+
}
|
|
1481
|
+
|
|
1482
|
+
// src/passes/transform-layout.ts
|
|
1483
|
+
function transformState2(state) {
|
|
1484
|
+
if (!state) return {};
|
|
1485
|
+
const result = {};
|
|
1486
|
+
for (const [name, field] of Object.entries(state)) {
|
|
1487
|
+
result[name] = {
|
|
1488
|
+
type: field.type,
|
|
1489
|
+
initial: field.initial
|
|
1490
|
+
};
|
|
1491
|
+
}
|
|
1492
|
+
return result;
|
|
1493
|
+
}
|
|
1494
|
+
function transformActions2(actions) {
|
|
1495
|
+
if (!actions) return [];
|
|
1496
|
+
return actions.map((action) => ({
|
|
1497
|
+
name: action.name,
|
|
1498
|
+
steps: action.steps.map((step) => {
|
|
1499
|
+
if (step.do === "set") {
|
|
1500
|
+
return {
|
|
1501
|
+
do: "set",
|
|
1502
|
+
target: step.target,
|
|
1503
|
+
value: { expr: "lit", value: null }
|
|
1504
|
+
// Simplified for now
|
|
1505
|
+
};
|
|
1506
|
+
}
|
|
1507
|
+
if (step.do === "update") {
|
|
1508
|
+
return {
|
|
1509
|
+
do: "update",
|
|
1510
|
+
target: step.target,
|
|
1511
|
+
operation: step.operation
|
|
1512
|
+
};
|
|
1513
|
+
}
|
|
1514
|
+
return {
|
|
1515
|
+
do: "fetch",
|
|
1516
|
+
url: { expr: "lit", value: "" }
|
|
1517
|
+
};
|
|
1518
|
+
})
|
|
1519
|
+
}));
|
|
1520
|
+
}
|
|
1521
|
+
function transformViewNode2(node, ctx) {
|
|
1522
|
+
switch (node.kind) {
|
|
1523
|
+
case "element": {
|
|
1524
|
+
const result = {
|
|
1525
|
+
kind: "element",
|
|
1526
|
+
tag: node.tag
|
|
1527
|
+
};
|
|
1528
|
+
if (node.props) {
|
|
1529
|
+
result.props = {};
|
|
1530
|
+
for (const [key, value] of Object.entries(node.props)) {
|
|
1531
|
+
if ("event" in value) {
|
|
1532
|
+
result.props[key] = {
|
|
1533
|
+
event: value.event,
|
|
1534
|
+
action: value.action
|
|
1535
|
+
};
|
|
1536
|
+
} else {
|
|
1537
|
+
result.props[key] = value;
|
|
1538
|
+
}
|
|
1539
|
+
}
|
|
1540
|
+
}
|
|
1541
|
+
if (node.children && node.children.length > 0) {
|
|
1542
|
+
result.children = node.children.map(
|
|
1543
|
+
(child) => transformViewNode2(child, ctx)
|
|
1544
|
+
);
|
|
1545
|
+
}
|
|
1546
|
+
return result;
|
|
1547
|
+
}
|
|
1548
|
+
case "text":
|
|
1549
|
+
return {
|
|
1550
|
+
kind: "text",
|
|
1551
|
+
value: node.value
|
|
1552
|
+
};
|
|
1553
|
+
case "if": {
|
|
1554
|
+
const result = {
|
|
1555
|
+
kind: "if",
|
|
1556
|
+
condition: node.condition,
|
|
1557
|
+
then: transformViewNode2(node.then, ctx)
|
|
1558
|
+
};
|
|
1559
|
+
if (node.else) {
|
|
1560
|
+
result.else = transformViewNode2(node.else, ctx);
|
|
1561
|
+
}
|
|
1562
|
+
return result;
|
|
1563
|
+
}
|
|
1564
|
+
case "each":
|
|
1565
|
+
return {
|
|
1566
|
+
kind: "each",
|
|
1567
|
+
items: node.items,
|
|
1568
|
+
as: node.as,
|
|
1569
|
+
body: transformViewNode2(node.body, ctx)
|
|
1570
|
+
};
|
|
1571
|
+
case "slot":
|
|
1572
|
+
return {
|
|
1573
|
+
kind: "slot",
|
|
1574
|
+
name: node.name
|
|
1575
|
+
};
|
|
1576
|
+
case "component": {
|
|
1577
|
+
const def = ctx.components[node.name];
|
|
1578
|
+
if (def) {
|
|
1579
|
+
return transformViewNode2(def.view, ctx);
|
|
1580
|
+
}
|
|
1581
|
+
return { kind: "element", tag: "div" };
|
|
1582
|
+
}
|
|
1583
|
+
case "markdown":
|
|
1584
|
+
return {
|
|
1585
|
+
kind: "markdown",
|
|
1586
|
+
content: node.content
|
|
1587
|
+
};
|
|
1588
|
+
case "code":
|
|
1589
|
+
return {
|
|
1590
|
+
kind: "code",
|
|
1591
|
+
language: node.language,
|
|
1592
|
+
content: node.content
|
|
1593
|
+
};
|
|
1594
|
+
}
|
|
1595
|
+
}
|
|
1596
|
+
function transformLayoutPass(layout, _context) {
|
|
1597
|
+
const ctx = {
|
|
1598
|
+
components: layout.components || {}
|
|
1599
|
+
};
|
|
1600
|
+
return {
|
|
1601
|
+
version: "1.0",
|
|
1602
|
+
type: "layout",
|
|
1603
|
+
state: transformState2(layout.state),
|
|
1604
|
+
actions: transformActions2(layout.actions),
|
|
1605
|
+
view: transformViewNode2(layout.view, ctx),
|
|
1606
|
+
components: layout.components
|
|
1607
|
+
};
|
|
1608
|
+
}
|
|
1609
|
+
function deepCloneNode(node) {
|
|
1610
|
+
return JSON.parse(JSON.stringify(node));
|
|
1611
|
+
}
|
|
1612
|
+
function replaceSlots(node, defaultContent, namedContent) {
|
|
1613
|
+
if (node.kind === "slot") {
|
|
1614
|
+
const slotName = node.name;
|
|
1615
|
+
if (slotName && namedContent?.[slotName]) {
|
|
1616
|
+
return deepCloneNode(namedContent[slotName]);
|
|
1617
|
+
}
|
|
1618
|
+
return deepCloneNode(defaultContent);
|
|
1619
|
+
}
|
|
1620
|
+
if (node.kind === "element") {
|
|
1621
|
+
const children = node.children;
|
|
1622
|
+
if (children && children.length > 0) {
|
|
1623
|
+
const newChildren = children.map((child) => replaceSlots(child, defaultContent, namedContent));
|
|
1624
|
+
return {
|
|
1625
|
+
...node,
|
|
1626
|
+
children: newChildren
|
|
1627
|
+
};
|
|
1628
|
+
}
|
|
1629
|
+
return node;
|
|
1630
|
+
}
|
|
1631
|
+
if (node.kind === "if") {
|
|
1632
|
+
const ifNode = node;
|
|
1633
|
+
const result = {
|
|
1634
|
+
...node,
|
|
1635
|
+
then: replaceSlots(ifNode.then, defaultContent, namedContent)
|
|
1636
|
+
};
|
|
1637
|
+
if (ifNode.else) {
|
|
1638
|
+
result.else = replaceSlots(ifNode.else, defaultContent, namedContent);
|
|
1639
|
+
}
|
|
1640
|
+
return result;
|
|
1641
|
+
}
|
|
1642
|
+
if (node.kind === "each") {
|
|
1643
|
+
const eachNode = node;
|
|
1644
|
+
return {
|
|
1645
|
+
...node,
|
|
1646
|
+
body: replaceSlots(eachNode.body, defaultContent, namedContent)
|
|
1647
|
+
};
|
|
1648
|
+
}
|
|
1649
|
+
return node;
|
|
1650
|
+
}
|
|
1651
|
+
function composeLayoutWithPage(layout, page, slots) {
|
|
1652
|
+
const layoutView = deepCloneNode(layout.view);
|
|
1653
|
+
const namedContent = slots ? Object.fromEntries(
|
|
1654
|
+
Object.entries(slots).map(([name, node]) => [name, node])
|
|
1655
|
+
) : void 0;
|
|
1656
|
+
const composedView = replaceSlots(layoutView, page.view, namedContent);
|
|
1657
|
+
const mergedState = {};
|
|
1658
|
+
for (const [name, field] of Object.entries(page.state)) {
|
|
1659
|
+
mergedState[name] = field;
|
|
1660
|
+
}
|
|
1661
|
+
for (const [name, field] of Object.entries(layout.state)) {
|
|
1662
|
+
if (name in mergedState) {
|
|
1663
|
+
mergedState[`$layout.${name}`] = field;
|
|
1664
|
+
} else {
|
|
1665
|
+
mergedState[name] = field;
|
|
1666
|
+
}
|
|
1667
|
+
}
|
|
1668
|
+
const getActionsArray = (actions) => {
|
|
1669
|
+
if (Array.isArray(actions)) return actions;
|
|
1670
|
+
if (typeof actions === "object" && actions !== null) {
|
|
1671
|
+
return Object.values(actions);
|
|
1672
|
+
}
|
|
1673
|
+
return [];
|
|
1674
|
+
};
|
|
1675
|
+
const pageActions = getActionsArray(page.actions);
|
|
1676
|
+
const layoutActions = getActionsArray(layout.actions);
|
|
1677
|
+
const pageActionNames = new Set(pageActions.map((a) => a.name));
|
|
1678
|
+
const mergedActions = {};
|
|
1679
|
+
for (const action of pageActions) {
|
|
1680
|
+
mergedActions[action.name] = action;
|
|
1681
|
+
}
|
|
1682
|
+
for (const action of layoutActions) {
|
|
1683
|
+
if (pageActionNames.has(action.name)) {
|
|
1684
|
+
const prefixedName = `$layout.${action.name}`;
|
|
1685
|
+
mergedActions[prefixedName] = { ...action, name: prefixedName };
|
|
1686
|
+
} else {
|
|
1687
|
+
mergedActions[action.name] = action;
|
|
1688
|
+
}
|
|
1689
|
+
}
|
|
1690
|
+
const mergedComponents = {
|
|
1691
|
+
...layout.components || {},
|
|
1692
|
+
...page.components || {}
|
|
1693
|
+
};
|
|
1694
|
+
const result = {
|
|
1695
|
+
version: "1.0",
|
|
1696
|
+
state: mergedState,
|
|
1697
|
+
actions: mergedActions,
|
|
1698
|
+
view: composedView
|
|
1699
|
+
};
|
|
1700
|
+
if (page.route) {
|
|
1701
|
+
result.route = page.route;
|
|
1702
|
+
}
|
|
1703
|
+
if (Object.keys(mergedComponents).length > 0) {
|
|
1704
|
+
result.components = mergedComponents;
|
|
1705
|
+
}
|
|
1706
|
+
return result;
|
|
1707
|
+
}
|
|
830
1708
|
export {
|
|
1709
|
+
analyzeLayoutPass,
|
|
831
1710
|
analyzePass,
|
|
832
1711
|
compile,
|
|
1712
|
+
composeLayoutWithPage,
|
|
833
1713
|
createUndefinedVarError2 as createUndefinedVarError,
|
|
1714
|
+
transformLayoutPass,
|
|
834
1715
|
transformPass,
|
|
835
1716
|
validatePass
|
|
836
1717
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@constela/compiler",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.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.5.0"
|
|
19
19
|
},
|
|
20
20
|
"devDependencies": {
|
|
21
21
|
"@types/node": "^20.10.0",
|