zemdomu 1.1.0 → 1.1.3
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/out/component-analyzer.js +461 -454
- package/out/linter.js +164 -138
- package/out/project-linter.js +87 -87
- package/out/rules/enforceListNesting.js +81 -78
- package/out/rules/noTabindexGreaterThanZero.js +33 -0
- package/out/rules/preventEmptyInlineTags.js +85 -83
- package/out/rules/requireLinkText.js +86 -86
- package/package.json +2 -2
|
@@ -1,454 +1,461 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
-
if (k2 === undefined) k2 = k;
|
|
4
|
-
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
-
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
-
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
-
}
|
|
8
|
-
Object.defineProperty(o, k2, desc);
|
|
9
|
-
}) : (function(o, m, k, k2) {
|
|
10
|
-
if (k2 === undefined) k2 = k;
|
|
11
|
-
o[k2] = m[k];
|
|
12
|
-
}));
|
|
13
|
-
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
-
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
-
}) : function(o, v) {
|
|
16
|
-
o["default"] = v;
|
|
17
|
-
});
|
|
18
|
-
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
-
var ownKeys = function(o) {
|
|
20
|
-
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
-
var ar = [];
|
|
22
|
-
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
-
return ar;
|
|
24
|
-
};
|
|
25
|
-
return ownKeys(o);
|
|
26
|
-
};
|
|
27
|
-
return function (mod) {
|
|
28
|
-
if (mod && mod.__esModule) return mod;
|
|
29
|
-
var result = {};
|
|
30
|
-
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
-
__setModuleDefault(result, mod);
|
|
32
|
-
return result;
|
|
33
|
-
};
|
|
34
|
-
})();
|
|
35
|
-
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
36
|
-
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
37
|
-
};
|
|
38
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
39
|
-
exports.ComponentAnalyzer = void 0;
|
|
40
|
-
const fs = __importStar(require("fs/promises"));
|
|
41
|
-
const path = __importStar(require("path"));
|
|
42
|
-
const parser_1 = require("@babel/parser");
|
|
43
|
-
const traverse_1 = __importDefault(require("@babel/traverse"));
|
|
44
|
-
const t = __importStar(require("@babel/types"));
|
|
45
|
-
const component_path_resolver_1 = require("./component-path-resolver");
|
|
46
|
-
class ComponentAnalyzer {
|
|
47
|
-
constructor(options, perf) {
|
|
48
|
-
this.componentRegistry = new Map();
|
|
49
|
-
this.importToComponentMap = new Map();
|
|
50
|
-
this.processingComponentStack = new Set(); // To prevent circular references
|
|
51
|
-
this.resolver = new component_path_resolver_1.ComponentPathResolver();
|
|
52
|
-
this.options = options;
|
|
53
|
-
this.perf = perf;
|
|
54
|
-
}
|
|
55
|
-
async analyzeFile(filePath) {
|
|
56
|
-
var _a;
|
|
57
|
-
const start = Date.now();
|
|
58
|
-
try {
|
|
59
|
-
const content = await fs.readFile(filePath, 'utf8');
|
|
60
|
-
if (!/\.(jsx|tsx)$/.test(filePath))
|
|
61
|
-
return null;
|
|
62
|
-
const { component, timings } = await this.extractComponentInfo(content, filePath);
|
|
63
|
-
timings.total = Date.now() - start;
|
|
64
|
-
(_a = this.perf) === null || _a === void 0 ? void 0 : _a.record(filePath, timings);
|
|
65
|
-
return component;
|
|
66
|
-
}
|
|
67
|
-
catch (e) {
|
|
68
|
-
console.error(`[ZemDomu] Error analyzing file ${filePath}:`, e);
|
|
69
|
-
return null;
|
|
70
|
-
}
|
|
71
|
-
}
|
|
72
|
-
async extractComponentInfo(content, filePath) {
|
|
73
|
-
var _a, _b;
|
|
74
|
-
const timings = {};
|
|
75
|
-
let t0 = Date.now();
|
|
76
|
-
const ast = (0, parser_1.parse)(content, { sourceType: 'module', plugins: ['typescript', 'jsx'] });
|
|
77
|
-
timings.parse = Date.now() - t0;
|
|
78
|
-
const componentName = path.basename(filePath, path.extname(filePath));
|
|
79
|
-
const componentDef = {
|
|
80
|
-
name: componentName,
|
|
81
|
-
filePath,
|
|
82
|
-
issues: new Map(),
|
|
83
|
-
usesComponents: [],
|
|
84
|
-
headings: []
|
|
85
|
-
};
|
|
86
|
-
// Track imported components
|
|
87
|
-
const importedComponents = new Map();
|
|
88
|
-
// Collect imports
|
|
89
|
-
t0 = Date.now();
|
|
90
|
-
(0, traverse_1.default)(ast, {
|
|
91
|
-
ImportDeclaration(path) {
|
|
92
|
-
const source = path.node.source.value;
|
|
93
|
-
path.node.specifiers.forEach(spec => {
|
|
94
|
-
if (t.isImportSpecifier(spec) || t.isImportDefaultSpecifier(spec)) {
|
|
95
|
-
const name = spec.local.name;
|
|
96
|
-
if (/^[A-Z]/.test(name)) {
|
|
97
|
-
importedComponents.set(name, source);
|
|
98
|
-
}
|
|
99
|
-
}
|
|
100
|
-
});
|
|
101
|
-
}
|
|
102
|
-
});
|
|
103
|
-
timings.collectImports = Date.now() - t0;
|
|
104
|
-
// Collect JSX usages and headings
|
|
105
|
-
t0 = Date.now();
|
|
106
|
-
(0, traverse_1.default)(ast, {
|
|
107
|
-
JSXElement(path) {
|
|
108
|
-
var _a, _b;
|
|
109
|
-
const elt = path.node.openingElement.name;
|
|
110
|
-
if (t.isJSXIdentifier(elt)) {
|
|
111
|
-
const name = elt.name;
|
|
112
|
-
const tag = name.toLowerCase();
|
|
113
|
-
// Record headings
|
|
114
|
-
if (/^h[1-6]$/.test(tag)) {
|
|
115
|
-
const level = parseInt(tag.charAt(1), 10);
|
|
116
|
-
const loc = (_a = elt.loc) === null || _a === void 0 ? void 0 : _a.start;
|
|
117
|
-
if (loc) {
|
|
118
|
-
componentDef.headings.push({
|
|
119
|
-
level,
|
|
120
|
-
line: loc.line - 1,
|
|
121
|
-
column: loc.column,
|
|
122
|
-
filePath
|
|
123
|
-
});
|
|
124
|
-
}
|
|
125
|
-
}
|
|
126
|
-
// Record component usage (only for capitalized components)
|
|
127
|
-
if (/^[A-Z]/.test(name)) {
|
|
128
|
-
const existingRef = componentDef.usesComponents.find(c => c.name === name);
|
|
129
|
-
const loc = (_b = elt.loc) === null || _b === void 0 ? void 0 : _b.start;
|
|
130
|
-
const location = loc ? { line: loc.line - 1, column: loc.column } : { line: 0, column: 0 };
|
|
131
|
-
if (existingRef) {
|
|
132
|
-
// Add usage location to existing reference
|
|
133
|
-
existingRef.usageLocations.push(location);
|
|
134
|
-
}
|
|
135
|
-
else {
|
|
136
|
-
// Create new component reference
|
|
137
|
-
const rawImportPath = importedComponents.get(name) || null;
|
|
138
|
-
componentDef.usesComponents.push({
|
|
139
|
-
name,
|
|
140
|
-
path: null, // Will be resolved later
|
|
141
|
-
rawImportPath,
|
|
142
|
-
sourceLocation: location,
|
|
143
|
-
usageLocations: [location]
|
|
144
|
-
});
|
|
145
|
-
}
|
|
146
|
-
}
|
|
147
|
-
}
|
|
148
|
-
}
|
|
149
|
-
});
|
|
150
|
-
timings.jsxCollect = Date.now() - t0;
|
|
151
|
-
// Store import mappings for this file
|
|
152
|
-
this.importToComponentMap.set(filePath, importedComponents);
|
|
153
|
-
// Resolve import paths
|
|
154
|
-
t0 = Date.now();
|
|
155
|
-
for (const ref of componentDef.usesComponents) {
|
|
156
|
-
if (ref.rawImportPath) {
|
|
157
|
-
const t1 = Date.now();
|
|
158
|
-
ref.path = await this.resolveComponentPath(ref.rawImportPath, filePath);
|
|
159
|
-
timings[`resolve:${ref.rawImportPath}`] = Date.now() - t1;
|
|
160
|
-
}
|
|
161
|
-
}
|
|
162
|
-
timings.resolvePaths = Date.now() - t0;
|
|
163
|
-
// Check for heading order issues within this component
|
|
164
|
-
t0 = Date.now();
|
|
165
|
-
if ((_a = this.options.rules) === null || _a === void 0 ? void 0 : _a.enforceHeadingOrder) {
|
|
166
|
-
let lastHeadingLevel = 0;
|
|
167
|
-
const sortedHeadings = [...componentDef.headings].sort((a, b) => {
|
|
168
|
-
if (a.line !== b.line)
|
|
169
|
-
return a.line - b.line;
|
|
170
|
-
return a.column - b.column;
|
|
171
|
-
});
|
|
172
|
-
for (const heading of sortedHeadings) {
|
|
173
|
-
if (lastHeadingLevel && heading.level > lastHeadingLevel + 1) {
|
|
174
|
-
componentDef.issues.set('enforceHeadingOrder', [
|
|
175
|
-
...(componentDef.issues.get('enforceHeadingOrder') || []),
|
|
176
|
-
{
|
|
177
|
-
line: heading.line,
|
|
178
|
-
column: heading.column,
|
|
179
|
-
message: `Heading level skipped: <h${heading.level}> after <h${lastHeadingLevel}>`,
|
|
180
|
-
rule: 'enforceHeadingOrder'
|
|
181
|
-
}
|
|
182
|
-
]);
|
|
183
|
-
}
|
|
184
|
-
lastHeadingLevel = heading.level;
|
|
185
|
-
}
|
|
186
|
-
}
|
|
187
|
-
// Synthetic single-H1 issues
|
|
188
|
-
if ((_b = this.options.rules) === null || _b === void 0 ? void 0 : _b.singleH1) {
|
|
189
|
-
const h1Results = componentDef.headings
|
|
190
|
-
.filter(h => h.level === 1)
|
|
191
|
-
.map(h => ({ line: h.line, column: h.column, message: '<h1>', rule: 'singleH1' }));
|
|
192
|
-
if (h1Results.length > 0) {
|
|
193
|
-
componentDef.issues.set('singleH1', h1Results);
|
|
194
|
-
}
|
|
195
|
-
}
|
|
196
|
-
timings.headingAnalysis = Date.now() - t0;
|
|
197
|
-
// Register component
|
|
198
|
-
this.componentRegistry.set(filePath, componentDef);
|
|
199
|
-
return { component: componentDef, timings };
|
|
200
|
-
}
|
|
201
|
-
async resolveComponentPath(importPath, currentPath) {
|
|
202
|
-
return this.resolver.resolve(importPath, currentPath);
|
|
203
|
-
}
|
|
204
|
-
registerComponent(component, issues) {
|
|
205
|
-
for (const issue of issues) {
|
|
206
|
-
const rule = issue.rule || this.getRuleType(issue.message);
|
|
207
|
-
if (!component.issues.has(rule))
|
|
208
|
-
component.issues.set(rule, []);
|
|
209
|
-
component.issues.get(rule).push(issue);
|
|
210
|
-
}
|
|
211
|
-
this.componentRegistry.set(component.filePath, component);
|
|
212
|
-
}
|
|
213
|
-
getRuleType(msg) {
|
|
214
|
-
if (msg.includes('<h1>'))
|
|
215
|
-
return 'singleH1';
|
|
216
|
-
if (msg.includes('Heading level'))
|
|
217
|
-
return 'enforceHeadingOrder';
|
|
218
|
-
if (msg.includes('<section>'))
|
|
219
|
-
return 'requireSectionHeading';
|
|
220
|
-
if (msg.includes('<img>'))
|
|
221
|
-
return 'requireAltText';
|
|
222
|
-
if (msg.includes('missing title attribute'))
|
|
223
|
-
return 'requireIframeTitle';
|
|
224
|
-
if (msg.includes('missing alt attribute') && msg.includes('input type="image"'))
|
|
225
|
-
return 'requireImageInputAlt';
|
|
226
|
-
if (msg.includes('<html>'))
|
|
227
|
-
return 'requireHtmlLang';
|
|
228
|
-
if (msg.includes('<button>'))
|
|
229
|
-
return 'requireButtonText';
|
|
230
|
-
if (msg.includes('Form control'))
|
|
231
|
-
return 'requireLabelForFormControls';
|
|
232
|
-
if (msg.includes('<li>'))
|
|
233
|
-
return 'enforceListNesting';
|
|
234
|
-
if (msg.includes('<a>'))
|
|
235
|
-
return msg.includes('href') ? 'requireHrefOnAnchors' : 'requireLinkText';
|
|
236
|
-
if (msg.includes('<table>'))
|
|
237
|
-
return 'requireTableCaption';
|
|
238
|
-
if (msg.includes('should not be empty'))
|
|
239
|
-
return 'preventEmptyInlineTags';
|
|
240
|
-
return 'other';
|
|
241
|
-
}
|
|
242
|
-
analyzeComponentTree() {
|
|
243
|
-
var _a;
|
|
244
|
-
const results = [];
|
|
245
|
-
const cross = (_a = this.options.crossComponentAnalysis) !== null && _a !== void 0 ? _a : true;
|
|
246
|
-
const rules = this.options.rules || {};
|
|
247
|
-
if (!cross)
|
|
248
|
-
return results;
|
|
249
|
-
if (rules.singleH1)
|
|
250
|
-
this.findCrossComponentH1Issues(results);
|
|
251
|
-
if (rules.enforceHeadingOrder)
|
|
252
|
-
this.findCrossComponentHeadingOrderIssues(results);
|
|
253
|
-
return results;
|
|
254
|
-
}
|
|
255
|
-
findCrossComponentH1Issues(results) {
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
const
|
|
263
|
-
if (
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
}
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
this.
|
|
385
|
-
const
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
const
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
const
|
|
442
|
-
const
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
36
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
37
|
+
};
|
|
38
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
39
|
+
exports.ComponentAnalyzer = void 0;
|
|
40
|
+
const fs = __importStar(require("fs/promises"));
|
|
41
|
+
const path = __importStar(require("path"));
|
|
42
|
+
const parser_1 = require("@babel/parser");
|
|
43
|
+
const traverse_1 = __importDefault(require("@babel/traverse"));
|
|
44
|
+
const t = __importStar(require("@babel/types"));
|
|
45
|
+
const component_path_resolver_1 = require("./component-path-resolver");
|
|
46
|
+
class ComponentAnalyzer {
|
|
47
|
+
constructor(options, perf) {
|
|
48
|
+
this.componentRegistry = new Map();
|
|
49
|
+
this.importToComponentMap = new Map();
|
|
50
|
+
this.processingComponentStack = new Set(); // To prevent circular references
|
|
51
|
+
this.resolver = new component_path_resolver_1.ComponentPathResolver();
|
|
52
|
+
this.options = options;
|
|
53
|
+
this.perf = perf;
|
|
54
|
+
}
|
|
55
|
+
async analyzeFile(filePath) {
|
|
56
|
+
var _a;
|
|
57
|
+
const start = Date.now();
|
|
58
|
+
try {
|
|
59
|
+
const content = await fs.readFile(filePath, 'utf8');
|
|
60
|
+
if (!/\.(jsx|tsx)$/.test(filePath))
|
|
61
|
+
return null;
|
|
62
|
+
const { component, timings } = await this.extractComponentInfo(content, filePath);
|
|
63
|
+
timings.total = Date.now() - start;
|
|
64
|
+
(_a = this.perf) === null || _a === void 0 ? void 0 : _a.record(filePath, timings);
|
|
65
|
+
return component;
|
|
66
|
+
}
|
|
67
|
+
catch (e) {
|
|
68
|
+
console.error(`[ZemDomu] Error analyzing file ${filePath}:`, e);
|
|
69
|
+
return null;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
async extractComponentInfo(content, filePath) {
|
|
73
|
+
var _a, _b;
|
|
74
|
+
const timings = {};
|
|
75
|
+
let t0 = Date.now();
|
|
76
|
+
const ast = (0, parser_1.parse)(content, { sourceType: 'module', plugins: ['typescript', 'jsx'] });
|
|
77
|
+
timings.parse = Date.now() - t0;
|
|
78
|
+
const componentName = path.basename(filePath, path.extname(filePath));
|
|
79
|
+
const componentDef = {
|
|
80
|
+
name: componentName,
|
|
81
|
+
filePath,
|
|
82
|
+
issues: new Map(),
|
|
83
|
+
usesComponents: [],
|
|
84
|
+
headings: []
|
|
85
|
+
};
|
|
86
|
+
// Track imported components
|
|
87
|
+
const importedComponents = new Map();
|
|
88
|
+
// Collect imports
|
|
89
|
+
t0 = Date.now();
|
|
90
|
+
(0, traverse_1.default)(ast, {
|
|
91
|
+
ImportDeclaration(path) {
|
|
92
|
+
const source = path.node.source.value;
|
|
93
|
+
path.node.specifiers.forEach(spec => {
|
|
94
|
+
if (t.isImportSpecifier(spec) || t.isImportDefaultSpecifier(spec)) {
|
|
95
|
+
const name = spec.local.name;
|
|
96
|
+
if (/^[A-Z]/.test(name)) {
|
|
97
|
+
importedComponents.set(name, source);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
});
|
|
103
|
+
timings.collectImports = Date.now() - t0;
|
|
104
|
+
// Collect JSX usages and headings
|
|
105
|
+
t0 = Date.now();
|
|
106
|
+
(0, traverse_1.default)(ast, {
|
|
107
|
+
JSXElement(path) {
|
|
108
|
+
var _a, _b;
|
|
109
|
+
const elt = path.node.openingElement.name;
|
|
110
|
+
if (t.isJSXIdentifier(elt)) {
|
|
111
|
+
const name = elt.name;
|
|
112
|
+
const tag = name.toLowerCase();
|
|
113
|
+
// Record headings
|
|
114
|
+
if (/^h[1-6]$/.test(tag)) {
|
|
115
|
+
const level = parseInt(tag.charAt(1), 10);
|
|
116
|
+
const loc = (_a = elt.loc) === null || _a === void 0 ? void 0 : _a.start;
|
|
117
|
+
if (loc) {
|
|
118
|
+
componentDef.headings.push({
|
|
119
|
+
level,
|
|
120
|
+
line: loc.line - 1,
|
|
121
|
+
column: loc.column,
|
|
122
|
+
filePath
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
// Record component usage (only for capitalized components)
|
|
127
|
+
if (/^[A-Z]/.test(name)) {
|
|
128
|
+
const existingRef = componentDef.usesComponents.find(c => c.name === name);
|
|
129
|
+
const loc = (_b = elt.loc) === null || _b === void 0 ? void 0 : _b.start;
|
|
130
|
+
const location = loc ? { line: loc.line - 1, column: loc.column } : { line: 0, column: 0 };
|
|
131
|
+
if (existingRef) {
|
|
132
|
+
// Add usage location to existing reference
|
|
133
|
+
existingRef.usageLocations.push(location);
|
|
134
|
+
}
|
|
135
|
+
else {
|
|
136
|
+
// Create new component reference
|
|
137
|
+
const rawImportPath = importedComponents.get(name) || null;
|
|
138
|
+
componentDef.usesComponents.push({
|
|
139
|
+
name,
|
|
140
|
+
path: null, // Will be resolved later
|
|
141
|
+
rawImportPath,
|
|
142
|
+
sourceLocation: location,
|
|
143
|
+
usageLocations: [location]
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
});
|
|
150
|
+
timings.jsxCollect = Date.now() - t0;
|
|
151
|
+
// Store import mappings for this file
|
|
152
|
+
this.importToComponentMap.set(filePath, importedComponents);
|
|
153
|
+
// Resolve import paths
|
|
154
|
+
t0 = Date.now();
|
|
155
|
+
for (const ref of componentDef.usesComponents) {
|
|
156
|
+
if (ref.rawImportPath) {
|
|
157
|
+
const t1 = Date.now();
|
|
158
|
+
ref.path = await this.resolveComponentPath(ref.rawImportPath, filePath);
|
|
159
|
+
timings[`resolve:${ref.rawImportPath}`] = Date.now() - t1;
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
timings.resolvePaths = Date.now() - t0;
|
|
163
|
+
// Check for heading order issues within this component
|
|
164
|
+
t0 = Date.now();
|
|
165
|
+
if ((_a = this.options.rules) === null || _a === void 0 ? void 0 : _a.enforceHeadingOrder) {
|
|
166
|
+
let lastHeadingLevel = 0;
|
|
167
|
+
const sortedHeadings = [...componentDef.headings].sort((a, b) => {
|
|
168
|
+
if (a.line !== b.line)
|
|
169
|
+
return a.line - b.line;
|
|
170
|
+
return a.column - b.column;
|
|
171
|
+
});
|
|
172
|
+
for (const heading of sortedHeadings) {
|
|
173
|
+
if (lastHeadingLevel && heading.level > lastHeadingLevel + 1) {
|
|
174
|
+
componentDef.issues.set('enforceHeadingOrder', [
|
|
175
|
+
...(componentDef.issues.get('enforceHeadingOrder') || []),
|
|
176
|
+
{
|
|
177
|
+
line: heading.line,
|
|
178
|
+
column: heading.column,
|
|
179
|
+
message: `Heading level skipped: <h${heading.level}> after <h${lastHeadingLevel}>`,
|
|
180
|
+
rule: 'enforceHeadingOrder'
|
|
181
|
+
}
|
|
182
|
+
]);
|
|
183
|
+
}
|
|
184
|
+
lastHeadingLevel = heading.level;
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
// Synthetic single-H1 issues
|
|
188
|
+
if ((_b = this.options.rules) === null || _b === void 0 ? void 0 : _b.singleH1) {
|
|
189
|
+
const h1Results = componentDef.headings
|
|
190
|
+
.filter(h => h.level === 1)
|
|
191
|
+
.map(h => ({ line: h.line, column: h.column, message: '<h1>', rule: 'singleH1' }));
|
|
192
|
+
if (h1Results.length > 0) {
|
|
193
|
+
componentDef.issues.set('singleH1', h1Results);
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
timings.headingAnalysis = Date.now() - t0;
|
|
197
|
+
// Register component
|
|
198
|
+
this.componentRegistry.set(filePath, componentDef);
|
|
199
|
+
return { component: componentDef, timings };
|
|
200
|
+
}
|
|
201
|
+
async resolveComponentPath(importPath, currentPath) {
|
|
202
|
+
return this.resolver.resolve(importPath, currentPath);
|
|
203
|
+
}
|
|
204
|
+
registerComponent(component, issues) {
|
|
205
|
+
for (const issue of issues) {
|
|
206
|
+
const rule = issue.rule || this.getRuleType(issue.message);
|
|
207
|
+
if (!component.issues.has(rule))
|
|
208
|
+
component.issues.set(rule, []);
|
|
209
|
+
component.issues.get(rule).push(issue);
|
|
210
|
+
}
|
|
211
|
+
this.componentRegistry.set(component.filePath, component);
|
|
212
|
+
}
|
|
213
|
+
getRuleType(msg) {
|
|
214
|
+
if (msg.includes('<h1>'))
|
|
215
|
+
return 'singleH1';
|
|
216
|
+
if (msg.includes('Heading level'))
|
|
217
|
+
return 'enforceHeadingOrder';
|
|
218
|
+
if (msg.includes('<section>'))
|
|
219
|
+
return 'requireSectionHeading';
|
|
220
|
+
if (msg.includes('<img>'))
|
|
221
|
+
return 'requireAltText';
|
|
222
|
+
if (msg.includes('missing title attribute'))
|
|
223
|
+
return 'requireIframeTitle';
|
|
224
|
+
if (msg.includes('missing alt attribute') && msg.includes('input type="image"'))
|
|
225
|
+
return 'requireImageInputAlt';
|
|
226
|
+
if (msg.includes('<html>'))
|
|
227
|
+
return 'requireHtmlLang';
|
|
228
|
+
if (msg.includes('<button>'))
|
|
229
|
+
return 'requireButtonText';
|
|
230
|
+
if (msg.includes('Form control'))
|
|
231
|
+
return 'requireLabelForFormControls';
|
|
232
|
+
if (msg.includes('<li>'))
|
|
233
|
+
return 'enforceListNesting';
|
|
234
|
+
if (msg.includes('<a>'))
|
|
235
|
+
return msg.includes('href') ? 'requireHrefOnAnchors' : 'requireLinkText';
|
|
236
|
+
if (msg.includes('<table>'))
|
|
237
|
+
return 'requireTableCaption';
|
|
238
|
+
if (msg.includes('should not be empty'))
|
|
239
|
+
return 'preventEmptyInlineTags';
|
|
240
|
+
return 'other';
|
|
241
|
+
}
|
|
242
|
+
analyzeComponentTree() {
|
|
243
|
+
var _a;
|
|
244
|
+
const results = [];
|
|
245
|
+
const cross = (_a = this.options.crossComponentAnalysis) !== null && _a !== void 0 ? _a : true;
|
|
246
|
+
const rules = this.options.rules || {};
|
|
247
|
+
if (!cross)
|
|
248
|
+
return results;
|
|
249
|
+
if (rules.singleH1)
|
|
250
|
+
this.findCrossComponentH1Issues(results);
|
|
251
|
+
if (rules.enforceHeadingOrder)
|
|
252
|
+
this.findCrossComponentHeadingOrderIssues(results);
|
|
253
|
+
return results;
|
|
254
|
+
}
|
|
255
|
+
findCrossComponentH1Issues(results) {
|
|
256
|
+
var _a;
|
|
257
|
+
const entryPoints = this.findEntryPoints();
|
|
258
|
+
for (const entry of entryPoints) {
|
|
259
|
+
const comps = this.findComponentsWithRule(entry, 'singleH1');
|
|
260
|
+
if (comps.length > 1) {
|
|
261
|
+
for (let i = 1; i < comps.length; i++) {
|
|
262
|
+
const comp = comps[i];
|
|
263
|
+
if (!comp || !comp.name) {
|
|
264
|
+
console.error('[ZemDomu] Missing component or name during cross-component analysis', comp);
|
|
265
|
+
continue;
|
|
266
|
+
}
|
|
267
|
+
const ref = this.findReferenceForComp(entry, comp.filePath);
|
|
268
|
+
if (ref) {
|
|
269
|
+
// Use first JSX usage location instead of import location
|
|
270
|
+
const location = ref.usageLocations[0] || ref.sourceLocation;
|
|
271
|
+
results.push({
|
|
272
|
+
filePath: entry.filePath,
|
|
273
|
+
line: location.line,
|
|
274
|
+
column: location.column,
|
|
275
|
+
message: `Multiple <h1> tags: component '${comp.name}' brings an extra <h1>. Use a lower-level heading.`,
|
|
276
|
+
rule: 'singleH1'
|
|
277
|
+
});
|
|
278
|
+
}
|
|
279
|
+
else {
|
|
280
|
+
const issue = (_a = comp.issues.get('singleH1')) === null || _a === void 0 ? void 0 : _a[0];
|
|
281
|
+
if (issue) {
|
|
282
|
+
results.push({
|
|
283
|
+
filePath: comp.filePath,
|
|
284
|
+
line: issue.line,
|
|
285
|
+
column: issue.column,
|
|
286
|
+
message: `Multiple <h1> across components - consider using lower-level headings.`,
|
|
287
|
+
rule: 'singleH1'
|
|
288
|
+
});
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
findReferenceForComp(root, targetPath) {
|
|
296
|
+
for (const ref of root.usesComponents) {
|
|
297
|
+
if (ref.path === targetPath)
|
|
298
|
+
return ref;
|
|
299
|
+
}
|
|
300
|
+
for (const ref of root.usesComponents) {
|
|
301
|
+
if (ref.path && this.componentRegistry.has(ref.path)) {
|
|
302
|
+
const nested = this.findReferenceForComp(this.componentRegistry.get(ref.path), targetPath);
|
|
303
|
+
if (nested)
|
|
304
|
+
return ref;
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
return null;
|
|
308
|
+
}
|
|
309
|
+
/**
|
|
310
|
+
* Improved implementation to find heading order issues across components
|
|
311
|
+
*/
|
|
312
|
+
findCrossComponentHeadingOrderIssues(results) {
|
|
313
|
+
const entryPoints = this.findEntryPoints();
|
|
314
|
+
for (const entry of entryPoints) {
|
|
315
|
+
// Process each entry point as a document root
|
|
316
|
+
this.processingComponentStack.clear();
|
|
317
|
+
this.analyzeHeadingHierarchy(entry, results);
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
/**
|
|
321
|
+
* Collects all headings from a component and its children in document order
|
|
322
|
+
* and checks for heading level issues
|
|
323
|
+
*/
|
|
324
|
+
analyzeHeadingHierarchy(component, results) {
|
|
325
|
+
var _a, _b, _c;
|
|
326
|
+
if (this.processingComponentStack.has(component.filePath)) {
|
|
327
|
+
// Avoid circular references
|
|
328
|
+
return;
|
|
329
|
+
}
|
|
330
|
+
this.processingComponentStack.add(component.filePath);
|
|
331
|
+
// Build a flattened view of all headings in document order
|
|
332
|
+
const allHeadings = this.collectHeadingsInDocumentOrder(component);
|
|
333
|
+
// Check for heading level issues
|
|
334
|
+
let lastLevel = 0;
|
|
335
|
+
for (const heading of allHeadings) {
|
|
336
|
+
if (lastLevel > 0 && heading.heading.level > lastLevel + 1) {
|
|
337
|
+
// We found a heading level skip
|
|
338
|
+
results.push({
|
|
339
|
+
filePath: ((_a = heading.usageLocation) === null || _a === void 0 ? void 0 : _a.filePath) || heading.heading.filePath,
|
|
340
|
+
line: ((_b = heading.usageLocation) === null || _b === void 0 ? void 0 : _b.line) || heading.heading.line,
|
|
341
|
+
column: ((_c = heading.usageLocation) === null || _c === void 0 ? void 0 : _c.column) || heading.heading.column,
|
|
342
|
+
message: `Cross-component heading level skipped: <h${heading.heading.level}> after <h${lastLevel}>`,
|
|
343
|
+
rule: 'enforceHeadingOrder'
|
|
344
|
+
});
|
|
345
|
+
}
|
|
346
|
+
lastLevel = heading.heading.level;
|
|
347
|
+
}
|
|
348
|
+
this.processingComponentStack.delete(component.filePath);
|
|
349
|
+
}
|
|
350
|
+
/**
|
|
351
|
+
* Collects all headings from a component and its children in document order
|
|
352
|
+
*/
|
|
353
|
+
collectHeadingsInDocumentOrder(component) {
|
|
354
|
+
// Sort headings within this component by line/column
|
|
355
|
+
const localHeadings = [...component.headings].sort((a, b) => {
|
|
356
|
+
if (a.line !== b.line)
|
|
357
|
+
return a.line - b.line;
|
|
358
|
+
return a.column - b.column;
|
|
359
|
+
}).map(h => ({
|
|
360
|
+
heading: h,
|
|
361
|
+
usageLocation: null
|
|
362
|
+
}));
|
|
363
|
+
// Sort child components by their usage location
|
|
364
|
+
const childComponents = component.usesComponents
|
|
365
|
+
.filter(ref => ref.path && this.componentRegistry.has(ref.path))
|
|
366
|
+
.sort((a, b) => {
|
|
367
|
+
const aLoc = a.usageLocations[0] || a.sourceLocation;
|
|
368
|
+
const bLoc = b.usageLocations[0] || b.sourceLocation;
|
|
369
|
+
if (aLoc.line !== bLoc.line)
|
|
370
|
+
return aLoc.line - bLoc.line;
|
|
371
|
+
return aLoc.column - bLoc.column;
|
|
372
|
+
});
|
|
373
|
+
// Merge headings and child component headings in document order
|
|
374
|
+
const allHeadings = [];
|
|
375
|
+
let headingIndex = 0;
|
|
376
|
+
let childIndex = 0;
|
|
377
|
+
// This merges the local headings with child component headings
|
|
378
|
+
// based on their position in the document
|
|
379
|
+
while (headingIndex < localHeadings.length || childIndex < childComponents.length) {
|
|
380
|
+
if (headingIndex >= localHeadings.length) {
|
|
381
|
+
// No more local headings, process remaining children
|
|
382
|
+
const childRef = childComponents[childIndex++];
|
|
383
|
+
if (childRef.path && this.componentRegistry.has(childRef.path) && !this.processingComponentStack.has(childRef.path)) {
|
|
384
|
+
const childComponent = this.componentRegistry.get(childRef.path);
|
|
385
|
+
const usageLoc = childRef.usageLocations[0] || childRef.sourceLocation;
|
|
386
|
+
const usageLocation = {
|
|
387
|
+
filePath: component.filePath,
|
|
388
|
+
line: usageLoc.line,
|
|
389
|
+
column: usageLoc.column
|
|
390
|
+
};
|
|
391
|
+
this.processingComponentStack.add(childRef.path);
|
|
392
|
+
const childHeadings = this.collectHeadingsInDocumentOrder(childComponent)
|
|
393
|
+
.map(h => ({
|
|
394
|
+
heading: h.heading,
|
|
395
|
+
usageLocation: h.usageLocation || usageLocation
|
|
396
|
+
}));
|
|
397
|
+
this.processingComponentStack.delete(childRef.path);
|
|
398
|
+
allHeadings.push(...childHeadings);
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
else if (childIndex >= childComponents.length) {
|
|
402
|
+
// No more children, add remaining local headings
|
|
403
|
+
allHeadings.push(localHeadings[headingIndex++]);
|
|
404
|
+
}
|
|
405
|
+
else {
|
|
406
|
+
// Compare positions to decide whether to add a local heading or process a child
|
|
407
|
+
const nextHeading = localHeadings[headingIndex];
|
|
408
|
+
const nextChild = childComponents[childIndex];
|
|
409
|
+
const childLoc = nextChild.usageLocations[0] || nextChild.sourceLocation;
|
|
410
|
+
if (nextHeading.heading.line < childLoc.line ||
|
|
411
|
+
(nextHeading.heading.line === childLoc.line && nextHeading.heading.column < childLoc.column)) {
|
|
412
|
+
// Local heading comes first
|
|
413
|
+
allHeadings.push(nextHeading);
|
|
414
|
+
headingIndex++;
|
|
415
|
+
}
|
|
416
|
+
else {
|
|
417
|
+
// Child component comes first
|
|
418
|
+
childIndex++;
|
|
419
|
+
if (nextChild.path && this.componentRegistry.has(nextChild.path) && !this.processingComponentStack.has(nextChild.path)) {
|
|
420
|
+
const childComponent = this.componentRegistry.get(nextChild.path);
|
|
421
|
+
const usageLocation = {
|
|
422
|
+
filePath: component.filePath,
|
|
423
|
+
line: childLoc.line,
|
|
424
|
+
column: childLoc.column
|
|
425
|
+
};
|
|
426
|
+
this.processingComponentStack.add(nextChild.path);
|
|
427
|
+
const childHeadings = this.collectHeadingsInDocumentOrder(childComponent)
|
|
428
|
+
.map(h => ({
|
|
429
|
+
heading: h.heading,
|
|
430
|
+
usageLocation: h.usageLocation || usageLocation
|
|
431
|
+
}));
|
|
432
|
+
this.processingComponentStack.delete(nextChild.path);
|
|
433
|
+
allHeadings.push(...childHeadings);
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
return allHeadings;
|
|
439
|
+
}
|
|
440
|
+
findEntryPoints() {
|
|
441
|
+
const all = Array.from(this.componentRegistry.values());
|
|
442
|
+
const imported = new Set();
|
|
443
|
+
all.forEach(c => c.usesComponents.forEach(r => r.path && imported.add(r.path)));
|
|
444
|
+
return all.filter(c => !imported.has(c.filePath));
|
|
445
|
+
}
|
|
446
|
+
findComponentsWithRule(root, rule) {
|
|
447
|
+
const res = [];
|
|
448
|
+
const visited = new Set();
|
|
449
|
+
const dfs = (c) => {
|
|
450
|
+
if (visited.has(c.filePath))
|
|
451
|
+
return;
|
|
452
|
+
visited.add(c.filePath);
|
|
453
|
+
if (c.issues.has(rule))
|
|
454
|
+
res.push(c);
|
|
455
|
+
c.usesComponents.forEach(r => r.path && this.componentRegistry.has(r.path) && dfs(this.componentRegistry.get(r.path)));
|
|
456
|
+
};
|
|
457
|
+
dfs(root);
|
|
458
|
+
return res;
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
exports.ComponentAnalyzer = ComponentAnalyzer;
|