@rettangoli/check 0.1.1 → 0.1.2
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/package.json +3 -2
- package/src/core/fe-contracts.js +212 -0
- package/src/core/parsers.js +1 -1
- package/src/rules/listenerConfig.js +1 -1
- package/src/rules/refs.js +1 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rettangoli/check",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.2",
|
|
4
4
|
"description": "Static contract checker for Rettangoli projects",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./src/index.js",
|
|
@@ -12,7 +12,9 @@
|
|
|
12
12
|
"rtgl-check": "./src/cli/bin.js"
|
|
13
13
|
},
|
|
14
14
|
"scripts": {
|
|
15
|
+
"ci": "npm run test:no-fe-contracts-import && npm run test:scenarios",
|
|
15
16
|
"test:scenarios": "node ./test/run-scenarios.js",
|
|
17
|
+
"test:no-fe-contracts-import": "node ./scripts/test-no-fe-contracts-import.js",
|
|
16
18
|
"test:diff-js-exports": "node ./scripts/differential-js-exports.js",
|
|
17
19
|
"test:fuzz-template-pipeline": "node ./scripts/fuzz-template-pipeline.js",
|
|
18
20
|
"test:fuzz-jempl-parser": "node ./scripts/fuzz-jempl-parser.js",
|
|
@@ -37,7 +39,6 @@
|
|
|
37
39
|
],
|
|
38
40
|
"license": "MIT",
|
|
39
41
|
"dependencies": {
|
|
40
|
-
"@rettangoli/fe": "1.0.0-rc3",
|
|
41
42
|
"jempl": "0.3.2-rc2",
|
|
42
43
|
"js-yaml": "^4.1.0",
|
|
43
44
|
"oxc-parser": "^0.112.0",
|
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
const PROP_PREFIX = ":";
|
|
2
|
+
|
|
3
|
+
const REF_ID_KEY_REGEX = /^[a-z][a-zA-Z0-9]*\*?$/;
|
|
4
|
+
const REF_CLASS_KEY_REGEX = /^[a-zA-Z][a-zA-Z0-9_-]*\*?$/;
|
|
5
|
+
const REF_ID_REGEX = /^[a-z][a-zA-Z0-9]*$/;
|
|
6
|
+
const GLOBAL_REF_KEYS = new Set(["window", "document"]);
|
|
7
|
+
|
|
8
|
+
export const collectBindingNames = (attrsString = "") => {
|
|
9
|
+
if (!attrsString) {
|
|
10
|
+
return [];
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const attrAssignmentRegex = /(\S+?)=(?:\"([^\"]*)\"|\'([^\']*)\'|([^\s]*))/g;
|
|
14
|
+
const booleanAttrRegex = /\b(\S+?)(?=\s|$)/g;
|
|
15
|
+
const processedAttrs = new Set();
|
|
16
|
+
const bindingNames = [];
|
|
17
|
+
let match;
|
|
18
|
+
|
|
19
|
+
while ((match = attrAssignmentRegex.exec(attrsString)) !== null) {
|
|
20
|
+
const rawBindingName = match[1];
|
|
21
|
+
processedAttrs.add(rawBindingName);
|
|
22
|
+
bindingNames.push(rawBindingName);
|
|
23
|
+
}
|
|
24
|
+
attrAssignmentRegex.lastIndex = 0;
|
|
25
|
+
|
|
26
|
+
let remainingAttrsString = attrsString;
|
|
27
|
+
const processedMatches = [];
|
|
28
|
+
while ((match = attrAssignmentRegex.exec(attrsString)) !== null) {
|
|
29
|
+
processedMatches.push(match[0]);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
processedMatches.forEach((processedMatch) => {
|
|
33
|
+
remainingAttrsString = remainingAttrsString.replace(processedMatch, " ");
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
let boolMatch;
|
|
37
|
+
while ((boolMatch = booleanAttrRegex.exec(remainingAttrsString)) !== null) {
|
|
38
|
+
const attrName = boolMatch[1];
|
|
39
|
+
if (attrName.startsWith(".")) {
|
|
40
|
+
continue;
|
|
41
|
+
}
|
|
42
|
+
if (
|
|
43
|
+
!processedAttrs.has(attrName)
|
|
44
|
+
&& !attrName.startsWith(PROP_PREFIX)
|
|
45
|
+
&& !attrName.includes("=")
|
|
46
|
+
) {
|
|
47
|
+
bindingNames.push(attrName);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
return [...new Set(bindingNames)];
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
export const createRefMatchers = (refs) => {
|
|
55
|
+
return Object.entries(refs || {}).map(([refKey, refConfig]) => {
|
|
56
|
+
if (GLOBAL_REF_KEYS.has(refKey)) {
|
|
57
|
+
return {
|
|
58
|
+
refKey,
|
|
59
|
+
refConfig,
|
|
60
|
+
targetType: "global",
|
|
61
|
+
isWildcard: false,
|
|
62
|
+
prefix: refKey,
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
let targetType = "id";
|
|
67
|
+
let rawKey = refKey;
|
|
68
|
+
if (refKey.startsWith(".")) {
|
|
69
|
+
targetType = "class";
|
|
70
|
+
rawKey = refKey.slice(1);
|
|
71
|
+
} else if (refKey.startsWith("#")) {
|
|
72
|
+
targetType = "id";
|
|
73
|
+
rawKey = refKey.slice(1);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const reservedBaseKey = rawKey.endsWith("*") ? rawKey.slice(0, -1) : rawKey;
|
|
77
|
+
if (GLOBAL_REF_KEYS.has(reservedBaseKey)) {
|
|
78
|
+
throw new Error(
|
|
79
|
+
`[Parser] Invalid ref key '${refKey}'. Reserved global keys must be exactly 'window' or 'document'.`,
|
|
80
|
+
);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
if (targetType === "id" && !REF_ID_KEY_REGEX.test(rawKey)) {
|
|
84
|
+
throw new Error(
|
|
85
|
+
`[Parser] Invalid ref key '${refKey}'. Use camelCase IDs (optional '#', optional '*') or class refs with '.' prefix.`,
|
|
86
|
+
);
|
|
87
|
+
}
|
|
88
|
+
if (targetType === "class" && !REF_CLASS_KEY_REGEX.test(rawKey)) {
|
|
89
|
+
throw new Error(
|
|
90
|
+
`[Parser] Invalid ref key '${refKey}'. Class refs must start with '.' and use class-compatible names (optional '*').`,
|
|
91
|
+
);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const isWildcard = rawKey.endsWith("*");
|
|
95
|
+
const prefix = isWildcard ? rawKey.slice(0, -1) : rawKey;
|
|
96
|
+
|
|
97
|
+
return {
|
|
98
|
+
refKey,
|
|
99
|
+
refConfig,
|
|
100
|
+
targetType,
|
|
101
|
+
isWildcard,
|
|
102
|
+
prefix,
|
|
103
|
+
};
|
|
104
|
+
});
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
export const validateElementIdForRefs = (elementIdForRefs) => {
|
|
108
|
+
if (!REF_ID_REGEX.test(elementIdForRefs)) {
|
|
109
|
+
throw new Error(
|
|
110
|
+
`[Parser] Invalid element id '${elementIdForRefs}' for refs. Use camelCase ids only. Kebab-case ids are not supported.`,
|
|
111
|
+
);
|
|
112
|
+
}
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
const assertBooleanEventOption = ({ optionName, optionValue, eventType, refKey }) => {
|
|
116
|
+
if (optionValue === undefined) {
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
if (typeof optionValue !== "boolean") {
|
|
120
|
+
throw new Error(
|
|
121
|
+
`[Parser] Invalid '${optionName}' for event '${eventType}' on ref '${refKey}'. Expected boolean.`,
|
|
122
|
+
);
|
|
123
|
+
}
|
|
124
|
+
};
|
|
125
|
+
|
|
126
|
+
const assertNumberEventOption = ({ optionName, optionValue, eventType, refKey }) => {
|
|
127
|
+
if (optionValue === undefined) {
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
if (
|
|
131
|
+
typeof optionValue !== "number"
|
|
132
|
+
|| Number.isNaN(optionValue)
|
|
133
|
+
|| !Number.isFinite(optionValue)
|
|
134
|
+
|| optionValue < 0
|
|
135
|
+
) {
|
|
136
|
+
throw new Error(
|
|
137
|
+
`[Parser] Invalid '${optionName}' for event '${eventType}' on ref '${refKey}'. Expected non-negative number.`,
|
|
138
|
+
);
|
|
139
|
+
}
|
|
140
|
+
};
|
|
141
|
+
|
|
142
|
+
export const validateEventConfig = ({ eventType, eventConfig, refKey }) => {
|
|
143
|
+
if (typeof eventConfig !== "object" || eventConfig === null) {
|
|
144
|
+
throw new Error(
|
|
145
|
+
`[Parser] Invalid event config for event '${eventType}' on ref '${refKey}'.`,
|
|
146
|
+
);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
const hasDebounce = Object.prototype.hasOwnProperty.call(eventConfig, "debounce");
|
|
150
|
+
const hasThrottle = Object.prototype.hasOwnProperty.call(eventConfig, "throttle");
|
|
151
|
+
|
|
152
|
+
assertBooleanEventOption({
|
|
153
|
+
optionName: "preventDefault",
|
|
154
|
+
optionValue: eventConfig.preventDefault,
|
|
155
|
+
eventType,
|
|
156
|
+
refKey,
|
|
157
|
+
});
|
|
158
|
+
assertBooleanEventOption({
|
|
159
|
+
optionName: "stopPropagation",
|
|
160
|
+
optionValue: eventConfig.stopPropagation,
|
|
161
|
+
eventType,
|
|
162
|
+
refKey,
|
|
163
|
+
});
|
|
164
|
+
assertBooleanEventOption({
|
|
165
|
+
optionName: "stopImmediatePropagation",
|
|
166
|
+
optionValue: eventConfig.stopImmediatePropagation,
|
|
167
|
+
eventType,
|
|
168
|
+
refKey,
|
|
169
|
+
});
|
|
170
|
+
assertBooleanEventOption({
|
|
171
|
+
optionName: "targetOnly",
|
|
172
|
+
optionValue: eventConfig.targetOnly,
|
|
173
|
+
eventType,
|
|
174
|
+
refKey,
|
|
175
|
+
});
|
|
176
|
+
assertBooleanEventOption({
|
|
177
|
+
optionName: "once",
|
|
178
|
+
optionValue: eventConfig.once,
|
|
179
|
+
eventType,
|
|
180
|
+
refKey,
|
|
181
|
+
});
|
|
182
|
+
assertNumberEventOption({
|
|
183
|
+
optionName: "debounce",
|
|
184
|
+
optionValue: eventConfig.debounce,
|
|
185
|
+
eventType,
|
|
186
|
+
refKey,
|
|
187
|
+
});
|
|
188
|
+
assertNumberEventOption({
|
|
189
|
+
optionName: "throttle",
|
|
190
|
+
optionValue: eventConfig.throttle,
|
|
191
|
+
eventType,
|
|
192
|
+
refKey,
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
if (hasDebounce && hasThrottle) {
|
|
196
|
+
throw new Error(
|
|
197
|
+
`[Parser] Event '${eventType}' on ref '${refKey}' cannot define both 'debounce' and 'throttle'.`,
|
|
198
|
+
);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
if (eventConfig.handler && eventConfig.action) {
|
|
202
|
+
throw new Error("Each listener can have handler or action but not both");
|
|
203
|
+
}
|
|
204
|
+
if (!eventConfig.handler && !eventConfig.action) {
|
|
205
|
+
throw new Error("Each listener must define either handler or action");
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
return {
|
|
209
|
+
hasDebounce,
|
|
210
|
+
hasThrottle,
|
|
211
|
+
};
|
|
212
|
+
};
|
package/src/core/parsers.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { load as loadYaml } from "js-yaml";
|
|
2
2
|
import { parseSync } from "oxc-parser";
|
|
3
|
-
import { collectBindingNames as collectFeBindingNames } from "
|
|
3
|
+
import { collectBindingNames as collectFeBindingNames } from "./fe-contracts.js";
|
|
4
4
|
import { parse as parseJempl } from "jempl";
|
|
5
5
|
import { parseElementKey as parseYahtmlElementKey } from "yahtml";
|
|
6
6
|
|
|
@@ -9,7 +9,7 @@ import { getModelFilePath, getYamlPathLine, isObjectRecord } from "./shared.js";
|
|
|
9
9
|
import {
|
|
10
10
|
validateElementIdForRefs,
|
|
11
11
|
validateEventConfig as validateFeEventConfig,
|
|
12
|
-
} from "
|
|
12
|
+
} from "../core/fe-contracts.js";
|
|
13
13
|
import { parseSync } from "oxc-parser";
|
|
14
14
|
import { parseNamedExportedFunctions } from "../core/exportedFunctions.js";
|
|
15
15
|
const GLOBAL_REF_KEYS = new Set(["window", "document"]);
|
package/src/rules/refs.js
CHANGED