zemdomu 1.0.0 → 1.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/out/component-analyzer.js +454 -0
- package/out/component-path-resolver.js +259 -0
- package/out/index.js +5 -1
- package/package.json +8 -4
|
@@ -0,0 +1,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
|
+
const entryPoints = this.findEntryPoints();
|
|
257
|
+
for (const entry of entryPoints) {
|
|
258
|
+
const comps = this.findComponentsWithRule(entry, 'singleH1');
|
|
259
|
+
if (comps.length > 1) {
|
|
260
|
+
for (let i = 1; i < comps.length; i++) {
|
|
261
|
+
const comp = comps[i];
|
|
262
|
+
const ref = this.findReferenceForComp(entry, comp.filePath);
|
|
263
|
+
if (ref) {
|
|
264
|
+
// Use first JSX usage location instead of import location
|
|
265
|
+
const location = ref.usageLocations[0] || ref.sourceLocation;
|
|
266
|
+
results.push({
|
|
267
|
+
filePath: entry.filePath,
|
|
268
|
+
line: location.line,
|
|
269
|
+
column: location.column,
|
|
270
|
+
message: `Multiple <h1> tags: component '${comp.name}' brings an extra <h1>. Use a lower-level heading.`,
|
|
271
|
+
rule: 'singleH1'
|
|
272
|
+
});
|
|
273
|
+
}
|
|
274
|
+
else {
|
|
275
|
+
const issue = comp.issues.get('singleH1')[0];
|
|
276
|
+
results.push({
|
|
277
|
+
filePath: comp.filePath,
|
|
278
|
+
line: issue.line,
|
|
279
|
+
column: issue.column,
|
|
280
|
+
message: `Multiple <h1> across components - consider using lower-level headings.`,
|
|
281
|
+
rule: 'singleH1'
|
|
282
|
+
});
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
findReferenceForComp(root, targetPath) {
|
|
289
|
+
for (const ref of root.usesComponents) {
|
|
290
|
+
if (ref.path === targetPath)
|
|
291
|
+
return ref;
|
|
292
|
+
}
|
|
293
|
+
for (const ref of root.usesComponents) {
|
|
294
|
+
if (ref.path && this.componentRegistry.has(ref.path)) {
|
|
295
|
+
const nested = this.findReferenceForComp(this.componentRegistry.get(ref.path), targetPath);
|
|
296
|
+
if (nested)
|
|
297
|
+
return ref;
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
return null;
|
|
301
|
+
}
|
|
302
|
+
/**
|
|
303
|
+
* Improved implementation to find heading order issues across components
|
|
304
|
+
*/
|
|
305
|
+
findCrossComponentHeadingOrderIssues(results) {
|
|
306
|
+
const entryPoints = this.findEntryPoints();
|
|
307
|
+
for (const entry of entryPoints) {
|
|
308
|
+
// Process each entry point as a document root
|
|
309
|
+
this.processingComponentStack.clear();
|
|
310
|
+
this.analyzeHeadingHierarchy(entry, results);
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
/**
|
|
314
|
+
* Collects all headings from a component and its children in document order
|
|
315
|
+
* and checks for heading level issues
|
|
316
|
+
*/
|
|
317
|
+
analyzeHeadingHierarchy(component, results) {
|
|
318
|
+
var _a, _b, _c;
|
|
319
|
+
if (this.processingComponentStack.has(component.filePath)) {
|
|
320
|
+
// Avoid circular references
|
|
321
|
+
return;
|
|
322
|
+
}
|
|
323
|
+
this.processingComponentStack.add(component.filePath);
|
|
324
|
+
// Build a flattened view of all headings in document order
|
|
325
|
+
const allHeadings = this.collectHeadingsInDocumentOrder(component);
|
|
326
|
+
// Check for heading level issues
|
|
327
|
+
let lastLevel = 0;
|
|
328
|
+
for (const heading of allHeadings) {
|
|
329
|
+
if (lastLevel > 0 && heading.heading.level > lastLevel + 1) {
|
|
330
|
+
// We found a heading level skip
|
|
331
|
+
results.push({
|
|
332
|
+
filePath: ((_a = heading.usageLocation) === null || _a === void 0 ? void 0 : _a.filePath) || heading.heading.filePath,
|
|
333
|
+
line: ((_b = heading.usageLocation) === null || _b === void 0 ? void 0 : _b.line) || heading.heading.line,
|
|
334
|
+
column: ((_c = heading.usageLocation) === null || _c === void 0 ? void 0 : _c.column) || heading.heading.column,
|
|
335
|
+
message: `Cross-component heading level skipped: <h${heading.heading.level}> after <h${lastLevel}>`,
|
|
336
|
+
rule: 'enforceHeadingOrder'
|
|
337
|
+
});
|
|
338
|
+
}
|
|
339
|
+
lastLevel = heading.heading.level;
|
|
340
|
+
}
|
|
341
|
+
this.processingComponentStack.delete(component.filePath);
|
|
342
|
+
}
|
|
343
|
+
/**
|
|
344
|
+
* Collects all headings from a component and its children in document order
|
|
345
|
+
*/
|
|
346
|
+
collectHeadingsInDocumentOrder(component) {
|
|
347
|
+
// Sort headings within this component by line/column
|
|
348
|
+
const localHeadings = [...component.headings].sort((a, b) => {
|
|
349
|
+
if (a.line !== b.line)
|
|
350
|
+
return a.line - b.line;
|
|
351
|
+
return a.column - b.column;
|
|
352
|
+
}).map(h => ({
|
|
353
|
+
heading: h,
|
|
354
|
+
usageLocation: null
|
|
355
|
+
}));
|
|
356
|
+
// Sort child components by their usage location
|
|
357
|
+
const childComponents = component.usesComponents
|
|
358
|
+
.filter(ref => ref.path && this.componentRegistry.has(ref.path))
|
|
359
|
+
.sort((a, b) => {
|
|
360
|
+
const aLoc = a.usageLocations[0] || a.sourceLocation;
|
|
361
|
+
const bLoc = b.usageLocations[0] || b.sourceLocation;
|
|
362
|
+
if (aLoc.line !== bLoc.line)
|
|
363
|
+
return aLoc.line - bLoc.line;
|
|
364
|
+
return aLoc.column - bLoc.column;
|
|
365
|
+
});
|
|
366
|
+
// Merge headings and child component headings in document order
|
|
367
|
+
const allHeadings = [];
|
|
368
|
+
let headingIndex = 0;
|
|
369
|
+
let childIndex = 0;
|
|
370
|
+
// This merges the local headings with child component headings
|
|
371
|
+
// based on their position in the document
|
|
372
|
+
while (headingIndex < localHeadings.length || childIndex < childComponents.length) {
|
|
373
|
+
if (headingIndex >= localHeadings.length) {
|
|
374
|
+
// No more local headings, process remaining children
|
|
375
|
+
const childRef = childComponents[childIndex++];
|
|
376
|
+
if (childRef.path && this.componentRegistry.has(childRef.path) && !this.processingComponentStack.has(childRef.path)) {
|
|
377
|
+
const childComponent = this.componentRegistry.get(childRef.path);
|
|
378
|
+
const usageLoc = childRef.usageLocations[0] || childRef.sourceLocation;
|
|
379
|
+
const usageLocation = {
|
|
380
|
+
filePath: component.filePath,
|
|
381
|
+
line: usageLoc.line,
|
|
382
|
+
column: usageLoc.column
|
|
383
|
+
};
|
|
384
|
+
this.processingComponentStack.add(childRef.path);
|
|
385
|
+
const childHeadings = this.collectHeadingsInDocumentOrder(childComponent)
|
|
386
|
+
.map(h => ({
|
|
387
|
+
heading: h.heading,
|
|
388
|
+
usageLocation: h.usageLocation || usageLocation
|
|
389
|
+
}));
|
|
390
|
+
this.processingComponentStack.delete(childRef.path);
|
|
391
|
+
allHeadings.push(...childHeadings);
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
else if (childIndex >= childComponents.length) {
|
|
395
|
+
// No more children, add remaining local headings
|
|
396
|
+
allHeadings.push(localHeadings[headingIndex++]);
|
|
397
|
+
}
|
|
398
|
+
else {
|
|
399
|
+
// Compare positions to decide whether to add a local heading or process a child
|
|
400
|
+
const nextHeading = localHeadings[headingIndex];
|
|
401
|
+
const nextChild = childComponents[childIndex];
|
|
402
|
+
const childLoc = nextChild.usageLocations[0] || nextChild.sourceLocation;
|
|
403
|
+
if (nextHeading.heading.line < childLoc.line ||
|
|
404
|
+
(nextHeading.heading.line === childLoc.line && nextHeading.heading.column < childLoc.column)) {
|
|
405
|
+
// Local heading comes first
|
|
406
|
+
allHeadings.push(nextHeading);
|
|
407
|
+
headingIndex++;
|
|
408
|
+
}
|
|
409
|
+
else {
|
|
410
|
+
// Child component comes first
|
|
411
|
+
childIndex++;
|
|
412
|
+
if (nextChild.path && this.componentRegistry.has(nextChild.path) && !this.processingComponentStack.has(nextChild.path)) {
|
|
413
|
+
const childComponent = this.componentRegistry.get(nextChild.path);
|
|
414
|
+
const usageLocation = {
|
|
415
|
+
filePath: component.filePath,
|
|
416
|
+
line: childLoc.line,
|
|
417
|
+
column: childLoc.column
|
|
418
|
+
};
|
|
419
|
+
this.processingComponentStack.add(nextChild.path);
|
|
420
|
+
const childHeadings = this.collectHeadingsInDocumentOrder(childComponent)
|
|
421
|
+
.map(h => ({
|
|
422
|
+
heading: h.heading,
|
|
423
|
+
usageLocation: h.usageLocation || usageLocation
|
|
424
|
+
}));
|
|
425
|
+
this.processingComponentStack.delete(nextChild.path);
|
|
426
|
+
allHeadings.push(...childHeadings);
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
return allHeadings;
|
|
432
|
+
}
|
|
433
|
+
findEntryPoints() {
|
|
434
|
+
const all = Array.from(this.componentRegistry.values());
|
|
435
|
+
const imported = new Set();
|
|
436
|
+
all.forEach(c => c.usesComponents.forEach(r => r.path && imported.add(r.path)));
|
|
437
|
+
return all.filter(c => !imported.has(c.filePath));
|
|
438
|
+
}
|
|
439
|
+
findComponentsWithRule(root, rule) {
|
|
440
|
+
const res = [];
|
|
441
|
+
const visited = new Set();
|
|
442
|
+
const dfs = (c) => {
|
|
443
|
+
if (visited.has(c.filePath))
|
|
444
|
+
return;
|
|
445
|
+
visited.add(c.filePath);
|
|
446
|
+
if (c.issues.has(rule))
|
|
447
|
+
res.push(c);
|
|
448
|
+
c.usesComponents.forEach(r => r.path && this.componentRegistry.has(r.path) && dfs(this.componentRegistry.get(r.path)));
|
|
449
|
+
};
|
|
450
|
+
dfs(root);
|
|
451
|
+
return res;
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
exports.ComponentAnalyzer = ComponentAnalyzer;
|
|
@@ -0,0 +1,259 @@
|
|
|
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
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.ComponentPathResolver = void 0;
|
|
37
|
+
const fs = __importStar(require("fs/promises"));
|
|
38
|
+
const path = __importStar(require("path"));
|
|
39
|
+
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
|
40
|
+
const glob = require('glob');
|
|
41
|
+
let vscodeApi;
|
|
42
|
+
try {
|
|
43
|
+
vscodeApi = require('vscode');
|
|
44
|
+
}
|
|
45
|
+
catch {
|
|
46
|
+
vscodeApi = undefined;
|
|
47
|
+
}
|
|
48
|
+
class ComponentPathResolver {
|
|
49
|
+
static setRootDir(dir) {
|
|
50
|
+
this.rootDir = dir;
|
|
51
|
+
}
|
|
52
|
+
static updateDevMode(dev) {
|
|
53
|
+
this.devMode = dev;
|
|
54
|
+
}
|
|
55
|
+
static async loadTsconfig() {
|
|
56
|
+
var _a;
|
|
57
|
+
if (this.tsconfigLoaded)
|
|
58
|
+
return;
|
|
59
|
+
this.tsconfigLoaded = true;
|
|
60
|
+
const folder = (_a = vscodeApi === null || vscodeApi === void 0 ? void 0 : vscodeApi.workspace.workspaceFolders) === null || _a === void 0 ? void 0 : _a[0];
|
|
61
|
+
const root = folder ? folder.uri.fsPath : this.rootDir;
|
|
62
|
+
const tsconfigPath = path.join(root, 'tsconfig.json');
|
|
63
|
+
try {
|
|
64
|
+
const buf = await fs.readFile(tsconfigPath, 'utf8');
|
|
65
|
+
const json = JSON.parse(buf);
|
|
66
|
+
const opts = json.compilerOptions || {};
|
|
67
|
+
const baseUrl = opts.baseUrl ? path.resolve(root, opts.baseUrl) : root;
|
|
68
|
+
const paths = opts.paths || {};
|
|
69
|
+
for (const [alias, targets] of Object.entries(paths)) {
|
|
70
|
+
const prefix = alias.replace(/\*$/, '').replace(/\/$/, '');
|
|
71
|
+
const wildcard = alias.includes('*');
|
|
72
|
+
const mapped = [];
|
|
73
|
+
for (const t of targets) {
|
|
74
|
+
const cleaned = t.replace(/\*$/, '').replace(/\/$/, '');
|
|
75
|
+
mapped.push(path.resolve(baseUrl, cleaned));
|
|
76
|
+
}
|
|
77
|
+
this.tsAliases.push({ prefix, wildcard, targets: mapped });
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
catch {
|
|
81
|
+
// ignore
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
async tryExtensions(base) {
|
|
85
|
+
if (path.extname(base)) {
|
|
86
|
+
if (await this.fileExists(base))
|
|
87
|
+
return base;
|
|
88
|
+
}
|
|
89
|
+
else {
|
|
90
|
+
const exts = ['.tsx', '.jsx', '.ts', '.js'];
|
|
91
|
+
for (const ext of exts) {
|
|
92
|
+
const candidate = `${base}${ext}`;
|
|
93
|
+
if (await this.fileExists(candidate))
|
|
94
|
+
return candidate;
|
|
95
|
+
}
|
|
96
|
+
for (const ext of exts) {
|
|
97
|
+
const candidate = path.join(base, `index${ext}`);
|
|
98
|
+
if (await this.fileExists(candidate))
|
|
99
|
+
return candidate;
|
|
100
|
+
}
|
|
101
|
+
if (await this.fileExists(base))
|
|
102
|
+
return base;
|
|
103
|
+
}
|
|
104
|
+
return null;
|
|
105
|
+
}
|
|
106
|
+
async resolveWithTsconfig(importPath) {
|
|
107
|
+
await ComponentPathResolver.loadTsconfig();
|
|
108
|
+
for (const entry of ComponentPathResolver.tsAliases) {
|
|
109
|
+
if (entry.wildcard) {
|
|
110
|
+
if (!importPath.startsWith(entry.prefix))
|
|
111
|
+
continue;
|
|
112
|
+
const rest = importPath.substring(entry.prefix.length);
|
|
113
|
+
for (const tgt of entry.targets) {
|
|
114
|
+
const base = path.join(tgt, rest);
|
|
115
|
+
const r = await this.tryExtensions(base);
|
|
116
|
+
if (r)
|
|
117
|
+
return r;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
else {
|
|
121
|
+
if (importPath === entry.prefix || importPath.startsWith(entry.prefix + '/')) {
|
|
122
|
+
let rest = '';
|
|
123
|
+
if (importPath.length > entry.prefix.length) {
|
|
124
|
+
rest = importPath.substring(entry.prefix.length);
|
|
125
|
+
if (rest.startsWith('/'))
|
|
126
|
+
rest = rest.substring(1);
|
|
127
|
+
}
|
|
128
|
+
for (const tgt of entry.targets) {
|
|
129
|
+
const base = rest ? path.join(tgt, rest) : tgt;
|
|
130
|
+
const r = await this.tryExtensions(base);
|
|
131
|
+
if (r)
|
|
132
|
+
return r;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
return null;
|
|
138
|
+
}
|
|
139
|
+
static normalizeKey(p) {
|
|
140
|
+
return p
|
|
141
|
+
.replace(/\\/g, '/')
|
|
142
|
+
.replace(/\/+$/, '')
|
|
143
|
+
.replace(/\.(tsx|ts|jsx|js)$/, '')
|
|
144
|
+
.replace(/\/index$/, '')
|
|
145
|
+
.toLowerCase();
|
|
146
|
+
}
|
|
147
|
+
async fileExists(p) {
|
|
148
|
+
if (ComponentPathResolver.statCache.has(p)) {
|
|
149
|
+
return ComponentPathResolver.statCache.get(p);
|
|
150
|
+
}
|
|
151
|
+
try {
|
|
152
|
+
await fs.stat(p);
|
|
153
|
+
ComponentPathResolver.statCache.set(p, true);
|
|
154
|
+
return true;
|
|
155
|
+
}
|
|
156
|
+
catch {
|
|
157
|
+
ComponentPathResolver.statCache.set(p, false);
|
|
158
|
+
return false;
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
async resolve(importPath, currentPath) {
|
|
162
|
+
const tStart = Date.now();
|
|
163
|
+
const rawKey = importPath.startsWith('.')
|
|
164
|
+
? path.resolve(path.dirname(currentPath), importPath)
|
|
165
|
+
: importPath;
|
|
166
|
+
const key = ComponentPathResolver.normalizeKey(rawKey);
|
|
167
|
+
if (ComponentPathResolver.unresolved.has(key))
|
|
168
|
+
return null;
|
|
169
|
+
if (ComponentPathResolver.resolveCache.has(key)) {
|
|
170
|
+
return ComponentPathResolver.resolveCache.get(key);
|
|
171
|
+
}
|
|
172
|
+
let result = null;
|
|
173
|
+
try {
|
|
174
|
+
if (importPath.startsWith('.')) {
|
|
175
|
+
const base = path.resolve(path.dirname(currentPath), importPath);
|
|
176
|
+
result = await this.tryExtensions(base);
|
|
177
|
+
}
|
|
178
|
+
else {
|
|
179
|
+
result = await this.resolveWithTsconfig(importPath);
|
|
180
|
+
if (!result) {
|
|
181
|
+
const prefix = importPath.split('/')[0];
|
|
182
|
+
let alias = ComponentPathResolver.aliasCache.get(prefix);
|
|
183
|
+
if (!alias) {
|
|
184
|
+
const pattern = `**/${prefix}/**/*.{tsx,jsx,ts,js}`;
|
|
185
|
+
const files = await new Promise((resolve, reject) => {
|
|
186
|
+
glob(pattern, { cwd: ComponentPathResolver.rootDir, ignore: '**/node_modules/**', nodir: true }, (err, matches) => (err ? reject(err) : resolve(matches)));
|
|
187
|
+
});
|
|
188
|
+
alias = new Map();
|
|
189
|
+
for (const relPath of files.slice(0, ComponentPathResolver.aliasFileLimit)) {
|
|
190
|
+
const rel = path.resolve(ComponentPathResolver.rootDir, relPath).replace(/\\/g, '/');
|
|
191
|
+
const idx = rel.lastIndexOf(`/${prefix}/`);
|
|
192
|
+
if (idx === -1)
|
|
193
|
+
continue;
|
|
194
|
+
const after = rel.substring(idx + prefix.length + 2).replace(/\.(tsx|ts|jsx|js)$/, '');
|
|
195
|
+
const key1 = ComponentPathResolver.normalizeKey(`${prefix}/${after}`);
|
|
196
|
+
alias.set(key1, rel);
|
|
197
|
+
if (after.endsWith('/index')) {
|
|
198
|
+
const trimmed = after.replace(/\/index$/, '');
|
|
199
|
+
alias.set(ComponentPathResolver.normalizeKey(`${prefix}/${trimmed}`), rel);
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
ComponentPathResolver.aliasCache.set(prefix, alias);
|
|
203
|
+
}
|
|
204
|
+
const normImport = ComponentPathResolver.normalizeKey(importPath);
|
|
205
|
+
result = alias.get(normImport) || null;
|
|
206
|
+
if (!result) {
|
|
207
|
+
const patterns = [
|
|
208
|
+
`**/${importPath}.{tsx,jsx,ts,js}`,
|
|
209
|
+
`**/${importPath}/index.{tsx,jsx,ts,js}`
|
|
210
|
+
];
|
|
211
|
+
for (const ptn of patterns) {
|
|
212
|
+
const pKey = `glob:${ptn}`;
|
|
213
|
+
if (ComponentPathResolver.resolveCache.has(pKey)) {
|
|
214
|
+
const cached = ComponentPathResolver.resolveCache.get(pKey);
|
|
215
|
+
if (cached) {
|
|
216
|
+
result = cached;
|
|
217
|
+
break;
|
|
218
|
+
}
|
|
219
|
+
continue;
|
|
220
|
+
}
|
|
221
|
+
const matches = await new Promise((resolve, reject) => {
|
|
222
|
+
glob(ptn, { cwd: ComponentPathResolver.rootDir, ignore: '**/node_modules/**', nodir: true }, (err, files) => (err ? reject(err) : resolve(files)));
|
|
223
|
+
});
|
|
224
|
+
if (matches.length) {
|
|
225
|
+
result = path.resolve(ComponentPathResolver.rootDir, matches[0]);
|
|
226
|
+
ComponentPathResolver.resolveCache.set(pKey, result);
|
|
227
|
+
break;
|
|
228
|
+
}
|
|
229
|
+
else {
|
|
230
|
+
ComponentPathResolver.resolveCache.set(pKey, null);
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
catch {
|
|
238
|
+
result = null;
|
|
239
|
+
}
|
|
240
|
+
ComponentPathResolver.resolveCache.set(key, result);
|
|
241
|
+
if (result === null)
|
|
242
|
+
ComponentPathResolver.unresolved.add(key);
|
|
243
|
+
const tTotal = Date.now() - tStart;
|
|
244
|
+
if (ComponentPathResolver.devMode) {
|
|
245
|
+
console.debug(`[ZemDomu] resolved ${importPath} -> ${result} (${tTotal}ms)`);
|
|
246
|
+
}
|
|
247
|
+
return result;
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
exports.ComponentPathResolver = ComponentPathResolver;
|
|
251
|
+
ComponentPathResolver.resolveCache = new Map();
|
|
252
|
+
ComponentPathResolver.statCache = new Map();
|
|
253
|
+
ComponentPathResolver.aliasCache = new Map();
|
|
254
|
+
ComponentPathResolver.unresolved = new Set();
|
|
255
|
+
ComponentPathResolver.devMode = false;
|
|
256
|
+
ComponentPathResolver.tsconfigLoaded = false;
|
|
257
|
+
ComponentPathResolver.tsAliases = [];
|
|
258
|
+
ComponentPathResolver.aliasFileLimit = 100;
|
|
259
|
+
ComponentPathResolver.rootDir = process.cwd();
|
package/out/index.js
CHANGED
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
// Exposes the core lint function and types
|
|
3
3
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
4
|
-
exports.lint = void 0;
|
|
4
|
+
exports.ComponentPathResolver = exports.ComponentAnalyzer = exports.lint = void 0;
|
|
5
5
|
var linter_1 = require("./linter");
|
|
6
6
|
Object.defineProperty(exports, "lint", { enumerable: true, get: function () { return linter_1.lint; } });
|
|
7
|
+
var component_analyzer_1 = require("./component-analyzer");
|
|
8
|
+
Object.defineProperty(exports, "ComponentAnalyzer", { enumerable: true, get: function () { return component_analyzer_1.ComponentAnalyzer; } });
|
|
9
|
+
var component_path_resolver_1 = require("./component-path-resolver");
|
|
10
|
+
Object.defineProperty(exports, "ComponentPathResolver", { enumerable: true, get: function () { return component_path_resolver_1.ComponentPathResolver; } });
|
package/package.json
CHANGED
|
@@ -1,12 +1,15 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "zemdomu",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.1",
|
|
4
4
|
"description": "Hello",
|
|
5
5
|
"main": "./out/index.js",
|
|
6
|
-
"files": [
|
|
6
|
+
"files": [
|
|
7
|
+
"out"
|
|
8
|
+
],
|
|
7
9
|
"private": false,
|
|
8
10
|
"scripts": {
|
|
9
|
-
"build":
|
|
11
|
+
"build": "tsc -p tsconfig.json",
|
|
12
|
+
"compile": "tsc -p tsconfig.json",
|
|
10
13
|
"test": "npm run compile && node tests/linter-labels.test.js && node tests/unique-ids.test.js"
|
|
11
14
|
},
|
|
12
15
|
"keywords": [],
|
|
@@ -25,6 +28,7 @@
|
|
|
25
28
|
"dependencies": {
|
|
26
29
|
"@babel/parser": "^7.27.0",
|
|
27
30
|
"@babel/traverse": "^7.27.0",
|
|
28
|
-
"@babel/types": "^7.27.0"
|
|
31
|
+
"@babel/types": "^7.27.0",
|
|
32
|
+
"glob": "^7.2.3"
|
|
29
33
|
}
|
|
30
34
|
}
|