biome-to-oxc 3.6.0 → 3.7.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.
package/README.md CHANGED
@@ -22,20 +22,31 @@ Current capabilities:
22
22
 
23
23
  - Converts `biome.json`/`biome.jsonc` to `.oxlintrc.json` and `.oxfmtrc.jsonc`
24
24
  - Maps 80+ Biome linter rules to Oxlint equivalents (native `typescript/` rules)
25
- - Substitutes a non-type-aware Oxlint rule for the three Biome rules that need no types but whose
26
- only Oxlint counterpart is type-aware, so a migration without tsgolint keeps their coverage
25
+ - Substitutes a non-type-aware Oxlint rule for the four Biome rules that need no types but whose
26
+ only Oxlint counterpart is type-aware, so a migration without tsgolint keeps their coverage, and
27
+ reports every other rule that maps only to a type-aware Oxlint rule as a semantic loss when
28
+ type-aware linting is off
27
29
  - Transforms Biome formatter options to Oxfmt (Prettier-compatible)
28
30
  - Normalizes `include`/`includes` field variations automatically
29
31
 
30
32
  ✅ **Preserves Representable Setup**
31
33
 
32
34
  - Ignore patterns, override file globs, and override exclusions
33
- - Negated `includes` exceptions (`!pattern`), translated into `ignorePatterns` for both tools
35
+ - Negated `includes` exceptions (`!pattern`), translated into `ignorePatterns` for both tools, and
36
+ patterns that re-include files after an exception, carried over as `!` entries (or, in an
37
+ override, as a second override for the re-included files)
38
+ - Root anchoring of `includes` patterns: Biome matches `!dist` and `*.gen.ts` at the config root
39
+ only, so they are written as `/dist` in `ignorePatterns` and `./*.gen.ts` in an override, where
40
+ Oxlint and Oxfmt would otherwise match them at any depth
41
+ - `includes` patterns Biome matches against no path — ending in `/`, starting with `/`, or an
42
+ exception starting with `./` — are left out with a warning instead of excluding the path
34
43
  - `.biomeignore` patterns migrated into the Oxlint **and** Oxfmt `ignorePatterns`. Biome 2.x does
35
44
  not read `.biomeignore` itself, so this deliberately narrows scope; the migration says so
36
45
  - Linter configuration overrides
37
46
  - Formatter configuration overrides when they can be represented as Oxfmt file-glob overrides
38
- - JavaScript globals
47
+ - JavaScript globals, plus `env: { browser: true, node: true }`, which gives Oxlint the browser and
48
+ Node.js globals Biome resolves without configuration (`no-undef`, `no-global-assign` and
49
+ `no-implied-eval` depend on it)
39
50
  - Rule severities (`error`/`warn`/`off`; `info` and `on` are accepted with explicit lossy-mapping warnings)
40
51
  - Explicit `printWidth` handling (no silent changes)
41
52
  - The set of formatted files: Oxfmt-only formats (YAML, TOML, Markdown) are excluded, and
@@ -308,7 +319,7 @@ Oxlint's broader `restriction` category.
308
319
 
309
320
  ### Type-Aware Rule Fallbacks
310
321
 
311
- Three Biome rules need no type information, but their only Oxlint counterpart is a type-aware
322
+ Four Biome rules need no type information, but their only Oxlint counterpart is a type-aware
312
323
  (tsgolint) rule. Type-aware linting is off unless the migration enables it, so mapping straight to
313
324
  the tsgolint rule would drop diagnostics the Biome config did produce. When type-aware linting is
314
325
  off, a non-type-aware Oxlint rule takes its place instead:
@@ -318,15 +329,18 @@ off, a non-type-aware Oxlint rule takes its place instead:
318
329
  | `useArrayFind` | `typescript/prefer-find` | `unicorn/prefer-array-find` |
319
330
  | `useIncludes` | `typescript/prefer-includes` | `unicorn/prefer-includes` |
320
331
  | `useStringStartsEndsWith` | `typescript/prefer-string-starts-ends-with` | `unicorn/prefer-string-starts-ends-with` |
332
+ | `useThrowOnlyError` | `typescript/only-throw-error` | `no-throw-literal` |
321
333
 
322
334
  The fallback replaces the type-aware rule rather than joining it, so turning type-aware linting on
323
- later can never report the same line twice. The first two pairs report exactly what the Biome rule
324
- reports; `unicorn/prefer-string-starts-ends-with` covers only the anchored-regex form of the four
335
+ later can never report the same line twice. `useArrayFind`, `useIncludes` and `useThrowOnlyError`
336
+ report exactly what the Biome rule reports; `unicorn/prefer-string-starts-ends-with` covers only the anchored-regex form of the four
325
337
  Biome flags, so that substitution is reported as a semantic loss.
326
338
 
327
339
  Every other Biome rule that maps only to a type-aware Oxlint rule — `noFloatingPromises`,
328
- `noMisusedPromises`, `useOptionalChain` and the rest — needs types on both sides and has no
329
- fallback.
340
+ `noMisusedPromises`, `useOptionalChain`, `useLiteralKeys` and the rest — has no non-type-aware
341
+ Oxlint counterpart. Oxlint skips type-aware rules unless type-aware linting is on, so a migration
342
+ without `--type-aware` reports each of them as a semantic loss instead of emitting a rule that
343
+ never runs.
330
344
 
331
345
  ### Assist Actions
332
346
 
@@ -428,8 +442,8 @@ npx oxlint --type-aware --type-check
428
442
  - Overlapping `overrides` are migrated as-is. Biome and Oxfmt both merge matching overrides field
429
443
  by field with later entries winning, so the two agree; a conformance test pins this
430
444
  - Type-aware linting is stable as of `oxlint-tsgolint` v7 and requires TypeScript 7; 60 of the 62
431
- targeted typescript-eslint rules are currently implemented, and Oxlint 1.85 does not yet expose
432
- `no-generated-empty-object-type`, so 59 can be enabled
445
+ targeted typescript-eslint rules are currently implemented (`naming-convention` and
446
+ `prefer-destructuring` are not), and Oxlint 1.86 and later can enable all 60
433
447
  - CSS, JSON and GraphQL formatter overrides are mapped but may need manual review
434
448
  - Prettier plugin support is not available in Oxfmt
435
449
  - Assist actions are migrated only when the Biome config names them explicitly; actions enabled
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- import { i as readJsonFile, n as DefaultReporter, r as findClosestPackageJson, t as migrate } from "../src-BN3SXk7r.mjs";
2
+ import { i as readJsonFile, n as DefaultReporter, r as findClosestPackageJson, t as migrate } from "../src-JKoQYJ0f.mjs";
3
3
  import { realpath } from "node:fs/promises";
4
4
  import { dirname } from "node:path";
5
5
  import { fileURLToPath } from "node:url";
