@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 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 loader(rulePath);
246
+ return require(rulePath);
138
247
  }
139
248
  const parentDir = path.resolve(dir, '..');
140
249
  if (parentDir === dir) {
@@ -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 logs = [];
117
- (0, ts_api_utils_1.forEachComment)(file, (fullText, { pos, end }) => {
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 = commentText.match(endReg);
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.0.4",
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.0.4",
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.0.4",
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": "6b48fe1a7b0f563f9d6cb48e07383824a29c011a"
41
+ "gitHead": "fc3aee61fa769d7475897ac91ebfcc929c7e1336"
42
42
  }