@tsslint/config 3.0.0-alpha.0 → 3.0.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.
@@ -0,0 +1,92 @@
1
+ #!/usr/bin/env -S node --experimental-strip-types --no-warnings
2
+
3
+ const path = require('path');
4
+ const fs = require('fs');
5
+ const nodeModulesDirs = [];
6
+
7
+ let dir = __dirname;
8
+
9
+ while (true) {
10
+ const nodeModuleDir = path.join(dir, 'node_modules');
11
+ if (fs.existsSync(nodeModuleDir)) {
12
+ nodeModulesDirs.push(nodeModuleDir);
13
+ }
14
+ const parentDir = path.resolve(dir, '..');
15
+ if (parentDir === dir) {
16
+ break;
17
+ }
18
+ dir = parentDir;
19
+ }
20
+
21
+ try {
22
+ const { generateESlintTypes } = require('../lib/eslint-gen');
23
+ const { generateTSLintTypes } = require('../lib/tslint-gen');
24
+
25
+ generateESlintTypes(nodeModulesDirs).then(({ dts, stats }) => {
26
+ fs.writeFileSync(path.resolve(__dirname, '..', 'lib', 'eslint-types.d.ts'), dts);
27
+
28
+ const indexPath = path.resolve(__dirname, '..', 'lib', 'eslint.d.ts');
29
+ if (fs.existsSync(indexPath)) {
30
+ let indexContent = fs.readFileSync(indexPath, 'utf8');
31
+ const fnIndex = indexContent.indexOf('export declare function importESLintRules');
32
+ const jsDocEnd = indexContent.lastIndexOf('*/', fnIndex) + 2;
33
+ const jsDocStart = indexContent.lastIndexOf('/**', jsDocEnd);
34
+
35
+ if (jsDocStart !== -1 && jsDocEnd !== -1 && jsDocStart < fnIndex) {
36
+ const statsTable = [
37
+ '| Plugin | Rules |',
38
+ '| :--- | :--- |',
39
+ ...Object.entries(stats)
40
+ .filter(([_, count]) => count > 0)
41
+ .sort((a, b) => b[1] - a[1])
42
+ .map(([name, count]) => `| <span>${name}</span> | ${count} |`),
43
+ ].join('\n * ');
44
+
45
+ const newJsDoc = `/**
46
+ * Converts an ESLint rules configuration to TSSLint rules.
47
+ *
48
+ * ${statsTable}
49
+ *
50
+ * If you have added new ESLint plugins, please run \`npx tsslint-docgen\` to update this list.
51
+ */`;
52
+ indexContent = indexContent.slice(0, jsDocStart) + newJsDoc + indexContent.slice(jsDocEnd);
53
+ fs.writeFileSync(indexPath, indexContent);
54
+ }
55
+ }
56
+ });
57
+ generateTSLintTypes(nodeModulesDirs).then(({ dts, stats }) => {
58
+ fs.writeFileSync(path.resolve(__dirname, '..', 'lib', 'tslint-types.d.ts'), dts);
59
+
60
+ const indexPath = path.resolve(__dirname, '..', 'lib', 'tslint.d.ts');
61
+ if (fs.existsSync(indexPath)) {
62
+ let indexContent = fs.readFileSync(indexPath, 'utf8');
63
+ const fnIndex = indexContent.indexOf('export declare function importTSLintRules');
64
+ const jsDocEnd = indexContent.lastIndexOf('*/', fnIndex) + 2;
65
+ const jsDocStart = indexContent.lastIndexOf('/**', jsDocEnd);
66
+
67
+ if (jsDocStart !== -1 && jsDocEnd !== -1 && jsDocStart < fnIndex) {
68
+ const statsTable = [
69
+ '| Dir | Rules |',
70
+ '| :--- | :--- |',
71
+ ...Object.entries(stats)
72
+ .filter(([_, count]) => count > 0)
73
+ .sort((a, b) => b[1] - a[1])
74
+ .map(([name, count]) => `| <span>${name}</span> | ${count} |`),
75
+ ].join('\n * ');
76
+
77
+ const newJsDoc = `/**
78
+ * Converts a TSLint rules configuration to TSSLint rules.
79
+ *
80
+ * ${statsTable}
81
+ *
82
+ * If you have added new TSLint plugins, please run \`npx tsslint-docgen\` to update this list.
83
+ */`;
84
+ indexContent = indexContent.slice(0, jsDocStart) + newJsDoc + indexContent.slice(jsDocEnd);
85
+ fs.writeFileSync(indexPath, indexContent);
86
+ }
87
+ }
88
+ });
89
+ }
90
+ catch (err) {
91
+ console.error(err);
92
+ }
package/index.d.ts CHANGED
@@ -1,7 +1,10 @@
1
1
  export * from '@tsslint/types';
2
+ export * from './lib/eslint.js';
2
3
  export { create as createCategoryPlugin } from './lib/plugins/category.js';
3
4
  export { create as createDiagnosticsPlugin } from './lib/plugins/diagnostics.js';
4
5
  export { create as createIgnorePlugin } from './lib/plugins/ignore.js';
6
+ export * from './lib/tsl.js';
7
+ export * from './lib/tslint.js';
5
8
  import type { Config, Plugin, Rule } from '@tsslint/types';
6
9
  export declare function defineRule(rule: Rule): Rule;
7
10
  export declare function definePlugin(plugin: Plugin): Plugin;
package/index.js CHANGED
@@ -20,12 +20,15 @@ exports.definePlugin = definePlugin;
20
20
  exports.defineConfig = defineConfig;
21
21
  exports.isCLI = isCLI;
22
22
  __exportStar(require("@tsslint/types"), exports);
23
+ __exportStar(require("./lib/eslint.js"), exports);
23
24
  var category_js_1 = require("./lib/plugins/category.js");
24
25
  Object.defineProperty(exports, "createCategoryPlugin", { enumerable: true, get: function () { return category_js_1.create; } });
25
26
  var diagnostics_js_1 = require("./lib/plugins/diagnostics.js");
26
27
  Object.defineProperty(exports, "createDiagnosticsPlugin", { enumerable: true, get: function () { return diagnostics_js_1.create; } });
