@sarj/eslint-plugin 15.26.8 → 15.28.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/dist/_docs-XCw_6XV9.d.cts +88 -0
- package/dist/_docs-XCw_6XV9.d.ts +88 -0
- package/dist/index.cjs +970 -840
- package/dist/index.d.cts +10 -89
- package/dist/index.d.ts +10 -89
- package/dist/index.js +968 -838
- package/dist/rule-examples.cjs +112 -0
- package/dist/rule-examples.d.cts +7 -0
- package/dist/rule-examples.d.ts +7 -0
- package/dist/rule-examples.js +77 -0
- package/package.json +4 -3
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __create = Object.create;
|
|
3
|
+
var __defProp = Object.defineProperty;
|
|
4
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
5
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
7
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
8
|
+
var __export = (target, all) => {
|
|
9
|
+
for (var name in all)
|
|
10
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
11
|
+
};
|
|
12
|
+
var __copyProps = (to, from, except, desc) => {
|
|
13
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
14
|
+
for (let key of __getOwnPropNames(from))
|
|
15
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
16
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
17
|
+
}
|
|
18
|
+
return to;
|
|
19
|
+
};
|
|
20
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
21
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
|
22
|
+
// file that has been converted to a CommonJS file using a Babel-
|
|
23
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
24
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
25
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
26
|
+
mod
|
|
27
|
+
));
|
|
28
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
29
|
+
|
|
30
|
+
// src/rule-examples.ts
|
|
31
|
+
var rule_examples_exports = {};
|
|
32
|
+
__export(rule_examples_exports, {
|
|
33
|
+
verifyRuleExamples: () => verifyRuleExamples
|
|
34
|
+
});
|
|
35
|
+
module.exports = __toCommonJS(rule_examples_exports);
|
|
36
|
+
var import_promises = require("fs/promises");
|
|
37
|
+
var import_node_os = require("os");
|
|
38
|
+
var import_node_path = require("path");
|
|
39
|
+
var parser = __toESM(require("@typescript-eslint/parser"), 1);
|
|
40
|
+
var import_eslint = require("eslint");
|
|
41
|
+
async function verifyRuleExamples(rule) {
|
|
42
|
+
const spec = rule.documentation;
|
|
43
|
+
if (spec === void 0 || !spec.publicExamples.some((item) => item.outcome === "match") || !spec.publicExamples.some((item) => item.outcome === "no-match")) {
|
|
44
|
+
throw new Error("rule requires documented public matching and non-matching examples");
|
|
45
|
+
}
|
|
46
|
+
for (const example of spec.examples) {
|
|
47
|
+
const root = await (0, import_promises.mkdtemp)((0, import_node_path.join)((0, import_node_os.tmpdir)(), "sarj-rule-example-"));
|
|
48
|
+
try {
|
|
49
|
+
for (const file of example.files) {
|
|
50
|
+
const path = fixturePath(root, file.path);
|
|
51
|
+
await (0, import_promises.mkdir)((0, import_node_path.dirname)(path), { recursive: true });
|
|
52
|
+
await (0, import_promises.writeFile)(path, file.source);
|
|
53
|
+
}
|
|
54
|
+
const tsconfig = (0, import_node_path.join)(root, "tsconfig.json");
|
|
55
|
+
if (!example.files.some((file) => file.path === "tsconfig.json")) {
|
|
56
|
+
await (0, import_promises.writeFile)(tsconfig, JSON.stringify({ compilerOptions: {
|
|
57
|
+
target: "ESNext",
|
|
58
|
+
module: "ESNext",
|
|
59
|
+
moduleResolution: "Bundler",
|
|
60
|
+
strict: true,
|
|
61
|
+
allowJs: true,
|
|
62
|
+
jsx: "preserve",
|
|
63
|
+
noEmit: true
|
|
64
|
+
}, include: ["**/*"] }));
|
|
65
|
+
}
|
|
66
|
+
const ruleId = `@sarj/${spec.ruleId}`;
|
|
67
|
+
const config = {
|
|
68
|
+
files: ["**/*.{ts,tsx,js,jsx,mts,cts,mjs,cjs}"],
|
|
69
|
+
languageOptions: { parser, parserOptions: { project: tsconfig, tsconfigRootDir: root } },
|
|
70
|
+
plugins: { "@sarj": { rules: { [spec.ruleId]: rule } } },
|
|
71
|
+
rules: { [ruleId]: ["error", ...rule.defaultOptions ?? []] }
|
|
72
|
+
};
|
|
73
|
+
await verifyExample(new import_eslint.Linter({ cwd: root }), config, example, root, ruleId);
|
|
74
|
+
} finally {
|
|
75
|
+
await (0, import_promises.rm)(root, { recursive: true, force: true });
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
return spec.examples.length;
|
|
79
|
+
}
|
|
80
|
+
function fixturePath(root, path) {
|
|
81
|
+
const resolved = (0, import_node_path.resolve)(root, path);
|
|
82
|
+
if (!resolved.startsWith(root + import_node_path.sep)) throw new Error(`unsafe example path: ${path}`);
|
|
83
|
+
return resolved;
|
|
84
|
+
}
|
|
85
|
+
async function verifyExample(linter, config, example, root, ruleId) {
|
|
86
|
+
const focus = example.files.find((file) => file.path === example.focusPath);
|
|
87
|
+
if (focus === void 0) throw new Error(`${example.id}: focus file is missing`);
|
|
88
|
+
const filename = fixturePath(root, focus.path);
|
|
89
|
+
const messages = linter.verify(focus.source, config, { filename });
|
|
90
|
+
if (messages.some((message) => message.fatal || message.ruleId !== ruleId)) {
|
|
91
|
+
throw new Error(`${example.id}: invalid fixture: ${messages.map((message) => message.message).join("; ")}`);
|
|
92
|
+
}
|
|
93
|
+
if (messages.length !== example.expectedCount) {
|
|
94
|
+
throw new Error(`${example.id}: expected ${example.expectedCount} findings, received ${messages.length}`);
|
|
95
|
+
}
|
|
96
|
+
for (const expected of example.fixedFiles ?? []) {
|
|
97
|
+
const input = example.files.find((file) => file.path === expected.path);
|
|
98
|
+
if (input === void 0) throw new Error(`${example.id}: fixed file has no original`);
|
|
99
|
+
const options = { filename: fixturePath(root, input.path) };
|
|
100
|
+
const fixed = linter.verifyAndFix(input.source, config, options);
|
|
101
|
+
if (fixed.output !== expected.source) throw new Error(`${example.id}: unexpected fixed source`);
|
|
102
|
+
await (0, import_promises.writeFile)(options.filename, fixed.output);
|
|
103
|
+
const repeated = linter.verifyAndFix(fixed.output, config, options);
|
|
104
|
+
if (repeated.fixed || repeated.output !== fixed.output || repeated.messages.length > 0) {
|
|
105
|
+
throw new Error(`${example.id}: fix is not clean and idempotent`);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
110
|
+
0 && (module.exports = {
|
|
111
|
+
verifyRuleExamples
|
|
112
|
+
});
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { D as DocumentedRule } from './_docs-XCw_6XV9.cjs';
|
|
2
|
+
import '@typescript-eslint/utils';
|
|
3
|
+
|
|
4
|
+
/** Execute all authored examples, including private regressions, in isolated projects. */
|
|
5
|
+
declare function verifyRuleExamples<Options extends readonly unknown[], MessageIds extends string>(rule: DocumentedRule<Options, MessageIds>): Promise<number>;
|
|
6
|
+
|
|
7
|
+
export { verifyRuleExamples };
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { D as DocumentedRule } from './_docs-XCw_6XV9.js';
|
|
2
|
+
import '@typescript-eslint/utils';
|
|
3
|
+
|
|
4
|
+
/** Execute all authored examples, including private regressions, in isolated projects. */
|
|
5
|
+
declare function verifyRuleExamples<Options extends readonly unknown[], MessageIds extends string>(rule: DocumentedRule<Options, MessageIds>): Promise<number>;
|
|
6
|
+
|
|
7
|
+
export { verifyRuleExamples };
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
// src/rule-examples.ts
|
|
2
|
+
import { mkdtemp, mkdir, rm, writeFile } from "fs/promises";
|
|
3
|
+
import { tmpdir } from "os";
|
|
4
|
+
import { dirname, join, resolve, sep } from "path";
|
|
5
|
+
import * as parser from "@typescript-eslint/parser";
|
|
6
|
+
import { Linter } from "eslint";
|
|
7
|
+
async function verifyRuleExamples(rule) {
|
|
8
|
+
const spec = rule.documentation;
|
|
9
|
+
if (spec === void 0 || !spec.publicExamples.some((item) => item.outcome === "match") || !spec.publicExamples.some((item) => item.outcome === "no-match")) {
|
|
10
|
+
throw new Error("rule requires documented public matching and non-matching examples");
|
|
11
|
+
}
|
|
12
|
+
for (const example of spec.examples) {
|
|
13
|
+
const root = await mkdtemp(join(tmpdir(), "sarj-rule-example-"));
|
|
14
|
+
try {
|
|
15
|
+
for (const file of example.files) {
|
|
16
|
+
const path = fixturePath(root, file.path);
|
|
17
|
+
await mkdir(dirname(path), { recursive: true });
|
|
18
|
+
await writeFile(path, file.source);
|
|
19
|
+
}
|
|
20
|
+
const tsconfig = join(root, "tsconfig.json");
|
|
21
|
+
if (!example.files.some((file) => file.path === "tsconfig.json")) {
|
|
22
|
+
await writeFile(tsconfig, JSON.stringify({ compilerOptions: {
|
|
23
|
+
target: "ESNext",
|
|
24
|
+
module: "ESNext",
|
|
25
|
+
moduleResolution: "Bundler",
|
|
26
|
+
strict: true,
|
|
27
|
+
allowJs: true,
|
|
28
|
+
jsx: "preserve",
|
|
29
|
+
noEmit: true
|
|
30
|
+
}, include: ["**/*"] }));
|
|
31
|
+
}
|
|
32
|
+
const ruleId = `@sarj/${spec.ruleId}`;
|
|
33
|
+
const config = {
|
|
34
|
+
files: ["**/*.{ts,tsx,js,jsx,mts,cts,mjs,cjs}"],
|
|
35
|
+
languageOptions: { parser, parserOptions: { project: tsconfig, tsconfigRootDir: root } },
|
|
36
|
+
plugins: { "@sarj": { rules: { [spec.ruleId]: rule } } },
|
|
37
|
+
rules: { [ruleId]: ["error", ...rule.defaultOptions ?? []] }
|
|
38
|
+
};
|
|
39
|
+
await verifyExample(new Linter({ cwd: root }), config, example, root, ruleId);
|
|
40
|
+
} finally {
|
|
41
|
+
await rm(root, { recursive: true, force: true });
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
return spec.examples.length;
|
|
45
|
+
}
|
|
46
|
+
function fixturePath(root, path) {
|
|
47
|
+
const resolved = resolve(root, path);
|
|
48
|
+
if (!resolved.startsWith(root + sep)) throw new Error(`unsafe example path: ${path}`);
|
|
49
|
+
return resolved;
|
|
50
|
+
}
|
|
51
|
+
async function verifyExample(linter, config, example, root, ruleId) {
|
|
52
|
+
const focus = example.files.find((file) => file.path === example.focusPath);
|
|
53
|
+
if (focus === void 0) throw new Error(`${example.id}: focus file is missing`);
|
|
54
|
+
const filename = fixturePath(root, focus.path);
|
|
55
|
+
const messages = linter.verify(focus.source, config, { filename });
|
|
56
|
+
if (messages.some((message) => message.fatal || message.ruleId !== ruleId)) {
|
|
57
|
+
throw new Error(`${example.id}: invalid fixture: ${messages.map((message) => message.message).join("; ")}`);
|
|
58
|
+
}
|
|
59
|
+
if (messages.length !== example.expectedCount) {
|
|
60
|
+
throw new Error(`${example.id}: expected ${example.expectedCount} findings, received ${messages.length}`);
|
|
61
|
+
}
|
|
62
|
+
for (const expected of example.fixedFiles ?? []) {
|
|
63
|
+
const input = example.files.find((file) => file.path === expected.path);
|
|
64
|
+
if (input === void 0) throw new Error(`${example.id}: fixed file has no original`);
|
|
65
|
+
const options = { filename: fixturePath(root, input.path) };
|
|
66
|
+
const fixed = linter.verifyAndFix(input.source, config, options);
|
|
67
|
+
if (fixed.output !== expected.source) throw new Error(`${example.id}: unexpected fixed source`);
|
|
68
|
+
await writeFile(options.filename, fixed.output);
|
|
69
|
+
const repeated = linter.verifyAndFix(fixed.output, config, options);
|
|
70
|
+
if (repeated.fixed || repeated.output !== fixed.output || repeated.messages.length > 0) {
|
|
71
|
+
throw new Error(`${example.id}: fix is not clean and idempotent`);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
export {
|
|
76
|
+
verifyRuleExamples
|
|
77
|
+
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sarj/eslint-plugin",
|
|
3
|
-
"version": "15.
|
|
3
|
+
"version": "15.28.0",
|
|
4
4
|
"packageManager": "npm@12.1.0",
|
|
5
5
|
"description": "Custom ESLint rules for hypermodern TypeScript / React / Next.js projects",
|
|
6
6
|
"type": "module",
|
|
@@ -34,12 +34,13 @@
|
|
|
34
34
|
},
|
|
35
35
|
"scripts": {
|
|
36
36
|
"build": "tsup",
|
|
37
|
-
"dogfood": "npm run build &&
|
|
37
|
+
"dogfood": "npm run build && npm run dogfood:built",
|
|
38
38
|
"lint": "eslint . --max-warnings 0",
|
|
39
39
|
"prepack": "npm run build && npm run verify-package",
|
|
40
40
|
"test": "vitest run",
|
|
41
41
|
"typecheck": "tsc --noEmit",
|
|
42
|
-
"verify-package": "node -e \"const fs=require('node:fs'),p=require('./package.json'),walk=x=>typeof x==='string'?[x]:x&&typeof x==='object'?Object.values(x).flatMap(walk):[];for(const file of new Set([p.main,p.module,p.types,...walk(p.exports)].filter(Boolean).map(x=>x.startsWith('./')?x.slice(2):x))){if(!file.startsWith('dist/'))throw new Error('export must live under dist: '+file);const stat=fs.statSync(file);if(!stat.isFile()||stat.size===0)throw new Error('missing or empty export: '+file)}\""
|
|
42
|
+
"verify-package": "node -e \"const fs=require('node:fs'),p=require('./package.json'),walk=x=>typeof x==='string'?[x]:x&&typeof x==='object'?Object.values(x).flatMap(walk):[];for(const file of new Set([p.main,p.module,p.types,...walk(p.exports)].filter(Boolean).map(x=>x.startsWith('./')?x.slice(2):x))){if(!file.startsWith('dist/'))throw new Error('export must live under dist: '+file);const stat=fs.statSync(file);if(!stat.isFile()||stat.size===0)throw new Error('missing or empty export: '+file)}\"",
|
|
43
|
+
"dogfood:built": "eslint --config eslint.dogfood.config.mjs src tests eslint.config.mjs eslint.dogfood.config.mjs module-constant-naming-options.mjs tsup.config.ts vitest.config.ts --max-warnings 0 && node -e \"import('./dist/index.js').then(({default:p}) => console.log('dogfood: '+Object.keys(p.rules).length+' TypeScript rules, 0 diagnostics'))\""
|
|
43
44
|
},
|
|
44
45
|
"keywords": [
|
|
45
46
|
"eslint",
|