@utoo/lint 0.2.9 → 0.3.5
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 +41 -11
- package/bin/fishlint.js +23 -3
- package/bin/migrate.js +476 -10
- package/bin/utoo-lint.js +15 -15
- package/configs/frontend.d.ts +54 -2
- package/configs/frontend.json +15 -0
- package/index.cjs +471 -75
- package/index.d.ts +15 -2
- package/index.js +471 -75
- package/lib/config-loader.cjs +16 -3
- package/lib/config-loader.js +16 -3
- package/package.json +8 -6
- package/schema.json +41 -1
package/bin/migrate.js
CHANGED
|
@@ -1,14 +1,19 @@
|
|
|
1
1
|
import { spawnSync } from "node:child_process";
|
|
2
|
-
import { existsSync, readFileSync, writeFileSync } from "node:fs";
|
|
3
|
-
import { dirname, extname, resolve as resolvePath } from "node:path";
|
|
2
|
+
import { existsSync, readFileSync, realpathSync, writeFileSync } from "node:fs";
|
|
3
|
+
import { basename, dirname, extname, relative, resolve as resolvePath } from "node:path";
|
|
4
4
|
import { pathToFileURL } from "node:url";
|
|
5
5
|
|
|
6
|
+
import { Legacy } from "@eslint/eslintrc";
|
|
7
|
+
import eslintJs from "@eslint/js";
|
|
8
|
+
|
|
6
9
|
import { Linter } from "../index.js";
|
|
7
10
|
|
|
8
11
|
const ESLINT_CONFIG_FILENAMES = [
|
|
9
12
|
"eslint.config.js",
|
|
10
13
|
"eslint.config.mjs",
|
|
11
14
|
"eslint.config.cjs",
|
|
15
|
+
".eslintrc.js",
|
|
16
|
+
".eslintrc.cjs",
|
|
12
17
|
".eslintrc.json",
|
|
13
18
|
".eslintrc"
|
|
14
19
|
];
|
|
@@ -16,7 +21,11 @@ const JAVASCRIPT_CONFIG_EXTENSIONS = new Set([".js", ".mjs", ".cjs"]);
|
|
|
16
21
|
const IGNORED_RULES = new Map([
|
|
17
22
|
["prettier/prettier", "formatting is outside utoo-lint"]
|
|
18
23
|
]);
|
|
19
|
-
|
|
24
|
+
// Add aliases only after reviewing the source rule against the target rule's
|
|
25
|
+
// implemented behavior and supported options.
|
|
26
|
+
const REVIEWED_RULE_ALIASES = new Map([
|
|
27
|
+
["no-native-reassign", "no-global-assign"],
|
|
28
|
+
["@typescript-eslint/no-invalid-this", "no-invalid-this"],
|
|
20
29
|
["@eslint-react/no-array-index-key", "react/no-array-index-key"],
|
|
21
30
|
["@eslint-react/dom-no-find-dom-node", "react/no-find-dom-node"],
|
|
22
31
|
["@eslint-react/dom-no-render-return-value", "react/no-render-return-value"],
|
|
@@ -24,6 +33,7 @@ const RULE_ALIASES = new Map([
|
|
|
24
33
|
["@eslint-react/rules-of-hooks", "react-hooks/rules-of-hooks"]
|
|
25
34
|
]);
|
|
26
35
|
const SCHEMA_URL = "https://raw.githubusercontent.com/utooland/utoo-lint/main/npm/utoo-lint/schema.json";
|
|
36
|
+
const MIGRATION_SOURCE = Symbol("migrationSource");
|
|
27
37
|
const JAVASCRIPT_CONFIG_LOADER_SCRIPT = `
|
|
28
38
|
import { writeFileSync } from "node:fs";
|
|
29
39
|
import { pathToFileURL } from "node:url";
|
|
@@ -184,10 +194,15 @@ function migrateEslintConfig(options) {
|
|
|
184
194
|
|
|
185
195
|
const eslintConfig = loadEslintConfig(sourcePath, options.cwd);
|
|
186
196
|
const supportedRuleIds = new Set(new Linter().getRules().keys());
|
|
187
|
-
const
|
|
197
|
+
const migrationInput = migrationInputFromConfig(
|
|
198
|
+
eslintConfig,
|
|
199
|
+
sourcePath,
|
|
200
|
+
realpathSync(dirname(options.output))
|
|
201
|
+
);
|
|
202
|
+
const entries = migrationInput.entries;
|
|
188
203
|
const migratedEntries = [];
|
|
189
204
|
const supportedRules = [];
|
|
190
|
-
|
|
205
|
+
let unsupportedRules = [];
|
|
191
206
|
const ignoredRules = [];
|
|
192
207
|
const translatedRules = [];
|
|
193
208
|
|
|
@@ -195,16 +210,18 @@ function migrateEslintConfig(options) {
|
|
|
195
210
|
const rules = {};
|
|
196
211
|
if (entry.rules && typeof entry.rules === "object") {
|
|
197
212
|
for (const [ruleId, ruleConfig] of Object.entries(entry.rules)) {
|
|
198
|
-
const migratedRuleId =
|
|
213
|
+
const migratedRuleId = REVIEWED_RULE_ALIASES.get(ruleId) ?? ruleId;
|
|
199
214
|
if (IGNORED_RULES.has(ruleId)) {
|
|
200
215
|
ignoredRules.push({ ruleId, reason: IGNORED_RULES.get(ruleId) });
|
|
201
216
|
continue;
|
|
202
217
|
}
|
|
203
218
|
if (!supportedRuleIds.has(migratedRuleId)) {
|
|
204
219
|
if (isRuleDisabled(ruleConfig)) {
|
|
220
|
+
unsupportedRules = removeDisabledUnsupportedRule(unsupportedRules, ruleId, entry);
|
|
205
221
|
continue;
|
|
206
222
|
}
|
|
207
|
-
|
|
223
|
+
const source = entry[MIGRATION_SOURCE];
|
|
224
|
+
unsupportedRules.push({ ruleId, scope: migrationRuleScope(entry), source });
|
|
208
225
|
continue;
|
|
209
226
|
}
|
|
210
227
|
rules[migratedRuleId] = migratableRuleConfig(ruleConfig);
|
|
@@ -217,7 +234,7 @@ function migrateEslintConfig(options) {
|
|
|
217
234
|
migratedEntries.push(migratableConfigEntry(entry, rules));
|
|
218
235
|
}
|
|
219
236
|
|
|
220
|
-
const config = Array.isArray(eslintConfig)
|
|
237
|
+
const config = Array.isArray(eslintConfig) || migratedEntries.length > 1
|
|
221
238
|
? [{ $schema: SCHEMA_URL }, ...migratedEntries]
|
|
222
239
|
: { $schema: SCHEMA_URL, ...(migratedEntries[0] ?? { rules: {} }) };
|
|
223
240
|
|
|
@@ -227,14 +244,410 @@ function migrateEslintConfig(options) {
|
|
|
227
244
|
source: sourcePath,
|
|
228
245
|
output: options.print ? null : options.output,
|
|
229
246
|
supportedRules: uniqueSorted(supportedRules),
|
|
230
|
-
unsupportedRules: uniqueSorted(unsupportedRules),
|
|
247
|
+
unsupportedRules: uniqueSorted(unsupportedRules.map((rule) => rule.ruleId)),
|
|
231
248
|
ignoredRules: uniqueByRuleId(ignoredRules),
|
|
232
249
|
translatedRules: uniqueTranslatedRules(translatedRules),
|
|
250
|
+
inheritedSources: migrationInput.inheritedSources,
|
|
251
|
+
unsupportedInheritedRules: uniqueUnsupportedInheritedRules(
|
|
252
|
+
unsupportedRules
|
|
253
|
+
.filter((rule) => rule.source?.inherited)
|
|
254
|
+
.map((rule) => ({
|
|
255
|
+
ruleId: rule.ruleId,
|
|
256
|
+
sourceName: rule.source.name,
|
|
257
|
+
sourcePath: rule.source.filePath
|
|
258
|
+
}))
|
|
259
|
+
),
|
|
233
260
|
optionDroppedRules: []
|
|
234
261
|
}
|
|
235
262
|
};
|
|
236
263
|
}
|
|
237
264
|
|
|
265
|
+
function migrationInputFromConfig(eslintConfig, sourcePath, outputDirectory) {
|
|
266
|
+
if (!isClassicEslintConfig(eslintConfig, sourcePath)) {
|
|
267
|
+
return { entries: normalizeConfigEntries(eslintConfig), inheritedSources: [] };
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
const basePath = realpathSync(dirname(sourcePath));
|
|
271
|
+
const factory = new MigrationConfigArrayFactory({
|
|
272
|
+
cwd: basePath,
|
|
273
|
+
getEslintRecommendedConfig: () => eslintJs.configs.recommended,
|
|
274
|
+
getEslintAllConfig: () => eslintJs.configs.all
|
|
275
|
+
});
|
|
276
|
+
|
|
277
|
+
let configArray;
|
|
278
|
+
try {
|
|
279
|
+
configArray = factory.create(eslintConfig, {
|
|
280
|
+
basePath,
|
|
281
|
+
filePath: sourcePath,
|
|
282
|
+
name: sourcePath
|
|
283
|
+
});
|
|
284
|
+
} catch (error) {
|
|
285
|
+
throw new Error(`utoo-lint migrate eslint: unable to resolve classic config ${sourcePath}: ${error.message}`);
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
return migrationInputFromClassicConfigArray(configArray, basePath, outputDirectory);
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
function isClassicEslintConfig(eslintConfig, sourcePath) {
|
|
292
|
+
if (/^\.eslintrc(?:\.|$)/u.test(basename(sourcePath))) {
|
|
293
|
+
return true;
|
|
294
|
+
}
|
|
295
|
+
return Boolean(
|
|
296
|
+
eslintConfig &&
|
|
297
|
+
!Array.isArray(eslintConfig) &&
|
|
298
|
+
typeof eslintConfig === "object" &&
|
|
299
|
+
(eslintConfig.extends || eslintConfig.overrides || eslintConfig.ignorePatterns)
|
|
300
|
+
);
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
class MigrationConfigArrayFactory extends Legacy.ConfigArrayFactory {
|
|
304
|
+
activeSources = [];
|
|
305
|
+
|
|
306
|
+
_loadPlugin(name, context) {
|
|
307
|
+
return super._loadPlugin(name, {
|
|
308
|
+
...context,
|
|
309
|
+
pluginBasePath: context.filePath ? dirname(context.filePath) : context.pluginBasePath
|
|
310
|
+
});
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
*_normalizeConfigData(configData, context) {
|
|
314
|
+
const source = classicContextSource(context);
|
|
315
|
+
const cycleIndex = this.activeSources.findIndex((active) => active.key === source.key);
|
|
316
|
+
if (cycleIndex !== -1) {
|
|
317
|
+
const chain = [...this.activeSources.slice(cycleIndex), source].map((active) => active.label).join(" -> ");
|
|
318
|
+
throw new Error(`Circular ESLint extends chain detected: ${chain}`);
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
this.activeSources.push(source);
|
|
322
|
+
try {
|
|
323
|
+
yield* super._normalizeConfigData(configData, context);
|
|
324
|
+
} finally {
|
|
325
|
+
this.activeSources.pop();
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
function classicContextSource(context) {
|
|
331
|
+
const label = classicSourceName(context.name ?? context.filePath ?? "inline config");
|
|
332
|
+
const key = label.startsWith("plugin:")
|
|
333
|
+
? `plugin:${context.filePath ?? ""}:${label}`
|
|
334
|
+
: context.filePath
|
|
335
|
+
? `file:${resolvePath(context.filePath)}`
|
|
336
|
+
: `name:${label}`;
|
|
337
|
+
return { key, label };
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
function migrationInputFromClassicConfigArray(configArray, basePath, outputDirectory) {
|
|
341
|
+
const entries = [];
|
|
342
|
+
const inheritedSources = [];
|
|
343
|
+
const records = [...configArray].map((element) => ({
|
|
344
|
+
element,
|
|
345
|
+
source: classicElementSource(element),
|
|
346
|
+
selector: rebaseClassicSelector(classicSelector(element.criteria), basePath, outputDirectory),
|
|
347
|
+
ignorePatterns: element.ignorePattern?.getPatternsRelativeTo(basePath) ?? []
|
|
348
|
+
}));
|
|
349
|
+
const scopedIgnores = new Map();
|
|
350
|
+
|
|
351
|
+
for (const record of records) {
|
|
352
|
+
if (record.element.criteria && record.ignorePatterns.length > 0) {
|
|
353
|
+
const key = classicOverrideChainKey(record.element);
|
|
354
|
+
scopedIgnores.set(key, uniquePatterns([...(scopedIgnores.get(key) ?? []), ...record.ignorePatterns]));
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
for (const { element, source, selector, ignorePatterns } of records) {
|
|
359
|
+
if (source.inherited) {
|
|
360
|
+
inheritedSources.push({ name: source.name, filePath: source.filePath });
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
const effectiveIgnorePatterns = (element.criteria
|
|
364
|
+
? scopedIgnores.get(classicOverrideChainKey(element)) ?? []
|
|
365
|
+
: ignorePatterns).map((pattern) => rebaseClassicPattern(pattern, basePath, outputDirectory, true));
|
|
366
|
+
const rebasedIgnorePatterns = ignorePatterns.map(
|
|
367
|
+
(pattern) => rebaseClassicPattern(pattern, basePath, outputDirectory, true)
|
|
368
|
+
);
|
|
369
|
+
const hasRules = element.rules && typeof element.rules === "object";
|
|
370
|
+
|
|
371
|
+
if (rebasedIgnorePatterns.length > 0 && !element.criteria) {
|
|
372
|
+
entries.push(withMigrationSource({ ignores: rebasedIgnorePatterns }, source));
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
if (hasRules) {
|
|
376
|
+
const entry = { ...selector, rules: element.rules };
|
|
377
|
+
if (effectiveIgnorePatterns.length > 0 && element.criteria) {
|
|
378
|
+
entry.ignores = uniquePatterns([...(entry.ignores ?? []), ...effectiveIgnorePatterns]);
|
|
379
|
+
}
|
|
380
|
+
entries.push(withMigrationSource(entry, source));
|
|
381
|
+
} else if (rebasedIgnorePatterns.length > 0 && element.criteria) {
|
|
382
|
+
entries.push(withMigrationSource({ ...selector, ignores: [...(selector.ignores ?? []), ...rebasedIgnorePatterns] }, source));
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
return {
|
|
387
|
+
entries,
|
|
388
|
+
inheritedSources: uniqueInheritedSources(inheritedSources)
|
|
389
|
+
};
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
function classicOverrideChainKey(element) {
|
|
393
|
+
const segments = String(element.name ?? "").split(" » ");
|
|
394
|
+
let overrideIndex = -1;
|
|
395
|
+
for (let index = 0; index < segments.length; index += 1) {
|
|
396
|
+
if (/#overrides\[\d+\]/u.test(segments[index])) {
|
|
397
|
+
overrideIndex = index;
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
return overrideIndex === -1
|
|
401
|
+
? `criteria:${classicCriteriaKey(element.criteria)}`
|
|
402
|
+
: `override:${segments.slice(0, overrideIndex + 1).join(" » ")}`;
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
function rebaseClassicSelector(selector, basePath, outputDirectory) {
|
|
406
|
+
return {
|
|
407
|
+
...(selector.files ? {
|
|
408
|
+
files: selector.files.map((value) => Array.isArray(value)
|
|
409
|
+
? value.map((pattern) => rebaseClassicPattern(pattern, basePath, outputDirectory))
|
|
410
|
+
: rebaseClassicPattern(value, basePath, outputDirectory))
|
|
411
|
+
} : {}),
|
|
412
|
+
...(selector.ignores ? {
|
|
413
|
+
ignores: selector.ignores.map(
|
|
414
|
+
(pattern) => rebaseClassicPattern(pattern, basePath, outputDirectory, true)
|
|
415
|
+
)
|
|
416
|
+
} : {})
|
|
417
|
+
};
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
function rebaseClassicPattern(pattern, basePath, outputDirectory, preserveBasenameMatch = false) {
|
|
421
|
+
const negated = pattern.startsWith("!");
|
|
422
|
+
const unsignedPattern = negated ? pattern.slice(1) : pattern;
|
|
423
|
+
const rootAnchored = unsignedPattern.startsWith("/");
|
|
424
|
+
const value = rootAnchored ? unsignedPattern.slice(1) : unsignedPattern;
|
|
425
|
+
if (preserveBasenameMatch && !rootAnchored && !value.includes("/")) {
|
|
426
|
+
return pattern;
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
const absolutePattern = resolvePath(basePath, value);
|
|
430
|
+
const relativePattern = normalizeMigrationPath(relative(outputDirectory, absolutePattern));
|
|
431
|
+
const rebased = relativePattern === ".." || relativePattern.startsWith("../")
|
|
432
|
+
? normalizeMigrationPath(absolutePattern)
|
|
433
|
+
: relativePattern || ".";
|
|
434
|
+
const anchoredPattern = rootAnchored && !rebased.includes("/") ? `/${rebased}` : rebased;
|
|
435
|
+
return negated ? `!${anchoredPattern}` : anchoredPattern;
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
function normalizeMigrationPath(path) {
|
|
439
|
+
return path.replaceAll("\\", "/");
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
function classicCriteriaKey(criteria) {
|
|
443
|
+
return JSON.stringify(classicSelector(criteria));
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
function classicSelector(criteria) {
|
|
447
|
+
if (!criteria) {
|
|
448
|
+
return {};
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
const includeGroups = [];
|
|
452
|
+
const ignores = [];
|
|
453
|
+
for (const patternGroup of criteria.patterns) {
|
|
454
|
+
includeGroups.push(uniquePatterns(
|
|
455
|
+
(patternGroup.includes ?? []).flatMap((matcher) => expandClassicExtglobs(classicIncludePattern(matcher)))
|
|
456
|
+
));
|
|
457
|
+
ignores.push(...(patternGroup.excludes ?? []).flatMap(
|
|
458
|
+
(matcher) => expandClassicExtglobs(classicExcludePattern(matcher))
|
|
459
|
+
));
|
|
460
|
+
}
|
|
461
|
+
const files = classicFileSelectors(includeGroups);
|
|
462
|
+
|
|
463
|
+
return {
|
|
464
|
+
...(files.length > 0 ? { files } : {}),
|
|
465
|
+
...(ignores.length > 0 ? { ignores: uniquePatterns(ignores) } : {})
|
|
466
|
+
};
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
function classicIncludePattern(matcher) {
|
|
470
|
+
return matcher.options?.matchBase && !matcher.pattern.includes("/")
|
|
471
|
+
? `**/${matcher.pattern}`
|
|
472
|
+
: matcher.pattern;
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
function classicExcludePattern(matcher) {
|
|
476
|
+
return matcher.options?.matchBase === false && !matcher.pattern.includes("/")
|
|
477
|
+
? `/${matcher.pattern}`
|
|
478
|
+
: matcher.pattern;
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
function expandClassicExtglobs(pattern) {
|
|
482
|
+
let expansions = [pattern];
|
|
483
|
+
while (expansions.some((value) => /@\([^()]*\)/u.test(value))) {
|
|
484
|
+
expansions = expansions.flatMap((value) => {
|
|
485
|
+
const match = /@\(([^()]*)\)/u.exec(value);
|
|
486
|
+
if (!match) {
|
|
487
|
+
return value;
|
|
488
|
+
}
|
|
489
|
+
const alternatives = match[1].split("|");
|
|
490
|
+
if (alternatives.some((alternative) => !alternative || /[*?[\]{}()]/u.test(alternative))) {
|
|
491
|
+
throw unsupportedClassicExtglobError(pattern);
|
|
492
|
+
}
|
|
493
|
+
return alternatives.map((alternative) =>
|
|
494
|
+
`${value.slice(0, match.index)}${alternative}${value.slice(match.index + match[0].length)}`
|
|
495
|
+
);
|
|
496
|
+
});
|
|
497
|
+
}
|
|
498
|
+
if (/[?+*!@]\(/u.test(expansions[0])) {
|
|
499
|
+
throw unsupportedClassicExtglobError(pattern);
|
|
500
|
+
}
|
|
501
|
+
return uniquePatterns(expansions);
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
function unsupportedClassicExtglobError(pattern) {
|
|
505
|
+
return new Error(
|
|
506
|
+
`utoo-lint migrate eslint: cannot migrate classic selector pattern "${pattern}": ` +
|
|
507
|
+
"only literal @(one|two) extglob alternatives are supported"
|
|
508
|
+
);
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
function classicFileSelectors(includeGroups) {
|
|
512
|
+
const groups = includeGroups.filter((group) => group.length > 0);
|
|
513
|
+
if (groups.length <= 1) {
|
|
514
|
+
return groups[0] ?? [];
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
let selectors = [[]];
|
|
518
|
+
for (const group of groups) {
|
|
519
|
+
selectors = selectors.flatMap((selector) => group.map((pattern) => [...selector, pattern]));
|
|
520
|
+
}
|
|
521
|
+
return selectors;
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
function uniquePatterns(patterns) {
|
|
525
|
+
return [...new Set(patterns)];
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
function migrationRuleScope(entry) {
|
|
529
|
+
if (!entry.files && !entry.ignores) {
|
|
530
|
+
return null;
|
|
531
|
+
}
|
|
532
|
+
return {
|
|
533
|
+
files: migratableFilePatterns(entry.files),
|
|
534
|
+
ignores: migratablePatterns(entry.ignores)
|
|
535
|
+
};
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
function removeDisabledUnsupportedRule(rules, ruleId, entry) {
|
|
539
|
+
const disabledScope = migrationRuleScope(entry);
|
|
540
|
+
const remaining = [];
|
|
541
|
+
for (const rule of rules) {
|
|
542
|
+
if (rule.ruleId !== ruleId) {
|
|
543
|
+
remaining.push(rule);
|
|
544
|
+
continue;
|
|
545
|
+
}
|
|
546
|
+
const scope = subtractDisabledScope(rule.scope, disabledScope);
|
|
547
|
+
if (scope !== undefined) {
|
|
548
|
+
remaining.push({ ...rule, scope });
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
return remaining;
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
function subtractDisabledScope(enabledScope, disabledScope) {
|
|
555
|
+
if (disabledScope === null) {
|
|
556
|
+
return undefined;
|
|
557
|
+
}
|
|
558
|
+
if (enabledScope === null) {
|
|
559
|
+
return enabledScope;
|
|
560
|
+
}
|
|
561
|
+
if (
|
|
562
|
+
disabledScope.ignores.length > 0 &&
|
|
563
|
+
JSON.stringify(enabledScope.ignores) !== JSON.stringify(disabledScope.ignores)
|
|
564
|
+
) {
|
|
565
|
+
return enabledScope;
|
|
566
|
+
}
|
|
567
|
+
if (disabledScope.files.length === 0) {
|
|
568
|
+
return undefined;
|
|
569
|
+
}
|
|
570
|
+
if (enabledScope.files.length === 0) {
|
|
571
|
+
return enabledScope;
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
const disabledGroups = disabledScope.files.map((selector) => Array.isArray(selector) ? selector : [selector]);
|
|
575
|
+
const files = enabledScope.files.filter((selector) => {
|
|
576
|
+
const enabledGroup = Array.isArray(selector) ? selector : [selector];
|
|
577
|
+
return !disabledGroups.some((disabledGroup) => selectorGroupCovers(enabledGroup, disabledGroup));
|
|
578
|
+
});
|
|
579
|
+
return files.length === 0 ? undefined : { ...enabledScope, files };
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
function selectorGroupCovers(enabledGroup, disabledGroup) {
|
|
583
|
+
return disabledGroup.every((disabledPattern) => enabledGroup.some((enabledPattern) =>
|
|
584
|
+
globPatternCovers(enabledPattern, disabledPattern)
|
|
585
|
+
));
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
function globPatternCovers(enabledPattern, disabledPattern) {
|
|
589
|
+
if (enabledPattern === disabledPattern) {
|
|
590
|
+
return true;
|
|
591
|
+
}
|
|
592
|
+
const enabledSegments = enabledPattern.split("/");
|
|
593
|
+
const disabledSegments = disabledPattern.split("/");
|
|
594
|
+
return globSegmentsCover(enabledSegments, disabledSegments, 0, 0);
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
function globSegmentsCover(enabled, disabled, enabledIndex, disabledIndex) {
|
|
598
|
+
if (disabledIndex === disabled.length) {
|
|
599
|
+
return enabledIndex === enabled.length;
|
|
600
|
+
}
|
|
601
|
+
if (disabled[disabledIndex] === "**") {
|
|
602
|
+
if (disabledIndex === disabled.length - 1) {
|
|
603
|
+
return true;
|
|
604
|
+
}
|
|
605
|
+
for (let index = enabledIndex; index <= enabled.length; index += 1) {
|
|
606
|
+
if (globSegmentsCover(enabled, disabled, index, disabledIndex + 1)) {
|
|
607
|
+
return true;
|
|
608
|
+
}
|
|
609
|
+
}
|
|
610
|
+
return false;
|
|
611
|
+
}
|
|
612
|
+
if (enabledIndex === enabled.length || enabled[enabledIndex] === "**") {
|
|
613
|
+
return false;
|
|
614
|
+
}
|
|
615
|
+
return globSegmentCovers(enabled[enabledIndex], disabled[disabledIndex]) &&
|
|
616
|
+
globSegmentsCover(enabled, disabled, enabledIndex + 1, disabledIndex + 1);
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
function globSegmentCovers(enabledSegment, disabledSegment) {
|
|
620
|
+
if (enabledSegment === disabledSegment) {
|
|
621
|
+
return true;
|
|
622
|
+
}
|
|
623
|
+
if (/[?[{]/u.test(disabledSegment)) {
|
|
624
|
+
return false;
|
|
625
|
+
}
|
|
626
|
+
const expression = new RegExp(`^${disabledSegment
|
|
627
|
+
.split("*")
|
|
628
|
+
.map((part) => part.replace(/[.+?^${}()|[\]\\]/gu, "\\$&"))
|
|
629
|
+
.join(".*")}$`);
|
|
630
|
+
return expression.test(enabledSegment);
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
function classicElementSource(element) {
|
|
634
|
+
const name = classicSourceName(element.name ?? element.filePath ?? "inline config");
|
|
635
|
+
return {
|
|
636
|
+
inherited: String(element.name ?? "").includes(" » "),
|
|
637
|
+
name,
|
|
638
|
+
filePath: element.filePath || null
|
|
639
|
+
};
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
function classicSourceName(name) {
|
|
643
|
+
return String(name).split(" » ").at(-1).replace(/#overrides\[\d+\]$/u, "");
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
function withMigrationSource(entry, source) {
|
|
647
|
+
Object.defineProperty(entry, MIGRATION_SOURCE, { value: source });
|
|
648
|
+
return entry;
|
|
649
|
+
}
|
|
650
|
+
|
|
238
651
|
function findEslintConfig(cwd) {
|
|
239
652
|
let current = cwd;
|
|
240
653
|
while (true) {
|
|
@@ -293,7 +706,7 @@ function migratableConfigEntry(entry, rules) {
|
|
|
293
706
|
if (typeof entry.name === "string") {
|
|
294
707
|
migrated.name = entry.name;
|
|
295
708
|
}
|
|
296
|
-
const files =
|
|
709
|
+
const files = migratableFilePatterns(entry.files);
|
|
297
710
|
if (files.length > 0) {
|
|
298
711
|
migrated.files = files;
|
|
299
712
|
}
|
|
@@ -307,6 +720,27 @@ function migratableConfigEntry(entry, rules) {
|
|
|
307
720
|
return migrated;
|
|
308
721
|
}
|
|
309
722
|
|
|
723
|
+
function migratableFilePatterns(value) {
|
|
724
|
+
const result = [];
|
|
725
|
+
const seen = new Set();
|
|
726
|
+
for (const pattern of Array.isArray(value) ? value : [value]) {
|
|
727
|
+
const migrated = Array.isArray(pattern)
|
|
728
|
+
? migratablePatterns(pattern)
|
|
729
|
+
: typeof pattern === "string"
|
|
730
|
+
? pattern
|
|
731
|
+
: null;
|
|
732
|
+
if (migrated === null || (Array.isArray(migrated) && migrated.length === 0)) {
|
|
733
|
+
continue;
|
|
734
|
+
}
|
|
735
|
+
const key = JSON.stringify(migrated);
|
|
736
|
+
if (!seen.has(key)) {
|
|
737
|
+
seen.add(key);
|
|
738
|
+
result.push(migrated);
|
|
739
|
+
}
|
|
740
|
+
}
|
|
741
|
+
return result;
|
|
742
|
+
}
|
|
743
|
+
|
|
310
744
|
function migratablePatterns(value) {
|
|
311
745
|
const patterns = [];
|
|
312
746
|
collectMigratablePatterns(value, patterns);
|
|
@@ -360,6 +794,28 @@ function uniqueTranslatedRules(values) {
|
|
|
360
794
|
return [...result.values()].sort((left, right) => left.sourceRuleId.localeCompare(right.sourceRuleId));
|
|
361
795
|
}
|
|
362
796
|
|
|
797
|
+
function uniqueInheritedSources(values) {
|
|
798
|
+
const result = new Map();
|
|
799
|
+
for (const value of values) {
|
|
800
|
+
result.set(`${value.name}\0${value.filePath ?? ""}`, value);
|
|
801
|
+
}
|
|
802
|
+
return [...result.values()].sort((left, right) =>
|
|
803
|
+
left.name.localeCompare(right.name) || String(left.filePath ?? "").localeCompare(String(right.filePath ?? ""))
|
|
804
|
+
);
|
|
805
|
+
}
|
|
806
|
+
|
|
807
|
+
function uniqueUnsupportedInheritedRules(values) {
|
|
808
|
+
const result = new Map();
|
|
809
|
+
for (const value of values) {
|
|
810
|
+
result.set(`${value.ruleId}\0${value.sourceName}\0${value.sourcePath ?? ""}`, value);
|
|
811
|
+
}
|
|
812
|
+
return [...result.values()].sort((left, right) =>
|
|
813
|
+
left.ruleId.localeCompare(right.ruleId) ||
|
|
814
|
+
left.sourceName.localeCompare(right.sourceName) ||
|
|
815
|
+
String(left.sourcePath ?? "").localeCompare(String(right.sourcePath ?? ""))
|
|
816
|
+
);
|
|
817
|
+
}
|
|
818
|
+
|
|
363
819
|
function writeReport(report, options, stream) {
|
|
364
820
|
if (options.report === "json") {
|
|
365
821
|
stream.write(JSON.stringify(report, null, 2) + "\n");
|
|
@@ -371,6 +827,9 @@ function writeReport(report, options, stream) {
|
|
|
371
827
|
stream.write(`utoo-lint migrate eslint: wrote ${report.output}\n`);
|
|
372
828
|
}
|
|
373
829
|
stream.write(`utoo-lint migrate eslint: migrated ${report.supportedRules.length} supported rule(s)\n`);
|
|
830
|
+
if (report.inheritedSources.length > 0) {
|
|
831
|
+
stream.write(`utoo-lint migrate eslint: resolved ${report.inheritedSources.length} inherited config source(s): ${report.inheritedSources.map(formatInheritedSource).join(", ")}\n`);
|
|
832
|
+
}
|
|
374
833
|
if (report.translatedRules.length > 0) {
|
|
375
834
|
stream.write(`utoo-lint migrate eslint: translated ${report.translatedRules.length} rule alias(es): ${report.translatedRules.map((rule) => `${rule.sourceRuleId} -> ${rule.targetRuleId}`).join(", ")}\n`);
|
|
376
835
|
}
|
|
@@ -380,6 +839,13 @@ function writeReport(report, options, stream) {
|
|
|
380
839
|
if (report.unsupportedRules.length > 0) {
|
|
381
840
|
stream.write(`utoo-lint migrate eslint: ${report.unsupportedRules.length} unsupported rule(s): ${report.unsupportedRules.join(", ")}\n`);
|
|
382
841
|
}
|
|
842
|
+
if (report.unsupportedInheritedRules.length > 0) {
|
|
843
|
+
stream.write(`utoo-lint migrate eslint: unsupported inherited rule source(s): ${report.unsupportedInheritedRules.map((rule) => `${rule.ruleId} from ${formatInheritedSource({ name: rule.sourceName, filePath: rule.sourcePath })}`).join(", ")}\n`);
|
|
844
|
+
}
|
|
845
|
+
}
|
|
846
|
+
|
|
847
|
+
function formatInheritedSource(source) {
|
|
848
|
+
return source.filePath ? `${source.name} (${pathToFileURL(source.filePath).href})` : source.name;
|
|
383
849
|
}
|
|
384
850
|
|
|
385
851
|
function printMigrateHelp() {
|
package/bin/utoo-lint.js
CHANGED
|
@@ -4,20 +4,20 @@ import { runCli } from "../index.js";
|
|
|
4
4
|
|
|
5
5
|
if (process.argv[2] === "migrate") {
|
|
6
6
|
const { runMigrate } = await import("./migrate.js");
|
|
7
|
-
process.
|
|
8
|
-
}
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
}
|
|
7
|
+
process.exitCode = await runMigrate(process.argv.slice(3));
|
|
8
|
+
} else {
|
|
9
|
+
let result;
|
|
10
|
+
try {
|
|
11
|
+
result = runCli(process.argv.slice(2), { stdio: "inherit" });
|
|
12
|
+
} catch (error) {
|
|
13
|
+
console.error(error.message);
|
|
14
|
+
process.exitCode = 1;
|
|
15
|
+
}
|
|
17
16
|
|
|
18
|
-
if (result
|
|
19
|
-
|
|
20
|
-
|
|
17
|
+
if (result?.error) {
|
|
18
|
+
console.error(`utoo-lint: failed to run native binary: ${result.error.message}`);
|
|
19
|
+
process.exitCode = 1;
|
|
20
|
+
} else if (result) {
|
|
21
|
+
process.exitCode = result.status ?? 1;
|
|
22
|
+
}
|
|
21
23
|
}
|
|
22
|
-
|
|
23
|
-
process.exit(result.status ?? 1);
|
package/configs/frontend.d.ts
CHANGED
|
@@ -1,8 +1,60 @@
|
|
|
1
|
-
import type { ConfigObject } from "../index.js";
|
|
1
|
+
import type { ConfigObject, RuleConfig } from "../index.js";
|
|
2
|
+
|
|
3
|
+
export type FrontendRuleId =
|
|
4
|
+
| "no-debugger"
|
|
5
|
+
| "no-alert"
|
|
6
|
+
| "no-console"
|
|
7
|
+
| "no-unused-vars"
|
|
8
|
+
| "prefer-const"
|
|
9
|
+
| "eqeqeq"
|
|
10
|
+
| "use-isnan"
|
|
11
|
+
| "valid-typeof"
|
|
12
|
+
| "no-constant-condition"
|
|
13
|
+
| "no-dupe-keys"
|
|
14
|
+
| "no-duplicate-imports"
|
|
15
|
+
| "no-script-url"
|
|
16
|
+
| "import/first"
|
|
17
|
+
| "import/newline-after-import"
|
|
18
|
+
| "import/no-duplicates"
|
|
19
|
+
| "import/no-self-import"
|
|
20
|
+
| "promise/no-nesting"
|
|
21
|
+
| "react-hooks/rules-of-hooks"
|
|
22
|
+
| "react-hooks/exhaustive-deps"
|
|
23
|
+
| "react/jsx-no-duplicate-props"
|
|
24
|
+
| "react/jsx-key"
|
|
25
|
+
| "react/jsx-no-target-blank"
|
|
26
|
+
| "react/jsx-pascal-case"
|
|
27
|
+
| "react/no-array-index-key"
|
|
28
|
+
| "react/no-children-prop"
|
|
29
|
+
| "react/no-danger"
|
|
30
|
+
| "react/no-danger-with-children"
|
|
31
|
+
| "react/no-find-dom-node"
|
|
32
|
+
| "react/no-forward-ref"
|
|
33
|
+
| "react/no-unstable-nested-components"
|
|
34
|
+
| "react/no-unescaped-entities"
|
|
35
|
+
| "react/void-dom-elements-no-children"
|
|
36
|
+
| "jsx-a11y/aria-props"
|
|
37
|
+
| "jsx-a11y/aria-unsupported-elements"
|
|
38
|
+
| "jsx-a11y/iframe-has-title"
|
|
39
|
+
| "jsx-a11y/img-redundant-alt"
|
|
40
|
+
| "jsx-a11y/no-access-key"
|
|
41
|
+
| "unused-imports/no-unused-imports"
|
|
42
|
+
| "@typescript-eslint/ban-types"
|
|
43
|
+
| "@typescript-eslint/no-unused-vars"
|
|
44
|
+
| "@typescript-eslint/no-use-before-define"
|
|
45
|
+
| "@typescript-eslint/no-shadow"
|
|
46
|
+
| "@typescript-eslint/no-redeclare"
|
|
47
|
+
| "@typescript-eslint/no-require-imports"
|
|
48
|
+
| "@typescript-eslint/consistent-type-assertions"
|
|
49
|
+
| "@typescript-eslint/consistent-type-definitions"
|
|
50
|
+
| "@typescript-eslint/no-empty-interface"
|
|
51
|
+
| "@typescript-eslint/no-non-null-asserted-optional-chain";
|
|
2
52
|
|
|
3
53
|
declare const frontend: ConfigObject & {
|
|
4
54
|
readonly $schema: string;
|
|
5
|
-
readonly
|
|
55
|
+
readonly files: ["src/**/*.{js,jsx,ts,tsx}"];
|
|
56
|
+
readonly ignores: ["dist", "coverage", "node_modules"];
|
|
57
|
+
readonly rules: Readonly<Record<FrontendRuleId, RuleConfig>>;
|
|
6
58
|
};
|
|
7
59
|
|
|
8
60
|
export default frontend;
|