zemdomu 1.0.0 โ 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +112 -112
- package/out/component-analyzer.js +454 -0
- package/out/component-path-resolver.js +259 -0
- package/out/index.js +7 -1
- package/out/project-linter.js +87 -0
- package/package.json +34 -30
package/README.md
CHANGED
|
@@ -1,112 +1,112 @@
|
|
|
1
|
-
ZemDomu
|
|
2
|
-
|
|
3
|
-
Semantic HTML linting engine for clean, accessible, and SEO-friendly markup. This package provides the shared core logic used by the ZemDomu VS Code extension and upcoming GitHub Action.
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
๐ง What is ZemDomu Core?
|
|
8
|
-
|
|
9
|
-
ZemDomu is a semantic-first linter that helps developers write better HTML and JSX by catching accessibility and structural issues. This package contains the framework-agnostic linting engine used by other tools in the ZemDomu ecosystem.
|
|
10
|
-
|
|
11
|
-
It parses .html, .jsx, and .tsx content and exposes a simple lint() function that returns semantic violations.
|
|
12
|
-
|
|
13
|
-
๐ Installation
|
|
14
|
-
|
|
15
|
-
npm install zemdomu
|
|
16
|
-
# or
|
|
17
|
-
yarn add zemdomu
|
|
18
|
-
|
|
19
|
-
โจ Features
|
|
20
|
-
|
|
21
|
-
โ
Lint semantic issues in HTML, JSX, and TSX
|
|
22
|
-
|
|
23
|
-
๐ฆ Works in Node.js, CI, or any JS runtime
|
|
24
|
-
|
|
25
|
-
โ๏ธ Extensible rule system
|
|
26
|
-
|
|
27
|
-
๐ Shared by extension and GitHub Action
|
|
28
|
-
|
|
29
|
-
๐งช Simple API: lint(content, options)
|
|
30
|
-
|
|
31
|
-
โ๏ธ Usage Example
|
|
32
|
-
|
|
33
|
-
import { lint } from 'zemdomu';
|
|
34
|
-
|
|
35
|
-
const html = '<img>';
|
|
36
|
-
const results = lint(html, { rules: { requireAltText: true } });
|
|
37
|
-
|
|
38
|
-
console.log(results);
|
|
39
|
-
// [
|
|
40
|
-
// {
|
|
41
|
-
// line: 0,
|
|
42
|
-
// column: 0,
|
|
43
|
-
// message: '<img> tag missing alt attribute',
|
|
44
|
-
// rule: 'requireAltText'
|
|
45
|
-
// }
|
|
46
|
-
// ]
|
|
47
|
-
|
|
48
|
-
// Custom rules can be supplied via the `customRules` option
|
|
49
|
-
// const myRule = { name: 'demo', checkHtml: () => [] };
|
|
50
|
-
// lint(html, { customRules: [myRule] });
|
|
51
|
-
|
|
52
|
-
๐ API
|
|
53
|
-
|
|
54
|
-
lint(content: string, options?: LinterOptions): LintResult[]
|
|
55
|
-
|
|
56
|
-
Parameters:
|
|
57
|
-
|
|
58
|
-
content โ HTML, JSX, or TSX string input
|
|
59
|
-
|
|
60
|
-
options.rules โ toggles for built-in rules
|
|
61
|
-
options.customRules โ array of additional rules
|
|
62
|
-
|
|
63
|
-
Example LinterOptions
|
|
64
|
-
|
|
65
|
-
interface LinterOptions {
|
|
66
|
-
rules: {
|
|
67
|
-
requireAltText: boolean;
|
|
68
|
-
// ...more rules to come
|
|
69
|
-
};
|
|
70
|
-
customRules?: Rule[];
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
Example LintResult
|
|
74
|
-
|
|
75
|
-
interface LintResult {
|
|
76
|
-
line: number;
|
|
77
|
-
column: number;
|
|
78
|
-
message: string;
|
|
79
|
-
rule: string;
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
๐ Related Tools
|
|
83
|
-
|
|
84
|
-
ZemDomu VS Code Extension
|
|
85
|
-
|
|
86
|
-
ZemDomu GitHub Action (coming soon)
|
|
87
|
-
|
|
88
|
-
๐ Development
|
|
89
|
-
|
|
90
|
-
git clone https://github.com/Zemdomu/ZemDomu-core.git
|
|
91
|
-
cd ZemDomu-core
|
|
92
|
-
npm install
|
|
93
|
-
npm run build
|
|
94
|
-
|
|
95
|
-
Tests and coverage support coming soon.
|
|
96
|
-
|
|
97
|
-
๐ค Contributing
|
|
98
|
-
|
|
99
|
-
We welcome contributions! If you'd like to add rules, improve parsing, or integrate new consumers:
|
|
100
|
-
|
|
101
|
-
Fork this repo
|
|
102
|
-
|
|
103
|
-
Add your logic inside src/rules or src/linter.ts
|
|
104
|
-
|
|
105
|
-
Write or update tests (if applicable)
|
|
106
|
-
|
|
107
|
-
Submit a pull request!
|
|
108
|
-
|
|
109
|
-
๐ License
|
|
110
|
-
|
|
111
|
-
MIT ยฉ 2025 Zacharias Eryd Berlin
|
|
112
|
-
|
|
1
|
+
ZemDomu
|
|
2
|
+
|
|
3
|
+
Semantic HTML linting engine for clean, accessible, and SEO-friendly markup. This package provides the shared core logic used by the ZemDomu VS Code extension and upcoming GitHub Action.
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
๐ง What is ZemDomu Core?
|
|
8
|
+
|
|
9
|
+
ZemDomu is a semantic-first linter that helps developers write better HTML and JSX by catching accessibility and structural issues. This package contains the framework-agnostic linting engine used by other tools in the ZemDomu ecosystem.
|
|
10
|
+
|
|
11
|
+
It parses .html, .jsx, and .tsx content and exposes a simple lint() function that returns semantic violations.
|
|
12
|
+
|
|
13
|
+
๐ Installation
|
|
14
|
+
|
|
15
|
+
npm install zemdomu
|
|
16
|
+
# or
|
|
17
|
+
yarn add zemdomu
|
|
18
|
+
|
|
19
|
+
โจ Features
|
|
20
|
+
|
|
21
|
+
โ
Lint semantic issues in HTML, JSX, and TSX
|
|
22
|
+
|
|
23
|
+
๐ฆ Works in Node.js, CI, or any JS runtime
|
|
24
|
+
|
|
25
|
+
โ๏ธ Extensible rule system
|
|
26
|
+
|
|
27
|
+
๐ Shared by extension and GitHub Action
|
|
28
|
+
|
|
29
|
+
๐งช Simple API: lint(content, options)
|
|
30
|
+
|
|
31
|
+
โ๏ธ Usage Example
|
|
32
|
+
|
|
33
|
+
import { lint } from 'zemdomu';
|
|
34
|
+
|
|
35
|
+
const html = '<img>';
|
|
36
|
+
const results = lint(html, { rules: { requireAltText: true } });
|
|
37
|
+
|
|
38
|
+
console.log(results);
|
|
39
|
+
// [
|
|
40
|
+
// {
|
|
41
|
+
// line: 0,
|
|
42
|
+
// column: 0,
|
|
43
|
+
// message: '<img> tag missing alt attribute',
|
|
44
|
+
// rule: 'requireAltText'
|
|
45
|
+
// }
|
|
46
|
+
// ]
|
|
47
|
+
|
|
48
|
+
// Custom rules can be supplied via the `customRules` option
|
|
49
|
+
// const myRule = { name: 'demo', checkHtml: () => [] };
|
|
50
|
+
// lint(html, { customRules: [myRule] });
|
|
51
|
+
|
|
52
|
+
๐ API
|
|
53
|
+
|
|
54
|
+
lint(content: string, options?: LinterOptions): LintResult[]
|
|
55
|
+
|
|
56
|
+
Parameters:
|
|
57
|
+
|
|
58
|
+
content โ HTML, JSX, or TSX string input
|
|
59
|
+
|
|
60
|
+
options.rules โ toggles for built-in rules
|
|
61
|
+
options.customRules โ array of additional rules
|
|
62
|
+
|
|
63
|
+
Example LinterOptions
|
|
64
|
+
|
|
65
|
+
interface LinterOptions {
|
|
66
|
+
rules: {
|
|
67
|
+
requireAltText: boolean;
|
|
68
|
+
// ...more rules to come
|
|
69
|
+
};
|
|
70
|
+
customRules?: Rule[];
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
Example LintResult
|
|
74
|
+
|
|
75
|
+
interface LintResult {
|
|
76
|
+
line: number;
|
|
77
|
+
column: number;
|
|
78
|
+
message: string;
|
|
79
|
+
rule: string;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
๐ Related Tools
|
|
83
|
+
|
|
84
|
+
ZemDomu VS Code Extension
|
|
85
|
+
|
|
86
|
+
ZemDomu GitHub Action (coming soon)
|
|
87
|
+
|
|
88
|
+
๐ Development
|
|
89
|
+
|
|
90
|
+
git clone https://github.com/Zemdomu/ZemDomu-core.git
|
|
91
|
+
cd ZemDomu-core
|
|
92
|
+
npm install
|
|
93
|
+
npm run build
|
|
94
|
+
|
|
95
|
+
Tests and coverage support coming soon.
|
|
96
|
+
|
|
97
|
+
๐ค Contributing
|
|
98
|
+
|
|
99
|
+
We welcome contributions! If you'd like to add rules, improve parsing, or integrate new consumers:
|
|
100
|
+
|
|
101
|
+
Fork this repo
|
|
102
|
+
|
|
103
|
+
Add your logic inside src/rules or src/linter.ts
|
|
104
|
+
|
|
105
|
+
Write or update tests (if applicable)
|
|
106
|
+
|
|
107
|
+
Submit a pull request!
|
|
108
|
+
|
|
109
|
+
๐ License
|
|
110
|
+
|
|
111
|
+
MIT ยฉ 2025 Zacharias Eryd Berlin
|
|
112
|
+
|
|
@@ -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,12 @@
|
|
|
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.ProjectLinter = 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; } });
|
|
11
|
+
var project_linter_1 = require("./project-linter");
|
|
12
|
+
Object.defineProperty(exports, "ProjectLinter", { enumerable: true, get: function () { return project_linter_1.ProjectLinter; } });
|
|
@@ -0,0 +1,87 @@
|
|
|
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.ProjectLinter = void 0;
|
|
37
|
+
const fs = __importStar(require("fs/promises"));
|
|
38
|
+
const linter_1 = require("./linter");
|
|
39
|
+
const component_analyzer_1 = require("./component-analyzer");
|
|
40
|
+
class ProjectLinter {
|
|
41
|
+
constructor(options = {}) {
|
|
42
|
+
this.opts = options;
|
|
43
|
+
this.analyzer = new component_analyzer_1.ComponentAnalyzer(this.opts);
|
|
44
|
+
}
|
|
45
|
+
clear() {
|
|
46
|
+
this.analyzer = new component_analyzer_1.ComponentAnalyzer(this.opts);
|
|
47
|
+
}
|
|
48
|
+
async lintFile(filePath, content) {
|
|
49
|
+
if (!content) {
|
|
50
|
+
content = await fs.readFile(filePath, 'utf8');
|
|
51
|
+
}
|
|
52
|
+
const results = (0, linter_1.lint)(content, this.opts);
|
|
53
|
+
const byFile = new Map();
|
|
54
|
+
byFile.set(filePath, [...results]);
|
|
55
|
+
const xmlMode = /\.(jsx|tsx)$/.test(filePath);
|
|
56
|
+
if (xmlMode) {
|
|
57
|
+
const component = await this.analyzer.analyzeFile(filePath);
|
|
58
|
+
if (component) {
|
|
59
|
+
this.analyzer.registerComponent(component, results);
|
|
60
|
+
}
|
|
61
|
+
if (this.opts.crossComponentAnalysis) {
|
|
62
|
+
const cross = this.analyzer.analyzeComponentTree();
|
|
63
|
+
for (const r of cross) {
|
|
64
|
+
if (!r.filePath)
|
|
65
|
+
continue;
|
|
66
|
+
if (!byFile.has(r.filePath))
|
|
67
|
+
byFile.set(r.filePath, []);
|
|
68
|
+
byFile.get(r.filePath).push(r);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
return byFile;
|
|
73
|
+
}
|
|
74
|
+
async lintFiles(filePaths) {
|
|
75
|
+
const aggregated = new Map();
|
|
76
|
+
for (const filePath of filePaths) {
|
|
77
|
+
const fileMap = await this.lintFile(filePath);
|
|
78
|
+
for (const [fp, res] of fileMap.entries()) {
|
|
79
|
+
if (!aggregated.has(fp))
|
|
80
|
+
aggregated.set(fp, []);
|
|
81
|
+
aggregated.get(fp).push(...res);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
return aggregated;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
exports.ProjectLinter = ProjectLinter;
|
package/package.json
CHANGED
|
@@ -1,30 +1,34 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "zemdomu",
|
|
3
|
-
"version": "1.
|
|
4
|
-
"description": "Hello",
|
|
5
|
-
"main": "./out/index.js",
|
|
6
|
-
"files": [
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
"
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
"
|
|
20
|
-
"@types/
|
|
21
|
-
"@types/
|
|
22
|
-
"
|
|
23
|
-
"
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
"
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
1
|
+
{
|
|
2
|
+
"name": "zemdomu",
|
|
3
|
+
"version": "1.1.0",
|
|
4
|
+
"description": "Hello",
|
|
5
|
+
"main": "./out/index.js",
|
|
6
|
+
"files": [
|
|
7
|
+
"out"
|
|
8
|
+
],
|
|
9
|
+
"private": false,
|
|
10
|
+
"scripts": {
|
|
11
|
+
"build": "tsc -p tsconfig.json",
|
|
12
|
+
"compile": "tsc -p tsconfig.json",
|
|
13
|
+
"test": "npm run compile && node tests/unique-ids.test.js && node tests/label-form-control.test.js && node tests/cross-component.test.js && node tests/heading-order.test.js && node tests/prevent-empty-inline.test.js && node tests/section-heading.test.js && node tests/img-alt.test.js && node tests/button-accessibility.test.js && node tests/html-lang.test.js && node tests/table-caption.test.js && node tests/all-rules.test.js"
|
|
14
|
+
},
|
|
15
|
+
"keywords": [],
|
|
16
|
+
"author": "",
|
|
17
|
+
"license": "ISC",
|
|
18
|
+
"devDependencies": {
|
|
19
|
+
"@types/babel__traverse": "^7.20.7",
|
|
20
|
+
"@types/babel-traverse": "^6.25.10",
|
|
21
|
+
"@types/babel-types": "^7.0.16",
|
|
22
|
+
"typescript": "^5.8.2",
|
|
23
|
+
"@types/node": "^22.15.17",
|
|
24
|
+
"@types/jest": "^29.5.3",
|
|
25
|
+
"esbuild": "^0.25.5",
|
|
26
|
+
"jest": "^29.7.0"
|
|
27
|
+
},
|
|
28
|
+
"dependencies": {
|
|
29
|
+
"@babel/parser": "^7.27.0",
|
|
30
|
+
"@babel/traverse": "^7.27.0",
|
|
31
|
+
"@babel/types": "^7.27.0",
|
|
32
|
+
"glob": "^7.2.3"
|
|
33
|
+
}
|
|
34
|
+
}
|