zemdomu 1.2.0 โ†’ 1.3.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,224 +1,224 @@
1
- # ZemDomu Core
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
- ## ๐Ÿง  What is ZemDomu?
6
-
7
- **ZemDomu** is a semantic-first linter that helps developers write better HTML and JSX by catching accessibility and structural issues. It parses `.html`, `.jsx` and `.tsx` files and exposes a simple `lint()` function that returns semantic violations.
8
-
9
- ## ๐Ÿš€ Installation
10
-
11
- ```bash
12
- npm install zemdomu
13
- # or
14
- yarn add zemdomu
15
- ```
16
-
17
- ## โœจ Features
18
-
19
- - โœ… Lint semantic issues in HTML, JSX and TSX
20
- - ๐Ÿ“ฆ Works in Node.js, CI or any JS runtime
21
- - โš™๏ธ Extensible rule system with simple custom rules
22
- - ๐Ÿ”€ Cross-component analysis for React/JSX projects
23
- - ๐Ÿš€ Command line interface with `--custom` and `--cross`
24
- - โš ๏ธ Configurable rule severity (`error`, `warning`, `off`)
25
- - ๐Ÿ“ˆ Performance diagnostics for profiling lint runs
26
- - ๐Ÿ“š Shared by the extension and GitHub Action
27
- - ๐Ÿงช Simple API: `lint(content, options)`
28
-
29
- ## โš™๏ธ Usage Example
30
-
31
- ```ts
32
- import { lint } from "zemdomu";
33
-
34
- const html = "<img>";
35
- const results = lint(html, { rules: { requireAltText: true } });
36
-
37
- console.log(results);
38
- // [
39
- // {
40
- // line: 0,
41
- // column: 0,
42
- // message: '<img> tag missing alt attribute',
43
- // rule: 'requireAltText'
44
- // }
45
- // ]
46
-
47
- // Custom rules can be supplied via the `customRules` option
48
- // const myRule = { name: 'demo', test: node => false, message: 'demo' };
49
- // lint(html, { customRules: [myRule] });
50
- ```
51
-
52
- ## ๐Ÿ“– API
53
-
54
- `lint(content: string, options?: LinterOptions): LintResult[]`
55
-
56
- **Parameters**
57
-
58
- - `content` โ€” HTML, JSX or TSX string input
59
- - `options.rules` โ€” severity settings for built-in rules
60
- - `options.customRules` โ€” array of additional rules
61
- - `options.filePath` โ€” optional source file path
62
- - `options.perf` โ€” attach a `PerformanceRecorder` instance
63
-
64
- **Example `LinterOptions`**
65
-
66
- ```ts
67
- interface LinterOptions {
68
- rules?: Record<string, 'error' | 'warning' | 'off'>;
69
- customRules?: Rule[];
70
- filePath?: string;
71
- perf?: PerformanceRecorder;
72
- }
73
- ```
74
-
75
- Example enabling one rule as a warning:
76
-
77
- ```ts
78
- const results = lint(html, {
79
- rules: { requireAltText: 'warning', uniqueIds: 'error' }
80
- });
81
- ```
82
-
83
- **Example `LintResult`**
84
-
85
- ```ts
86
- interface LintResult {
87
- line: number;
88
- column: number;
89
- message: string;
90
- rule: string;
91
- }
92
- ```
93
-
94
- ## ๐Ÿ›  CLI Usage
95
-
96
- Run the linter from the command line by installing the package globally or using
97
- `npx`. Provide one or more glob patterns to specify the files to lint. Patterns
98
- may be separated by spaces, commas or newlines:
99
-
100
- ```bash
101
- npx zemdomu "src/**/*.{html,jsx,tsx}" --custom my-rule.js
102
- npx zemdomu "src/**/*.html,src/**/*.jsx"
103
- ```
104
-
105
- Use `--custom` (or `-c`) to provide a path to a JavaScript or TypeScript module
106
- exporting a custom rule or array of rules. Use `--cross` to enable cross
107
- component analysis.
108
-
109
- ### Cross-Component Analysis
110
-
111
- When analysing JSX projects you can track `<h1>` usage or similar patterns
112
- across component boundaries. Instantiate `ProjectLinter` with the
113
- `crossComponentAnalysis` option or pass `--cross` to the CLI. Use
114
- `crossComponentDepth` (or `--cross-depth`) to limit how deep component trees are
115
- traversed during analysis:
116
-
117
- ```ts
118
- import { ProjectLinter } from 'zemdomu';
119
- const linter = new ProjectLinter({ crossComponentAnalysis: true, crossComponentDepth: 2 });
120
- await linter.lintFile('App.jsx');
121
- ```
122
-
123
- ```bash
124
- npx zemdomu "src/**/*.{jsx,tsx}" --cross --cross-depth 2
125
- ```
126
-
127
- ### Performance Diagnostics
128
-
129
- Attach a `PerformanceDiagnostics` recorder to gather timing information for each
130
- file and rule:
131
-
132
- ```ts
133
- import { lint, PerformanceDiagnostics } from 'zemdomu';
134
- const perf = new PerformanceDiagnostics();
135
- lint(code, { perf });
136
- console.log(perf.getAsJSON());
137
- ```
138
-
139
- ## ๐Ÿ“ Writing Custom Rules
140
-
141
- Custom rules are simple objects implementing the `Rule` interface. At minimum
142
- provide a `name`, a `test` function that returns `true` when a node violates the
143
- rule and a `message` describing the problem:
144
-
145
- ```ts
146
- interface Rule {
147
- name: string;
148
- test(node: any): boolean;
149
- message: string;
150
- }
151
- ```
152
-
153
- ```js
154
- // my-rule.js
155
- module.exports = {
156
- name: 'noFooDiv',
157
- test: node => node.type === 'element' && node.tagName === 'foo',
158
- message: '<foo> is not allowed'
159
- };
160
- ```
161
-
162
- Use it programmatically:
163
-
164
- ```ts
165
- import { lint } from 'zemdomu';
166
- const results = lint('<foo></foo>', { customRules: [require('./my-rule')] });
167
- ```
168
-
169
- ### Helper Utilities
170
-
171
- For more advanced rules you may need direct access to the parsed HTML or JSX
172
- AST. ZemDomu exposes a few helpers to make this easier:
173
-
174
- ```ts
175
- import {
176
- parseHtml,
177
- visitHtml,
178
- getAttr,
179
- getJsxAttr,
180
- getTag,
181
- ElementNode,
182
- HtmlVisitor,
183
- } from 'zemdomu';
184
- ```
185
-
186
- `parseHtml` returns the root `ElementNode`. The `visitHtml` function performs a
187
- simple depthโ€‘first traversal using an `HtmlVisitor` with optional `enter` and
188
- `exit` callbacks. Utility functions like `getAttr` and `getJsxAttr` help reading
189
- attributes, while `getTag` resolves JSX element names.
190
-
191
- Or via the CLI:
192
-
193
- ```bash
194
- npx zemdomu file.html --custom my-rule.js
195
- ```
196
-
197
- ## ๐Ÿ”— Related Tools
198
-
199
- - [ZemDomu VS Code Extension](https://marketplace.visualstudio.com/items?itemName=ZachariasErydBerlin.zemdomu)
200
- - ZemDomu GitHub Action (coming soon)
201
-
202
- ## ๐Ÿ›  Development
203
-
204
- ```bash
205
- git clone https://github.com/Zemdomu/ZemDomu-core.git
206
- cd ZemDomu-core
207
- npm install
208
- npm run build
209
- ```
210
-
211
- Tests and coverage support coming soon.
212
-
213
- ## ๐Ÿค Contributing
214
-
215
- We welcome contributions! If you'd like to add rules, improve parsing or integrate new consumers:
216
-
217
- 1. Fork this repo
218
- 2. Add your logic inside `src/rules` or `src/linter.ts`
219
- 3. Write or update tests (if applicable)
220
- 4. Submit a pull request!
221
-
222
- ## ๐Ÿ“„ License
223
-
224
- MIT ยฉ 2025 Zacharias Eryd Berlin
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
+ ## ๐Ÿง  What is ZemDomu?
6
+
7
+ **ZemDomu** is a semantic-first linter that helps developers write better HTML and JSX by catching accessibility and structural issues. It parses `.html`, `.jsx` and `.tsx` files and exposes a simple `lint()` function that returns semantic violations.
8
+
9
+ ## ๐Ÿš€ Installation
10
+
11
+ ```bash
12
+ npm install zemdomu
13
+ # or
14
+ yarn add zemdomu
15
+ ```
16
+
17
+ ## โœจ Features
18
+
19
+ - โœ… Lint semantic issues in HTML, JSX and TSX
20
+ - ๐Ÿ“ฆ Works in Node.js, CI or any JS runtime
21
+ - โš™๏ธ Extensible rule system with simple custom rules
22
+ - ๐Ÿ”€ Cross-component analysis for React/JSX projects
23
+ - ๐Ÿš€ Command line interface with `--custom` and `--cross`
24
+ - โš ๏ธ Configurable rule severity (`error`, `warning`, `off`)
25
+ - ๐Ÿ“ˆ Performance diagnostics for profiling lint runs
26
+ - ๐Ÿ“š Shared by the extension and GitHub Action
27
+ - ๐Ÿงช Simple API: `lint(content, options)`
28
+
29
+ ## โš™๏ธ Usage Example
30
+
31
+ ```ts
32
+ import { lint } from "zemdomu";
33
+
34
+ const html = "<img>";
35
+ const results = lint(html, { rules: { requireAltText: true } });
36
+
37
+ console.log(results);
38
+ // [
39
+ // {
40
+ // line: 0,
41
+ // column: 0,
42
+ // message: '<img> tag missing alt attribute',
43
+ // rule: 'requireAltText'
44
+ // }
45
+ // ]
46
+
47
+ // Custom rules can be supplied via the `customRules` option
48
+ // const myRule = { name: 'demo', test: node => false, message: 'demo' };
49
+ // lint(html, { customRules: [myRule] });
50
+ ```
51
+
52
+ ## ๐Ÿ“– API
53
+
54
+ `lint(content: string, options?: LinterOptions): LintResult[]`
55
+
56
+ **Parameters**
57
+
58
+ - `content` โ€” HTML, JSX or TSX string input
59
+ - `options.rules` โ€” severity settings for built-in rules
60
+ - `options.customRules` โ€” array of additional rules
61
+ - `options.filePath` โ€” optional source file path
62
+ - `options.perf` โ€” attach a `PerformanceRecorder` instance
63
+
64
+ **Example `LinterOptions`**
65
+
66
+ ```ts
67
+ interface LinterOptions {
68
+ rules?: Record<string, 'error' | 'warning' | 'off'>;
69
+ customRules?: Rule[];
70
+ filePath?: string;
71
+ perf?: PerformanceRecorder;
72
+ }
73
+ ```
74
+
75
+ Example enabling one rule as a warning:
76
+
77
+ ```ts
78
+ const results = lint(html, {
79
+ rules: { requireAltText: 'warning', uniqueIds: 'error' }
80
+ });
81
+ ```
82
+
83
+ **Example `LintResult`**
84
+
85
+ ```ts
86
+ interface LintResult {
87
+ line: number;
88
+ column: number;
89
+ message: string;
90
+ rule: string;
91
+ }
92
+ ```
93
+
94
+ ## ๐Ÿ›  CLI Usage
95
+
96
+ Run the linter from the command line by installing the package globally or using
97
+ `npx`. Provide one or more glob patterns to specify the files to lint. Patterns
98
+ may be separated by spaces, commas or newlines:
99
+
100
+ ```bash
101
+ npx zemdomu "src/**/*.{html,jsx,tsx}" --custom my-rule.js
102
+ npx zemdomu "src/**/*.html,src/**/*.jsx"
103
+ ```
104
+
105
+ Use `--custom` (or `-c`) to provide a path to a JavaScript or TypeScript module
106
+ exporting a custom rule or array of rules. Use `--cross` to enable cross
107
+ component analysis.
108
+
109
+ ### Cross-Component Analysis
110
+
111
+ When analysing JSX projects you can track `<h1>` usage or similar patterns
112
+ across component boundaries. Instantiate `ProjectLinter` with the
113
+ `crossComponentAnalysis` option or pass `--cross` to the CLI. Use
114
+ `crossComponentDepth` (or `--cross-depth`) to limit how deep component trees are
115
+ traversed during analysis:
116
+
117
+ ```ts
118
+ import { ProjectLinter } from 'zemdomu';
119
+ const linter = new ProjectLinter({ crossComponentAnalysis: true, crossComponentDepth: 2 });
120
+ await linter.lintFile('App.jsx');
121
+ ```
122
+
123
+ ```bash
124
+ npx zemdomu "src/**/*.{jsx,tsx}" --cross --cross-depth 2
125
+ ```
126
+
127
+ ### Performance Diagnostics
128
+
129
+ Attach a `PerformanceDiagnostics` recorder to gather timing information for each
130
+ file and rule:
131
+
132
+ ```ts
133
+ import { lint, PerformanceDiagnostics } from 'zemdomu';
134
+ const perf = new PerformanceDiagnostics();
135
+ lint(code, { perf });
136
+ console.log(perf.getAsJSON());
137
+ ```
138
+
139
+ ## ๐Ÿ“ Writing Custom Rules
140
+
141
+ Custom rules are simple objects implementing the `Rule` interface. At minimum
142
+ provide a `name`, a `test` function that returns `true` when a node violates the
143
+ rule and a `message` describing the problem:
144
+
145
+ ```ts
146
+ interface Rule {
147
+ name: string;
148
+ test(node: any): boolean;
149
+ message: string;
150
+ }
151
+ ```
152
+
153
+ ```js
154
+ // my-rule.js
155
+ module.exports = {
156
+ name: 'noFooDiv',
157
+ test: node => node.type === 'element' && node.tagName === 'foo',
158
+ message: '<foo> is not allowed'
159
+ };
160
+ ```
161
+
162
+ Use it programmatically:
163
+
164
+ ```ts
165
+ import { lint } from 'zemdomu';
166
+ const results = lint('<foo></foo>', { customRules: [require('./my-rule')] });
167
+ ```
168
+
169
+ ### Helper Utilities
170
+
171
+ For more advanced rules you may need direct access to the parsed HTML or JSX
172
+ AST. ZemDomu exposes a few helpers to make this easier:
173
+
174
+ ```ts
175
+ import {
176
+ parseHtml,
177
+ visitHtml,
178
+ getAttr,
179
+ getJsxAttr,
180
+ getTag,
181
+ ElementNode,
182
+ HtmlVisitor,
183
+ } from 'zemdomu';
184
+ ```
185
+
186
+ `parseHtml` returns the root `ElementNode`. The `visitHtml` function performs a
187
+ simple depthโ€‘first traversal using an `HtmlVisitor` with optional `enter` and
188
+ `exit` callbacks. Utility functions like `getAttr` and `getJsxAttr` help reading
189
+ attributes, while `getTag` resolves JSX element names.
190
+
191
+ Or via the CLI:
192
+
193
+ ```bash
194
+ npx zemdomu file.html --custom my-rule.js
195
+ ```
196
+
197
+ ## ๐Ÿ”— Related Tools
198
+
199
+ - [ZemDomu VS Code Extension](https://marketplace.visualstudio.com/items?itemName=ZachariasErydBerlin.zemdomu)
200
+ - ZemDomu GitHub Action (coming soon)
201
+
202
+ ## ๐Ÿ›  Development
203
+
204
+ ```bash
205
+ git clone https://github.com/Zemdomu/ZemDomu-core.git
206
+ cd ZemDomu-core
207
+ npm install
208
+ npm run build
209
+ ```
210
+
211
+ Tests and coverage support coming soon.
212
+
213
+ ## ๐Ÿค Contributing
214
+
215
+ We welcome contributions! If you'd like to add rules, improve parsing or integrate new consumers:
216
+
217
+ 1. Fork this repo
218
+ 2. Add your logic inside `src/rules` or `src/linter.ts`
219
+ 3. Write or update tests (if applicable)
220
+ 4. Submit a pull request!
221
+
222
+ ## ๐Ÿ“„ License
223
+
224
+ MIT ยฉ 2025 Zacharias Eryd Berlin
package/out/linter.js ADDED
File without changes
package/out/src/cli.js CHANGED
@@ -31,7 +31,13 @@ async function run() {
31
31
  const file = args[++i];
32
32
  if (!file)
33
33
  throw new Error('Missing file for --custom');
34
- const mod = require(path_1.default.resolve(file));
34
+ const resolved = path_1.default.resolve(file);
35
+ const customDir = path_1.default.resolve('custom-rules');
36
+ const relative = path_1.default.relative(customDir, resolved);
37
+ if (relative.startsWith('..') || path_1.default.isAbsolute(relative)) {
38
+ throw new Error('Custom rule file must be inside ./custom-rules');
39
+ }
40
+ const mod = require(resolved);
35
41
  const rules = (_a = mod.default) !== null && _a !== void 0 ? _a : mod;
36
42
  if (Array.isArray(rules))
37
43
  customRules.push(...rules);
@@ -32,11 +32,17 @@ var __importStar = (this && this.__importStar) || (function () {
32
32
  return result;
33
33
  };
34
34
  })();
35
+ var __importDefault = (this && this.__importDefault) || function (mod) {
36
+ return (mod && mod.__esModule) ? mod : { "default": mod };
37
+ };
35
38
  Object.defineProperty(exports, "__esModule", { value: true });
36
39
  exports.ProjectLinter = void 0;
37
40
  const fs = __importStar(require("fs/promises"));
41
+ const path_1 = __importDefault(require("path"));
42
+ const ts = __importStar(require("typescript"));
38
43
  const linter_1 = require("./linter");
39
44
  const component_analyzer_1 = require("./component-analyzer");
45
+ const collectLocalDeps_1 = require("./utils/collectLocalDeps");
40
46
  class ProjectLinter {
41
47
  constructor(options = {}) {
42
48
  this.opts = options;
@@ -47,7 +53,7 @@ class ProjectLinter {
47
53
  }
48
54
  async lintFile(filePath, content) {
49
55
  if (!content) {
50
- content = await fs.readFile(filePath, 'utf8');
56
+ content = await fs.readFile(filePath, "utf8");
51
57
  }
52
58
  const results = (0, linter_1.lint)(content, { ...this.opts, filePath });
53
59
  const byFile = new Map();
@@ -55,9 +61,8 @@ class ProjectLinter {
55
61
  const xmlMode = /\.(jsx|tsx)$/.test(filePath);
56
62
  if (xmlMode) {
57
63
  const component = await this.analyzer.analyzeFile(filePath);
58
- if (component) {
64
+ if (component)
59
65
  this.analyzer.registerComponent(component, results);
60
- }
61
66
  if (this.opts.crossComponentAnalysis) {
62
67
  const cross = this.analyzer.analyzeComponentTree();
63
68
  for (const r of cross) {
@@ -72,8 +77,30 @@ class ProjectLinter {
72
77
  return byFile;
73
78
  }
74
79
  async lintFiles(filePaths) {
80
+ var _a, _b, _c;
81
+ const root = (_a = this.opts.rootDir) !== null && _a !== void 0 ? _a : process.cwd();
82
+ const configPath = ts.findConfigFile(root, ts.sys.fileExists, "tsconfig.json");
83
+ let baseUrl;
84
+ let paths;
85
+ if (configPath) {
86
+ const cfg = ts.readConfigFile(configPath, ts.sys.readFile).config;
87
+ const cfgDir = path_1.default.dirname(configPath);
88
+ baseUrl = ((_b = cfg === null || cfg === void 0 ? void 0 : cfg.compilerOptions) === null || _b === void 0 ? void 0 : _b.baseUrl)
89
+ ? path_1.default.resolve(cfgDir, cfg.compilerOptions.baseUrl)
90
+ : undefined;
91
+ paths = (_c = cfg === null || cfg === void 0 ? void 0 : cfg.compilerOptions) === null || _c === void 0 ? void 0 : _c.paths;
92
+ }
93
+ const targets = this.opts.crossComponentAnalysis
94
+ ? (0, collectLocalDeps_1.collectLocalDeps)(filePaths, {
95
+ rootDir: root,
96
+ baseUrl,
97
+ paths,
98
+ maxDepth: this.opts.crossComponentDepth,
99
+ })
100
+ : filePaths;
101
+ const uniqueTargets = Array.from(new Set(targets.map((p) => path_1.default.resolve(p))));
75
102
  const aggregated = new Map();
76
- for (const filePath of filePaths) {
103
+ for (const filePath of uniqueTargets) {
77
104
  const fileMap = await this.lintFile(filePath);
78
105
  for (const [fp, res] of fileMap.entries()) {
79
106
  if (!aggregated.has(fp))
@@ -0,0 +1,132 @@
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.collectLocalDeps = collectLocalDeps;
40
+ const fs_1 = __importDefault(require("fs"));
41
+ const path_1 = __importDefault(require("path"));
42
+ const ts = __importStar(require("typescript"));
43
+ const EXTS = [".tsx", ".ts", ".jsx", ".js"];
44
+ function resolveWithExtensions(base) {
45
+ if (fs_1.default.existsSync(base) && fs_1.default.statSync(base).isFile())
46
+ return base;
47
+ for (const ext of EXTS) {
48
+ const p = base + ext;
49
+ if (fs_1.default.existsSync(p) && fs_1.default.statSync(p).isFile())
50
+ return p;
51
+ }
52
+ for (const ext of EXTS) {
53
+ const p = path_1.default.join(base, "index" + ext);
54
+ if (fs_1.default.existsSync(p) && fs_1.default.statSync(p).isFile())
55
+ return p;
56
+ }
57
+ return null;
58
+ }
59
+ function resolveAlias(spec, fileDir, ctx) {
60
+ if (spec.startsWith(".") || spec.startsWith("/")) {
61
+ return resolveWithExtensions(path_1.default.resolve(fileDir, spec));
62
+ }
63
+ const { baseUrl, paths } = ctx;
64
+ if (baseUrl && paths) {
65
+ for (const [pattern, targets] of Object.entries(paths)) {
66
+ const starIdx = pattern.indexOf("*");
67
+ if (starIdx >= 0) {
68
+ const pre = pattern.slice(0, starIdx);
69
+ const post = pattern.slice(starIdx + 1);
70
+ if (spec.startsWith(pre) && spec.endsWith(post)) {
71
+ const middle = spec.slice(pre.length, spec.length - post.length);
72
+ for (const t of targets) {
73
+ const candidate = t.replace("*", middle);
74
+ const abs = path_1.default.resolve(baseUrl, candidate);
75
+ const hit = resolveWithExtensions(abs);
76
+ if (hit)
77
+ return hit;
78
+ }
79
+ }
80
+ }
81
+ else if (spec === pattern) {
82
+ for (const t of targets) {
83
+ const abs = path_1.default.resolve(baseUrl, t);
84
+ const hit = resolveWithExtensions(abs);
85
+ if (hit)
86
+ return hit;
87
+ }
88
+ }
89
+ }
90
+ }
91
+ return null;
92
+ }
93
+ function collectLocalDeps(entries, ctx) {
94
+ const root = path_1.default.resolve(ctx.rootDir);
95
+ const seen = new Set();
96
+ const q = entries.map((p) => ({
97
+ file: path_1.default.resolve(p),
98
+ depth: 0,
99
+ }));
100
+ while (q.length) {
101
+ const { file, depth } = q.pop();
102
+ if (seen.has(file))
103
+ continue;
104
+ seen.add(file);
105
+ if (ctx.maxDepth !== undefined && depth >= ctx.maxDepth)
106
+ continue;
107
+ let code = "";
108
+ try {
109
+ code = fs_1.default.readFileSync(file, "utf8");
110
+ }
111
+ catch {
112
+ continue;
113
+ }
114
+ const sf = ts.createSourceFile(file, code, ts.ScriptTarget.Latest, true);
115
+ sf.forEachChild((node) => {
116
+ let spec;
117
+ if (ts.isImportDeclaration(node) && node.moduleSpecifier) {
118
+ spec = node.moduleSpecifier.text;
119
+ }
120
+ else if (ts.isExportDeclaration(node) && node.moduleSpecifier) {
121
+ spec = node.moduleSpecifier.text;
122
+ }
123
+ if (!spec)
124
+ return;
125
+ const resolved = resolveAlias(spec, path_1.default.dirname(file), ctx);
126
+ if (resolved && resolved.startsWith(root)) {
127
+ q.push({ file: resolved, depth: depth + 1 });
128
+ }
129
+ });
130
+ }
131
+ return Array.from(seen);
132
+ }
@@ -6,11 +6,14 @@ const { spawnSync } = require('child_process');
6
6
  const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'zd-cli-'));
7
7
  const htmlFile = path.join(tmp, 'test.html');
8
8
  fs.writeFileSync(htmlFile, '<foo></foo>');
9
- const ruleFile = path.join(tmp, 'my-rule.js');
9
+ const customDir = path.join(tmp, 'custom-rules');
10
+ fs.mkdirSync(customDir);
11
+ const ruleFile = path.join(customDir, 'my-rule.js');
10
12
  fs.writeFileSync(ruleFile, `module.exports = {\n name: 'noFoo',\n test: n => (n.type === 'element' && n.tagName === 'foo') || (n.type === 'JSXElement' && n.openingElement && n.openingElement.name && n.openingElement.name.name === 'foo'),\n message: 'Foo elements are not allowed'\n};\n`);
11
13
  const cli = path.join(__dirname, '..', 'src', 'cli.js');
12
14
  const result = spawnSync('node', [cli, htmlFile, '--custom', ruleFile], {
13
- encoding: 'utf8'
15
+ encoding: 'utf8',
16
+ cwd: tmp,
14
17
  });
15
18
  if (result.status === 0) {
16
19
  console.error(result.stdout, result.stderr);
@@ -22,3 +25,18 @@ if (!output.includes('Foo elements are not allowed')) {
22
25
  throw new Error('Expected custom rule message');
23
26
  }
24
27
  console.log('CLI custom rule test passed');
28
+ // Should error when rule file is outside the custom-rules directory
29
+ const outsideRule = path.join(tmp, 'other-rule.js');
30
+ fs.writeFileSync(outsideRule, 'module.exports = {};');
31
+ const bad = spawnSync('node', [cli, htmlFile, '--custom', outsideRule], {
32
+ encoding: 'utf8',
33
+ cwd: tmp,
34
+ });
35
+ if (bad.status === 0) {
36
+ throw new Error('Expected CLI to fail for outside rule');
37
+ }
38
+ if (!bad.stderr.includes('Custom rule file must be inside ./custom-rules')) {
39
+ console.error(bad.stdout, bad.stderr);
40
+ throw new Error('Expected directory restriction error');
41
+ }
42
+ console.log('CLI custom rule path restriction test passed');
@@ -0,0 +1,7 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.default = Button;
4
+ const jsx_runtime_1 = require("react/jsx-runtime");
5
+ function Button() {
6
+ return ((0, jsx_runtime_1.jsxs)("div", { children: [(0, jsx_runtime_1.jsx)("h1", { children: "Button" }), (0, jsx_runtime_1.jsx)("h5", { children: "Subsection" })] }));
7
+ }
@@ -0,0 +1,11 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.default = Page;
7
+ const jsx_runtime_1 = require("react/jsx-runtime");
8
+ const Section_1 = __importDefault(require("@alias/Section"));
9
+ function Page() {
10
+ return ((0, jsx_runtime_1.jsxs)("main", { children: [(0, jsx_runtime_1.jsx)("h1", { children: "Hello" }), (0, jsx_runtime_1.jsx)(Section_1.default, {})] }));
11
+ }
@@ -0,0 +1,11 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.default = Section;
7
+ const jsx_runtime_1 = require("react/jsx-runtime");
8
+ const SubSection_1 = __importDefault(require("@alias/SubSection"));
9
+ function Section() {
10
+ return ((0, jsx_runtime_1.jsxs)("section", { children: [(0, jsx_runtime_1.jsx)("h2", { children: "Section Title" }), (0, jsx_runtime_1.jsx)(SubSection_1.default, {})] }));
11
+ }
@@ -0,0 +1,11 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.default = SubSection;
7
+ const jsx_runtime_1 = require("react/jsx-runtime");
8
+ const Button_1 = __importDefault(require("@alias/Button"));
9
+ function SubSection() {
10
+ return ((0, jsx_runtime_1.jsxs)("div", { children: [(0, jsx_runtime_1.jsx)("h3", { children: "SubSection" }), (0, jsx_runtime_1.jsx)(Button_1.default, {})] }));
11
+ }
@@ -0,0 +1,31 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ const assert_1 = __importDefault(require("assert"));
7
+ const path_1 = __importDefault(require("path"));
8
+ const index_1 = require("../../src/index");
9
+ describe("cross component heading order (ts-path-alias)", () => {
10
+ it("follows @alias/* from PageAlias and finds violations", async () => {
11
+ var _a, _b;
12
+ const pagePath = path_1.default.resolve(__dirname, "../../../tests/crossComponent/Page.tsx");
13
+ const linter = new index_1.ProjectLinter({
14
+ crossComponentAnalysis: true,
15
+ rules: {
16
+ singleH1: "error",
17
+ enforceHeadingOrder: "error",
18
+ requireButtonText: "off",
19
+ },
20
+ });
21
+ const map = await linter.lintFiles([pagePath]);
22
+ const results = Array.from(map.values()).flat();
23
+ const byRule = results.reduce((a, r) => {
24
+ var _a;
25
+ a[r.rule] = ((_a = a[r.rule]) !== null && _a !== void 0 ? _a : 0) + 1;
26
+ return a;
27
+ }, {});
28
+ assert_1.default.ok(((_a = byRule["singleH1"]) !== null && _a !== void 0 ? _a : 0) >= 1);
29
+ assert_1.default.ok(((_b = byRule["enforceHeadingOrder"]) !== null && _b !== void 0 ? _b : 0) >= 1);
30
+ });
31
+ });
@@ -0,0 +1,31 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ const assert_1 = __importDefault(require("assert"));
7
+ const path_1 = __importDefault(require("path"));
8
+ const index_1 = require("../../src/index");
9
+ describe("cross component heading order (entry-only)", () => {
10
+ it("follows imports from Page and finds violations", async () => {
11
+ var _a, _b;
12
+ const pagePath = path_1.default.resolve(__dirname, "../../../tests/crossComponent/Page.tsx");
13
+ const linter = new index_1.ProjectLinter({
14
+ crossComponentAnalysis: true,
15
+ rules: {
16
+ singleH1: "error",
17
+ enforceHeadingOrder: "error",
18
+ requireButtonText: "off",
19
+ },
20
+ });
21
+ const map = await linter.lintFiles([pagePath]);
22
+ const results = Array.from(map.values()).flat();
23
+ const byRule = results.reduce((a, r) => {
24
+ var _a;
25
+ a[r.rule] = ((_a = a[r.rule]) !== null && _a !== void 0 ? _a : 0) + 1;
26
+ return a;
27
+ }, {});
28
+ assert_1.default.ok(((_a = byRule["singleH1"]) !== null && _a !== void 0 ? _a : 0) >= 1);
29
+ assert_1.default.ok(((_b = byRule["enforceHeadingOrder"]) !== null && _b !== void 0 ? _b : 0) >= 1);
30
+ });
31
+ });
@@ -0,0 +1,45 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ // tests/crossComponent/cross-heading-order.test.ts
7
+ const assert_1 = __importDefault(require("assert"));
8
+ const path_1 = __importDefault(require("path"));
9
+ const index_1 = require("../../src/index");
10
+ describe("cross component heading order", () => {
11
+ it("detects heading order and h1 issues across components", async () => {
12
+ var _a, _b;
13
+ const buttonPath = path_1.default.resolve(__dirname, "../../../tests/crossComponent/Button.tsx");
14
+ const sectionPath = path_1.default.resolve(__dirname, "../../../tests/crossComponent/Section.tsx");
15
+ const subSectionPath = path_1.default.resolve(__dirname, "../../../tests/crossComponent/SubSection.tsx");
16
+ const pagePath = path_1.default.resolve(__dirname, "../../../tests/crossComponent/Page.tsx");
17
+ const linter = new index_1.ProjectLinter({
18
+ crossComponentAnalysis: true,
19
+ rules: {
20
+ singleH1: "error",
21
+ enforceHeadingOrder: "error",
22
+ requireButtonText: "off",
23
+ },
24
+ });
25
+ const map = await linter.lintFiles([
26
+ buttonPath,
27
+ sectionPath,
28
+ subSectionPath,
29
+ pagePath,
30
+ ]);
31
+ const results = Array.from(map.values()).flat();
32
+ // Presence checks
33
+ assert_1.default.ok(results.some((r) => r.rule === "singleH1"), "Expected cross-component singleH1 warning");
34
+ assert_1.default.ok(results.some((r) => r.rule === "enforceHeadingOrder"), "Expected cross-component heading order warning");
35
+ // Count checks (looser; tighten later if you want)
36
+ const byRule = results.reduce((acc, r) => {
37
+ var _a;
38
+ acc[r.rule] = ((_a = acc[r.rule]) !== null && _a !== void 0 ? _a : 0) + 1;
39
+ return acc;
40
+ }, {});
41
+ assert_1.default.ok(((_a = byRule["singleH1"]) !== null && _a !== void 0 ? _a : 0) >= 1, "Expected at least one singleH1");
42
+ assert_1.default.ok(((_b = byRule["enforceHeadingOrder"]) !== null && _b !== void 0 ? _b : 0) >= 1, // set to >=2 if you expect more
43
+ "Expected at least one enforceHeadingOrder");
44
+ });
45
+ });
package/package.json CHANGED
@@ -1,54 +1,60 @@
1
- {
2
- "name": "zemdomu",
3
- "version": "1.2.0",
4
- "description": "Semantic HTML linter for HTML, JSX, and TSX. Detects accessibility, SEO, and structure issues before deployment.",
5
- "main": "./out/index.js",
6
- "bin": {
7
- "zemdomu": "./out/cli.js"
8
- },
9
- "files": [
10
- "out"
11
- ],
12
- "private": false,
13
- "scripts": {
14
- "build": "tsc -p tsconfig.json",
15
- "compile": "tsc -p tsconfig.json",
16
- "test": "npm run compile && mocha out/**/*.test.js"
17
- },
18
- "keywords": [
19
- "html",
20
- "jsx",
21
- "tsx",
22
- "linter",
23
- "accessibility",
24
- "semantic-html",
25
- "seo",
26
- "cli",
27
- "vscode",
28
- "github-action"
29
- ],
30
- "author": "Zacharias Eryd Berlin",
31
- "license": "ISC",
32
- "devDependencies": {
33
- "@types/babel__traverse": "^7.20.7",
34
- "@types/babel-traverse": "^6.25.10",
35
- "@types/babel-types": "^7.0.16",
36
- "@types/glob": "^8.1.0",
37
- "@types/mocha": "^10.0.10",
38
- "@types/node": "^22.15.17",
39
- "esbuild": "^0.25.5",
40
- "mocha": "^10.8.2",
41
- "typescript": "^5.8.2"
42
- },
43
- "dependencies": {
44
- "@babel/parser": "^7.27.0",
45
- "@babel/traverse": "^7.27.0",
46
- "@babel/types": "^7.27.0",
47
- "glob": "^7.2.3"
48
- },
49
- "jest": {
50
- "transform": {
51
- "^.+\\.tsx?$": "ts-jest"
52
- }
53
- }
54
- }
1
+ {
2
+ "name": "zemdomu",
3
+ "version": "1.3.2",
4
+ "description": "Semantic HTML linter for HTML, JSX, and TSX. Detects accessibility, SEO, and structure issues before deployment.",
5
+ "main": "./out/index.js",
6
+ "types": "./out/src/index.d.ts",
7
+ "bin": {
8
+ "zemdomu": "./out/src/cli.js"
9
+ },
10
+ "files": [
11
+ "out"
12
+ ],
13
+ "private": false,
14
+ "scripts": {
15
+ "build": "tsc -p tsconfig.json",
16
+ "compile": "tsc -p tsconfig.json",
17
+ "test": "npm run compile && mocha out/**/*.test.js",
18
+ "prepublishOnly": "npm run compile && npm test"
19
+ },
20
+ "keywords": [
21
+ "html",
22
+ "jsx",
23
+ "tsx",
24
+ "linter",
25
+ "accessibility",
26
+ "semantic-html",
27
+ "seo",
28
+ "cli",
29
+ "vscode",
30
+ "github-action"
31
+ ],
32
+ "author": "Zacharias Eryd Berlin",
33
+ "license": "ISC",
34
+ "devDependencies": {
35
+ "@types/babel__traverse": "^7.20.7",
36
+ "@types/babel-traverse": "^6.25.10",
37
+ "@types/babel-types": "^7.0.16",
38
+ "@types/glob": "^8.1.0",
39
+ "@types/mocha": "^10.0.10",
40
+ "@types/node": "^22.15.17",
41
+ "@types/react": "^19.1.9",
42
+ "@types/react-dom": "^19.1.7",
43
+ "esbuild": "^0.25.5",
44
+ "mocha": "^10.8.2",
45
+ "typescript": "^5.8.2"
46
+ },
47
+ "dependencies": {
48
+ "@babel/parser": "^7.27.0",
49
+ "@babel/traverse": "^7.27.0",
50
+ "@babel/types": "^7.27.0",
51
+ "glob": "^7.2.3",
52
+ "react": "^19.1.1",
53
+ "react-dom": "^19.1.1"
54
+ },
55
+ "jest": {
56
+ "transform": {
57
+ "^.+\\.tsx?$": "ts-jest"
58
+ }
59
+ }
60
+ }