@@ -18,7 +18,10 @@ export interface BiomeConfig {
18
18
  export interface BiomeFilesConfig {
19
19
  include?: string[];
20
20
  includes?: string[];
21
- /** Negated `includes` entries (`!pattern`), split out during normalization. */
21
+ /**
22
+ * Exclusions split out of `includes` during normalization, in source order: an exception
23
+ * (`!pattern`) as `pattern`, and a pattern re-included after one as `!pattern`.
24
+ */
22
25
  exclude?: string[];
23
26
  ignore?: string[];
24
27
  ignoreUnknown?: boolean;
@@ -35,7 +38,10 @@ export interface BiomeLinterConfig {
35
38
  enabled?: boolean;
36
39
  include?: string[];
37
40
  includes?: string[];
38
- /** Negated `includes` entries (`!pattern`), split out during normalization. */
41
+ /**
42
+ * Exclusions split out of `includes` during normalization, in source order: an exception
43
+ * (`!pattern`) as `pattern`, and a pattern re-included after one as `!pattern`.
44
+ */
39
45
  exclude?: string[];
40
46
  ignore?: string[];
41
47
  rules?: BiomeLinterRules;
@@ -84,7 +90,10 @@ export interface BiomeFormatterConfig {
84
90
  enabled?: boolean;
85
91
  include?: string[];
86
92
  includes?: string[];
87
- /** Negated `includes` entries (`!pattern`), split out during normalization. */
93
+ /**
94
+ * Exclusions split out of `includes` during normalization, in source order: an exception
95
+ * (`!pattern`) as `pattern`, and a pattern re-included after one as `!pattern`.
96
+ */
88
97
  exclude?: string[];
89
98
  ignore?: string[];
90
99
  formatWithErrors?: boolean;
@@ -397,6 +406,8 @@ export interface PackageUpdateSummary {
397
406
  scriptsUpdated: PackageScriptUpdate[];
398
407
  dependenciesRemoved: PackageDependencyRemoval[];
399
408
  devDependencies: PackageDevDependencyChange[];
409
+ /** Package scripts that still invoke Biome after the update, which keep Biome in use. */
410
+ scriptsStillUsingBiome: string[];
400
411
  changed: boolean;
401
412
  /** Lockfile that dependency changes invalidated, when one was detected. */
402
413
  lockfile?: LockfileStatus;
@@ -1,2 +1,2 @@
1
- import { t as migrate } from "../src-BN3SXk7r.mjs";
1
+ import { t as migrate } from "../src-JKoQYJ0f.mjs";
2
2
  export { migrate };
@@ -1,6 +1,6 @@
1
1
  import { createRequire } from "node:module";
2
2
  import { access, copyFile, mkdir, readFile, rename, stat, unlink, writeFile } from "node:fs/promises";
3
- import { basename, dirname, isAbsolute, join, posix, relative, resolve } from "node:path";
3
+ import { basename, dirname, join, posix, relative, resolve } from "node:path";
4
4
  import { fileURLToPath } from "node:url";
5
5
  import pc from "picocolors";
6
6
  import { z } from "zod";
@@ -215,13 +215,32 @@ async function loadBiomeIgnorePatterns(projectDir, reporter) {
215
215
  //#endregion
216
216
  //#region src/schema-normalizer.ts
217
217
  /**
218
+ * Biome 2 anchors every `includes` pattern at the config root, so `dist` and `*.gen.ts` only
219
+ * match there. Oxlint and Oxfmt match a pattern without a `/` at any depth, and anchor one
220
+ * with a leading `/` in `ignorePatterns` but with `./` in an override's `files` and
221
+ * `excludeFiles`, where a leading `/` matches nothing.
222
+ */
223
+ function anchorAtRoot(pattern, target) {
224
+ if (pattern.includes("/") || pattern === "**") return pattern;
225
+ return `${target === "ignorePatterns" ? "/" : "./"}${pattern}`;
226
+ }
227
+ /** Why Biome 2 matches `pattern` against no path, if it does. */
228
+ function findUnmatchableReason(pattern) {
229
+ const body = pattern.replace(/^!{1,2}/u, "");
230
+ if (body.endsWith("/")) return "ends with \"/\"";
231
+ if (body.startsWith("/")) return "starts with \"/\"";
232
+ return body !== pattern && body.startsWith("./") ? "starts with \"./\"" : void 0;
233
+ }
234
+ /**
218
235
  * Splits a Biome selection into positive selectors and negated exceptions.
219
236
  *
220
237
  * Biome 2.x `includes` mixes both: a bare pattern selects files, `!pattern` excludes
221
- * them again, and `!!pattern` force-ignores a path at the scanner level. The legacy
222
- * `include` field carries positive selectors only.
238
+ * them again, and `!!pattern` force-ignores a path at the scanner level. The patterns
239
+ * apply in order, so a bare pattern after an exception re-includes what it matches, and a
240
+ * pattern ending in `/` matches nothing. The legacy `include` field carries positive
241
+ * selectors only.
223
242
  */
224
- function normalizeIncludeFields(obj, fieldName, reporter) {
243
+ function normalizeIncludeFields(obj, fieldName, reporter, target = "ignorePatterns") {
225
244
  if (obj.include && obj.includes) {
226
245
  reporter.warn(`Both 'include' and 'includes' found in ${fieldName}. Using 'include' and ignoring 'includes'.`);
227
246
  return {
@@ -229,36 +248,154 @@ function normalizeIncludeFields(obj, fieldName, reporter) {
229
248
  exclude: void 0
230
249
  };
231
250
  }
232
- if (obj.includes) return splitIncludes(obj.includes, fieldName, reporter);
251
+ if (obj.includes) return splitIncludes(obj.includes, fieldName, reporter, target);
233
252
  return {
234
253
  include: obj.include,
235
254
  exclude: void 0
236
255
  };
237
256
  }
238
- function splitIncludes(includes, fieldName, reporter) {
257
+ function splitIncludes(includes, fieldName, reporter, target) {
239
258
  const include = [];
240
259
  const exclude = [];
241
260
  for (const pattern of includes) {
261
+ const unmatchable = findUnmatchableReason(pattern);
262
+ if (unmatchable && pattern.startsWith("!")) {
263
+ const body = pattern.replace(/^!{1,2}/u, "");
264
+ const intended = anchorAtRoot(`${body.replace(/^\.?\//u, "")}${body.endsWith("/") ? "**" : ""}`, target);
265
+ const destination = target === "ignorePatterns" ? "the Oxlint and Oxfmt ignorePatterns" : "the excludeFiles of the migrated override";
266
+ reporter.warn(`Biome exception "${pattern}" in ${fieldName} ${unmatchable} and matches no file in Biome, so it excludes nothing and nothing was migrated for it. If the path was meant to be excluded, add "${intended}" to ${destination}.`);
267
+ continue;
268
+ }
269
+ if (unmatchable) reporter.warn(`Biome pattern "${pattern}" in ${fieldName} ${unmatchable} and matches no file in Biome, so it selects nothing.`);
242
270
  if (pattern.startsWith("!!")) {
243
- exclude.push(pattern.slice(2));
271
+ exclude.push(anchorAtRoot(pattern.slice(2), target));
244
272
  reporter.loss(`Biome force-ignore pattern "${pattern}" in ${fieldName} was migrated as a plain ignore; Oxc has no scanner-level force-ignore, so files reachable through other tooling paths may still be processed.`);
245
273
  continue;
246
274
  }
247
275
  if (pattern.startsWith("!")) {
248
- exclude.push(pattern.slice(1));
276
+ exclude.push(anchorAtRoot(pattern.slice(1), target));
249
277
  continue;
250
278
  }
251
- include.push(pattern);
279
+ const anchored = unmatchable ? pattern : anchorAtRoot(pattern.replace(/^\.\//u, ""), target);
280
+ if (exclude.length === 0) {
281
+ include.push(target === "override" ? anchored : pattern);
282
+ continue;
283
+ }
284
+ if (unmatchable) continue;
285
+ const blockedBy = target === "ignorePatterns" ? findExcludedAncestor(anchored, exclude) : null;
286
+ if (blockedBy) {
287
+ reporter.warn(`Biome pattern "${pattern}" in ${fieldName} re-includes nothing: an earlier exception still excludes the directory "${blockedBy}", and Biome does not look inside an excluded directory, so nothing was migrated for it. Re-include "${blockedBy}" first if its files were meant to be processed.`);
288
+ continue;
289
+ }
290
+ exclude.push(`!${anchored}`);
252
291
  }
253
292
  return {
254
293
  include: include.length > 0 ? include : void 0,
255
294
  exclude: exclude.length > 0 ? exclude : void 0
256
295
  };
257
296
  }
297
+ /** Patterns that select every file, which Biome 2.x configs list before their `!` exceptions. */
298
+ const MATCH_ALL_PATTERNS = /* @__PURE__ */ new Set(["**", "**/*"]);
299
+ /**
300
+ * Drops a positive selection that already selects every file. `["**", "!dist"]` is Biome's
301
+ * canonical way to exclude a directory, and the positive part narrows nothing, so keeping
302
+ * it would report an unrepresentable include selection for a config that has none.
303
+ */
304
+ function withoutMatchAllSelection(selection) {
305
+ return selection.include?.some((pattern) => MATCH_ALL_PATTERNS.has(pattern)) ? {
306
+ ...selection,
307
+ include: void 0
308
+ } : selection;
309
+ }
310
+ function isReinclude(pattern) {
311
+ return pattern.startsWith("!");
312
+ }
313
+ const GLOB_SEGMENT = /[*?[{]/u;
314
+ /** Whether a glob can match files at more than one directory depth or in several directories. */
315
+ function hasDirectoryGlob(pattern) {
316
+ return pattern.replace(/^!/u, "").split("/").slice(0, -1).some((segment) => GLOB_SEGMENT.test(segment));
317
+ }
318
+ /** A pattern as a project-relative glob, without the `!` or the root anchor it was given. */
319
+ function globBody(pattern) {
320
+ return pattern.replace(/^!/u, "").replace(/^\.?\//u, "");
321
+ }
322
+ /**
323
+ * The directory that stops Biome from reaching anything `pattern` names, if there is one.
324
+ *
325
+ * Biome walks the project and skips a directory whose last matching `includes` pattern is
326
+ * an exception, so a re-include of a file below one has no effect. Only the directories in
327
+ * the pattern's literal prefix are checked: those the pattern itself needs to be walked.
328
+ */
329
+ function findExcludedAncestor(pattern, ordered) {
330
+ const segments = globBody(pattern).split("/").slice(0, -1);
331
+ const literalDepth = segments.findIndex((segment) => GLOB_SEGMENT.test(segment));
332
+ const directories = segments.slice(0, literalDepth === -1 ? segments.length : literalDepth).map((_, index, literal) => literal.slice(0, index + 1).join("/"));
333
+ for (const directory of directories) {
334
+ const lastMatch = ordered.findLast((entry) => posix.matchesGlob(directory, globBody(entry)));
335
+ if (lastMatch !== void 0 && !isReinclude(lastMatch)) return directory;
336
+ }
337
+ return null;
338
+ }
339
+ /**
340
+ * Joins the `files` exclusions with a tool's own for one `ignorePatterns` list. A `!` entry
341
+ * lifts every earlier pattern it matches, so the list carrying one goes first and cannot lift
342
+ * the other list's exclusions. `normalizeBiomeConfig` leaves at most one of the two with any.
343
+ */
344
+ function joinExclusions(filesExclude = [], toolExclude = []) {
345
+ return toolExclude.some(isReinclude) ? [...toolExclude, ...filesExclude] : [...filesExclude, ...toolExclude];
346
+ }
347
+ /**
348
+ * `files.includes` bounds what a tool's own `includes` can select, but both end up in one
349
+ * `ignorePatterns` list, where a re-include from either would lift the other's exclusions.
350
+ * When both carry re-includes only those of `files` are kept.
351
+ */
352
+ function withoutToolReincludes(exclude, fieldName, filesExclude, reporter) {
353
+ const reincludes = exclude?.filter(isReinclude) ?? [];
354
+ if (!exclude || reincludes.length === 0 || !filesExclude?.some(isReinclude)) return exclude;
355
+ reporter.loss(`Biome ${fieldName}.includes re-includes ${reincludes.map((pattern) => pattern.slice(1)).join(", ")} after a negated pattern, and files.includes re-includes files too; one ignorePatterns list cannot keep the two sets of exceptions apart, so the files ${fieldName}.includes re-includes stay excluded.`);
356
+ return exclude.filter((pattern) => !isReinclude(pattern));
357
+ }
358
+ /**
359
+ * Splits an override selection that re-includes files into selections that need no
360
+ * re-include. An override's `excludeFiles` cannot carry an exception to an exclusion, but
361
+ * Biome selects a file when the last pattern matching it is a positive one, which is the
362
+ * union of every positive pattern less the exceptions listed after it. So the patterns
363
+ * ahead of the first exception keep every exception, and each run of re-includes keeps
364
+ * only the exceptions that follow it.
365
+ */
366
+ function splitReincludes(selection) {
367
+ const ordered = selection.exclude ?? [];
368
+ if (!ordered.some(isReinclude)) return [selection];
369
+ const exceptionsFrom = (index) => {
370
+ const exceptions = ordered.slice(index).filter((pattern) => !isReinclude(pattern));
371
+ return exceptions.length > 0 ? exceptions : void 0;
372
+ };
373
+ const leading = selection.include ?? [];
374
+ const selections = leading.length > 0 ? [{
375
+ include: leading,
376
+ exclude: exceptionsFrom(0)
377
+ }] : [];
378
+ let run;
379
+ for (const [index, pattern] of ordered.entries()) {
380
+ if (!isReinclude(pattern)) {
381
+ run = void 0;
382
+ continue;
383
+ }
384
+ if (!run) {
385
+ run = [];
386
+ selections.push({
387
+ include: run,
388
+ exclude: exceptionsFrom(index)
389
+ });
390
+ }
391
+ run.push(pattern.slice(1));
392
+ }
393
+ return selections;
394
+ }
258
395
  function normalizeBiomeConfig(config, reporter) {
259
396
  const normalized = { ...config };
260
397
  if (normalized.files) {
261
- const { include, exclude } = normalizeIncludeFields(normalized.files, "files", reporter);
398
+ const { include, exclude } = withoutMatchAllSelection(normalizeIncludeFields(normalized.files, "files", reporter));
262
399
  normalized.files = {
263
400
  ...normalized.files,
264
401
  include,
@@ -267,32 +404,36 @@ function normalizeBiomeConfig(config, reporter) {
267
404
  };
268
405
  }
269
406
  if (normalized.linter) {
270
- const { include, exclude } = normalizeIncludeFields(normalized.linter, "linter", reporter);
407
+ const { include, exclude } = withoutMatchAllSelection(normalizeIncludeFields(normalized.linter, "linter", reporter));
271
408
  normalized.linter = {
272
409
  ...normalized.linter,
273
410
  include,
274
- exclude,
411
+ exclude: withoutToolReincludes(exclude, "linter", normalized.files?.exclude, reporter),
275
412
  includes: void 0
276
413
  };
277
414
  }
278
415
  if (normalized.formatter) {
279
- const { include, exclude } = normalizeIncludeFields(normalized.formatter, "formatter", reporter);
416
+ const { include, exclude } = withoutMatchAllSelection(normalizeIncludeFields(normalized.formatter, "formatter", reporter));
280
417
  normalized.formatter = {
281
418
  ...normalized.formatter,
282
419
  include,
283
- exclude,
420
+ exclude: withoutToolReincludes(exclude, "formatter", normalized.files?.exclude, reporter),
284
421
  includes: void 0
285
422
  };
286
423
  }
287
- if (normalized.overrides) normalized.overrides = normalized.overrides.map((override, index) => {
288
- const { include, exclude } = normalizeIncludeFields(override, `overrides[${index}]`, reporter);
289
- return {
290
- ...override,
291
- include,
292
- exclude,
293
- includes: void 0
294
- };
295
- });
424
+ if (normalized.overrides) {
425
+ const overrides = [];
426
+ for (const [index, override] of normalized.overrides.entries()) {
427
+ const selection = normalizeIncludeFields(override, `overrides[${index}]`, reporter, "override");
428
+ for (const { include, exclude } of splitReincludes(selection)) overrides.push({
429
+ ...override,
430
+ include,
431
+ exclude,
432
+ includes: void 0
433
+ });
434
+ }
435
+ normalized.overrides = overrides;
436
+ }
296
437
  return normalized;
297
438
  }
298
439
  //#endregion
@@ -317,7 +458,7 @@ const SUPPORTED_TOP_LEVEL_FIELDS = /* @__PURE__ */ new Set([
317
458
  const IncludeFieldsSchema = z.object({
318
459
  include: z.array(z.string()).optional(),
319
460
  includes: z.array(z.string()).optional()
320
- }).passthrough();
461
+ }).loose();
321
462
  const BiomeRuleSeveritySchema = z.union([z.enum([
322
463
  "off",
323
464
  "on",
@@ -333,7 +474,7 @@ const BiomeRuleSeveritySchema = z.union([z.enum([
333
474
  "error"
334
475
  ]),
335
476
  options: z.unknown().optional()
336
- }).passthrough()]);
477
+ }).loose()]);
337
478
  const BiomeRuleGroupSchema = z.object({
338
479
  recommended: z.boolean().optional(),
339
480
  all: z.boolean().optional(),
@@ -383,7 +524,7 @@ const BiomeFormatterConfigSchema = IncludeFieldsSchema.extend({
383
524
  "never"
384
525
  ]).optional(),
385
526
  trailingNewline: z.boolean().optional()
386
- }).passthrough();
527
+ }).loose();
387
528
  const BiomeJavaScriptFormatterSchema = z.object({
388
529
  enabled: z.boolean().optional(),
389
530
  quoteStyle: z.enum(["single", "double"]).optional(),
@@ -414,21 +555,21 @@ const BiomeJavaScriptFormatterSchema = z.object({
414
555
  lineWidth: z.number().int().optional(),
415
556
  operatorLinebreak: z.enum(["after", "before"]).optional(),
416
557
  trailingNewline: z.boolean().optional()
417
- }).passthrough();
558
+ }).loose();
418
559
  const BiomeJavaScriptConfigSchema = z.object({
419
560
  parser: z.object({
420
561
  unsafeParameterDecoratorsEnabled: z.boolean().optional(),
421
562
  jsxEverywhere: z.boolean().optional()
422
- }).passthrough().optional(),
563
+ }).loose().optional(),
423
564
  formatter: BiomeJavaScriptFormatterSchema.optional(),
424
- linter: z.object({ enabled: z.boolean().optional() }).passthrough().optional(),
565
+ linter: z.object({ enabled: z.boolean().optional() }).loose().optional(),
425
566
  globals: z.array(z.string()).optional()
426
- }).passthrough();
567
+ }).loose();
427
568
  const BiomeJsonConfigSchema = z.object({
428
569
  parser: z.object({
429
570
  allowComments: z.boolean().optional(),
430
571
  allowTrailingCommas: z.boolean().optional()
431
- }).passthrough().optional(),
572
+ }).loose().optional(),
432
573
  formatter: z.object({
433
574
  enabled: z.boolean().optional(),
434
575
  expand: z.enum([
@@ -447,11 +588,11 @@ const BiomeJsonConfigSchema = z.object({
447
588
  lineWidth: z.number().int().optional(),
448
589
  trailingCommas: z.enum(["none", "all"]).optional(),
449
590
  trailingNewline: z.boolean().optional()
450
- }).passthrough().optional(),
451
- linter: z.object({ enabled: z.boolean().optional() }).passthrough().optional()
452
- }).passthrough();
591
+ }).loose().optional(),
592
+ linter: z.object({ enabled: z.boolean().optional() }).loose().optional()
593
+ }).loose();
453
594
  const BiomeCssConfigSchema = z.object({
454
- parser: z.object({ cssModules: z.boolean().optional() }).passthrough().optional(),
595
+ parser: z.object({ cssModules: z.boolean().optional() }).loose().optional(),
455
596
  formatter: z.object({
456
597
  enabled: z.boolean().optional(),
457
598
  indentStyle: z.enum(["tab", "space"]).optional(),
@@ -465,9 +606,9 @@ const BiomeCssConfigSchema = z.object({
465
606
  lineWidth: z.number().int().optional(),
466
607
  quoteStyle: z.enum(["single", "double"]).optional(),
467
608
  trailingNewline: z.boolean().optional()
468
- }).passthrough().optional(),
469
- linter: z.object({ enabled: z.boolean().optional() }).passthrough().optional()
470
- }).passthrough();
609
+ }).loose().optional(),
610
+ linter: z.object({ enabled: z.boolean().optional() }).loose().optional()
611
+ }).loose();
471
612
  const BiomeGraphqlConfigSchema = z.object({
472
613
  formatter: z.object({
473
614
  enabled: z.boolean().optional(),
@@ -483,18 +624,18 @@ const BiomeGraphqlConfigSchema = z.object({
483
624
  lineWidth: z.number().int().optional(),
484
625
  quoteStyle: z.enum(["single", "double"]).optional(),
485
626
  trailingNewline: z.boolean().optional()
486
- }).passthrough().optional(),
487
- linter: z.object({ enabled: z.boolean().optional() }).passthrough().optional()
488
- }).passthrough();
627
+ }).loose().optional(),
628
+ linter: z.object({ enabled: z.boolean().optional() }).loose().optional()
629
+ }).loose();
489
630
  const BiomeLinterConfigSchema = IncludeFieldsSchema.extend({
490
631
  enabled: z.boolean().optional(),
491
632
  ignore: z.array(z.string()).optional(),
492
633
  rules: BiomeLinterRulesSchema.optional()
493
- }).passthrough();
634
+ }).loose();
494
635
  const BiomeAssistActionSchema = z.union([z.enum(["off", "on"]), z.object({
495
636
  level: z.enum(["off", "on"]),
496
637
  options: z.unknown().optional()
497
- }).passthrough()]);
638
+ }).loose()]);
498
639
  const BiomeAssistConfigSchema = z.object({
499
640
  enabled: z.boolean().optional(),
500
641
  includes: z.array(z.string()).optional(),
@@ -508,8 +649,8 @@ const BiomeAssistConfigSchema = z.object({
508
649
  "none"
509
650
  ]).optional()
510
651
  }).catchall(z.union([BiomeAssistActionSchema, z.boolean()])).optional()