27
28
  var ignore_js_1 = require("./lib/plugins/ignore.js");
28
29
  Object.defineProperty(exports, "createIgnorePlugin", { enumerable: true, get: function () { return ignore_js_1.create; } });
30
+ __exportStar(require("./lib/tsl.js"), exports);
31
+ __exportStar(require("./lib/tslint.js"), exports);
29
32
  function defineRule(rule) {
30
33
  return rule;
31
34
  }
@@ -0,0 +1,4 @@
1
+ export declare function generateESlintTypes(nodeModulesDirs: string[], loader?: (mod: string) => Promise<any>): Promise<{
2
+ dts: string;
3
+ stats: Record<string, number>;
4
+ }>;
@@ -0,0 +1,302 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.generateESlintTypes = generateESlintTypes;
4
+ const fs = require("fs");
5
+ const path = require("path");
6
+ const variableNameRegex = /^[a-zA-Z_$][0-9a-zA-Z_$]*$/;
7
+ async function generateESlintTypes(nodeModulesDirs, loader = async (mod) => {
8
+ try {
9
+ return require(mod);
10
+ }
11
+ catch {
12
+ return await import(mod);
13
+ }
14
+ }) {
15
+ let indentLevel = 0;
16
+ let dts = '';
17
+ let defId = 0;
18
+ line(`export interface ESLintRulesConfig {`);
19
+ indentLevel++;
20
+ const visited = new Set();
21
+ const defs = new Map();
22
+ const stats = {};
23
+ for (const nodeModulesDir of nodeModulesDirs) {
24
+ const pkgs = readdirDirSync(nodeModulesDir);
25
+ for (const pkg of pkgs) {
26
+ if (pkg.startsWith('@')) {
27
+ const subPkgs = readdirDirSync(path.join(nodeModulesDir, pkg));
28
+ for (const subPkg of subPkgs) {
29
+ if (subPkg === 'eslint-plugin' || subPkg.startsWith('eslint-plugin-')) {
30
+ const pluginName = `${pkg}/${subPkg}`;
31
+ let plugin = await loader(pluginName);
32
+ if ('default' in plugin) {
33
+ plugin = plugin.default;
34
+ }
35
+ if (plugin.rules) {
36
+ stats[pluginName] = 0;
37
+ for (const ruleName in plugin.rules) {
38
+ const rule = plugin.rules[ruleName];
39
+ if (subPkg === 'eslint-plugin') {
40
+ if (addRule(pkg, ruleName, rule)) {
41
+ stats[pluginName]++;
42
+ }
43
+ }
44
+ else {
45
+ if (addRule(pkg, `${subPkg.slice('eslint-plugin-'.length)}/${ruleName}`, rule)) {
46
+ stats[pluginName]++;
47
+ }
48
+ }
49
+ }
50
+ }
51
+ }
52
+ }
53
+ }
54
+ else if (pkg.startsWith('eslint-plugin-')) {
55
+ let plugin = await loader(pkg);
56
+ if ('default' in plugin) {
57
+ plugin = plugin.default;
58
+ }
59
+ if (plugin.rules) {
60
+ const scope = pkg.replace('eslint-plugin-', '');
61
+ stats[pkg] = 0;
62
+ for (const ruleName in plugin.rules) {
63
+ const rule = plugin.rules[ruleName];
64
+ if (addRule(scope, ruleName, rule)) {
65
+ stats[pkg]++;
66
+ }
67
+ }
68
+ }
69
+ }
70
+ else if (pkg === 'eslint') {
71
+ const rulesDir = path.join(nodeModulesDir, pkg, 'lib', 'rules');
72
+ const ruleFiles = fs.readdirSync(rulesDir);
73
+ stats['eslint'] = 0;
74
+ for (const ruleFile of ruleFiles) {
75
+ if (ruleFile.endsWith('.js')) {
76
+ const ruleName = ruleFile.replace('.js', '');
77
+ const rule = await loader(path.join(rulesDir, ruleFile));
78
+ if (addRule(undefined, ruleName, rule)) {
79
+ stats['eslint']++;
80
+ }
81
+ }
82
+ }
83
+ }
84
+ }
85
+ }
86
+ indentLevel--;
87
+ line(`}`);
88
+ line(``);
89
+ for (const [typeName, typeString] of defs.values()) {
90
+ line(`type ${typeName} = ${typeString};`);
91
+ }
92
+ return { dts, stats };
93
+ function addRule(scope, ruleName, rule) {
94
+ let ruleKey;
95
+ if (scope) {
96
+ ruleKey = `${scope}/${ruleName}`;
97
+ }
98
+ else {
99
+ ruleKey = `${ruleName}`;
100
+ }
101
+ if (visited.has(ruleKey)) {
102
+ return false;
103
+ }
104
+ visited.add(ruleKey);
105
+ const meta = rule.meta ?? {};
106
+ const { description, url } = meta.docs ?? {};
107
+ const { schema } = meta;
108
+ if (description || url) {
109
+ line(`/**`);
110
+ if (description) {
111
+ line(` * ${description.replace(/\*\//g, '* /')}`);
112
+ }
113
+ if (url) {
114
+ line(` * @see ${url}`);
115
+ }
116
+ line(` */`);
117
+ }
118
+ let optionsType;
119
+ if (schema) {
120
+ if (Array.isArray(schema)) {
121
+ const optionsTypes = [];
122
+ for (const item of schema) {
123
+ const itemType = parseSchema(schema, item, indentLevel);
124
+ optionsTypes.push(itemType);
125
+ }
126
+ optionsType = `[`;
127
+ optionsType += optionsTypes
128
+ .map(type => `(${type})?`)
129
+ .join(', ');
130
+ optionsType += `]`;
131
+ }
132
+ else {
133
+ optionsType = parseSchema(schema, schema, indentLevel);
134
+ }
135
+ }
136
+ if (optionsType) {
137
+ line(`'${ruleKey}'?: ${optionsType},`);
138
+ }
139
+ else {
140
+ line(`'${ruleKey}'?: any[],`);
141
+ }
142
+ return true;
143
+ }
144
+ function line(line) {
145
+ dts += indent(indentLevel) + line + '\n';
146
+ }
147
+ function parseSchema(schema, item, indentLevel) {
148
+ if (typeof item === 'object') {
149
+ if (item.$ref) {
150
+ const paths = item.$ref
151
+ .replace('#/items/', '#/')
152
+ .split('/').slice(1);
153
+ let current = schema;
154
+ for (const path of paths) {
155
+ try {
156
+ current = current[path];
157
+ }
158
+ catch {
159
+ current = undefined;
160
+ break;
161
+ }
162
+ }
163
+ if (current) {
164
+ let resolved = defs.get(current);
165
+ if (!resolved) {
166
+ resolved = [`Def${defId++}_${paths[paths.length - 1]}`, parseSchema(schema, current, 0)];
167
+ defs.set(current, resolved);
168
+ }
169
+ return resolved[0];
170
+ }
171
+ else {
172
+ console.error(`Failed to resolve schema path: ${item.$ref}`);
173
+ return 'unknown';
174
+ }
175
+ }
176
+ else if (Array.isArray(item)) {
177
+ return item.map(item => parseSchema(schema, item, indentLevel)).join(' | ');
178
+ }
179
+ else if (Array.isArray(item.type)) {
180
+ return item.type.map((type) => parseSchema(schema, type, indentLevel)).join(' | ');
181
+ }
182
+ else if (item.properties) {
183
+ let res = `{\n`;
184
+ indentLevel++;
185
+ const properties = item.properties;
186
+ const requiredArr = item.required ?? [];
187
+ for (const key in properties) {
188
+ const property = properties[key];
189
+ if (property.description) {
190
+ res += indent(indentLevel) + `/**\n`;
191
+ res += indent(indentLevel) + ` * ${property.description.replace(/\*\//g, '* /')}\n`;
192
+ res += indent(indentLevel) + ` */\n`;
193
+ }
194
+ const propertyType = parseSchema(schema, property, indentLevel);
195
+ const isRequired = requiredArr.includes(key);
196
+ if (!variableNameRegex.test(key)) {
197
+ res += indent(indentLevel) + `'${key}'${isRequired ? '' : '?'}: ${propertyType},\n`;
198
+ }
199
+ else {
200
+ res += indent(indentLevel) + `${key}${isRequired ? '' : '?'}: ${propertyType},\n`;
201
+ }
202
+ }
203
+ indentLevel--;
204
+ res += indent(indentLevel) + `}`;
205
+ if (item.additionalProperties) {
206
+ res += ` & `;
207
+ res += parseAdditionalProperties(schema, item.additionalProperties, indentLevel);
208
+ }
209
+ return res;
210
+ }
211
+ else if (Array.isArray(item.required)) {
212
+ let res = `{ `;
213
+ const propertiesType = [];
214
+ for (const key of item.required) {
215
+ const propertyType = `any`;
216
+ if (!variableNameRegex.test(key)) {
217
+ propertiesType.push(`'${key}': ${propertyType}`);
218
+ }
219
+ else {
220
+ propertiesType.push(`${key}: ${propertyType}`);
221
+ }
222
+ }
223
+ res += propertiesType.join(', ');
224
+ res += ` }`;
225
+ return res;
226
+ }
227
+ else if (item.const) {
228
+ return JSON.stringify(item.const);
229
+ }
230
+ else if (item.type === 'array') {
231
+ if (Array.isArray(item.items)) {
232
+ return `[${item.items.map((item) => parseSchema(schema, item, indentLevel)).join(', ')}]`;
233
+ }
234
+ if (item.items) {
235
+ return `(${parseSchema(schema, item.items, indentLevel)})[]`;
236
+ }
237
+ return `any[]`;
238
+ }
239
+ else if (item.enum) {
240
+ return item.enum.map((v) => JSON.stringify(v)).join(' | ');
241
+ }
242
+ else if (item.type) {
243
+ return parseSchema(schema, item.type, indentLevel);
244
+ }
245
+ else if (item.anyOf) {
246
+ return item.anyOf.map((item) => parseSchema(schema, item, indentLevel)).join(' | ');
247
+ }
248
+ else if (item.oneOf) {
249
+ return item.oneOf.map((item) => parseSchema(schema, item, indentLevel)).join(' | ');
250
+ }
251
+ }
252
+ else if (item === 'string' || item === 'boolean' || item === 'null' || item === 'number') {
253
+ return item;
254
+ }
255
+ else if (item === 'object') {
256
+ if (item.additionalProperties) {
257
+ return parseAdditionalProperties(schema, item.additionalProperties, indentLevel);
258
+ }
259
+ else {
260
+ return `{ [key: string]: unknown }`;
261
+ }
262
+ }
263
+ else if (item === 'integer') {
264
+ return 'number';
265
+ }
266
+ else if (item === 'array') {
267
+ return 'any[]';
268
+ }
269
+ return 'unknown';
270
+ }
271
+ function indent(indentLevel) {
272
+ return '\t'.repeat(indentLevel);
273
+ }
274
+ function parseAdditionalProperties(schema, item, indentLevel) {
275
+ if (item === true) {
276
+ return `{ [key: string]: unknown }`;
277
+ }
278
+ else {
279
+ return `{ [key: string]: ${parseSchema(schema, item, indentLevel)} }`;
280
+ }
281
+ }
282
+ function readdirDirSync(_path) {
283
+ return fs.readdirSync(_path, { withFileTypes: true })
284
+ .filter(dirent => {
285
+ if (dirent.isDirectory()) {
286
+ return true;
287
+ }
288
+ if (dirent.isSymbolicLink()) {
289
+ const fullPath = path.join(_path, dirent.name);
290
+ try {
291
+ return fs.statSync(fullPath).isDirectory();
292
+ }
293
+ catch {
294
+ return false;
295
+ }
296
+ }
297
+ return false;
298
+ })
299
+ .map(dirent => dirent.name);
300
+ }
301
+ }
302
+ //# sourceMappingURL=eslint-gen.js.map
@@ -0,0 +1,3 @@
1
+ export interface ESLintRulesConfig extends Record<string, any[]> {
2
+ [key: string]: any[];
3
+ }
@@ -0,0 +1,3 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ //# sourceMappingURL=eslint-types.js.map
@@ -0,0 +1,15 @@
1
+ import type * as TSSLint from '@tsslint/types';
2
+ import type * as ESLint from 'eslint';
3
+ import type { ESLintRulesConfig } from './eslint-types.js';
4
+ type Severity = boolean | 'error' | 'warn';
5
+ /**
6
+ * Converts an ESLint rules configuration to TSSLint rules.
7
+ *
8
+ * ⚠️ **Type definitions not generated**
9
+ *
10
+ * Please run `npx tsslint-docgen` to update them.
11
+ */
12
+ export declare function importESLintRules(config: {
13
+ [K in keyof ESLintRulesConfig]: Severity | [Severity, ...ESLintRulesConfig[K]];
14
+ }, context?: Partial<ESLint.Rule.RuleContext>): Promise<TSSLint.Rules>;
15
+ export {};
package/lib/eslint.js ADDED
@@ -0,0 +1,110 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.importESLintRules = importESLintRules;
4
+ const fs = require("fs");
5
+ const path = require("path");
6
+ const noop = () => { };
7
+ const plugins = {};
8
+ const loader = async (moduleName) => {
9
+ let mod;
10
+ try {
11
+ mod ??= require(moduleName);
12
+ }
13
+ catch { }
14
+ try {
15
+ mod ??= await import(moduleName);
16
+ }
17
+ catch { }
18
+ if (mod && 'default' in mod) {
19
+ return mod.default;
20
+ }
21
+ return mod;
22
+ };
23
+ /**
24
+ * Converts an ESLint rules configuration to TSSLint rules.
25
+ *
26
+ * ⚠️ **Type definitions not generated**
27
+ *
28
+ * Please run `npx tsslint-docgen` to update them.
29
+ */
30
+ async function importESLintRules(config, context = {}) {
31
+ let convertRule;
32
+ try {
33
+ ({ convertRule } = await import('@tsslint/compat-eslint'));
34
+ }
35
+ catch {
36
+ throw new Error('Please install @tsslint/compat-eslint to use importESLintRules().');
37
+ }
38
+ const rules = {};
39
+ for (const [rule, severityOrOptions] of Object.entries(config)) {
40
+ let severity;
41
+ let options;
42
+ if (Array.isArray(severityOrOptions)) {
43
+ [severity, ...options] = severityOrOptions;
44
+ }
45
+ else {
46
+ severity = severityOrOptions;
47
+ options = [];
48
+ }
49
+ if (!severity) {
50
+ rules[rule] = noop;
51
+ continue;
52
+ }
53
+ const ruleModule = await loadRuleByKey(rule);
54
+ if (!ruleModule) {
55
+ throw new Error(`Failed to resolve rule "${rule}".`);
56
+ }
57
+ rules[rule] = convertRule(ruleModule, options, { id: rule, ...context }, severity === 'error'
58
+ ? 1
59
+ : severity === 'warn'
60
+ ? 0
61
+ : 3);
62
+ }
63
+ return rules;
64
+ }
65
+ function* resolveRuleKey(rule) {
66
+ const slashIndex = rule.indexOf('/');
67
+ if (slashIndex !== -1) {
68
+ let pluginName = rule.startsWith('@')
69
+ ? `${rule.slice(0, slashIndex)}/eslint-plugin`
70
+ : `eslint-plugin-${rule.slice(0, slashIndex)}`;
71
+ let ruleName = rule.slice(slashIndex + 1);
72
+ yield [pluginName, ruleName];
73
+ if (ruleName.indexOf('/') >= 0) {
74
+ pluginName += `-${ruleName.slice(0, ruleName.indexOf('/'))}`;
75
+ ruleName = ruleName.slice(ruleName.indexOf('/') + 1);
76
+ yield [pluginName, ruleName];
77
+ }
78
+ }
79
+ else {
80
+ yield [undefined, rule];
81
+ }
82
+ }
83
+ async function loadRuleByKey(rule) {
84
+ for (const resolved of resolveRuleKey(rule)) {
85
+ const ruleModule = await loadRule(...resolved);
86
+ if (ruleModule) {
87
+ return ruleModule;
88
+ }
89
+ }
90
+ }
91
+ async function loadRule(pluginName, ruleName) {
92
+ if (pluginName) {
93
+ plugins[pluginName] ??= loader(pluginName);
94
+ const plugin = await plugins[pluginName];
95
+ return plugin?.rules[ruleName];
96
+ }
97
+ let dir = __dirname;
98
+ while (true) {
99
+ const rulePath = path.join(dir, 'node_modules', 'eslint', 'lib', 'rules', `${ruleName}.js`);
100
+ if (fs.existsSync(rulePath)) {
101
+ return loader(rulePath);
102
+ }
103
+ const parentDir = path.resolve(dir, '..');
104
+ if (parentDir === dir) {
105
+ break;
106
+ }
107
+ dir = parentDir;
108
+ }
109
+ }
110
+ //# sourceMappingURL=eslint.js.map
@@ -16,7 +16,7 @@ function create(config, source = 'tsslint') {
16
16
  }
