@rettangoli/check 0.0.1
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 +295 -0
- package/ROADMAP.md +175 -0
- package/package.json +46 -0
- package/src/cli/bin.js +325 -0
- package/src/cli/check.js +232 -0
- package/src/cli/index.js +1 -0
- package/src/core/analyze.js +227 -0
- package/src/core/discovery.js +83 -0
- package/src/core/exportedFunctions.js +235 -0
- package/src/core/model.js +898 -0
- package/src/core/parsers.js +2726 -0
- package/src/core/registry.js +779 -0
- package/src/core/schema.js +161 -0
- package/src/core/scopeGraph.js +1400 -0
- package/src/core/semantic.js +329 -0
- package/src/diagnostics/autofix.js +191 -0
- package/src/diagnostics/catalog.js +89 -0
- package/src/index.js +2 -0
- package/src/reporters/index.js +13 -0
- package/src/reporters/json.js +42 -0
- package/src/reporters/sarif.js +213 -0
- package/src/reporters/text.js +145 -0
- package/src/rules/compatibility.js +318 -0
- package/src/rules/constants.js +22 -0
- package/src/rules/crossFileSymbols.js +108 -0
- package/src/rules/expression.js +338 -0
- package/src/rules/feParity.js +65 -0
- package/src/rules/index.js +39 -0
- package/src/rules/jempl.js +80 -0
- package/src/rules/lifecycle.js +4 -0
- package/src/rules/listenerConfig.js +556 -0
- package/src/rules/listenerSymbols.js +49 -0
- package/src/rules/methods.js +117 -0
- package/src/rules/refs.js +20 -0
- package/src/rules/schema.js +118 -0
- package/src/rules/shared.js +20 -0
- package/src/rules/yahtmlAttrs.js +238 -0
- package/src/semantic/engine.js +778 -0
- package/src/semantic/index.js +9 -0
- package/src/types/lattice.js +281 -0
- package/src/utils/case.js +9 -0
- package/src/utils/fs.js +30 -0
|
@@ -0,0 +1,779 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
3
|
+
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
4
|
+
import { load as loadYaml } from "js-yaml";
|
|
5
|
+
import { parseSync } from "oxc-parser";
|
|
6
|
+
import { walkFiles } from "../utils/fs.js";
|
|
7
|
+
import { toCamelCase, toKebabCase } from "../utils/case.js";
|
|
8
|
+
|
|
9
|
+
const ensureRuntimeStubs = () => {
|
|
10
|
+
if (typeof globalThis.HTMLElement === "undefined") {
|
|
11
|
+
globalThis.HTMLElement = class {};
|
|
12
|
+
}
|
|
13
|
+
if (typeof globalThis.CSSStyleSheet === "undefined") {
|
|
14
|
+
globalThis.CSSStyleSheet = class {
|
|
15
|
+
replaceSync() {}
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
if (typeof globalThis.CustomEvent === "undefined") {
|
|
19
|
+
globalThis.CustomEvent = class {
|
|
20
|
+
constructor(type, init = {}) {
|
|
21
|
+
this.type = type;
|
|
22
|
+
this.detail = init.detail;
|
|
23
|
+
}
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
const createContract = ({ tagName }) => {
|
|
29
|
+
return {
|
|
30
|
+
tagName,
|
|
31
|
+
attrs: new Set(),
|
|
32
|
+
props: new Set(),
|
|
33
|
+
requiredProps: new Set(),
|
|
34
|
+
events: new Set(),
|
|
35
|
+
eventTypes: new Map(),
|
|
36
|
+
propTypes: new Map(),
|
|
37
|
+
source: new Set(),
|
|
38
|
+
};
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
const getOrCreateContract = (registryMap, tagName) => {
|
|
42
|
+
if (!registryMap.has(tagName)) {
|
|
43
|
+
registryMap.set(tagName, createContract({ tagName }));
|
|
44
|
+
}
|
|
45
|
+
return registryMap.get(tagName);
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
const isObjectRecord = (value) => {
|
|
49
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
const hasNonEmptyComponentName = (componentName) => {
|
|
53
|
+
return typeof componentName === "string" && componentName !== "";
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
const hasNonBlankComponentName = (componentName) => {
|
|
57
|
+
return typeof componentName === "string" && componentName.trim() !== "";
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
const addSchemaPropsToContract = ({ contract, schemaYaml = {} }) => {
|
|
61
|
+
const properties = schemaYaml?.propsSchema?.properties;
|
|
62
|
+
const requiredProps = Array.isArray(schemaYaml?.propsSchema?.required)
|
|
63
|
+
? schemaYaml.propsSchema.required
|
|
64
|
+
: [];
|
|
65
|
+
if (!isObjectRecord(properties)) {
|
|
66
|
+
requiredProps.forEach((propKey) => {
|
|
67
|
+
if (typeof propKey === "string" && propKey.trim() === propKey && propKey.length > 0) {
|
|
68
|
+
contract.requiredProps.add(propKey);
|
|
69
|
+
}
|
|
70
|
+
});
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const propertyKeys = Object.keys(properties);
|
|
75
|
+
propertyKeys.forEach((propKey) => {
|
|
76
|
+
if (!propKey || propKey.trim() !== propKey) {
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
contract.props.add(propKey);
|
|
80
|
+
contract.props.add(toCamelCase(propKey));
|
|
81
|
+
contract.attrs.add(toKebabCase(propKey));
|
|
82
|
+
const propertySchema = properties[propKey];
|
|
83
|
+
if (propertySchema && typeof propertySchema === "object" && !Array.isArray(propertySchema)) {
|
|
84
|
+
const aliases = [propKey, toCamelCase(propKey), toKebabCase(propKey)];
|
|
85
|
+
aliases.forEach((alias) => {
|
|
86
|
+
if (alias) {
|
|
87
|
+
contract.propTypes.set(alias, propertySchema);
|
|
88
|
+
}
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
requiredProps.forEach((propKey) => {
|
|
94
|
+
if (typeof propKey !== "string" || propKey.trim() !== propKey || propKey.length === 0) {
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
contract.requiredProps.add(propKey);
|
|
98
|
+
});
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
const addSchemaEventsToContract = ({ contract, schemaYaml = {} }) => {
|
|
102
|
+
const events = schemaYaml?.events;
|
|
103
|
+
if (Array.isArray(events)) {
|
|
104
|
+
events.forEach((eventName) => {
|
|
105
|
+
if (typeof eventName === "string" && eventName.trim() === eventName && eventName.length > 0) {
|
|
106
|
+
contract.events.add(eventName);
|
|
107
|
+
if (!contract.eventTypes.has(eventName)) {
|
|
108
|
+
contract.eventTypes.set(eventName, {});
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
});
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
if (!isObjectRecord(events)) {
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
Object.keys(events).forEach((eventName) => {
|
|
120
|
+
if (typeof eventName === "string" && eventName.trim() === eventName && eventName.length > 0) {
|
|
121
|
+
contract.events.add(eventName);
|
|
122
|
+
const eventSchema = events[eventName];
|
|
123
|
+
if (eventSchema && typeof eventSchema === "object" && !Array.isArray(eventSchema)) {
|
|
124
|
+
contract.eventTypes.set(eventName, eventSchema);
|
|
125
|
+
} else if (!contract.eventTypes.has(eventName)) {
|
|
126
|
+
contract.eventTypes.set(eventName, {});
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
});
|
|
130
|
+
};
|
|
131
|
+
|
|
132
|
+
const buildSchemaRegistry = ({
|
|
133
|
+
schemas = [],
|
|
134
|
+
source,
|
|
135
|
+
isValidComponentName = () => false,
|
|
136
|
+
normalizeComponentName = (componentName) => componentName,
|
|
137
|
+
}) => {
|
|
138
|
+
return schemas.reduce((registryMap, schemaYaml = {}) => {
|
|
139
|
+
const rawComponentName = schemaYaml?.componentName;
|
|
140
|
+
const componentName = normalizeComponentName(rawComponentName);
|
|
141
|
+
if (!isValidComponentName(componentName)) {
|
|
142
|
+
return registryMap;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
const contract = getOrCreateContract(registryMap, componentName);
|
|
146
|
+
contract.source.add(source);
|
|
147
|
+
addSchemaPropsToContract({ contract, schemaYaml });
|
|
148
|
+
addSchemaEventsToContract({ contract, schemaYaml });
|
|
149
|
+
return registryMap;
|
|
150
|
+
}, new Map());
|
|
151
|
+
};
|
|
152
|
+
|
|
153
|
+
const addNormalizedModelSchemaToContract = ({ contract, normalizedSchema }) => {
|
|
154
|
+
const normalizedProps = normalizedSchema?.props;
|
|
155
|
+
if (normalizedProps?.byName instanceof Map) {
|
|
156
|
+
normalizedProps.byName.forEach((propertySchema, propKey) => {
|
|
157
|
+
if (!propKey) {
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
160
|
+
contract.props.add(propKey);
|
|
161
|
+
contract.props.add(toCamelCase(propKey));
|
|
162
|
+
contract.attrs.add(toKebabCase(propKey));
|
|
163
|
+
if (propertySchema && typeof propertySchema === "object" && !Array.isArray(propertySchema)) {
|
|
164
|
+
const aliases = [propKey, toCamelCase(propKey), toKebabCase(propKey)];
|
|
165
|
+
aliases.forEach((alias) => {
|
|
166
|
+
if (alias) {
|
|
167
|
+
contract.propTypes.set(alias, propertySchema);
|
|
168
|
+
}
|
|
169
|
+
});
|
|
170
|
+
}
|
|
171
|
+
});
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
const requiredProps = Array.isArray(normalizedProps?.requiredNames)
|
|
175
|
+
? normalizedProps.requiredNames
|
|
176
|
+
: [];
|
|
177
|
+
requiredProps.forEach((propKey) => {
|
|
178
|
+
if (propKey) {
|
|
179
|
+
contract.requiredProps.add(propKey);
|
|
180
|
+
}
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
const eventNames = Array.isArray(normalizedSchema?.events?.names)
|
|
184
|
+
? normalizedSchema.events.names
|
|
185
|
+
: [];
|
|
186
|
+
eventNames.forEach((eventName) => {
|
|
187
|
+
if (eventName) {
|
|
188
|
+
contract.events.add(eventName);
|
|
189
|
+
if (!contract.eventTypes.has(eventName)) {
|
|
190
|
+
contract.eventTypes.set(eventName, {});
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
if (normalizedSchema?.events?.byName instanceof Map) {
|
|
196
|
+
normalizedSchema.events.byName.forEach((eventSchema, eventName) => {
|
|
197
|
+
if (!eventName) {
|
|
198
|
+
return;
|
|
199
|
+
}
|
|
200
|
+
if (eventSchema && typeof eventSchema === "object" && !Array.isArray(eventSchema)) {
|
|
201
|
+
contract.eventTypes.set(eventName, eventSchema);
|
|
202
|
+
} else if (!contract.eventTypes.has(eventName)) {
|
|
203
|
+
contract.eventTypes.set(eventName, {});
|
|
204
|
+
}
|
|
205
|
+
});
|
|
206
|
+
}
|
|
207
|
+
};
|
|
208
|
+
|
|
209
|
+
const mergeContracts = (target, source) => {
|
|
210
|
+
source.forEach((contract, tagName) => {
|
|
211
|
+
const targetContract = getOrCreateContract(target, tagName);
|
|
212
|
+
contract.attrs.forEach((attr) => targetContract.attrs.add(attr));
|
|
213
|
+
contract.props.forEach((prop) => targetContract.props.add(prop));
|
|
214
|
+
contract.requiredProps.forEach((prop) => targetContract.requiredProps.add(prop));
|
|
215
|
+
contract.events.forEach((eventName) => targetContract.events.add(eventName));
|
|
216
|
+
contract.eventTypes.forEach((schema, eventName) => targetContract.eventTypes.set(eventName, schema));
|
|
217
|
+
contract.propTypes.forEach((schema, propName) => targetContract.propTypes.set(propName, schema));
|
|
218
|
+
contract.source.forEach((from) => targetContract.source.add(from));
|
|
219
|
+
});
|
|
220
|
+
};
|
|
221
|
+
|
|
222
|
+
const mergeRegistryMaps = (registries = []) => {
|
|
223
|
+
return registries.reduce((merged, registry) => {
|
|
224
|
+
mergeContracts(merged, registry);
|
|
225
|
+
return merged;
|
|
226
|
+
}, new Map());
|
|
227
|
+
};
|
|
228
|
+
|
|
229
|
+
const extractUiComponentContracts = ({ uiDir }) => {
|
|
230
|
+
const componentsDir = path.join(uiDir, "src", "components");
|
|
231
|
+
|
|
232
|
+
if (!existsSync(componentsDir)) {
|
|
233
|
+
return new Map();
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
const schemas = [];
|
|
237
|
+
const files = walkFiles([componentsDir]).filter((filePath) => filePath.endsWith(".schema.yaml"));
|
|
238
|
+
files.forEach((schemaFilePath) => {
|
|
239
|
+
try {
|
|
240
|
+
schemas.push(loadYaml(readFileSync(schemaFilePath, "utf8")) ?? {});
|
|
241
|
+
} catch {
|
|
242
|
+
// Ignore malformed schema files in registry generation.
|
|
243
|
+
}
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
return buildSchemaRegistry({
|
|
247
|
+
schemas,
|
|
248
|
+
source: "ui-components",
|
|
249
|
+
isValidComponentName: hasNonEmptyComponentName,
|
|
250
|
+
});
|
|
251
|
+
};
|
|
252
|
+
|
|
253
|
+
const parseProgramWithOxc = ({ sourceCode = "", filePath = "unknown.js" } = {}) => {
|
|
254
|
+
try {
|
|
255
|
+
const parsed = parseSync(filePath, sourceCode, { sourceType: "unambiguous" });
|
|
256
|
+
if (!parsed?.program?.body || !Array.isArray(parsed.program.body)) {
|
|
257
|
+
return null;
|
|
258
|
+
}
|
|
259
|
+
if (Array.isArray(parsed.errors) && parsed.errors.length > 0) {
|
|
260
|
+
return null;
|
|
261
|
+
}
|
|
262
|
+
return parsed.program;
|
|
263
|
+
} catch {
|
|
264
|
+
return null;
|
|
265
|
+
}
|
|
266
|
+
};
|
|
267
|
+
|
|
268
|
+
const getStringLiteralValue = (node) => {
|
|
269
|
+
if (!node || typeof node !== "object") {
|
|
270
|
+
return null;
|
|
271
|
+
}
|
|
272
|
+
if (node.type === "Literal" && typeof node.value === "string") {
|
|
273
|
+
return node.value;
|
|
274
|
+
}
|
|
275
|
+
if (node.type === "StringLiteral" && typeof node.value === "string") {
|
|
276
|
+
return node.value;
|
|
277
|
+
}
|
|
278
|
+
return null;
|
|
279
|
+
};
|
|
280
|
+
|
|
281
|
+
const getIdentifierName = (node) => {
|
|
282
|
+
if (!node || typeof node !== "object") {
|
|
283
|
+
return null;
|
|
284
|
+
}
|
|
285
|
+
if (node.type === "Identifier" && typeof node.name === "string" && node.name) {
|
|
286
|
+
return node.name;
|
|
287
|
+
}
|
|
288
|
+
return null;
|
|
289
|
+
};
|
|
290
|
+
|
|
291
|
+
const parseEntryImports = (entryCode, filePath = "entry.js") => {
|
|
292
|
+
const imports = new Map();
|
|
293
|
+
const program = parseProgramWithOxc({ sourceCode: entryCode, filePath });
|
|
294
|
+
if (program) {
|
|
295
|
+
program.body.forEach((statement) => {
|
|
296
|
+
if (statement?.type !== "ImportDeclaration") {
|
|
297
|
+
return;
|
|
298
|
+
}
|
|
299
|
+
const importPath = getStringLiteralValue(statement.source);
|
|
300
|
+
if (!importPath || !importPath.startsWith(".")) {
|
|
301
|
+
return;
|
|
302
|
+
}
|
|
303
|
+
const specifiers = Array.isArray(statement.specifiers) ? statement.specifiers : [];
|
|
304
|
+
specifiers.forEach((specifier) => {
|
|
305
|
+
const localName = getIdentifierName(specifier?.local);
|
|
306
|
+
if (localName) {
|
|
307
|
+
imports.set(localName, importPath);
|
|
308
|
+
}
|
|
309
|
+
});
|
|
310
|
+
});
|
|
311
|
+
return imports;
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
const importRegex = /import\s+([A-Za-z0-9_$]+)\s+from\s+['\"](\.[^'\"]+)['\"];?/g;
|
|
315
|
+
let match = importRegex.exec(entryCode);
|
|
316
|
+
while (match) {
|
|
317
|
+
imports.set(match[1], match[2]);
|
|
318
|
+
match = importRegex.exec(entryCode);
|
|
319
|
+
}
|
|
320
|
+
return imports;
|
|
321
|
+
};
|
|
322
|
+
|
|
323
|
+
const parseCustomElementDefines = (entryCode, filePath = "entry.js") => {
|
|
324
|
+
const definitions = [];
|
|
325
|
+
const program = parseProgramWithOxc({ sourceCode: entryCode, filePath });
|
|
326
|
+
if (program) {
|
|
327
|
+
program.body.forEach((statement) => {
|
|
328
|
+
if (statement?.type !== "ExpressionStatement") {
|
|
329
|
+
return;
|
|
330
|
+
}
|
|
331
|
+
const expression = statement.expression;
|
|
332
|
+
if (expression?.type !== "CallExpression") {
|
|
333
|
+
return;
|
|
334
|
+
}
|
|
335
|
+
const callee = expression.callee;
|
|
336
|
+
if (
|
|
337
|
+
callee?.type !== "MemberExpression"
|
|
338
|
+
|| callee.computed
|
|
339
|
+
|| getIdentifierName(callee.object) !== "customElements"
|
|
340
|
+
|| getIdentifierName(callee.property) !== "define"
|
|
341
|
+
) {
|
|
342
|
+
return;
|
|
343
|
+
}
|
|
344
|
+
const args = Array.isArray(expression.arguments) ? expression.arguments : [];
|
|
345
|
+
if (args.length < 2) {
|
|
346
|
+
return;
|
|
347
|
+
}
|
|
348
|
+
const tagName = getStringLiteralValue(args[0]);
|
|
349
|
+
if (!tagName) {
|
|
350
|
+
return;
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
const constructorArg = args[1];
|
|
354
|
+
let symbol = getIdentifierName(constructorArg);
|
|
355
|
+
if (!symbol && constructorArg?.type === "CallExpression") {
|
|
356
|
+
const calleeName = getIdentifierName(constructorArg.callee);
|
|
357
|
+
const firstArg = Array.isArray(constructorArg.arguments) ? constructorArg.arguments[0] : undefined;
|
|
358
|
+
if (
|
|
359
|
+
calleeName
|
|
360
|
+
&& (
|
|
361
|
+
firstArg === undefined
|
|
362
|
+
|| (firstArg?.type === "ObjectExpression" && Array.isArray(firstArg.properties) && firstArg.properties.length === 0)
|
|
363
|
+
)
|
|
364
|
+
) {
|
|
365
|
+
symbol = calleeName;
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
if (!symbol) {
|
|
369
|
+
return;
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
definitions.push({ tagName, symbol });
|
|
373
|
+
});
|
|
374
|
+
return definitions;
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
const defineRegex = /customElements\.define\(\s*['\"]([^'\"]+)['\"]\s*,\s*([A-Za-z0-9_$]+)\(\{\}\)\s*\)/g;
|
|
378
|
+
let match = defineRegex.exec(entryCode);
|
|
379
|
+
while (match) {
|
|
380
|
+
definitions.push({
|
|
381
|
+
tagName: match[1],
|
|
382
|
+
symbol: match[2],
|
|
383
|
+
});
|
|
384
|
+
match = defineRegex.exec(entryCode);
|
|
385
|
+
}
|
|
386
|
+
return definitions;
|
|
387
|
+
};
|
|
388
|
+
|
|
389
|
+
const collectObjectExpressionKeys = (node) => {
|
|
390
|
+
if (!node || node.type !== "ObjectExpression" || !Array.isArray(node.properties)) {
|
|
391
|
+
return [];
|
|
392
|
+
}
|
|
393
|
+
const keys = [];
|
|
394
|
+
node.properties.forEach((property) => {
|
|
395
|
+
if (!property || property.type !== "Property" || property.computed) {
|
|
396
|
+
return;
|
|
397
|
+
}
|
|
398
|
+
const keyName = getIdentifierName(property.key) || getStringLiteralValue(property.key);
|
|
399
|
+
if (keyName) {
|
|
400
|
+
keys.push(keyName);
|
|
401
|
+
}
|
|
402
|
+
});
|
|
403
|
+
return keys;
|
|
404
|
+
};
|
|
405
|
+
|
|
406
|
+
const extractTopLevelObjectKeys = ({ sourceCode, objectName, filePath = "source.js" }) => {
|
|
407
|
+
const program = parseProgramWithOxc({ sourceCode, filePath });
|
|
408
|
+
if (program) {
|
|
409
|
+
const keys = new Set();
|
|
410
|
+
const consumeDeclaration = (declaration) => {
|
|
411
|
+
if (!declaration || declaration.type !== "VariableDeclaration" || !Array.isArray(declaration.declarations)) {
|
|
412
|
+
return;
|
|
413
|
+
}
|
|
414
|
+
declaration.declarations.forEach((declarator) => {
|
|
415
|
+
if (getIdentifierName(declarator?.id) !== objectName) {
|
|
416
|
+
return;
|
|
417
|
+
}
|
|
418
|
+
collectObjectExpressionKeys(declarator.init).forEach((key) => keys.add(key));
|
|
419
|
+
});
|
|
420
|
+
};
|
|
421
|
+
|
|
422
|
+
program.body.forEach((statement) => {
|
|
423
|
+
if (statement?.type === "VariableDeclaration") {
|
|
424
|
+
consumeDeclaration(statement);
|
|
425
|
+
return;
|
|
426
|
+
}
|
|
427
|
+
if (statement?.type === "ExportNamedDeclaration") {
|
|
428
|
+
consumeDeclaration(statement.declaration);
|
|
429
|
+
}
|
|
430
|
+
});
|
|
431
|
+
|
|
432
|
+
if (keys.size > 0) {
|
|
433
|
+
return [...keys];
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
const marker = `const ${objectName} = {`;
|
|
438
|
+
const markerIndex = sourceCode.indexOf(marker);
|
|
439
|
+
if (markerIndex === -1) {
|
|
440
|
+
return [];
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
const startIndex = markerIndex + marker.length;
|
|
444
|
+
let braceDepth = 1;
|
|
445
|
+
let quote = null;
|
|
446
|
+
let i = startIndex;
|
|
447
|
+
|
|
448
|
+
while (i < sourceCode.length) {
|
|
449
|
+
const chr = sourceCode[i];
|
|
450
|
+
if (chr === "\"" || chr === "'") {
|
|
451
|
+
if (quote === chr) {
|
|
452
|
+
quote = null;
|
|
453
|
+
} else if (!quote) {
|
|
454
|
+
quote = chr;
|
|
455
|
+
}
|
|
456
|
+
i += 1;
|
|
457
|
+
continue;
|
|
458
|
+
}
|
|
459
|
+
if (!quote) {
|
|
460
|
+
if (chr === "{") braceDepth += 1;
|
|
461
|
+
if (chr === "}") braceDepth -= 1;
|
|
462
|
+
if (braceDepth === 0) {
|
|
463
|
+
break;
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
i += 1;
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
const body = sourceCode.slice(startIndex, i);
|
|
470
|
+
const keys = [];
|
|
471
|
+
let depth = 0;
|
|
472
|
+
quote = null;
|
|
473
|
+
let buffer = "";
|
|
474
|
+
|
|
475
|
+
for (let idx = 0; idx < body.length; idx += 1) {
|
|
476
|
+
const chr = body[idx];
|
|
477
|
+
if (chr === "\"" || chr === "'") {
|
|
478
|
+
if (quote === chr) {
|
|
479
|
+
quote = null;
|
|
480
|
+
} else if (!quote) {
|
|
481
|
+
quote = chr;
|
|
482
|
+
}
|
|
483
|
+
buffer += chr;
|
|
484
|
+
continue;
|
|
485
|
+
}
|
|
486
|
+
if (quote) {
|
|
487
|
+
buffer += chr;
|
|
488
|
+
continue;
|
|
489
|
+
}
|
|
490
|
+
if (chr === "{") {
|
|
491
|
+
depth += 1;
|
|
492
|
+
buffer += chr;
|
|
493
|
+
continue;
|
|
494
|
+
}
|
|
495
|
+
if (chr === "}") {
|
|
496
|
+
depth -= 1;
|
|
497
|
+
buffer += chr;
|
|
498
|
+
continue;
|
|
499
|
+
}
|
|
500
|
+
if (depth === 0 && chr === ",") {
|
|
501
|
+
const pair = buffer.trim();
|
|
502
|
+
if (pair) {
|
|
503
|
+
const keyMatch = pair.match(/^['"]?([A-Za-z0-9_-]+)['"]?\s*:/);
|
|
504
|
+
if (keyMatch) {
|
|
505
|
+
keys.push(keyMatch[1]);
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
buffer = "";
|
|
509
|
+
continue;
|
|
510
|
+
}
|
|
511
|
+
buffer += chr;
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
const lastPair = buffer.trim();
|
|
515
|
+
if (lastPair) {
|
|
516
|
+
const keyMatch = lastPair.match(/^['"]?([A-Za-z0-9_-]+)['"]?\s*:/);
|
|
517
|
+
if (keyMatch) {
|
|
518
|
+
keys.push(keyMatch[1]);
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
return keys;
|
|
523
|
+
};
|
|
524
|
+
|
|
525
|
+
const collectBracketSelectorAttrs = ({ sourceText = "", attrs }) => {
|
|
526
|
+
if (!sourceText || !(attrs instanceof Set)) {
|
|
527
|
+
return;
|
|
528
|
+
}
|
|
529
|
+
const hostSelectorRegex = /\[([a-zA-Z][a-zA-Z0-9-]*)\b(?:=[^\]]*)?\]/g;
|
|
530
|
+
let match = hostSelectorRegex.exec(sourceText);
|
|
531
|
+
while (match) {
|
|
532
|
+
attrs.add(match[1]);
|
|
533
|
+
match = hostSelectorRegex.exec(sourceText);
|
|
534
|
+
}
|
|
535
|
+
};
|
|
536
|
+
|
|
537
|
+
const extractPrimitiveAttrsFromSourceRegexLegacy = (sourceCode = "") => {
|
|
538
|
+
const attrs = new Set();
|
|
539
|
+
const hostSelectorRegex = /\[([a-zA-Z][a-zA-Z0-9-]*)\b(?:=[^\]]*)?\]/g;
|
|
540
|
+
const attrCallRegex = /(?:hasAttribute|getAttribute|setAttribute|removeAttribute)\(\s*['"]([a-zA-Z][a-zA-Z0-9-]*)['"]/g;
|
|
541
|
+
|
|
542
|
+
let match = hostSelectorRegex.exec(sourceCode);
|
|
543
|
+
while (match) {
|
|
544
|
+
attrs.add(match[1]);
|
|
545
|
+
match = hostSelectorRegex.exec(sourceCode);
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
match = attrCallRegex.exec(sourceCode);
|
|
549
|
+
while (match) {
|
|
550
|
+
attrs.add(match[1]);
|
|
551
|
+
match = attrCallRegex.exec(sourceCode);
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
return attrs;
|
|
555
|
+
};
|
|
556
|
+
|
|
557
|
+
const extractPrimitiveAttrsFromSource = (sourceCode = "", filePath = "primitive.js") => {
|
|
558
|
+
const program = parseProgramWithOxc({ sourceCode, filePath });
|
|
559
|
+
if (!program) {
|
|
560
|
+
return extractPrimitiveAttrsFromSourceRegexLegacy(sourceCode);
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
const attrs = new Set();
|
|
564
|
+
const visit = (node) => {
|
|
565
|
+
if (Array.isArray(node)) {
|
|
566
|
+
node.forEach((item) => visit(item));
|
|
567
|
+
return;
|
|
568
|
+
}
|
|
569
|
+
if (!node || typeof node !== "object") {
|
|
570
|
+
return;
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
if (node.type === "CallExpression") {
|
|
574
|
+
const callee = node.callee;
|
|
575
|
+
if (callee?.type === "MemberExpression" && !callee.computed) {
|
|
576
|
+
const methodName = getIdentifierName(callee.property);
|
|
577
|
+
if (["hasAttribute", "getAttribute", "setAttribute", "removeAttribute"].includes(methodName)) {
|
|
578
|
+
const firstArg = Array.isArray(node.arguments) ? node.arguments[0] : undefined;
|
|
579
|
+
const attrName = getStringLiteralValue(firstArg);
|
|
580
|
+
if (attrName && /^[a-zA-Z][a-zA-Z0-9-]*$/u.test(attrName)) {
|
|
581
|
+
attrs.add(attrName);
|
|
582
|
+
}
|
|
583
|
+
}
|
|
584
|
+
}
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
if (node.type === "Literal" || node.type === "StringLiteral") {
|
|
588
|
+
const value = getStringLiteralValue(node);
|
|
589
|
+
if (value) {
|
|
590
|
+
collectBracketSelectorAttrs({ sourceText: value, attrs });
|
|
591
|
+
}
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
if (node.type === "TemplateLiteral" && Array.isArray(node.quasis)) {
|
|
595
|
+
node.quasis.forEach((quasi) => {
|
|
596
|
+
const raw = quasi?.value?.raw;
|
|
597
|
+
const cooked = quasi?.value?.cooked;
|
|
598
|
+
if (typeof raw === "string") {
|
|
599
|
+
collectBracketSelectorAttrs({ sourceText: raw, attrs });
|
|
600
|
+
} else if (typeof cooked === "string") {
|
|
601
|
+
collectBracketSelectorAttrs({ sourceText: cooked, attrs });
|
|
602
|
+
}
|
|
603
|
+
});
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
Object.values(node).forEach((value) => visit(value));
|
|
607
|
+
};
|
|
608
|
+
|
|
609
|
+
visit(program.body);
|
|
610
|
+
return attrs;
|
|
611
|
+
};
|
|
612
|
+
|
|
613
|
+
const expandWithResponsiveAndHover = (baseKeys = []) => {
|
|
614
|
+
const breakpoints = ["sm", "md", "lg", "xl"];
|
|
615
|
+
const result = new Set();
|
|
616
|
+
baseKeys.forEach((key) => {
|
|
617
|
+
result.add(key);
|
|
618
|
+
result.add(`h-${key}`);
|
|
619
|
+
breakpoints.forEach((bp) => {
|
|
620
|
+
result.add(`${bp}-${key}`);
|
|
621
|
+
result.add(`${bp}-h-${key}`);
|
|
622
|
+
});
|
|
623
|
+
});
|
|
624
|
+
return result;
|
|
625
|
+
};
|
|
626
|
+
|
|
627
|
+
const buildGlobalStyleAttrs = ({ uiDir }) => {
|
|
628
|
+
const attrs = new Set();
|
|
629
|
+
const commonPath = path.join(uiDir, "src", "common.js");
|
|
630
|
+
const stylesDir = path.join(uiDir, "src", "styles");
|
|
631
|
+
|
|
632
|
+
if (existsSync(commonPath)) {
|
|
633
|
+
const commonSource = readFileSync(commonPath, "utf8");
|
|
634
|
+
const styleMapKeys = extractTopLevelObjectKeys({
|
|
635
|
+
sourceCode: commonSource,
|
|
636
|
+
objectName: "styleMap",
|
|
637
|
+
filePath: commonPath,
|
|
638
|
+
});
|
|
639
|
+
expandWithResponsiveAndHover(styleMapKeys).forEach((attr) => attrs.add(attr));
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
if (existsSync(stylesDir)) {
|
|
643
|
+
const styleFiles = walkFiles([stylesDir]).filter((filePath) => filePath.endsWith(".js"));
|
|
644
|
+
const styleKeys = new Set();
|
|
645
|
+
styleFiles.forEach((styleFilePath) => {
|
|
646
|
+
const styleSource = readFileSync(styleFilePath, "utf8");
|
|
647
|
+
extractTopLevelObjectKeys({
|
|
648
|
+
sourceCode: styleSource,
|
|
649
|
+
objectName: "styles",
|
|
650
|
+
filePath: styleFilePath,
|
|
651
|
+
}).forEach((key) => styleKeys.add(key));
|
|
652
|
+
extractPrimitiveAttrsFromSource(styleSource, styleFilePath).forEach((key) => styleKeys.add(key));
|
|
653
|
+
});
|
|
654
|
+
expandWithResponsiveAndHover([...styleKeys]).forEach((attr) => attrs.add(attr));
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
return attrs;
|
|
658
|
+
};
|
|
659
|
+
|
|
660
|
+
const getPrimitiveContractsFromEntry = async ({ entryPath, globalStyleAttrs = new Set() }) => {
|
|
661
|
+
const result = new Map();
|
|
662
|
+
if (!existsSync(entryPath)) {
|
|
663
|
+
return result;
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
const entryCode = readFileSync(entryPath, "utf8");
|
|
667
|
+
const imports = parseEntryImports(entryCode, entryPath);
|
|
668
|
+
const definitions = parseCustomElementDefines(entryCode, entryPath);
|
|
669
|
+
|
|
670
|
+
ensureRuntimeStubs();
|
|
671
|
+
|
|
672
|
+
for (const definition of definitions) {
|
|
673
|
+
const importPath = imports.get(definition.symbol);
|
|
674
|
+
if (!importPath || !importPath.includes("/primitives/")) {
|
|
675
|
+
continue;
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
const absolutePrimitivePath = path.resolve(path.dirname(entryPath), importPath);
|
|
679
|
+
const primitiveUrl = pathToFileURL(absolutePrimitivePath).href;
|
|
680
|
+
|
|
681
|
+
try {
|
|
682
|
+
const primitiveSource = readFileSync(absolutePrimitivePath, "utf8");
|
|
683
|
+
const primitiveModule = await import(primitiveUrl);
|
|
684
|
+
if (typeof primitiveModule.default !== "function") {
|
|
685
|
+
continue;
|
|
686
|
+
}
|
|
687
|
+
const primitiveClass = primitiveModule.default({});
|
|
688
|
+
const observed = Array.isArray(primitiveClass?.observedAttributes)
|
|
689
|
+
? primitiveClass.observedAttributes
|
|
690
|
+
: [];
|
|
691
|
+
|
|
692
|
+
const contract = getOrCreateContract(result, definition.tagName);
|
|
693
|
+
contract.source.add("ui-primitives");
|
|
694
|
+
observed.forEach((attr) => {
|
|
695
|
+
contract.attrs.add(attr);
|
|
696
|
+
});
|
|
697
|
+
extractPrimitiveAttrsFromSource(primitiveSource, absolutePrimitivePath).forEach((attr) => {
|
|
698
|
+
contract.attrs.add(attr);
|
|
699
|
+
});
|
|
700
|
+
globalStyleAttrs.forEach((attr) => {
|
|
701
|
+
contract.attrs.add(attr);
|
|
702
|
+
});
|
|
703
|
+
} catch {
|
|
704
|
+
// Ignore primitive loading failures in registry generation.
|
|
705
|
+
}
|
|
706
|
+
}
|
|
707
|
+
|
|
708
|
+
return result;
|
|
709
|
+
};
|
|
710
|
+
|
|
711
|
+
const resolveBundledUiDir = () => {
|
|
712
|
+
const currentDir = path.dirname(fileURLToPath(import.meta.url));
|
|
713
|
+
return path.resolve(currentDir, "../../../rettangoli-ui");
|
|
714
|
+
};
|
|
715
|
+
|
|
716
|
+
const collectPrimitiveContractsFromEntries = async ({ uiDir, globalStyleAttrs }) => {
|
|
717
|
+
const result = new Map();
|
|
718
|
+
const entryNames = ["entry-iife-ui.js", "entry-iife-layout.js"];
|
|
719
|
+
|
|
720
|
+
for (const entryName of entryNames) {
|
|
721
|
+
const primitiveContracts = await getPrimitiveContractsFromEntry({
|
|
722
|
+
entryPath: path.join(uiDir, "src", entryName),
|
|
723
|
+
globalStyleAttrs,
|
|
724
|
+
});
|
|
725
|
+
mergeContracts(result, primitiveContracts);
|
|
726
|
+
}
|
|
727
|
+
|
|
728
|
+
return result;
|
|
729
|
+
};
|
|
730
|
+
|
|
731
|
+
export const buildUiRegistry = async ({ workspaceRoot = process.cwd() } = {}) => {
|
|
732
|
+
const uiCandidates = [
|
|
733
|
+
path.resolve(workspaceRoot, "packages", "rettangoli-ui"),
|
|
734
|
+
resolveBundledUiDir(),
|
|
735
|
+
];
|
|
736
|
+
|
|
737
|
+
const uiDir = uiCandidates.find((candidate) => existsSync(candidate));
|
|
738
|
+
if (!uiDir) {
|
|
739
|
+
return new Map();
|
|
740
|
+
}
|
|
741
|
+
|
|
742
|
+
const componentContracts = extractUiComponentContracts({ uiDir });
|
|
743
|
+
const globalStyleAttrs = buildGlobalStyleAttrs({ uiDir });
|
|
744
|
+
const primitiveContracts = await collectPrimitiveContractsFromEntries({
|
|
745
|
+
uiDir,
|
|
746
|
+
globalStyleAttrs,
|
|
747
|
+
});
|
|
748
|
+
|
|
749
|
+
return mergeRegistryMaps([componentContracts, primitiveContracts]);
|
|
750
|
+
};
|
|
751
|
+
|
|
752
|
+
export const buildProjectSchemaRegistry = ({ models = [] }) => {
|
|
753
|
+
const registryMap = new Map();
|
|
754
|
+
|
|
755
|
+
models.forEach((model) => {
|
|
756
|
+
const normalizedSchema = model?.schema?.normalized;
|
|
757
|
+
if (!normalizedSchema) {
|
|
758
|
+
return;
|
|
759
|
+
}
|
|
760
|
+
|
|
761
|
+
const componentName = normalizedSchema.componentName;
|
|
762
|
+
if (!hasNonBlankComponentName(componentName)) {
|
|
763
|
+
return;
|
|
764
|
+
}
|
|
765
|
+
|
|
766
|
+
const contract = getOrCreateContract(registryMap, componentName);
|
|
767
|
+
contract.source.add("project-schema");
|
|
768
|
+
addNormalizedModelSchemaToContract({ contract, normalizedSchema });
|
|
769
|
+
});
|
|
770
|
+
|
|
771
|
+
return registryMap;
|
|
772
|
+
};
|
|
773
|
+
|
|
774
|
+
export const buildMergedRegistry = async ({ models = [], workspaceRoot = process.cwd() } = {}) => {
|
|
775
|
+
const uiRegistry = await buildUiRegistry({ workspaceRoot });
|
|
776
|
+
const projectRegistry = buildProjectSchemaRegistry({ models });
|
|
777
|
+
|
|
778
|
+
return mergeRegistryMaps([uiRegistry, projectRegistry]);
|
|
779
|
+
};
|