511
- }).passthrough().optional()
512
- }).passthrough();
652
+ }).loose().optional()
653
+ }).loose();
513
654
  const BiomeOverrideSchema = z.object({
514
655
  include: z.array(z.string()).optional(),
515
656
  includes: z.array(z.string()).optional(),
@@ -521,7 +662,7 @@ const BiomeOverrideSchema = z.object({
521
662
  json: BiomeJsonConfigSchema.optional(),
522
663
  css: BiomeCssConfigSchema.optional(),
523
664
  graphql: BiomeGraphqlConfigSchema.optional()
524
- }).passthrough();
665
+ }).loose();
525
666
  const BiomeConfigSchema = z.object({
526
667
  $schema: z.string().optional(),
527
668
  extends: z.union([z.string(), z.array(z.string())]).optional(),
@@ -532,14 +673,14 @@ const BiomeConfigSchema = z.object({
532
673
  ignore: z.array(z.string()).optional(),
533
674
  ignoreUnknown: z.boolean().optional(),
534
675
  maxSize: z.number().int().optional()
535
- }).passthrough().optional(),
676
+ }).loose().optional(),
536
677
  vcs: z.object({
537
678
  enabled: z.boolean().optional(),
538
679
  clientKind: z.literal("git").optional(),
539
680
  useIgnoreFile: z.boolean().optional(),
540
681
  root: z.string().optional(),
541
682
  defaultBranch: z.string().optional()
542
- }).passthrough().optional(),
683
+ }).loose().optional(),
543
684
  linter: BiomeLinterConfigSchema.optional(),
544
685
  formatter: BiomeFormatterConfigSchema.optional(),
545
686
  assist: BiomeAssistConfigSchema.optional(),