17
17
  }
18
18
  return diagnostics;
19
- }
19
+ },
20
20
  });
21
21
  function match(code) {
22
22
  if (matchCache.has(code)) {
@@ -48,7 +48,7 @@ function create(cmdOption, reportsUnusedComments) {
48
48
  : nextLineRules.length
49
49
  ? 'Ignore 1 issue in next line'
50
50
  : undefined,
51
- }
51
+ },
52
52
  };
53
53
  if (result) {
54
54
  result.entries.push(item);
@@ -108,8 +108,8 @@ function create(cmdOption, reportsUnusedComments) {
108
108
  };
109
109
  return {
110
110
  resolveDiagnostics(file, results) {
111
- if (!reportsUnusedComments &&
112
- !results.some(error => error.source === 'tsslint')) {
111
+ if (!reportsUnusedComments
112
+ && !results.some(error => error.source === 'tsslint')) {
113
113
  return results;
114
114
  }
115
115
  const comments = new Map();
package/lib/tsl.d.ts ADDED
@@ -0,0 +1,3 @@
1
+ import type { Rule } from '@tsslint/config';
2
+ import type * as TSL from 'tsl' with { 'resolution-mode': 'import' };
3
+ export declare function fromTSLRules(tslRules: TSL.Rule<unknown>[]): Record<string, Rule>;
package/lib/tsl.js ADDED
@@ -0,0 +1,70 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.fromTSLRules = fromTSLRules;
4
+ const fs = require("fs");
5
+ const path = require("path");
6
+ function fromTSLRules(tslRules) {
7
+ let dir = __dirname;
8
+ let tslDir;
9
+ while (true) {
10
+ const potential = path.join(dir, 'node_modules', 'tsl');
11
+ if (fs.existsSync(potential)) {
12
+ tslDir = potential;
13
+ break;
14
+ }
15
+ const parentDir = path.dirname(dir);
16
+ if (parentDir === dir) {
17
+ break;
18
+ }
19
+ dir = parentDir;
20
+ }
21
+ if (!tslDir) {
22
+ throw new Error('Failed to find tsl package in node_modules.');
23
+ }
24
+ const visitorEntriesFile = fs.readdirSync(tslDir).find(f => /^visitorEntries-.*\.js$/.test(f));
25
+ if (!visitorEntriesFile) {
26
+ throw new Error('Failed to find visitorEntries file in tsl package.');
27
+ }
28
+ const { getContextUtils, visitorEntries } = require(path.join(tslDir, visitorEntriesFile));
29
+ const rules = {};
30
+ for (const tslRule of tslRules) {
31
+ rules[tslRule.name] = convertTSLRule(tslRule, visitorEntries);
32
+ }
33
+ return rules;
34
+ function convertTSLRule(rule, visitorEntries) {
35
+ return ({ typescript: ts, file, program, report }) => {
36
+ const context1 = {
37
+ checker: program.getTypeChecker(),
38
+ rawChecker: program.getTypeChecker(),
39
+ sourceFile: file,
40
+ report: descriptor => {
41
+ if ('node' in descriptor) {
42
+ report(descriptor.message, descriptor.node.getStart(file), descriptor.node.getEnd())
43
+ .at(new Error(), 1);
44
+ }
45
+ else {
46
+ report(descriptor.message, descriptor.start, descriptor.end)
47
+ .at(new Error(), 1);
48
+ }
49
+ },
50
+ compilerOptions: program.getCompilerOptions(),
51
+ program,
52
+ utils: getContextUtils(() => program),
53
+ };
54
+ const context2 = { ...context1, data: rule.createData?.(context1) };
55
+ ts.forEachChild(file, function cb(node) {
56
+ const nodeType = visitorEntries.find(e => e[0] === node.kind)?.[1];
57
+ if (nodeType) {
58
+ // @ts-expect-error
59
+ rule.visitor[nodeType]?.(context2, node);
60
+ }
61
+ ts.forEachChild(node, cb);
62
+ if (nodeType) {
63
+ // @ts-expect-error
64
+ rule.visitor[`${nodeType}_exit`]?.(context2, node);
65
+ }
66
+ });
67
+ };
68
+ }
69
+ }
70
+ //# sourceMappingURL=tsl.js.map
@@ -0,0 +1,4 @@
1
+ export declare function generateTSLintTypes(nodeModulesDirs: string[], loader?: (mod: string) => Promise<any>): Promise<{
2
+ dts: string;
3
+ stats: Record<string, number>;
4
+ }>;
@@ -0,0 +1,299 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.generateTSLintTypes = generateTSLintTypes;
4
+ const fs = require("fs");
5
+ const path = require("path");
6
+ const tslint_1 = require("./tslint");
7
+ const variableNameRegex = /^[a-zA-Z_$][0-9a-zA-Z_$]*$/;
8
+ async function generateTSLintTypes(nodeModulesDirs, loader = async (mod) => {
9
+ try {
10
+ return require(mod);
11
+ }
12
+ catch {
13
+ return await import(mod);
14
+ }
15
+ }) {
16
+ let indentLevel = 0;
17
+ let dts = '';
18
+ let defId = 0;
19
+ line(`export interface TSLintRulesConfig {`);
20
+ indentLevel++;
21
+ const visited = new Set();
22
+ const defs = new Map();
23
+ const stats = {};
24
+ const rulesDirectories = (0, tslint_1.getTSLintRulesDirectories)();
25
+ for (const [rawDir, rulesDir] of rulesDirectories) {
26
+ if (fs.existsSync(rulesDir)) {
27
+ const ruleFiles = fs.readdirSync(rulesDir);
28
+ stats[rawDir] = 0;
29
+ for (const ruleFile of ruleFiles) {
30
+ if (ruleFile.endsWith('Rule.js') || ruleFile.endsWith('Rule.ts')) {
31
+ const camelCaseName = ruleFile.slice(0, -'Rule.js'.length);
32
+ const ruleName = camelCaseName.replace(/[A-Z]/g, (c, i) => (i === 0 ? c.toLowerCase() : '-' + c.toLowerCase()));
33
+ if (!visited.has(ruleName)) {
34
+ visited.add(ruleName);
35
+ try {
36
+ const rule = (await loader(path.join(rulesDir, ruleFile))).Rule;
37
+ addRule(ruleName, rule.metadata, rawDir);
38
+ stats[rawDir]++;
39
+ }
40
+ catch (e) {
41
+ addRule(ruleName, undefined, rawDir, e);
42
+ stats[rawDir]++;
43
+ }
44
+ }
45
+ }
46
+ }
47
+ }
48
+ }
49
+ // 2. Scan TSLint core rules
50
+ for (const nodeModulesDir of nodeModulesDirs) {
51
+ const tslintDir = path.join(nodeModulesDir, 'tslint', 'lib', 'rules');
52
+ if (fs.existsSync(tslintDir)) {
53
+ const ruleFiles = fs.readdirSync(tslintDir);
54
+ stats['tslint'] = stats['tslint'] || 0;
55
+ for (const ruleFile of ruleFiles) {
56
+ if (ruleFile.endsWith('Rule.js')) {
57
+ const camelCaseName = ruleFile.replace('Rule.js', '');
58
+ const ruleName = camelCaseName.replace(/[A-Z]/g, (c, i) => (i === 0 ? c.toLowerCase() : '-' + c.toLowerCase()));
59
+ if (!visited.has(ruleName)) {
60
+ visited.add(ruleName);
61
+ const rule = (await loader(path.join(tslintDir, ruleFile))).Rule;
62
+ addRule(ruleName, rule.metadata);
63
+ stats['tslint']++;
64
+ }
65
+ }
66
+ }
67
+ }
68
+ }
69
+ indentLevel--;
70
+ line(`}`);
71
+ line(``);
72
+ for (const [typeName, typeString] of defs.values()) {
73
+ line(`type ${typeName} = ${typeString};`);
74
+ }
75
+ return { dts, stats };
76
+ function addRule(ruleName, metadata, rulesDir, error) {
77
+ if (metadata || rulesDir || error) {
78
+ line(`/**`);
79
+ if (rulesDir) {
80
+ line(` * @rulesDirectory ${rulesDir}`);
81
+ }
82
+ if (error) {
83
+ line(` * @error ${error.message || error.toString()}`);
84
+ }
85
+ if (metadata?.description) {
86
+ for (const lineText of metadata.description.trim().split('\n')) {
87
+ line(` * ${lineText.replace(/\*\//g, '* /')}`);
88
+ }
89
+ }
90
+ if (metadata?.descriptionDetails) {
91
+ line(` *`);
92
+ for (const lineText of metadata.descriptionDetails.trim().split('\n')) {
93
+ line(` * ${lineText.replace(/\*\//g, '* /')}`);
94
+ }
95
+ }
96
+ if (metadata?.rationale) {
97
+ line(` *`);
98
+ line(` * @rationale`);
99
+ for (const lineText of metadata.rationale.trim().split('\n')) {
100
+ line(` * ${lineText.replace(/\*\//g, '* /')}`);
101
+ }
102
+ }
103
+ if (metadata?.optionsDescription) {
104
+ line(` *`);
105
+ line(` * @options`);
106
+ for (const lineText of metadata.optionsDescription.trim().split('\n')) {
107
+ line(` * ${lineText.replace(/\*\//g, '* /')}`);
108
+ }
109
+ }
110
+ if (metadata?.optionExamples) {
111
+ line(` *`);
112
+ line(` * @example`);
113
+ for (const example of metadata.optionExamples) {
114
+ if (typeof example === 'string') {
115
+ for (const lineText of example.trim().split('\n')) {
116
+ line(` * ${lineText.replace(/\*\//g, '* /')}`);
117
+ }
118
+ }
119
+ else {
120
+ line(` * ${JSON.stringify(ruleName)}: ${JSON.stringify(example)}`);
121
+ }
122
+ }
123
+ }
124
+ if (metadata?.type) {
125
+ line(` *`);
126
+ line(` * @type ${metadata.type}`);
127
+ }
128
+ if (metadata?.typescriptOnly) {
129
+ line(` *`);
130
+ line(` * @typescriptOnly`);
131
+ }
132
+ line(` */`);
133
+ }
134
+ let optionsType;
135
+ const schema = metadata?.options;
136
+ if (schema) {
137
+ if (Array.isArray(schema)) {
138
+ const optionsTypes = [];
139
+ for (const item of schema) {
140
+ const itemType = parseSchema(schema, item, indentLevel);
141
+ optionsTypes.push(itemType);
142
+ }
143
+ optionsType = `[`;
144
+ optionsType += optionsTypes
145
+ .map(type => `(${type})?`)
146
+ .join(', ');
147
+ optionsType += `]`;
148
+ }
149
+ else {
150
+ optionsType = parseSchema(schema, schema, indentLevel);
151
+ }
152
+ }
153
+ if (optionsType) {
154
+ line(`'${ruleName}'?: ${optionsType},`);
155
+ }
156
+ else {
157
+ line(`'${ruleName}'?: any[],`);
158
+ }
159
+ }
160
+ function line(line) {
161
+ dts += indent(indentLevel) + line + '\n';
162
+ }
163
+ function parseSchema(schema, item, indentLevel) {
164
+ if (typeof item === 'object' && item !== null) {
165
+ if (item.$ref) {
166
+ const paths = item.$ref
167
+ .replace('#/items/', '#/')
168
+ .split('/').slice(1);
169
+ let current = schema;
170
+ for (const path of paths) {
171
+ try {
172
+ current = current[path];
173
+ }
174
+ catch {
175
+ current = undefined;
176
+ break;
177
+ }
178
+ }
179
+ if (current) {
180
+ let resolved = defs.get(current);
181
+ if (!resolved) {
182
+ resolved = [`Def${defId++}_${paths[paths.length - 1]}`, parseSchema(schema, current, 0)];
183
+ defs.set(current, resolved);
184
+ }
185
+ return resolved[0];
186
+ }
187
+ else {
188
+ console.error(`Failed to resolve schema path: ${item.$ref}`);
189
+ return 'unknown';
190
+ }
191
+ }
192
+ else if (Array.isArray(item)) {
193
+ return item.map(item => parseSchema(schema, item, indentLevel)).join(' | ');
194
+ }
195
+ else if (Array.isArray(item.type)) {
196
+ return item.type.map((type) => parseSchema(schema, type, indentLevel)).join(' | ');
197
+ }
198
+ else if (item.properties) {
199
+ let res = `{\n`;
200
+ indentLevel++;
201
+ const properties = item.properties;
202
+ const requiredArr = item.required ?? [];
203
+ for (const key in properties) {
204
+ const property = properties[key];
205
+ if (property.description) {
206
+ res += indent(indentLevel) + `/**\n`;
207
+ res += indent(indentLevel) + ` * ${property.description.replace(/\*\//g, '* /')}\n`;
208
+ res += indent(indentLevel) + ` */\n`;
209
+ }
210
+ const propertyType = parseSchema(schema, property, indentLevel);
211
+ const isRequired = requiredArr.includes(key);
212
+ if (!variableNameRegex.test(key)) {
213
+ res += indent(indentLevel) + `'${key}'${isRequired ? '' : '?'}: ${propertyType},\n`;
214
+ }
215
+ else {
216
+ res += indent(indentLevel) + `${key}${isRequired ? '' : '?'}: ${propertyType},\n`;
217
+ }
218
+ }
219
+ indentLevel--;
220
+ res += indent(indentLevel) + `}`;
221
+ if (item.additionalProperties) {
222
+ res += ` & `;
223
+ res += parseAdditionalProperties(schema, item.additionalProperties, indentLevel);
224
+ }
225
+ return res;
226
+ }
227
+ else if (Array.isArray(item.required)) {
228
+ let res = `{ `;
229
+ const propertiesType = [];
230
+ for (const key of item.required) {
231
+ const propertyType = `any`;
232
+ if (!variableNameRegex.test(key)) {
233
+ propertiesType.push(`'${key}': ${propertyType}`);
234
+ }
235
+ else {
236
+ propertiesType.push(`${key}: ${propertyType}`);
237
+ }
238
+ }
239
+ res += propertiesType.join(', ');
240
+ res += ` }`;
241
+ return res;
242
+ }
243
+ else if (item.const) {
244
+ return JSON.stringify(item.const);
245
+ }
246
+ else if (item.type === 'array') {
247
+ if (Array.isArray(item.items)) {
248
+ return `[${item.items.map((item) => parseSchema(schema, item, indentLevel)).join(', ')}]`;
249
+ }
250
+ if (item.items) {
251
+ return `(${parseSchema(schema, item.items, indentLevel)})[]`;
252
+ }
253
+ return `any[]`;
254
+ }
255
+ else if (item.enum) {
256
+ return item.enum.map((v) => JSON.stringify(v)).join(' | ');
257
+ }
258
+ else if (item.type) {
259
+ return parseSchema(schema, item.type, indentLevel);
260
+ }
261
+ else if (item.anyOf) {
262
+ return item.anyOf.map((item) => parseSchema(schema, item, indentLevel)).join(' | ');
263
+ }
264
+ else if (item.oneOf) {
265
+ return item.oneOf.map((item) => parseSchema(schema, item, indentLevel)).join(' | ');
266
+ }
267
+ }
268
+ else if (item === 'string' || item === 'boolean' || item === 'null' || item === 'number') {
269
+ return item;
270
+ }
271
+ else if (item === 'object') {
272
+ if (item.additionalProperties) {
273
+ return parseAdditionalProperties(schema, item.additionalProperties, indentLevel);
274
+ }
275
+ else {
276
+ return `{ [key: string]: unknown }`;
277
+ }
278
+ }
279
+ else if (item === 'integer') {
280
+ return 'number';
281
+ }
282
+ else if (item === 'array') {
283
+ return 'any[]';
284
+ }
285
+ return 'unknown';
286
+ }
287
+ function indent(indentLevel) {
288
+ return '\t'.repeat(indentLevel);
289
+ }
290
+ function parseAdditionalProperties(schema, item, indentLevel) {
291
+ if (item === true) {
292
+ return `{ [key: string]: unknown }`;
293
+ }
294
+ else {
295
+ return `{ [key: string]: ${parseSchema(schema, item, indentLevel)} }`;
296
+ }
297
+ }
298
+ }
299
+ //# sourceMappingURL=tslint-gen.js.map
@@ -0,0 +1,3 @@
1
+ export interface TSLintRulesConfig extends Record<string, any[]> {
2
+ [key: string]: any[];
3
+ }
@@ -0,0 +1,3 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ //# sourceMappingURL=tslint-types.js.map
@@ -0,0 +1,15 @@
1
+ import type * as TSSLint from '@tsslint/types';
2
+ import type { TSLintRulesConfig } from './tslint-types.js';
3
+ type Severity = boolean | 'error' | 'warn';
4
+ /**
5
+ * Converts a TSLint rules configuration to TSSLint rules.
6
+ *
7
+ * ⚠️ **Type definitions not generated**
8
+ *
9
+ * Please run `npx tsslint-docgen` to update them.
10
+ */
11
+ export declare function importTSLintRules(config: {
12
+ [K in keyof TSLintRulesConfig]: Severity | [Severity, ...TSLintRulesConfig[K]];
13
+ }): Promise<TSSLint.Rules>;
14
+ export declare function getTSLintRulesDirectories(): [string, string][];
15
+ export {};
package/lib/tslint.js ADDED
@@ -0,0 +1,155 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.importTSLintRules = importTSLintRules;
4
+ exports.getTSLintRulesDirectories = getTSLintRulesDirectories;
5
+ const fs = require("fs");
6
+ const path = require("path");
7
+ const noop = () => { };
8
+ /**
9
+ * Converts a TSLint rules configuration to TSSLint rules.
10
+ *
11
+ * ⚠️ **Type definitions not generated**
12
+ *
13
+ * Please run `npx tsslint-docgen` to update them.
14
+ */
15
+ async function importTSLintRules(config) {
16
+ const rules = {};
17
+ const rulesDirectories = getTSLintRulesDirectories();
18
+ for (const [ruleName, severityOrOptions] of Object.entries(config)) {
19
+ let severity;
20
+ let options;
21
+ if (Array.isArray(severityOrOptions)) {
22
+ [severity, ...options] = severityOrOptions;
23
+ }
24
+ else {
25
+ severity = severityOrOptions;
26
+ options = [];
27
+ }
28
+ if (!severity) {
29
+ rules[ruleName] = noop;
30
+ continue;
31
+ }
32
+ const ruleModule = await loadTSLintRule(ruleName, rulesDirectories);
33
+ if (!ruleModule) {
34
+ throw new Error(`Failed to resolve TSLint rule "${ruleName}".`);
35
+ }
36
+ rules[ruleName] = convertRule(ruleModule, options, severity === 'error'
37
+ ? 1
38
+ : severity === 'warn'
39
+ ? 0
40
+ : 3);
41
+ }
42
+ return rules;
43
+ }
44
+ function getTSLintRulesDirectories() {
45
+ const directories = [];
46
+ let dir = __dirname;
47
+ while (true) {
48
+ const tslintJsonPath = path.join(dir, 'tslint.json');
49
+ if (fs.existsSync(tslintJsonPath)) {
50
+ try {
51
+ let content = fs.readFileSync(tslintJsonPath, 'utf8');
52
+ // Remove comments
53
+ content = content.replace(/\/\*[\s\S]*?\*\/|([^\\:]|^)\/\/.*$/gm, '$1');
54
+ const tslintJson = JSON.parse(content);
55
+ if (tslintJson.rulesDirectory) {
56
+ const rulesDirs = Array.isArray(tslintJson.rulesDirectory)
57
+ ? tslintJson.rulesDirectory
58
+ : [tslintJson.rulesDirectory];
59
+ for (const rulesDir of rulesDirs) {
60
+ directories.push([rulesDir, path.resolve(dir, rulesDir)]);
61
+ }
62
+ }
63
+ }
64
+ catch (e) {
65
+ // Ignore parse errors
66
+ }
67
+ break;
68
+ }
69
+ const parentDir = path.resolve(dir, '..');
70
+ if (parentDir === dir) {
71
+ break;
72
+ }
73
+ dir = parentDir;
74
+ }
75
+ return directories;
76
+ }
77
+ async function loadTSLintRule(ruleName, rulesDirectories) {
78
+ const camelCaseName = ruleName.replace(/-([a-z])/g, (_, c) => c.toUpperCase());
79
+ const ruleFileName = `${camelCaseName.charAt(0).toUpperCase() + camelCaseName.slice(1)}Rule.js`;
80
+ for (const [, rulesDir] of rulesDirectories) {
81
+ const rulePath = path.resolve(rulesDir, ruleFileName);
82
+ if (fs.existsSync(rulePath)) {
83
+ const mod = require(rulePath);
84
+ return mod.Rule;
85
+ }
86
+ }
87
+ let dir = __dirname;
88
+ while (true) {
89
+ const nodeModulesDir = path.join(dir, 'node_modules');
90
+ if (fs.existsSync(nodeModulesDir)) {
91
+ const tslintDir = path.join(nodeModulesDir, 'tslint', 'lib', 'rules');
92
+ if (fs.existsSync(tslintDir)) {
93
+ const rulePath = path.join(tslintDir, ruleFileName);
94
+ if (fs.existsSync(rulePath)) {
95
+ const mod = require(rulePath);
96
+ return mod.Rule;
97
+ }
98
+ }
99
+ }
100
+ const parentDir = path.resolve(dir, '..');
101
+ if (parentDir === dir) {
102
+ break;
103
+ }
104
+ dir = parentDir;
105
+ }
106
+ }
107
+ function convertRule(Rule, ruleArguments = [], category = 3) {
108
+ const rule = new Rule({
109
+ ruleName: Rule.metadata?.ruleName ?? 'unknown',
110
+ ruleArguments,
111
+ ruleSeverity: 'warning',
112
+ disabledIntervals: [],
113
+ });
114
+ return ({ typescript: ts, file, report, ...ctx }) => {
115
+ if (Rule.metadata?.typescriptOnly) {
116
+ const scriptKind = file.scriptKind;
117
+ if (scriptKind === ts.ScriptKind.JS || scriptKind === ts.ScriptKind.JSX) {
118
+ return;
119
+ }
120
+ }
121
+ const failures = 'applyWithProgram' in rule
122
+ ? rule.applyWithProgram(file, ctx.program)
123
+ : rule.apply(file);
124
+ for (const failure of failures) {
125
+ const reporter = report(failure.getFailure(), failure.getStartPosition().getPosition(), failure.getEndPosition().getPosition()).at(new Error(), Number.MAX_VALUE);
126
+ if (category === 0) {
127
+ reporter.asWarning();
128
+ }
129
+ else if (category === 1) {
130
+ reporter.asError();
131
+ }
132
+ else if (category === 2) {
133
+ reporter.asSuggestion();
134
+ }
135
+ if (failure.hasFix()) {
136
+ const ruleName = Rule.metadata?.ruleName;
137
+ reporter.withFix(ruleName ? `Fix with ${ruleName}` : 'Fix', () => {
138
+ const fix = failure.getFix();
139
+ const replaces = Array.isArray(fix) ? fix : fix ? [fix] : [];
140
+ return [{
141
+ fileName: file.fileName,
142
+ textChanges: replaces.map(replace => ({
143
+ newText: replace.text,
144
+ span: {
145
+ start: replace.start,
146
+ length: replace.length,
147
+ },
148
+ })),
149
+ }];
150
+ });
151
+ }
152
+ }
153
+ };
154
+ }
155
+ //# sourceMappingURL=tslint.js.map
package/package.json CHANGED
@@ -1,7 +1,13 @@
1
1
  {
2
2
  "name": "@tsslint/config",
3
- "version": "3.0.0-alpha.0",
3
+ "version": "3.0.0",
4
4
  "license": "MIT",
5
+ "engines": {
6
+ "node": ">=22.6.0"
7
+ },
8
+ "bin": {
9
+ "tsslint-docgen": "./bin/tsslint-docgen.js"
10
+ },
5
11
  "files": [
6
12
  "**/*.js",
7
13
  "**/*.d.ts"
@@ -12,9 +18,25 @@
12
18
  "directory": "packages/config"
13
19
  },
14
20
  "dependencies": {
15
- "@tsslint/types": "3.0.0-alpha.0",
21
+ "@tsslint/types": "3.0.0",
16
22
  "minimatch": "^10.0.1",
17
23
  "ts-api-utils": "^2.0.0"
18
24
  },
19
- "gitHead": "ec683ca05f4360fac720bd8c567f4d9460d255e1"
20
- }
25
+ "devDependencies": {
26
+ "@tsslint/compat-eslint": "3.0.0",
27
+ "tslint": "^6.1.3"
28
+ },
29
+ "peerDependencies": {
30
+ "@tsslint/compat-eslint": "3.0.0-alpha.0",
31
+ "tsl": "^1.0.28"
32
+ },
33
+ "peerDependenciesMeta": {
34
+ "@tsslint/compat-eslint": {
35
+ "optional": true
36
+ },
37
+ "tsl": {
38
+ "optional": true
39
+ }
40
+ },
41
+ "gitHead": "69bc86cf2dd81f9e48fc15ed9b9f0351fa2fc19e"
42
+ }