@constela/compiler 0.15.22 → 0.16.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 +5 -1
- package/dist/index.js +156 -2
- package/package.json +2 -2
package/dist/index.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Program, ConstelaError, Expression, IslandStrategy, IslandStrategyOptions, StylePreset, LayoutProgram, ComponentDef, ViewNode } from '@constela/core';
|
|
1
|
+
import { Program, ConstelaError, Expression, TransitionDirective, IslandStrategy, IslandStrategyOptions, StylePreset, LayoutProgram, ComponentDef, ViewNode } from '@constela/core';
|
|
2
2
|
export { createUndefinedVarError } from '@constela/core';
|
|
3
3
|
|
|
4
4
|
/**
|
|
@@ -27,6 +27,7 @@ interface AnalyzePassSuccess {
|
|
|
27
27
|
ok: true;
|
|
28
28
|
ast: Program;
|
|
29
29
|
context: AnalysisContext;
|
|
30
|
+
warnings: ConstelaError[];
|
|
30
31
|
}
|
|
31
32
|
interface AnalyzePassFailure {
|
|
32
33
|
ok: false;
|
|
@@ -417,6 +418,7 @@ interface CompiledTextNode {
|
|
|
417
418
|
interface CompiledIfNode {
|
|
418
419
|
kind: 'if';
|
|
419
420
|
condition: CompiledExpression;
|
|
421
|
+
transition?: TransitionDirective;
|
|
420
422
|
then: CompiledNode;
|
|
421
423
|
else?: CompiledNode;
|
|
422
424
|
}
|
|
@@ -426,6 +428,7 @@ interface CompiledEachNode {
|
|
|
426
428
|
as: string;
|
|
427
429
|
index?: string;
|
|
428
430
|
key?: CompiledExpression;
|
|
431
|
+
transition?: TransitionDirective;
|
|
429
432
|
body: CompiledNode;
|
|
430
433
|
}
|
|
431
434
|
interface CompiledMarkdownNode {
|
|
@@ -581,6 +584,7 @@ declare function transformPass(ast: Program, _context: AnalysisContext, importDa
|
|
|
581
584
|
interface CompileSuccess {
|
|
582
585
|
ok: true;
|
|
583
586
|
program: CompiledProgram;
|
|
587
|
+
warnings?: ConstelaError[];
|
|
584
588
|
}
|
|
585
589
|
interface CompileFailure {
|
|
586
590
|
ok: false;
|
package/dist/index.js
CHANGED
|
@@ -14,6 +14,151 @@ function validatePass(input) {
|
|
|
14
14
|
};
|
|
15
15
|
}
|
|
16
16
|
|
|
17
|
+
// src/passes/a11y-validate.ts
|
|
18
|
+
import {
|
|
19
|
+
createA11yImgNoAltError,
|
|
20
|
+
createA11yButtonNoLabelError,
|
|
21
|
+
createA11yAnchorNoLabelError,
|
|
22
|
+
createA11yInputNoLabelError,
|
|
23
|
+
createA11yHeadingSkipError,
|
|
24
|
+
createA11yPositiveTabindexError,
|
|
25
|
+
createA11yDuplicateIdError
|
|
26
|
+
} from "@constela/core";
|
|
27
|
+
var HEADING_RE = /^h([1-6])$/;
|
|
28
|
+
function getHeadingLevel(tag) {
|
|
29
|
+
const match = HEADING_RE.exec(tag);
|
|
30
|
+
return match ? Number(match[1]) : 0;
|
|
31
|
+
}
|
|
32
|
+
var FORM_INPUT_TAGS = /* @__PURE__ */ new Set(["input", "textarea", "select"]);
|
|
33
|
+
function hasTextChild(children) {
|
|
34
|
+
if (!children) return false;
|
|
35
|
+
return children.some((child) => child.kind === "text");
|
|
36
|
+
}
|
|
37
|
+
function validateElementNode(node, path, ctx) {
|
|
38
|
+
const { tag, props, children } = node;
|
|
39
|
+
if (tag === "img") {
|
|
40
|
+
if (!props || !("alt" in props)) {
|
|
41
|
+
ctx.warnings.push(createA11yImgNoAltError(path));
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
if (tag === "button") {
|
|
45
|
+
const hasAriaLabel = props != null && "aria-label" in props;
|
|
46
|
+
if (!hasAriaLabel && !hasTextChild(children)) {
|
|
47
|
+
ctx.warnings.push(createA11yButtonNoLabelError(path));
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
if (tag === "a") {
|
|
51
|
+
const hasAriaLabel = props != null && "aria-label" in props;
|
|
52
|
+
if (!hasAriaLabel && !hasTextChild(children)) {
|
|
53
|
+
ctx.warnings.push(createA11yAnchorNoLabelError(path));
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
if (FORM_INPUT_TAGS.has(tag)) {
|
|
57
|
+
const hasAriaLabel = props != null && "aria-label" in props;
|
|
58
|
+
const hasAriaLabelledby = props != null && "aria-labelledby" in props;
|
|
59
|
+
if (!hasAriaLabel && !hasAriaLabelledby) {
|
|
60
|
+
ctx.warnings.push(createA11yInputNoLabelError(tag, path));
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
const headingLevel = getHeadingLevel(tag);
|
|
64
|
+
if (headingLevel > 0) {
|
|
65
|
+
if (ctx.maxHeadingLevel > 0 && headingLevel > ctx.maxHeadingLevel + 1) {
|
|
66
|
+
ctx.warnings.push(
|
|
67
|
+
createA11yHeadingSkipError(headingLevel, ctx.maxHeadingLevel + 1, path)
|
|
68
|
+
);
|
|
69
|
+
}
|
|
70
|
+
ctx.maxHeadingLevel = Math.max(ctx.maxHeadingLevel, headingLevel);
|
|
71
|
+
}
|
|
72
|
+
if (props && "tabindex" in props) {
|
|
73
|
+
const tabindexExpr = props["tabindex"];
|
|
74
|
+
if (tabindexExpr && tabindexExpr.expr === "lit" && typeof tabindexExpr.value === "number" && tabindexExpr.value > 0) {
|
|
75
|
+
ctx.warnings.push(
|
|
76
|
+
createA11yPositiveTabindexError(tabindexExpr.value, path)
|
|
77
|
+
);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
if (props && "id" in props) {
|
|
81
|
+
const idExpr = props["id"];
|
|
82
|
+
if (idExpr && idExpr.expr === "lit" && typeof idExpr.value === "string") {
|
|
83
|
+
if (ctx.seenIds.has(idExpr.value)) {
|
|
84
|
+
ctx.warnings.push(createA11yDuplicateIdError(idExpr.value, path));
|
|
85
|
+
} else {
|
|
86
|
+
ctx.seenIds.add(idExpr.value);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
function validateNode(node, path, ctx) {
|
|
92
|
+
switch (node.kind) {
|
|
93
|
+
case "element":
|
|
94
|
+
validateElementNode(node, path, ctx);
|
|
95
|
+
if (node.children) {
|
|
96
|
+
for (let i = 0; i < node.children.length; i++) {
|
|
97
|
+
const child = node.children[i];
|
|
98
|
+
if (child) {
|
|
99
|
+
validateNode(child, `${path}/children/${i}`, ctx);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
break;
|
|
104
|
+
case "text":
|
|
105
|
+
break;
|
|
106
|
+
case "if":
|
|
107
|
+
validateNode(node.then, `${path}/then`, ctx);
|
|
108
|
+
if (node.else) {
|
|
109
|
+
validateNode(node.else, `${path}/else`, ctx);
|
|
110
|
+
}
|
|
111
|
+
break;
|
|
112
|
+
case "each":
|
|
113
|
+
validateNode(node.body, `${path}/body`, ctx);
|
|
114
|
+
break;
|
|
115
|
+
case "component":
|
|
116
|
+
if (node.children) {
|
|
117
|
+
for (let i = 0; i < node.children.length; i++) {
|
|
118
|
+
const child = node.children[i];
|
|
119
|
+
if (child) {
|
|
120
|
+
validateNode(child, `${path}/children/${i}`, ctx);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
break;
|
|
125
|
+
case "slot":
|
|
126
|
+
case "markdown":
|
|
127
|
+
case "code":
|
|
128
|
+
break;
|
|
129
|
+
case "portal":
|
|
130
|
+
if (node.children) {
|
|
131
|
+
for (let i = 0; i < node.children.length; i++) {
|
|
132
|
+
const child = node.children[i];
|
|
133
|
+
if (child) {
|
|
134
|
+
validateNode(child, `${path}/children/${i}`, ctx);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
break;
|
|
139
|
+
case "island":
|
|
140
|
+
validateNode(node.content, `${path}/content`, ctx);
|
|
141
|
+
break;
|
|
142
|
+
case "suspense":
|
|
143
|
+
validateNode(node.fallback, `${path}/fallback`, ctx);
|
|
144
|
+
validateNode(node.content, `${path}/content`, ctx);
|
|
145
|
+
break;
|
|
146
|
+
case "errorBoundary":
|
|
147
|
+
validateNode(node.fallback, `${path}/fallback`, ctx);
|
|
148
|
+
validateNode(node.content, `${path}/content`, ctx);
|
|
149
|
+
break;
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
function validateA11y(program) {
|
|
153
|
+
const ctx = {
|
|
154
|
+
warnings: [],
|
|
155
|
+
maxHeadingLevel: 0,
|
|
156
|
+
seenIds: /* @__PURE__ */ new Set()
|
|
157
|
+
};
|
|
158
|
+
validateNode(program.view, "/view", ctx);
|
|
159
|
+
return ctx.warnings;
|
|
160
|
+
}
|
|
161
|
+
|
|
17
162
|
// src/passes/analyze.ts
|
|
18
163
|
import {
|
|
19
164
|
createUndefinedStateError,
|
|
@@ -1307,6 +1452,7 @@ function analyzePass(programAst) {
|
|
|
1307
1452
|
insideLayout: isLayout
|
|
1308
1453
|
})
|
|
1309
1454
|
);
|
|
1455
|
+
const a11yWarnings = validateA11y(programAst);
|
|
1310
1456
|
if (errors.length > 0) {
|
|
1311
1457
|
return {
|
|
1312
1458
|
ok: false,
|
|
@@ -1316,7 +1462,8 @@ function analyzePass(programAst) {
|
|
|
1316
1462
|
return {
|
|
1317
1463
|
ok: true,
|
|
1318
1464
|
ast: programAst,
|
|
1319
|
-
context
|
|
1465
|
+
context,
|
|
1466
|
+
warnings: a11yWarnings
|
|
1320
1467
|
};
|
|
1321
1468
|
}
|
|
1322
1469
|
|
|
@@ -1929,6 +2076,9 @@ function transformViewNode(node, ctx) {
|
|
|
1929
2076
|
condition: transformExpression(node.condition, ctx),
|
|
1930
2077
|
then: transformViewNode(node.then, ctx)
|
|
1931
2078
|
};
|
|
2079
|
+
if (node.transition) {
|
|
2080
|
+
compiledIf.transition = node.transition;
|
|
2081
|
+
}
|
|
1932
2082
|
if (node.else) {
|
|
1933
2083
|
compiledIf.else = transformViewNode(node.else, ctx);
|
|
1934
2084
|
}
|
|
@@ -1941,6 +2091,9 @@ function transformViewNode(node, ctx) {
|
|
|
1941
2091
|
as: node.as,
|
|
1942
2092
|
body: transformViewNode(node.body, ctx)
|
|
1943
2093
|
};
|
|
2094
|
+
if (node.transition) {
|
|
2095
|
+
compiledEach.transition = node.transition;
|
|
2096
|
+
}
|
|
1944
2097
|
if (node.index) {
|
|
1945
2098
|
compiledEach.index = node.index;
|
|
1946
2099
|
}
|
|
@@ -2215,7 +2368,8 @@ function compile(input) {
|
|
|
2215
2368
|
const program = transformPass(analyzeResult.ast, analyzeResult.context);
|
|
2216
2369
|
return {
|
|
2217
2370
|
ok: true,
|
|
2218
|
-
program
|
|
2371
|
+
program,
|
|
2372
|
+
warnings: analyzeResult.warnings
|
|
2219
2373
|
};
|
|
2220
2374
|
}
|
|
2221
2375
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@constela/compiler",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.16.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.23.0"
|
|
19
19
|
},
|
|
20
20
|
"devDependencies": {
|
|
21
21
|
"@types/node": "^20.10.0",
|