@@ -549,8 +690,8 @@ const BiomeConfigSchema = z.object({
549
690
  graphql: BiomeGraphqlConfigSchema.optional(),
550
691
  html: z.record(z.string(), z.unknown()).optional(),
551
692
  overrides: z.array(BiomeOverrideSchema).optional()
552
- }).passthrough();
553
- const WorkspacePackageJsonSchema$1 = z.object({ workspaces: z.union([z.array(z.string()), z.record(z.string(), z.unknown())]).optional() }).passthrough();
693
+ }).loose();
694
+ const WorkspacePackageJsonSchema$1 = z.object({ workspaces: z.union([z.array(z.string()), z.record(z.string(), z.unknown())]).optional() }).loose();
554
695
  async function findBiomeConfig(startDir) {
555
696
  for (const name of BIOME_CONFIG_NAMES) {
556
697
  const configPath = resolve(startDir, name);
@@ -1024,6 +1165,7 @@ const GLOBAL_FORMATTER_KEYS = /* @__PURE__ */ new Set([
1024
1165
  "enabled",
1025
1166
  "include",
1026
1167
  "includes",
1168
+ "exclude",
1027
1169
  "ignore",
1028
1170
  "formatWithErrors",
1029
1171
  "indentStyle",
@@ -1227,12 +1369,11 @@ function mapOrganizeImportsOptions(value, reporter) {
1227
1369
  }
1228
1370
  function mapIgnorePatterns$1(biomeConfig, oxfmtConfig, additionalIgnorePatterns) {
1229
1371
  const ignorePatterns = [
1372
+ ...joinExclusions(biomeConfig.files?.exclude, biomeConfig.formatter?.exclude),
1230
1373
  ...oxfmtConfig.ignorePatterns ?? [],
1231
1374
  ...additionalIgnorePatterns,
1232
1375
  ...biomeConfig.files?.ignore ?? [],
1233
- ...biomeConfig.formatter?.ignore ?? [],
1234
- ...biomeConfig.files?.exclude ?? [],
1235
- ...biomeConfig.formatter?.exclude ?? []
1376
+ ...biomeConfig.formatter?.ignore ?? []
1236
1377
  ];
1237
1378
  if (ignorePatterns.length > 0) oxfmtConfig.ignorePatterns = [...new Set(ignorePatterns)];
1238
1379
  }
@@ -1306,7 +1447,7 @@ function validatePatternGroups(configName, groups) {
1306
1447
  function findPatternProblem(pattern) {
1307
1448
  const body = pattern.startsWith("!") ? pattern.slice(1) : pattern;
1308
1449
  if (body.split("/").includes("..")) return "escapes the config directory with \"..\", which Oxlint and Oxfmt reject";
1309
- if (body.startsWith("/") || /^[A-Za-z]:[/\\]/u.test(body)) return "is an absolute path, but patterns are resolved within the config directory";
1450
+ if (/^[A-Za-z]:[/\\]/u.test(body)) return "is an absolute path, but patterns are resolved within the config directory";
1310
1451
  }
1311
1452
  //#endregion
1312
1453
  //#region src/js-plugin-scaffolder.ts
@@ -1328,6 +1469,14 @@ const RESERVED_JS_PLUGIN_NAMES = /* @__PURE__ */ new Set([
1328
1469
  "eslint"
1329
1470
  ]);
1330
1471
  const UNSUPPORTED_BIOME_RULE_GUIDANCE = {
1472
+ noDeprecatedImports: {
1473
+ nativeAlternatives: ["typescript/no-deprecated"],
1474
+ notes: "Not a drop-in replacement: typescript/no-deprecated is type-aware and reports each use of a deprecated symbol, wherever it was declared, rather than the import statement, so an imported but unused deprecated symbol is not reported."
1475
+ },
1476
+ noForIn: {
1477
+ nativeAlternatives: ["guard-for-in", "typescript/no-for-in-array"],
1478
+ notes: "Neither forbids every for-in loop: guard-for-in reports the ones whose body is not wrapped in an own-property check, and the type-aware typescript/no-for-in-array reports only loops over arrays."
1479
+ },
1331
1480
  noReExportAll: {
1332
1481
  nativeAlternatives: ["import/no-cycle", "import/no-self-import"],
1333
1482
  jsPluginSpecifiers: ["eslint-plugin-import"],
@@ -1540,12 +1689,21 @@ const REACT_COMPILER_RULES = [
1540
1689
  * is reported once per migration so the narrowing is visible rather than silent.
1541
1690
  */
1542
1691
  const PARTIAL_RULE_MAPPING_NOTES = {
1692
+ noAssignInExpressions: "Biome rule noAssignInExpressions was mapped to no-cond-assign in `always` mode, which reports assignments inside conditions but not assignments nested in other expressions, such as `const a = (b = 1)` or a call argument.",
1543
1693
  noComponentHookFactories: "Biome rule noComponentHookFactories was mapped to react/no-unstable-nested-components, which reports nested component definitions but not nested custom hook definitions.",
1694
+ noBarrelFile: "Biome rule noBarrelFile was mapped to oxc/no-barrel-file, which only counts `export *` re-exports and reports a file once it re-exports more modules than its threshold (100 by default); Biome reports any file that re-exports, including through named `export { ... } from`.",
1695
+ noExcessiveCognitiveComplexity: "Biome rule noExcessiveCognitiveComplexity was mapped to complexity, which scores cyclomatic complexity rather than cognitive complexity; the threshold carries over, but the two scores differ, so a different set of functions is reported.",
1696
+ noMeaninglessVoidOperator: "Biome rule noMeaninglessVoidOperator was mapped to typescript/no-meaningless-void-operator, which reports `void` applied to a value whose type is `void` or `undefined` but not `void` applied to any other non-call operand, such as `void 1` or `void value`.",
1544
1697
  noInlineStyles: "Biome rule noInlineStyles was mapped to react/forbid-dom-props, which reports the `style` prop on JSX DOM elements but not `style` passed through React.createElement() or written in HTML, Vue, Svelte or Astro templates.",
1698
+ noSelfImport: "Biome rule noSelfImport was mapped to import/no-self-import, which reports static `import` and `export ... from` statements that resolve to their own file but not `require()` calls or dynamic `import()` expressions that do.",
1699
+ noReactObjectTypeAsDefaultProp: "Biome rule noReactObjectTypeAsDefaultProp was mapped to react/no-object-type-as-default-prop, which reports array, object, function, class, regex, `new` and JSX default prop values but not a `Symbol()` default.",
1545
1700
  noReactPropAssignments: "Biome rule noReactPropAssignments was mapped to the React Compiler rule react/immutability, which reports prop mutation alongside other mutations of values React treats as immutable.",
1546
1701
  noUnresolvedImports: "Biome rule noUnresolvedImports was mapped to import/named, which reports named imports a JavaScript module does not export but skips TypeScript files entirely and does not report import paths that cannot be resolved.",
1547
1702
  noUnsafeIframeSandbox: "Biome rule noUnsafeIframeSandbox was mapped to react/iframe-missing-sandbox, which reports `allow-scripts` combined with `allow-same-origin` only when the sandbox value is a plain string attribute, not a `{\"...\"}` or template-literal expression; it also reports iframes with no or an invalid sandbox attribute, which Biome leaves to useIframeSandbox.",
1703
+ useConsistentCurlyBraces: "Biome rule useConsistentCurlyBraces was mapped to react/jsx-curly-brace-presence, which reports unnecessary braces around string literals in JSX props and children but not a JSX element passed as a prop value without braces (`foo=<Bar />`).",
1704
+ useArrowFunction: "Biome rule useArrowFunction was mapped to prefer-arrow-callback, which reports function expressions passed as callbacks but not ones assigned to variables or properties, such as `const f = function () {}`.",
1548
1705
  useExplicitType: "Biome rule useExplicitType was mapped to typescript/explicit-function-return-type, which requires return types on functions and methods but not types on variables or parameters.",
1706
+ noUselessSwitchCase: "Biome rule noUselessSwitchCase was mapped to unicorn/no-useless-switch-case, which reports a case that falls through into a trailing `default` but not one next to a `default` placed earlier in the switch.",
1549
1707
  useStaticResponseMethods: "Biome rule useStaticResponseMethods was mapped to unicorn/prefer-response-static-json, which reports `new Response(JSON.stringify(...))` but not the `new Response(null, { status, headers: { Location } })` form Biome rewrites to Response.redirect()."
1550
1708
  };
1551
1709
  /**
@@ -1562,8 +1720,77 @@ const TYPE_AWARE_RULE_FALLBACKS = {
1562
1720
  useArrayFind: "unicorn/prefer-array-find",
1563
1721
  useFind: "unicorn/prefer-array-find",
1564
1722
  useIncludes: "unicorn/prefer-includes",
1565
- useStringStartsEndsWith: "unicorn/prefer-string-starts-ends-with"
1723
+ useStringStartsEndsWith: "unicorn/prefer-string-starts-ends-with",
1724
+ useThrowOnlyError: "no-throw-literal"
1566
1725
  };
1726
+ /**
1727
+ * Oxlint rules implemented by tsgolint, which only run when type-aware linting is on. A Biome
1728
+ * rule that maps onto one of them with no fallback above loses its diagnostics in a migration
1729
+ * that leaves type-aware linting off, so that is reported rather than emitted silently.
1730
+ * `rule-inventory.test.ts` keeps this list equal to `docs/tsgolint-rules.tsv`.
1731
+ */
1732
+ const TYPE_AWARE_OXLINT_RULES = /* @__PURE__ */ new Set([
1733
+ "typescript/await-thenable",
1734
+ "typescript/consistent-return",
1735
+ "typescript/consistent-type-exports",
1736
+ "typescript/dot-notation",
1737
+ "typescript/no-array-delete",
1738
+ "typescript/no-base-to-string",
1739
+ "typescript/no-confusing-void-expression",
1740
+ "typescript/no-deprecated",
1741
+ "typescript/no-duplicate-type-constituents",
1742
+ "typescript/no-floating-promises",
1743
+ "typescript/no-for-in-array",
1744
+ "typescript/no-generated-empty-object-type",
1745
+ "typescript/no-implied-eval",
1746
+ "typescript/no-meaningless-void-operator",
1747
+ "typescript/no-misused-promises",
1748
+ "typescript/no-misused-spread",
1749
+ "typescript/no-mixed-enums",
1750
+ "typescript/no-redundant-type-constituents",
1751
+ "typescript/no-unnecessary-boolean-literal-compare",
1752
+ "typescript/no-unnecessary-condition",
1753
+ "typescript/no-unnecessary-qualifier",
1754
+ "typescript/no-unnecessary-template-expression",
1755
+ "typescript/no-unnecessary-type-arguments",
1756
+ "typescript/no-unnecessary-type-assertion",
1757
+ "typescript/no-unnecessary-type-conversion",
1758
+ "typescript/no-unnecessary-type-parameters",
1759
+ "typescript/no-unsafe-argument",
1760
+ "typescript/no-unsafe-assignment",
1761
+ "typescript/no-unsafe-call",
1762
+ "typescript/no-unsafe-enum-comparison",
1763
+ "typescript/no-unsafe-member-access",
1764
+ "typescript/no-unsafe-return",
1765
+ "typescript/no-unsafe-type-assertion",
1766
+ "typescript/no-unsafe-unary-minus",
1767
+ "typescript/no-useless-default-assignment",
1768
+ "typescript/non-nullable-type-assertion-style",
1769
+ "typescript/only-throw-error",
1770
+ "typescript/prefer-find",
1771
+ "typescript/prefer-includes",
1772
+ "typescript/prefer-nullish-coalescing",
1773
+ "typescript/prefer-optional-chain",
1774
+ "typescript/prefer-promise-reject-errors",
1775
+ "typescript/prefer-readonly",
1776
+ "typescript/prefer-readonly-parameter-types",
1777
+ "typescript/prefer-reduce-type-parameter",
1778
+ "typescript/prefer-regexp-exec",
1779
+ "typescript/prefer-return-this-type",
1780
+ "typescript/prefer-string-starts-ends-with",
1781
+ "typescript/promise-function-async",
1782
+ "typescript/related-getter-setter-pairs",
1783
+ "typescript/require-array-sort-compare",
1784
+ "typescript/require-await",
1785
+ "typescript/restrict-plus-operands",
1786
+ "typescript/restrict-template-expressions",
1787
+ "typescript/return-await",
1788
+ "typescript/strict-boolean-expressions",
1789
+ "typescript/strict-void-return",
1790
+ "typescript/switch-exhaustiveness-check",
1791
+ "typescript/unbound-method",
1792
+ "typescript/use-unknown-in-catch-callback-variable"
1793
+ ]);
1567
1794
  /** Fallbacks that cover less than the type-aware rule they stand in for. */
1568
1795
  const TYPE_AWARE_FALLBACK_NOTES = { useStringStartsEndsWith: "Biome rule useStringStartsEndsWith was mapped to unicorn/prefer-string-starts-ends-with because type-aware linting is off; it reports anchored RegExp#test() prefix checks but not the indexOf(), slice() and charAt() comparisons Biome also reports. Enabling type-aware linting maps it to typescript/prefer-string-starts-ends-with instead, which covers all four." };
1569
1796
  const BIOME_TO_OXLINT_RULE_MAP = {
@@ -1628,7 +1855,11 @@ const BIOME_TO_OXLINT_RULE_MAP = {
1628
1855
  noDocumentImportInPage: "nextjs/no-document-import-in-page",
1629
1856
  noDoneCallback: "jest/no-done-callback",
1630
1857
  noDivRegex: "no-div-regex",
1631
- noEmptyBlockStatements: "no-empty",
1858
+ noEmptyBlockStatements: [
1859
+ "no-empty",
1860
+ "no-empty-function",
1861
+ "no-empty-static-block"
1862
+ ],
1632
1863
  noEmptyCharacterClassInRegex: "no-empty-character-class",
1633
1864
  noEmptyInterface: "typescript/no-empty-interface",
1634
1865
  noEmptyPattern: "no-empty-pattern",
@@ -1664,7 +1895,7 @@ const BIOME_TO_OXLINT_RULE_MAP = {
1664
1895
  noImplicitCoercions: "no-implicit-coercion",
1665
1896
  noInnerDeclarations: "no-inner-declarations",
1666
1897
  noInlineStyles: "react/forbid-dom-props",
1667
- noImplicitBoolean: "no-implicit-coercion",
1898
+ noImplicitBoolean: "react/jsx-boolean-value",
1668
1899
  noImpliedEval: "no-implied-eval",
1669
1900
  noIncrementDecrement: "no-plusplus",
1670
1901
  noInferrableTypes: "typescript/no-inferrable-types",
@@ -1682,6 +1913,7 @@ const BIOME_TO_OXLINT_RULE_MAP = {
1682
1913
  noLabelVar: "no-label-var",
1683
1914
  noLoopFunc: "no-loop-func",
1684
1915
  noMagicNumbers: "no-magic-numbers",
1916
+ noMeaninglessVoidOperator: "typescript/no-meaningless-void-operator",
1685
1917
  noMisleadingCharacterClass: "no-misleading-character-class",
1686
1918
  noMisleadingInstantiator: "typescript/no-misused-new",
1687
1919
  noMisrefactoredShorthandAssign: "oxc/misrefactored-assign-op",
@@ -1714,6 +1946,7 @@ const BIOME_TO_OXLINT_RULE_MAP = {
1714
1946
  noProto: "no-proto",
1715
1947
  noPrototypeBuiltins: "no-prototype-builtins",
1716
1948
  noRedeclare: "no-redeclare",
1949
+ noReactObjectTypeAsDefaultProp: "react/no-object-type-as-default-prop",
1717
1950
  noReactPropAssignments: "react/immutability",
1718
1951
  noRedundantAlt: "jsx-a11y/img-redundant-alt",
1719
1952
  noRedundantRoles: "jsx-a11y/no-redundant-roles",
@@ -1734,6 +1967,7 @@ const BIOME_TO_OXLINT_RULE_MAP = {
1734
1967
  noScriptUrl: "no-script-url",
1735
1968
  noSelfCompare: "no-self-compare",
1736
1969
  noSelfAssign: "no-self-assign",
1970
+ noSelfImport: "import/no-self-import",
1737
1971
  noSetterReturn: "no-setter-return",
1738
1972
  noShadow: "no-shadow",
1739
1973
  noShadowRestrictedNames: "no-shadow-restricted-names",
@@ -1754,7 +1988,6 @@ const BIOME_TO_OXLINT_RULE_MAP = {
1754
1988
  noUnusedExpressions: "no-unused-expressions",
1755
1989
  noUnusedFunctionParameters: "no-unused-vars",
1756
1990
  noUnusedImports: "no-unused-vars",
1757
- noUnnecessaryContinue: "no-continue",
1758
1991
  noUnmodifiedLoopCondition: "no-unmodified-loop-condition",
1759
1992
  noUnnecessaryConditions: "typescript/no-unnecessary-condition",
1760
1993
  noUnnecessaryTemplateExpression: "typescript/no-unnecessary-template-expression",
@@ -1774,7 +2007,6 @@ const BIOME_TO_OXLINT_RULE_MAP = {
1774
2007
  noUnusedVariables: "no-unused-vars",
1775
2008
  noUnwantedPolyfillio: "nextjs/no-unwanted-polyfillio",
1776
2009
  noUselessCatch: "no-useless-catch",
1777
- noUselessContinue: "no-continue",
1778
2010
  noUselessCatchBinding: "unicorn/prefer-optional-catch-binding",
1779
2011
  noUselessConstructor: "no-useless-constructor",
1780
2012
  noUselessElse: "no-else-return",
@@ -1830,7 +2062,7 @@ const BIOME_TO_OXLINT_RULE_MAP = {
1830
2062
  useConsistentArrayType: "typescript/array-type",
1831
2063
  useConsistentArrowReturn: "arrow-body-style",
1832
2064
  useConsistentBuiltinInstantiation: "no-new-wrappers",
1833
- useConsistentCurlyBraces: "curly",
2065
+ useConsistentCurlyBraces: "react/jsx-curly-brace-presence",
1834
2066
  useConsistentEnumValueType: "typescript/no-mixed-enums",
1835
2067
  useConsistentFunctionStyle: "func-style",
1836
2068
  useConsistentMethodSignatures: "typescript/method-signature-style",
@@ -1905,6 +2137,7 @@ const BIOME_TO_OXLINT_RULE_MAP = {
1905
2137
  useObjectSpread: "prefer-object-spread",
1906
2138
  useOptionalChain: "typescript/prefer-optional-chain",
1907
2139
  useParseIntRadix: "radix",
2140
+ usePromiseRejectErrors: "prefer-promise-reject-errors",
1908
2141
  useReadonlyClassProperties: "typescript/prefer-readonly",
1909
2142
  useReactFunctionComponentDefinition: "react/function-component-definition",
1910
2143
  useReactCompiler: REACT_COMPILER_RULES,
@@ -1918,10 +2151,10 @@ const BIOME_TO_OXLINT_RULE_MAP = {
1918
2151
  useShorthandAssign: "operator-assignment",
1919
2152
  useShorthandFunctionType: "typescript/prefer-function-type",
1920
2153
  useSingleVarDeclarator: "one-var",
1921
- useSimplifiedLogicExpression: "unicorn/prefer-logical-operator-over-ternary",
1922
2154
  useSpread: "prefer-spread",
1923
2155
  useSpreadOverApply: "prefer-spread",
1924
2156
  useStaticResponseMethods: "unicorn/prefer-response-static-json",
2157
+ useStrictBooleanExpressions: "typescript/strict-boolean-expressions",
1925
2158
  useStringStartsEndsWith: "typescript/prefer-string-starts-ends-with",
1926
2159
  useSymbolDescription: "symbol-description",
1927
2160
  useTemplate: "prefer-template",
@@ -1968,12 +2201,13 @@ function normalizeOxlintRuleName(ruleName) {
1968
2201
  return ruleName;
1969
2202
  }
1970
2203
  function mapBiomeRuleOptionsToOxlintSeverity(biomeName, severity, biomeSeverity, reporter) {
1971
- if (typeof severity !== "string") return severity;
2204
+ if (typeof severity !== "string" || severity === "off") return severity;
1972
2205
  if (biomeName === "useVueNextTickPromise") return [severity, "promise"];
1973
2206
  const options = getBiomeRuleOptions(biomeSeverity);
1974
2207
  if (biomeName === "noExcessiveNestedCallbacks") return [severity, { max: options && isNonNegativeInteger(options.max) ? options.max : 5 }];
1975
2208
  if (biomeName === "useErrorCause") return [severity, { requireCatchParameter: options && typeof options.requireCatchParameter === "boolean" ? options.requireCatchParameter : true }];
1976
2209
  if (biomeName === "useMaxParams") return [severity, { max: options && isNonNegativeInteger(options.max) ? options.max : 4 }];
2210
+ if (biomeName === "noExcessiveCognitiveComplexity") return [severity, { max: options && isNonNegativeInteger(options.maxAllowedComplexity) ? options.maxAllowedComplexity : 15 }];
1977
2211
  if (biomeName === "noExcessiveLinesPerFile") return [severity, {
1978
2212
  max: options && isNonNegativeInteger(options.maxLines) ? options.maxLines : 300,
1979
2213
  skipBlankLines: options?.skipBlankLines === true
@@ -1984,6 +2218,34 @@ function mapBiomeRuleOptionsToOxlintSeverity(biomeName, severity, biomeSeverity,
1984
2218
  IIFEs: options?.skipIifes !== true
1985
2219
  }];
1986
2220
  if (biomeName === "useSingleVarDeclarator") return [severity, "never"];
2221
+ if (biomeName === "noAssignInExpressions") return [severity, "always"];
2222
+ if (biomeName === "noConfusingLabels") {
2223
+ if (options?.allowedLabels !== void 0) reporter.loss("Biome rule noConfusingLabels option \"allowedLabels\" is not supported by Oxlint no-labels; those labels are reported again unless they label a loop.");
2224
+ return [severity, { allowLoop: true }];
2225
+ }
2226
+ if (biomeName === "noDoubleEquals") return options?.ignoreNull === false ? [severity, "always"] : [
2227
+ severity,
2228
+ "always",
2229
+ { null: "ignore" }
2230
+ ];
2231
+ if (biomeName === "noRestrictedGlobals") {
2232
+ const denied = options?.deniedGlobals;
2233
+ return [
2234
+ severity,
2235
+ "event",
2236
+ "error",
2237
+ ...Array.isArray(denied) ? denied.filter((name) => typeof name === "string") : isRecord(denied) ? Object.entries(denied).map(([name, message]) => typeof message === "string" ? {
2238
+ name,
2239
+ message
2240
+ } : name) : []
2241
+ ];
2242
+ }
2243
+ if (biomeName === "useConsistentMemberAccessibility") {
2244
+ if (options?.accessibility === "explicit") return [severity, { accessibility: "explicit" }];
2245
+ if (options?.accessibility === "none") reporter.loss("Biome rule useConsistentMemberAccessibility option \"accessibility\": \"none\" is not supported by Oxlint explicit-member-accessibility, which can forbid `public` but not `protected` or `private`; it was migrated as `no-public`.");
2246
+ return [severity, { accessibility: "no-public" }];
2247
+ }
2248
+ if (biomeName === "noImplicitBoolean") return [severity, "always"];
1987
2249
  if (biomeName === "noInlineStyles") return [severity, { forbid: ["style"] }];
1988
2250
  if (biomeName === "useImportExtensions") {
1989
2251
  if (options?.extensionMappings !== void 0 || options?.forceJsExtensions !== void 0) reporter.loss("Biome rule useImportExtensions options \"extensionMappings\" and \"forceJsExtensions\" are not supported by Oxlint import/extensions; the rule requires an extension without rewriting it.");
@@ -2146,30 +2408,37 @@ function mapBiomeRuleSeverity(severity, reporter, biomeRuleName) {
2146
2408
  warnUnsupportedSeverityOnce(rawSeverity, reporter, biomeRuleName);
2147
2409
  return "warn";
2148
2410
  }
2149
- function mapBiomeRuleToOxlintRules(biomeName, reporter, typeAware) {
2411
+ /**
2412
+ * Maps a Biome rule to its Oxlint rules. Coverage gaps are only reported for an `enabled`
2413
+ * rule: a rule the Biome config turns off produced no diagnostics, so nothing is lost when
2414
+ * the Oxlint side covers less of it, or none of it.
2415
+ */
2416
+ function mapBiomeRuleToOxlintRules(biomeName, reporter, typeAware, enabled = true) {
2150
2417
  const fallback = typeAware ? void 0 : TYPE_AWARE_RULE_FALLBACKS[biomeName];
2151
2418
  if (fallback) {
2152
2419
  const fallbackNote = TYPE_AWARE_FALLBACK_NOTES[biomeName];
2153
- if (fallbackNote) warnPartialMappingOnce(biomeName, fallbackNote, reporter);
2420
+ if (fallbackNote && enabled) warnPartialMappingOnce(biomeName, fallbackNote, reporter);
2154
2421
  return [normalizeOxlintRuleName(fallback)];
2155
2422
  }
2156
2423
  const mapped = BIOME_TO_OXLINT_RULE_MAP[biomeName];
2157
2424
  if (mapped) {
2158
2425
  const partialMappingNote = PARTIAL_RULE_MAPPING_NOTES[biomeName];
2159
- if (partialMappingNote) warnPartialMappingOnce(biomeName, partialMappingNote, reporter);
2160
- return [...new Set((Array.isArray(mapped) ? mapped : [mapped]).map((ruleName) => normalizeOxlintRuleName(ruleName)))];
2426
+ if (partialMappingNote && enabled) warnPartialMappingOnce(biomeName, partialMappingNote, reporter);
2427
+ const ruleNames = [...new Set((Array.isArray(mapped) ? mapped : [mapped]).map(normalizeOxlintRuleName))];
2428
+ if (!typeAware && enabled) for (const ruleName of ruleNames.filter((name) => TYPE_AWARE_OXLINT_RULES.has(name))) warnPartialMappingOnce(`${biomeName}::${ruleName}`, `Biome rule ${biomeName} was mapped to ${ruleName}, a type-aware Oxlint rule that only runs with type-aware linting, so its diagnostics are not reported. Re-run the migration with --type-aware to keep them.`, reporter);
2429
+ return ruleNames;
2161
2430
  }
2162
- warnUnmappedRuleOnce(biomeName, reporter);
2431
+ if (enabled) warnUnmappedRuleOnce(biomeName, reporter);
2163
2432
  return [];
2164
2433
  }
2165
- function warnPartialMappingOnce(biomeName, note, reporter) {
2434
+ function warnPartialMappingOnce(key, note, reporter) {
2166
2435
  let warnedRules = WARNED_PARTIAL_MAPPINGS_BY_REPORTER.get(reporter);
2167
2436
  if (!warnedRules) {
2168
2437
  warnedRules = /* @__PURE__ */ new Set();
2169
2438
  WARNED_PARTIAL_MAPPINGS_BY_REPORTER.set(reporter, warnedRules);
2170
2439
  }
2171
- if (warnedRules.has(biomeName)) return;
2172
- warnedRules.add(biomeName);
2440
+ if (warnedRules.has(key)) return;
2441
+ warnedRules.add(key);
2173
2442
  reporter.loss(note);
2174
2443
  }
2175
2444
  function warnUnmappedRuleOnce(biomeName, reporter) {
@@ -2215,6 +2484,7 @@ function extractRulesFromBiomeConfig(linterRules, reporter, options = {}) {
2215
2484
  sourceRulesSkipped
2216
2485
  };
2217
2486
  applyTopLevelPreset(linterRules, categories, reporter, applyImplicitRecommended);
2487
+ const sourcesByOxlintRule = /* @__PURE__ */ new Map();
2218
2488
  for (const [key, value] of Object.entries(linterRules)) {
2219
2489
  if (key === "recommended" || key === "all" || key === "preset") continue;
2220
2490
  if (typeof value === "boolean") continue;
@@ -2233,11 +2503,18 @@ function extractRulesFromBiomeConfig(linterRules, reporter, options = {}) {
2233
2503
  if (ruleName === "recommended" || ruleName === "all" || ruleName === "preset") continue;
2234
2504
  if (typeof ruleSeverity === "boolean" || ruleSeverity === void 0) continue;
2235
2505
  if (!isBiomeRuleSeverity(ruleSeverity)) continue;
2236
- const oxlintRuleNames = mapBiomeRuleToOxlintRules(ruleName, reporter, typeAware);
2506
+ const oxlintRuleNames = mapBiomeRuleToOxlintRules(ruleName, reporter, typeAware, (typeof ruleSeverity === "string" ? ruleSeverity : ruleSeverity.level) !== "off");
2237
2507
  if (oxlintRuleNames.length > 0) {
2238
2508
  sourceRulesConverted.add(ruleName);
2239
2509
  const oxlintSeverity = mapBiomeRuleOptionsToOxlintSeverity(ruleName, mapBiomeRuleSeverity(ruleSeverity, reporter, ruleName), ruleSeverity, reporter);
2240
- for (const oxlintRuleName of oxlintRuleNames) rules[oxlintRuleName] = oxlintSeverity;
2510
+ for (const oxlintRuleName of oxlintRuleNames) {
2511
+ const sources = sourcesByOxlintRule.get(oxlintRuleName) ?? [];
2512
+ sources.push({
2513
+ biomeRule: ruleName,
2514
+ severity: oxlintSeverity
2515
+ });
2516
+ sourcesByOxlintRule.set(oxlintRuleName, sources);
2517
+ }
2241
2518
  } else sourceRulesSkipped.add(ruleName);
2242
2519
  }
2243
2520
  const groupPreset = value.preset;
@@ -2245,6 +2522,7 @@ function extractRulesFromBiomeConfig(linterRules, reporter, options = {}) {
2245
2522
  if (oxlintCategory && groupEnabled !== void 0) categories[oxlintCategory] = groupEnabled ? "warn" : "off";
2246
2523
  }
2247
2524
  }
2525
+ for (const [oxlintRuleName, sources] of sourcesByOxlintRule) rules[oxlintRuleName] = resolveSharedRuleSeverity(oxlintRuleName, sources, reporter);
2248
2526
  return {
2249
2527
  rules,
2250
2528
  categories,
@@ -2252,6 +2530,48 @@ function extractRulesFromBiomeConfig(linterRules, reporter, options = {}) {
2252
2530
  sourceRulesSkipped
2253
2531
  };
2254
2532
  }
2533
+ const SEVERITY_RANK = {
2534
+ off: 0,
2535
+ warn: 1,
2536
+ error: 2
2537
+ };
2538
+ function severityLevel(severity) {
2539
+ return typeof severity === "string" ? severity : severity[0];
2540
+ }
2541
+ /**
2542
+ * Resolves one Oxlint rule that several configured Biome rules map onto.
2543
+ *
2544
+ * Writing each source in turn would let whichever Biome rule the config lists last decide,
2545
+ * so `noUnusedFunctionParameters: "off"` listed after `noUnusedVariables: "error"` would turn
2546
+ * off `no-unused-vars` and drop every unused-variable diagnostic. Instead the strongest
2547
+ * severity wins, because an enabled Biome rule's diagnostics are behaviour to keep, while a
2548
+ * disabled one only means that Biome rule stayed quiet. What the shared rule then reports
2549
+ * beyond the enabled sources is surfaced rather than hidden.
2550
+ */
2551
+ function resolveSharedRuleSeverity(oxlintRuleName, sources, reporter) {
2552
+ const [first] = sources;
2553
+ const enabled = sources.filter(({ severity }) => severityLevel(severity) !== "off");
2554
+ if (!first || sources.length === 1 || enabled.length === 0) return first?.severity ?? "off";
2555
+ const strongest = enabled.reduce((current, candidate) => SEVERITY_RANK[severityLevel(candidate.severity)] > SEVERITY_RANK[severityLevel(current.severity)] ? candidate : current);
2556
+ const optionSets = /* @__PURE__ */ new Map();
2557
+ for (const { severity } of enabled) if (typeof severity !== "string") optionSets.set(JSON.stringify(severity.slice(1)), severity.slice(1));
2558
+ if (optionSets.size > 1) reporter.loss(`Biome rules ${formatRuleList(enabled)} all map to Oxlint ${oxlintRuleName} with different options; Oxlint accepts one set, so the options of ${strongest.biomeRule} were kept and the others were not migrated.`);
2559
+ const level = severityLevel(strongest.severity);
2560
+ const options = typeof strongest.severity === "string" ? [...optionSets.values()][0] ?? [] : strongest.severity.slice(1);
2561
+ const disabled = sources.filter(({ severity }) => severityLevel(severity) === "off");
2562
+ if (oxlintRuleName === "no-unused-vars" && disabled.length === 1 && disabled[0]?.biomeRule === "noUnusedFunctionParameters") {
2563
+ const [currentOptions] = options;
2564
+ return [level, {
2565
+ ...isRecord(currentOptions) ? currentOptions : {},
2566
+ args: "none"
2567
+ }];
2568
+ }
2569
+ if (disabled.length > 0) reporter.warn(`Biome ${formatRuleList(disabled)} ${disabled.length === 1 ? "is" : "are"} off, but Oxlint ${oxlintRuleName} also implements ${formatRuleList(enabled)}, which ${enabled.length === 1 ? "is" : "are"} on; Oxlint will also report what ${disabled.length === 1 ? "that rule" : "those rules"} would have reported.`);
2570
+ return options.length > 0 ? [level, ...options] : level;
2571
+ }
2572
+ function formatRuleList(sources) {
2573
+ return sources.map(({ biomeRule }) => biomeRule).join(", ");
2574
+ }
2255
2575
  function applyTopLevelPreset(linterRules, categories, reporter, applyImplicitRecommended) {
2256
2576
  const preset = linterRules.preset ?? (linterRules.all === true ? "all" : linterRules.all === false || linterRules.recommended === false ? "none" : linterRules.recommended === true ? "recommended" : applyImplicitRecommended ? "recommended" : void 0);
2257
2577
  if (!preset) return;
@@ -2377,7 +2697,7 @@ function generateOxlintConfig(biomeConfig, reporter, options = {}) {
2377
2697
  if (options.enableImportGraph && !linterDisabled) addImportGraphRecipe(oxlintConfig, options.importCycleMaxDepth ?? 3);
2378
2698
  determinePlugins(oxlintConfig);
2379
2699
  mapIgnorePatterns(biomeConfig, oxlintConfig, options.biomeIgnorePatterns ?? []);
2380
- mapEnvironment(biomeConfig, oxlintConfig);
2700
+ mapEnvironment(biomeConfig, oxlintConfig, linterDisabled);
2381
2701
  mapTypeAwareOptions(oxlintConfig, options.typeAware ?? false, options.typeCheck ?? false);
2382
2702
  mapSettings(biomeConfig, oxlintConfig, options.typeAwareProfile ?? "standard");
2383
2703
  return {
@@ -2396,6 +2716,8 @@ function mapTypeAwareOptions(oxlintConfig, typeAware, typeCheck) {
2396
2716
  function warnAboutUnrepresentableIncludes(biomeConfig, reporter) {
2397
2717
  const positiveSelectors = [...biomeConfig.files?.include ?? [], ...biomeConfig.linter?.include ?? []];
2398
2718
  if (positiveSelectors.length > 0) reporter.loss(`Biome files/linter positive include selection (${positiveSelectors.join(", ")}) cannot be represented in an Oxlint config; Oxlint will lint every file not covered by ignorePatterns, which is a wider set. Pass equivalent paths to the Oxlint CLI before replacing Biome.`);
2719
+ const reincludes = joinExclusions(biomeConfig.files?.exclude, biomeConfig.linter?.exclude).filter((pattern) => pattern.startsWith("!") && hasDirectoryGlob(pattern)).map((pattern) => pattern.slice(1));
2720
+ if (reincludes.length > 0) reporter.warn(`Biome re-includes ${reincludes.join(", ")} after a negated pattern, which was migrated as "!" entries in the Oxlint ignorePatterns. Oxlint lifts the exclusion for every file such an entry matches, while Biome does not look inside a directory an earlier exception excluded, so Oxlint may lint files below one that Biome skipped.`);
2399
2721
  }
2400
2722
  function addImportGraphRecipe(oxlintConfig, maxDepth) {
2401
2723
  oxlintConfig.rules ??= {};
@@ -2441,15 +2763,25 @@ function sortPlugins(plugins) {
2441
2763
  }
2442
2764
  function mapIgnorePatterns(biomeConfig, oxlintConfig, additionalIgnorePatterns) {
2443
2765
  const ignorePatterns = [
2766
+ ...joinExclusions(biomeConfig.files?.exclude, biomeConfig.linter?.exclude),
2444
2767
  ...additionalIgnorePatterns,
2445
2768
  ...biomeConfig.files?.ignore ?? [],
2446
- ...biomeConfig.linter?.ignore ?? [],
2447
- ...biomeConfig.files?.exclude ?? [],
2448
- ...biomeConfig.linter?.exclude ?? []
2769
+ ...biomeConfig.linter?.ignore ?? []
2449
2770
  ];
2450
2771
  if (ignorePatterns.length > 0) oxlintConfig.ignorePatterns = [...new Set(ignorePatterns)];
2451
2772
  }
2452
- function mapEnvironment(biomeConfig, oxlintConfig) {
2773
+ /**
2774
+ * Biome resolves browser and Node.js globals without any configuration, while Oxlint only
2775
+ * knows ECMAScript builtins until an `env` names more. Without these two environments
2776
+ * `no-undef` reports `window`, `console` and `process`, and `no-global-assign` and
2777
+ * `no-implied-eval` stop recognising the globals they guard, so each would diverge from the
2778
+ * Biome rule it replaces. Both environments together match Biome's set of known globals.
2779
+ */
2780
+ function mapEnvironment(biomeConfig, oxlintConfig, linterDisabled) {
2781
+ if (!linterDisabled) oxlintConfig.env = {
2782
+ browser: true,
2783
+ node: true
2784
+ };
2453
2785
  if (biomeConfig.javascript?.globals) {
2454
2786
  const globals = {};
2455
2787
  for (const globalVar of biomeConfig.javascript.globals) globals[globalVar] = "readonly";
@@ -2494,7 +2826,7 @@ const PackageJsonSchema = z.object({
2494
2826
  const ToolVersionManifestSchema = z.object({
2495
2827
  dependencies: z.record(z.string(), z.string()).optional(),
2496
2828
  devDependencies: z.record(z.string(), z.string()).optional()
2497
- }).passthrough();
2829
+ }).loose();
2498
2830
  let recommendedToolVersionsPromise;
2499
2831
  async function updatePackageJson(projectDir, reporter, dryRun, options = {}) {
2500
2832
  const packageJsonPath = await findClosestPackageJson(projectDir) ?? resolve(projectDir, "package.json");
@@ -2505,6 +2837,7 @@ async function updatePackageJson(projectDir, reporter, dryRun, options = {}) {
2505
2837
  scriptsUpdated: [],
2506
2838
  dependenciesRemoved: [],
2507
2839
  devDependencies: [],
2840
+ scriptsStillUsingBiome: [],
2508
2841
  changed: false
2509
2842
  };
2510
2843
  try {
@@ -2533,16 +2866,16 @@ async function updatePackageJson(projectDir, reporter, dryRun, options = {}) {
2533
2866
  oxfmtCommand
2534
2867
  }) || modified;
2535
2868
  const scriptsStillUsingBiome = Object.entries(packageJson.scripts ?? {}).filter(([, script]) => containsBiomeExecutable(script)).map(([name]) => name);
2869
+ summary.scriptsStillUsingBiome = scriptsStillUsingBiome;
2536
2870
  if (!removeBiomeAllowed) reporter.info("Keeping @biomejs/biome: removing it requires --delete and a migration with no semantic losses.");
2537
- else if (scriptsStillUsingBiome.length > 0) reporter.warn(`Keeping @biomejs/biome because these package scripts still invoke Biome: ${scriptsStillUsingBiome.join(", ")}`);
2871
+ else if (scriptsStillUsingBiome.length > 0) reporter.warn(`Keeping @biomejs/biome and the Biome config because these package scripts still invoke Biome: ${scriptsStillUsingBiome.join(", ")}`);
2538
2872
  else {
2539
2873
  if (packageJson.devDependencies) modified = removeBiomeDependency(packageJson.devDependencies, "devDependencies", summary.dependenciesRemoved, reporter) || modified;
2540
2874
  if (packageJson.dependencies) modified = removeBiomeDependency(packageJson.dependencies, "dependencies", summary.dependenciesRemoved, reporter) || modified;
2541
2875
  }
2542
- packageJson.devDependencies ??= {};
2543
- modified = ensureDevDependency(packageJson.devDependencies, "oxlint", recommendedVersions.oxlint, summary.devDependencies) || modified;
2544
- modified = ensureDevDependency(packageJson.devDependencies, "oxfmt", recommendedVersions.oxfmt, summary.devDependencies) || modified;
2545
- if (needsTypeAwareDependency) modified = ensureDevDependency(packageJson.devDependencies, "oxlint-tsgolint", recommendedVersions["oxlint-tsgolint"], summary.devDependencies) || modified;
2876
+ modified = ensureDevDependency(packageJson, "oxlint", recommendedVersions.oxlint, summary.devDependencies) || modified;
2877
+ modified = ensureDevDependency(packageJson, "oxfmt", recommendedVersions.oxfmt, summary.devDependencies) || modified;
2878
+ if (needsTypeAwareDependency) modified = ensureDevDependency(packageJson, "oxlint-tsgolint", recommendedVersions["oxlint-tsgolint"], summary.devDependencies) || modified;
2546
2879
  summary.changed = modified;
2547
2880
  const dependenciesChanged = summary.dependenciesRemoved.length > 0 || summary.devDependencies.some((change) => change.action !== "already-present");
2548
2881
  summary.lockfile = await detectLockfile(dirname(packageJsonPath), dependenciesChanged);
@@ -2769,7 +3102,7 @@ function replaceBiomeCommand(script, command, checkReplacement, fixByStrategy, d
2769
3102
  const cleanedArgs = args.replace(/\s--(write|fix|unsafe)\b/gu, "").trim();
2770
3103
  const suffix = cleanedArgs.length > 0 ? ` ${cleanedArgs}` : "";
2771
3104
  const effectiveFixStrategy = hasUnsafe ? escalateFixStrategy(defaultFixStrategy, "dangerous") : defaultFixStrategy;
2772
- return `${leading}${applySuffixToCommands(hasFix || hasUnsafe ? fixByStrategy[effectiveFixStrategy] : checkReplacement, suffix)}`;
3105
+ return `${leading}${applySuffixToCommands(hasFix ? fixByStrategy[effectiveFixStrategy] : checkReplacement, suffix)}`;
2773
3106
  }),
2774
3107
  didReplace
2775
3108
  };
@@ -2815,9 +3148,18 @@ function removeBiomeDependency(dependencies, dependencyType, removals, reporter)
2815
3148
  }
2816
3149
  return modified;
2817
3150
  }
2818
- function ensureDevDependency(dependencies, name, version, changes) {
3151
+ /**
3152
+ * Makes sure an Oxc tool is installed at least at the version this migration targets.
3153
+ *
3154
+ * An existing entry is only raised when it is a plain semver range whose minimum is older
3155
+ * than the target. A newer range, a `catalog:`/`workspace:` protocol or a dist-tag is the
3156
+ * project's own choice, and an entry under `dependencies` already installs the tool, so
3157
+ * those are left alone rather than downgraded, broken, or duplicated.
3158
+ */
3159
+ function ensureDevDependency(packageJson, name, version, changes) {
3160
+ const dependencies = [packageJson.devDependencies, packageJson.dependencies].find((candidate) => candidate?.[name] !== void 0) ?? (packageJson.devDependencies ??= {});
2819
3161
  const existing = dependencies[name];
2820
- if (!existing) {
3162
+ if (existing === void 0) {
2821
3163
  dependencies[name] = version;
2822
3164
  changes.push({
2823
3165
  name,
@@ -2826,21 +3168,32 @@ function ensureDevDependency(dependencies, name, version, changes) {
2826
3168
  });
2827
3169
  return true;
2828
3170
  }
2829
- if (existing !== version) {
2830
- dependencies[name] = version;
3171
+ if (!isOlderSemverRange(existing, version)) {
2831
3172
  changes.push({
2832
3173
  name,
2833
- action: "updated",
2834
- from: existing,
2835
- to: version
3174
+ action: "already-present",
3175
+ to: existing
2836
3176
  });
2837
- return true;
3177
+ return false;
2838
3178
  }
3179
+ dependencies[name] = version;
2839
3180
  changes.push({
2840
3181
  name,
2841
- action: "already-present",
2842
- to: existing
3182
+ action: "updated",
3183
+ from: existing,
3184
+ to: version
2843
3185
  });
3186
+ return true;
3187
+ }
3188
+ const SEMVER_RANGE_PATTERN = /^(?:\^|~|>=)?(\d+)\.(\d+)\.(\d+)/u;
3189
+ function isOlderSemverRange(existing, target) {
3190
+ const existingVersion = SEMVER_RANGE_PATTERN.exec(existing.trim());
3191
+ const targetVersion = SEMVER_RANGE_PATTERN.exec(target.trim());
3192
+ if (!existingVersion || !targetVersion) return false;
3193
+ for (let index = 1; index <= 3; index++) {
3194
+ const difference = Number(existingVersion[index]) - Number(targetVersion[index]);
3195
+ if (difference !== 0) return difference < 0;
3196
+ }
2844
3197
  return false;
2845
3198
  }
2846
3199
  async function getRecommendedToolVersions() {
@@ -3075,7 +3428,7 @@ const LEGACY_BIOME_CONFIG_NAMES = [
3075
3428
  ".biome.jsonc"
3076
3429
  ];
3077
3430
  var MigrationStepFailedError = class extends Error {};
3078
- const WorkspacePackageJsonSchema = z.object({ workspaces: z.union([z.array(z.string()), z.record(z.string(), z.unknown())]).optional() }).passthrough();
3431
+ const WorkspacePackageJsonSchema = z.object({ workspaces: z.union([z.array(z.string()), z.record(z.string(), z.unknown())]).optional() }).loose();
3079
3432
  async function migrate(options = {}, reporter = new CollectingReporter()) {
3080
3433
  const cwd = process.cwd();
3081
3434
  const biomeConfigPath = options.configPath ? resolve(cwd, options.configPath) : await findBiomeConfig(cwd);
@@ -3248,7 +3601,7 @@ async function runMigration(options, reporter, biomeConfigPath) {
3248
3601
  await updateTurboConfig(projectDir, reporter, false, options.signal);
3249
3602
  throwIfMigrationErrorsIncreased(turboErrorCount, reporter);
3250
3603
  }
3251
- if (cleanupAllowed) {
3604
+ if (cleanupAllowed && !biomeStillInvoked(packageJsonSummary)) {
3252
3605
  deleteWasAttempted = true;
3253
3606
  deletedLegacyFiles = await cleanupLegacyBiomeFiles(projectDir, biomeConfigPath, false, reporter, options.signal);
3254
3607
  }
@@ -3277,7 +3630,7 @@ async function runMigration(options, reporter, biomeConfigPath) {
3277
3630
  await updateTurboConfig(projectDir, reporter, true, options.signal);
3278
3631
  throwIfMigrationErrorsIncreased(turboErrorCount, reporter);
3279
3632
  }
3280
- if (cleanupAllowed) {
3633
+ if (cleanupAllowed && !biomeStillInvoked(packageJsonSummary)) {
3281
3634
  deleteWasAttempted = true;
3282
3635
  deletedLegacyFiles = await cleanupLegacyBiomeFiles(projectDir, biomeConfigPath, true, reporter, options.signal);
3283
3636
  }
@@ -3300,6 +3653,7 @@ async function runMigration(options, reporter, biomeConfigPath) {
3300
3653
  deletedLegacyFiles,
3301
3654
  dryRun: options.dryRun ?? false,
3302
3655
  semanticLosses: reporter.getLosses(),
3656
+ scriptsStillUsingBiome: packageJsonSummary?.scriptsStillUsingBiome ?? [],
3303
3657
  suggestions
3304
3658
  });
3305
3659
  if (packageJsonSummary?.lockfile?.stale) suggestions.push(`Dependencies changed: run \`${packageJsonSummary.lockfile.installCommand}\` to refresh ${packageJsonSummary.lockfile.path} before any frozen-lockfile install.`);
@@ -3369,7 +3723,7 @@ async function buildSuggestions({ biomeIgnorePatterns, detectedIntegrations, opt
3369
3723
  suggestions.push(" pnpm add -D oxlint-tsgolint@latest");
3370
3724
  suggestions.push(` ${typeAwareCommand}`);
3371
3725
  suggestions.push("Type-aware mode uses the tsgolint backend.");
3372
- suggestions.push("Type-aware linting is stable as of tsgolint v7; it implements 60 of typescript-eslint’s 62 type-aware rules, and Oxlint does not yet expose no-generated-empty-object-type, so check those three if you relied on them.");
3726
+ suggestions.push("Type-aware linting is stable as of tsgolint v7; it implements 60 of typescript-eslint’s 62 type-aware rules, so check naming-convention and prefer-destructuring if you relied on them.");
3373
3727
  suggestions.push("TypeScript compatibility caveat: migrate deprecated tsconfig options if needed (typescript-go / TS7+ behavior).");
3374
3728
  suggestions.push(`Resolved type-aware profile: ${typeAwareProfile}`);
3375
3729
  }
@@ -3415,7 +3769,7 @@ function reconcileVcsIgnoreSettings(biomeConfig, reporter) {
3415
3769
  reporter.warn("Biome vcs is enabled without useIgnoreFile, so Biome ignored .gitignore. Oxfmt reads .gitignore by default and will therefore skip files Biome formatted; pass --ignore-path to override.");
3416
3770
  if (vcs.root !== void 0 || vcs.defaultBranch !== void 0) reporter.loss("Biome vcs.root/vcs.defaultBranch scoping (used by Biome’s changed-files filtering) has no Oxlint or Oxfmt equivalent and was not migrated.");
3417
3771
  }
3418
- function buildCleanupOutcome({ requested, cleanupAllowed, deleteWasAttempted, deletedLegacyFiles, dryRun, semanticLosses, suggestions }) {
3772
+ function buildCleanupOutcome({ requested, cleanupAllowed, deleteWasAttempted, deletedLegacyFiles, dryRun, semanticLosses, scriptsStillUsingBiome, suggestions }) {
3419
3773
  if (!requested) return {
3420
3774
  requested: false,
3421
3775
  performed: false,
@@ -3433,6 +3787,16 @@ function buildCleanupOutcome({ requested, cleanupAllowed, deleteWasAttempted, de
3433
3787
  files: []
3434
3788
  };
3435
3789
  }
3790
+ if (scriptsStillUsingBiome.length > 0) {
3791
+ const blockedReason = `package scripts still invoke Biome: ${scriptsStillUsingBiome.join(", ")}`;
3792
+ suggestions.push(`--delete was skipped because ${blockedReason}. Deleting the Biome config would leave them running with Biome's defaults; update them (or re-run with --update-scripts), then re-run with --delete.`);
3793
+ return {
3794
+ requested: true,
3795
+ performed: false,
3796
+ blockedReason,
3797
+ files: []
3798
+ };
3799
+ }
3436
3800
  if (!deleteWasAttempted) {
3437
3801
  const blockedReason = "the migration did not complete successfully";
3438
3802
  suggestions.push(`--delete skipped because ${blockedReason}.`);
@@ -3460,6 +3824,10 @@ function buildCleanupOutcome({ requested, cleanupAllowed, deleteWasAttempted, de
3460
3824
  files: deletedLegacyFiles
3461
3825
  };
3462
3826
  }
3827
+ /** Scripts that still run Biome need its config, so it cannot be deleted from under them. */
3828
+ function biomeStillInvoked(summary) {
3829
+ return (summary?.scriptsStillUsingBiome.length ?? 0) > 0;
3830
+ }
3463
3831
  async function detectWorkspaceMonorepo(projectDir) {
3464
3832
  const packageJsonPath = resolve(projectDir, "package.json");
3465
3833
  if (!await pathExists(packageJsonPath)) return false;
@@ -3546,10 +3914,7 @@ function countRebasablePatterns(oxlintConfig, oxfmtConfig) {
3546
3914
  oxfmtConfig.ignorePatterns,
3547
3915
  ...(oxlintConfig.overrides ?? []).flatMap((override) => [override.files, override.excludeFiles]),
3548
3916
  ...(oxfmtConfig.overrides ?? []).flatMap((override) => [override.files, override.excludeFiles])
3549
- ].reduce((total, patterns) => total + (patterns ?? []).filter((pattern) => !isAbsolute(stripNegation(pattern))).length, 0);
3550
- }
3551
- function stripNegation(pattern) {
3552
- return pattern.startsWith("!") ? pattern.slice(1) : pattern;
3917
+ ].reduce((total, patterns) => total + (patterns ?? []).length, 0);
3553
3918
  }
3554
3919
  function rebasePatterns(patterns, projectFromOutput) {
3555
3920
  if (!patterns || !projectFromOutput) return patterns;
@@ -3557,7 +3922,6 @@ function rebasePatterns(patterns, projectFromOutput) {
3557
3922
  const negated = pattern.startsWith("!");
3558
3923
  const prefix = negated ? "!" : "";
3559
3924
  const body = negated ? pattern.slice(1) : pattern;
3560
- if (isAbsolute(body)) return pattern;
3561
3925
  return `${prefix}${posix.normalize(`${projectFromOutput}/${body.replace(/^\//u, "")}`)}`;
3562
3926
  });
3563
3927
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "biome-to-oxc",
3
- "version": "3.6.0",
3
+ "version": "3.7.0",
4
4
  "description": "Migrate from Biome to Oxc ecosystem (oxlint + oxfmt + oxlint-tsgolint)",
5
5
  "keywords": [
6
6
  "biome",
@@ -58,20 +58,20 @@
58
58
  },
59
59
  "devDependencies": {
60
60
  "@arethetypeswrong/core": "^0.18.5",
61
- "@biomejs/biome": "^2.5.14",
62
- "@types/node": "^26.6.3",
63
- "@vitest/coverage-v8": "^5.0.2",
64
- "oxfmt": "^0.70.0",
65
- "oxlint": "^1.85.0",
61
+ "@biomejs/biome": "^2.5.15",
62
+ "@types/node": "^26.6.4",
63
+ "@vitest/coverage-v8": "^5.0.3",
64
+ "oxfmt": "^0.72.0",
65
+ "oxlint": "^1.87.0",
66
66
  "oxlint-tsgolint": "^7.0.2003",
67
- "publint": "^0.3.24",
68
- "tsdown": "rc",
67
+ "publint": "^0.3.25",
68
+ "tsdown": "0.23.0",
69
69
  "typescript": "next",
70
- "vitest": "^5.0.2"
70
+ "vitest": "^5.0.3"
71
71
  },
72
72
  "engines": {
73
- "node": ">=24.19.0",
74
- "pnpm": ">=11.25.0"
73
+ "node": ">=24.21.0",
74
+ "pnpm": ">=11.28.5"
75
75
  },
76
- "packageManager": "pnpm@11.25.0"
76
+ "packageManager": "pnpm@11.28.5"
77
77
  }