@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/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
- const RULE_ALIASES = new Map([
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 entries = normalizeConfigEntries(eslintConfig);
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
- const unsupportedRules = [];
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 = RULE_ALIASES.get(ruleId) ?? ruleId;
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
- unsupportedRules.push(ruleId);
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 = migratablePatterns(entry.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.exit(await runMigrate(process.argv.slice(3)));
8
- }
9
-
10
- let result;
11
- try {
12
- result = runCli(process.argv.slice(2), { stdio: "inherit" });
13
- } catch (error) {
14
- console.error(error.message);
15
- process.exit(1);
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.error) {
19
- console.error(`utoo-lint: failed to run native binary: ${result.error.message}`);
20
- process.exit(1);
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);
@@ -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 rules: NonNullable<ConfigObject["rules"]>;
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;