@tsslint/config 3.0.4 → 3.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +32 -0
- package/lib/eslint.js +110 -1
- package/lib/plugins/ignore.js +28 -10
- package/package.json +4 -4
package/README.md
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# @tsslint/config
|
|
2
|
+
|
|
3
|
+
Public API for `tsslint.config.ts`: `defineConfig`, `defineRule`, `definePlugin`, bundled plugin factories (`createIgnorePlugin`, `createCategoryPlugin`, `createDiagnosticsPlugin`), and importers for ESLint / TSLint / TSL rules.
|
|
4
|
+
|
|
5
|
+
## Usage
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install @tsslint/config --save-dev
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
```ts
|
|
12
|
+
import { defineConfig } from '@tsslint/config';
|
|
13
|
+
|
|
14
|
+
export default defineConfig({
|
|
15
|
+
rules: {
|
|
16
|
+
'no-debugger': ({ typescript: ts, file, report }) => {
|
|
17
|
+
ts.forEachChild(file, function visit(node) {
|
|
18
|
+
if (node.kind === ts.SyntaxKind.DebuggerStatement) {
|
|
19
|
+
report('Debugger statement is not allowed.', node.getStart(file), node.getEnd());
|
|
20
|
+
}
|
|
21
|
+
ts.forEachChild(node, visit);
|
|
22
|
+
});
|
|
23
|
+
},
|
|
24
|
+
},
|
|
25
|
+
});
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
`defineRule` / `definePlugin` are available for authoring rules or plugins in their own files, where the context type can't be inferred from the surrounding config.
|
|
29
|
+
|
|
30
|
+
The package also ships a `tsslint-docgen` binary that generates JSDoc for imported ESLint/TSLint rules so they autocomplete in your config.
|
|
31
|
+
|
|
32
|
+
See the [root README](../../README.md) for rule authoring, ESLint/TSLint/TSL interop, plugins, and caching.
|
package/lib/eslint.js
CHANGED
|
@@ -54,6 +54,96 @@ const loader = async (moduleName) => {
|
|
|
54
54
|
}
|
|
55
55
|
return mod;
|
|
56
56
|
};
|
|
57
|
+
// Per-plugin "where do individual rule files live" cache. Filled the first
|
|
58
|
+
// time we successfully load a rule from that plugin, then reused for every
|
|
59
|
+
// subsequent rule from the same plugin so we never have to require the
|
|
60
|
+
// whole plugin (which on `@typescript-eslint/eslint-plugin` is ~400 ms
|
|
61
|
+
// cold start to load all ~150 rules eagerly).
|
|
62
|
+
//
|
|
63
|
+
// `null` means "we tried to detect a per-rule layout and couldn't — fall
|
|
64
|
+
// back to whole-plugin require for any rule from this plugin".
|
|
65
|
+
const pluginRuleLoaders = new Map();
|
|
66
|
+
// Common per-rule directory layouts in published ESLint plugins.
|
|
67
|
+
// `dist/rules/<name>.js` — @typescript-eslint, eslint-plugin-jsdoc (newer)
|
|
68
|
+
// `lib/rules/<name>.js` — eslint-plugin-react / import / vue / n / jsx-a11y
|
|
69
|
+
// `rules/<name>.js` — eslint-plugin-unicorn / promise
|
|
70
|
+
// `build/rules/<name>.js` — some Babel-built plugins
|
|
71
|
+
// `src/rules/<name>.js` — published-from-source plugins (rare)
|
|
72
|
+
//
|
|
73
|
+
// Each candidate is probed with the FIRST rule we're asked to load. The
|
|
74
|
+
// directory that has that rule's file wins; remember the layout so later
|
|
75
|
+
// rules go straight to disk by absolute path.
|
|
76
|
+
const RULE_DIR_CANDIDATES = ['dist/rules', 'lib/rules', 'rules', 'build/rules', 'src/rules'];
|
|
77
|
+
const RULE_FILE_EXTS = ['.js', '.cjs'];
|
|
78
|
+
function detectRuleLoader(pluginName, probeRuleName) {
|
|
79
|
+
let pkgRoot;
|
|
80
|
+
try {
|
|
81
|
+
pkgRoot = path.dirname(require.resolve(`${pluginName}/package.json`));
|
|
82
|
+
}
|
|
83
|
+
catch {
|
|
84
|
+
return null;
|
|
85
|
+
}
|
|
86
|
+
for (const dir of RULE_DIR_CANDIDATES) {
|
|
87
|
+
for (const ext of RULE_FILE_EXTS) {
|
|
88
|
+
const probePath = path.join(pkgRoot, dir, probeRuleName + ext);
|
|
89
|
+
if (!fs.existsSync(probePath))
|
|
90
|
+
continue;
|
|
91
|
+
// Lock in this dir + ext for every subsequent rule from this plugin.
|
|
92
|
+
// Absolute-path `require()` bypasses the package's `exports` field
|
|
93
|
+
// (which in @typescript-eslint blocks `dist/rules/<name>` access),
|
|
94
|
+
// so this works even when the plugin doesn't expose individual
|
|
95
|
+
// rules as a public subpath.
|
|
96
|
+
//
|
|
97
|
+
// Pre-warm the plugin's shared dependency tree behind a
|
|
98
|
+
// stable, plugin-named profile frame. Without this, the
|
|
99
|
+
// first rule we happen to load gets blamed for ~150–250 ms
|
|
100
|
+
// of `@typescript-eslint/utils` + `scope-manager` +
|
|
101
|
+
// `typescript-estree` etc. transitive load — and which rule
|
|
102
|
+
// that is depends on the user's config-object key order.
|
|
103
|
+
// The pre-warm gives that cost a deterministic attribution
|
|
104
|
+
// (`<pluginName>:init` shows up in the flame graph) and
|
|
105
|
+
// leaves every subsequent rule frame as pure rule-body
|
|
106
|
+
// load time.
|
|
107
|
+
//
|
|
108
|
+
// `//# sourceURL=` directive lets Chrome DevTools surface
|
|
109
|
+
// these `new Function`-derived frames in flame graphs /
|
|
110
|
+
// Bottom-Up / search; without it the SharedFunctionInfo
|
|
111
|
+
// has an empty url and DevTools hides the frame as native.
|
|
112
|
+
const initKey = JSON.stringify(pluginName + ':init');
|
|
113
|
+
const probeAbs = path.join(pkgRoot, dir, probeRuleName + ext);
|
|
114
|
+
new Function('warm', `({ ${initKey}: () => warm() })[${initKey}]();\n//# sourceURL=tsslint-rule-loader/${pluginName}/init.js`)(() => {
|
|
115
|
+
try {
|
|
116
|
+
require(probeAbs);
|
|
117
|
+
}
|
|
118
|
+
catch { }
|
|
119
|
+
});
|
|
120
|
+
// Per-rule named thunk. NamedEvaluation on a computed
|
|
121
|
+
// property key (`{ [name]: () => ... }`) sets
|
|
122
|
+
// `Function.prototype.name` at runtime — visible to stack
|
|
123
|
+
// traces, but V8's CPU profile uses the parse-time-inferred
|
|
124
|
+
// SharedFunctionInfo name and ignores runtime renames. To
|
|
125
|
+
// get a parse-time literal name we compile per-thunk source
|
|
126
|
+
// with `new Function`, baking the rule name in as a string
|
|
127
|
+
// literal. Same trick ESLint core's `LazyLoadingRuleMap`
|
|
128
|
+
// achieves with static-keyed object literals.
|
|
129
|
+
return ruleName => {
|
|
130
|
+
const filePath = path.join(pkgRoot, dir, ruleName + ext);
|
|
131
|
+
const key = JSON.stringify(ruleName);
|
|
132
|
+
const thunk = new Function('requireFn', `return ({ ${key}: () => requireFn() })[${key}];\n//# sourceURL=tsslint-rule-loader/${pluginName}/${ruleName}.js`)(() => {
|
|
133
|
+
try {
|
|
134
|
+
const m = require(filePath);
|
|
135
|
+
return (m && 'default' in m ? m.default : m);
|
|
136
|
+
}
|
|
137
|
+
catch {
|
|
138
|
+
return undefined;
|
|
139
|
+
}
|
|
140
|
+
});
|
|
141
|
+
return thunk();
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
return null;
|
|
146
|
+
}
|
|
57
147
|
/**
|
|
58
148
|
* Converts an ESLint rules configuration to TSSLint rules.
|
|
59
149
|
*
|
|
@@ -126,6 +216,25 @@ async function loadRuleByKey(rule) {
|
|
|
126
216
|
}
|
|
127
217
|
async function loadRule(pluginName, ruleName) {
|
|
128
218
|
if (pluginName) {
|
|
219
|
+
// Try per-rule lazy load first — saves loading the whole plugin's
|
|
220
|
+
// ~all-rules-eager bundle. On a 30-rule typescript-eslint config
|
|
221
|
+
// this saves ~150 ms cold start vs requiring the whole plugin.
|
|
222
|
+
let lazy = pluginRuleLoaders.get(pluginName);
|
|
223
|
+
if (lazy === undefined) {
|
|
224
|
+
lazy = detectRuleLoader(pluginName, ruleName);
|
|
225
|
+
pluginRuleLoaders.set(pluginName, lazy);
|
|
226
|
+
}
|
|
227
|
+
if (lazy) {
|
|
228
|
+
const r = lazy(ruleName);
|
|
229
|
+
if (r)
|
|
230
|
+
return r;
|
|
231
|
+
// Layout was detected but this specific rule's file doesn't
|
|
232
|
+
// exist there (rule renamed / moved / lives under a sub-path).
|
|
233
|
+
// Fall through to whole-plugin load below.
|
|
234
|
+
}
|
|
235
|
+
// Fallback: ESM-only plugins, plugins with no recognisable layout,
|
|
236
|
+
// or rules whose file doesn't sit at `<dir>/<name>.js`. Pay the
|
|
237
|
+
// eager cost once per plugin.
|
|
129
238
|
plugins[pluginName] ??= loader(pluginName);
|
|
130
239
|
const plugin = await plugins[pluginName];
|
|
131
240
|
return plugin?.rules[ruleName];
|
|
@@ -134,7 +243,7 @@ async function loadRule(pluginName, ruleName) {
|
|
|
134
243
|
while (true) {
|
|
135
244
|
const rulePath = path.join(dir, 'node_modules', 'eslint', 'lib', 'rules', `${ruleName}.js`);
|
|
136
245
|
if (fs.existsSync(rulePath)) {
|
|
137
|
-
return
|
|
246
|
+
return require(rulePath);
|
|
138
247
|
}
|
|
139
248
|
const parentDir = path.resolve(dir, '..');
|
|
140
249
|
if (parentDir === dir) {
|
package/lib/plugins/ignore.js
CHANGED
|
@@ -2,6 +2,28 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.create = create;
|
|
4
4
|
const ts_api_utils_1 = require("ts-api-utils");
|
|
5
|
+
// `forEachComment` walks every token of the source file; if multiple ignore
|
|
6
|
+
// plugins are configured (one per directive form) we'd walk the same file
|
|
7
|
+
// 3+ times per lint pass. Cache the comment list once per ts.SourceFile so
|
|
8
|
+
// every plugin shares a single sweep. The WeakMap drops entries when the
|
|
9
|
+
// SourceFile is GC'd.
|
|
10
|
+
const sharedFileComments = new WeakMap();
|
|
11
|
+
function getFileComments(file) {
|
|
12
|
+
let comments = sharedFileComments.get(file);
|
|
13
|
+
if (!comments) {
|
|
14
|
+
comments = [];
|
|
15
|
+
(0, ts_api_utils_1.forEachComment)(file, (fullText, { pos, end }) => {
|
|
16
|
+
const start = pos + 2; // strip leading `//` or `/*`
|
|
17
|
+
comments.push({
|
|
18
|
+
pos: start,
|
|
19
|
+
end,
|
|
20
|
+
text: fullText.substring(start, end),
|
|
21
|
+
});
|
|
22
|
+
});
|
|
23
|
+
sharedFileComments.set(file, comments);
|
|
24
|
+
}
|
|
25
|
+
return comments;
|
|
26
|
+
}
|
|
5
27
|
function create(cmdOption, reportsUnusedComments) {
|
|
6
28
|
const mode = typeof cmdOption === 'string' ? 'singleLine' : 'multiLine';
|
|
7
29
|
const [cmd, endCmd] = Array.isArray(cmdOption) ? cmdOption : [cmdOption, undefined];
|
|
@@ -113,14 +135,10 @@ function create(cmdOption, reportsUnusedComments) {
|
|
|
113
135
|
return results;
|
|
114
136
|
}
|
|
115
137
|
const comments = new Map();
|
|
116
|
-
const
|
|
117
|
-
|
|
118
|
-
pos += 2; // Trim the // or /* characters
|
|
119
|
-
const commentText = fullText.substring(pos, end);
|
|
120
|
-
logs.push(commentText);
|
|
121
|
-
const startComment = commentText.match(reg);
|
|
138
|
+
for (const c of getFileComments(file)) {
|
|
139
|
+
const startComment = c.text.match(reg);
|
|
122
140
|
if (startComment?.index !== undefined) {
|
|
123
|
-
const index = startComment.index + pos;
|
|
141
|
+
const index = startComment.index + c.pos;
|
|
124
142
|
const ruleId = startComment.groups?.ruleId;
|
|
125
143
|
if (!comments.has(ruleId)) {
|
|
126
144
|
comments.set(ruleId, []);
|
|
@@ -143,9 +161,9 @@ function create(cmdOption, reportsUnusedComments) {
|
|
|
143
161
|
});
|
|
144
162
|
}
|
|
145
163
|
else if (endReg) {
|
|
146
|
-
const endComment =
|
|
164
|
+
const endComment = c.text.match(endReg);
|
|
147
165
|
if (endComment?.index !== undefined) {
|
|
148
|
-
const index = endComment.index + pos;
|
|
166
|
+
const index = endComment.index + c.pos;
|
|
149
167
|
const prevLine = file.getLineAndCharacterOfPosition(index).line;
|
|
150
168
|
const ruleId = endComment.groups?.ruleId;
|
|
151
169
|
const disabledLines = comments.get(ruleId);
|
|
@@ -154,7 +172,7 @@ function create(cmdOption, reportsUnusedComments) {
|
|
|
154
172
|
}
|
|
155
173
|
}
|
|
156
174
|
}
|
|
157
|
-
}
|
|
175
|
+
}
|
|
158
176
|
let reportedRules = reportedRulesOfFile.get(file.fileName);
|
|
159
177
|
if (!reportedRules) {
|
|
160
178
|
reportedRules = [];
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tsslint/config",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.1.1",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"engines": {
|
|
6
6
|
"node": ">=22.6.0"
|
|
@@ -18,12 +18,12 @@
|
|
|
18
18
|
"directory": "packages/config"
|
|
19
19
|
},
|
|
20
20
|
"dependencies": {
|
|
21
|
-
"@tsslint/types": "3.
|
|
21
|
+
"@tsslint/types": "3.1.1",
|
|
22
22
|
"minimatch": "^10.0.1",
|
|
23
23
|
"ts-api-utils": "^2.0.0"
|
|
24
24
|
},
|
|
25
25
|
"devDependencies": {
|
|
26
|
-
"@tsslint/compat-eslint": "3.
|
|
26
|
+
"@tsslint/compat-eslint": "3.1.1",
|
|
27
27
|
"tslint": "^6.1.3"
|
|
28
28
|
},
|
|
29
29
|
"peerDependencies": {
|
|
@@ -38,5 +38,5 @@
|
|
|
38
38
|
"optional": true
|
|
39
39
|
}
|
|
40
40
|
},
|
|
41
|
-
"gitHead": "
|
|
41
|
+
"gitHead": "fc3aee61fa769d7475897ac91ebfcc929c7e1336"
|
|
42
42
|
}
|