biome-to-oxc 3.0.1 → 3.6.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,6 +22,8 @@ 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
27
  - Transforms Biome formatter options to Oxfmt (Prettier-compatible)
26
28
  - Normalizes `include`/`includes` field variations automatically
27
29
 
@@ -37,7 +39,10 @@ Current capabilities:
37
39
  - Rule severities (`error`/`warn`/`off`; `info` and `on` are accepted with explicit lossy-mapping warnings)
38
40
  - Explicit `printWidth` handling (no silent changes)
39
41
  - The set of formatted files: Oxfmt-only formats (YAML, TOML, Markdown) are excluded, and
40
- `sortPackageJson` is pinned off, so migrating never reformats files Biome left alone
42
+ `sortPackageJson` is pinned off unless the Biome config turns the matching assist action on, so
43
+ migrating never reformats files Biome left alone
44
+ - Assist actions: `organizeImports` and `useSortedPackageJson` become the Oxfmt options that
45
+ implement them, and every other `assist.actions.source` entry is reported as a loss
41
46
 
42
47
  ✅ **Refuses to Lose Behaviour Silently**
43
48
 
@@ -301,6 +306,52 @@ Oxlint's broader `restriction` category.
301
306
  | bracketSpacing | bracketSpacing | Direct mapping |
302
307
  | bracketSameLine | bracketSameLine | Direct mapping |
303
308
 
309
+ ### Type-Aware Rule Fallbacks
310
+
311
+ Three Biome rules need no type information, but their only Oxlint counterpart is a type-aware
312
+ (tsgolint) rule. Type-aware linting is off unless the migration enables it, so mapping straight to
313
+ the tsgolint rule would drop diagnostics the Biome config did produce. When type-aware linting is
314
+ off, a non-type-aware Oxlint rule takes its place instead:
315
+
316
+ | Biome Rule | Type-aware mapping | Fallback when type-aware is off |
317
+ | ------------------------- | ------------------------------------------- | ---------------------------------------- |
318
+ | `useArrayFind` | `typescript/prefer-find` | `unicorn/prefer-array-find` |
319
+ | `useIncludes` | `typescript/prefer-includes` | `unicorn/prefer-includes` |
320
+ | `useStringStartsEndsWith` | `typescript/prefer-string-starts-ends-with` | `unicorn/prefer-string-starts-ends-with` |
321
+
322
+ 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
325
+ Biome flags, so that substitution is reported as a semantic loss.
326
+
327
+ 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.
330
+
331
+ ### Assist Actions
332
+
333
+ Biome's assist actions are code actions rather than lint rules. Oxfmt implements two of them as
334
+ formatter options; the rest have no Oxc equivalent and are reported as semantic losses.
335
+
336
+ | Biome Assist Action | Oxfmt Option | Notes |
337
+ | ---------------------- | ----------------- | ---------------------------------------------------------------- |
338
+ | `organizeImports` | `sortImports` | `options.sortBareImports` → `sortImports.sortSideEffects` |
339
+ | `useSortedPackageJson` | `sortPackageJson` | Lifts the default pin that keeps Oxfmt from sorting package.json |
340
+
341
+ Only actions the Biome config names explicitly are migrated. Biome enables `organizeImports`
342
+ through its recommended set even when the config never mentions it, but both Oxfmt options default
343
+ to off here, so deriving them from a preset would start rewriting imports in projects whose Biome
344
+ config never asked for it. When a preset is what turned the actions on, the migration reports it.
345
+
346
+ Biome's `organizeImports` `groups` are matcher predicates (`:NODE:`, source globs) while Oxfmt's
347
+ are a fixed set of group names, so custom group ordering is reported as a loss rather than guessed
348
+ at. Per-override `assist` is not migrated either, and is likewise reported.
349
+
350
+ Biome and Oxfmt sort imports and `package.json` keys into different orders, so the first Oxfmt run
351
+ after migrating either action reorders once; the migration warns about it. And because Biome sorts
352
+ imports by default through its recommended assist set, a config that never settles
353
+ `organizeImports` also gets a warning: Oxfmt leaves imports alone unless `sortImports` is set.
354
+
304
355
  ## After Migration
305
356
 
306
357
  1. **Review generated configs**
@@ -376,10 +427,14 @@ npx oxlint --type-aware --type-check
376
427
  - Oxfmt is in beta; review formatting changes before replacing the existing formatter
377
428
  - Overlapping `overrides` are migrated as-is. Biome and Oxfmt both merge matching overrides field
378
429
  by field with later entries winning, so the two agree; a conformance test pins this
379
- - Type-aware linting is stable as of `oxlint-tsgolint` v7 and requires TypeScript 7; 59 of the 61
380
- targeted typescript-eslint rules are currently implemented
381
- - CSS and JSON formatter overrides are mapped but may need manual review
430
+ - 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
433
+ - CSS, JSON and GraphQL formatter overrides are mapped but may need manual review
382
434
  - Prettier plugin support is not available in Oxfmt
435
+ - Assist actions are migrated only when the Biome config names them explicitly; actions enabled
436
+ through `recommended` or a preset are reported rather than derived
437
+ - `organizeImports` custom `groups` and `identifierOrder` have no Oxfmt counterpart and are reported
383
438
  - Some Biome rules still require JS plugin fallback until native rule parity is available
384
439
 
385
440
  ## Migration Report
@@ -7,7 +7,6 @@ interface CliRuntimeOptions {
7
7
  stderr?: CliWriteStream;
8
8
  stdout?: CliWriteStream;
9
9
  }
10
- declare function runCli(argv: string[], runtimeOptions?: CliRuntimeOptions): Promise<number>;
11
- declare function createTerminationHandler(signalName: NodeJS.Signals, controller: AbortController, stderr: CliWriteStream): () => void;
12
- //#endregion
13
- export { createTerminationHandler, runCli };
10
+ export declare function runCli(argv: string[], runtimeOptions?: CliRuntimeOptions): Promise<number>;
11
+ export declare function createTerminationHandler(signalName: NodeJS.Signals, controller: AbortController, stderr: CliWriteStream): () => void;
12
+ //#endregion
@@ -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-C66EI3xm.mjs";
2
+ import { i as readJsonFile, n as DefaultReporter, r as findClosestPackageJson, t as migrate } from "../src-BN3SXk7r.mjs";
3
3
  import { realpath } from "node:fs/promises";
4
4
  import { dirname } from "node:path";
5
5
  import { fileURLToPath } from "node:url";
@@ -1,5 +1,5 @@
1
1
  //#region src/types.d.ts
2
- interface BiomeConfig {
2
+ export interface BiomeConfig {
3
3
  $schema?: string;
4
4
  extends?: string | string[];
5
5
  root?: boolean;
@@ -7,13 +7,15 @@ interface BiomeConfig {
7
7
  vcs?: BiomeVcsConfig;
8
8
  linter?: BiomeLinterConfig;
9
9
  formatter?: BiomeFormatterConfig;
10
+ assist?: BiomeAssistConfig;
10
11
  javascript?: BiomeJavaScriptConfig;
11
12
  json?: BiomeJsonConfig;
12
13
  css?: BiomeCssConfig;
14
+ graphql?: BiomeGraphqlConfig;
13
15
  html?: Record<string, unknown>;
14
16
  overrides?: BiomeOverride[];
15
17
  }
16
- interface BiomeFilesConfig {
18
+ export interface BiomeFilesConfig {
17
19
  include?: string[];
18
20
  includes?: string[];
19
21
  /** Negated `includes` entries (`!pattern`), split out during normalization. */
@@ -22,14 +24,14 @@ interface BiomeFilesConfig {
22
24
  ignoreUnknown?: boolean;
23
25
  maxSize?: number;
24
26
  }
25
- interface BiomeVcsConfig {
27
+ export interface BiomeVcsConfig {
26
28
  enabled?: boolean;
27
29
  clientKind?: 'git';
28
30
  useIgnoreFile?: boolean;
29
31
  root?: string;
30
32
  defaultBranch?: string;
31
33
  }
32
- interface BiomeLinterConfig {
34
+ export interface BiomeLinterConfig {
33
35
  enabled?: boolean;
34
36
  include?: string[];
35
37
  includes?: string[];
@@ -38,24 +40,47 @@ interface BiomeLinterConfig {
38
40
  ignore?: string[];
39
41
  rules?: BiomeLinterRules;
40
42
  }
41
- interface BiomeLinterRules {
43
+ export interface BiomeLinterRules {
42
44
  recommended?: boolean;
43
45
  all?: boolean;
44
46
  preset?: 'recommended' | 'all' | 'none';
45
47
  [category: string]: boolean | BiomeRuleGroup | BiomeRuleSeverityValue | 'recommended' | 'all' | 'none' | undefined;
46
48
  }
47
- interface BiomeRuleGroup {
49
+ export interface BiomeRuleGroup {
48
50
  recommended?: boolean;
49
51
  all?: boolean;
50
52
  preset?: 'recommended' | 'all' | 'none';
51
53
  [rule: string]: BiomeRuleSeverity | boolean | 'recommended' | 'all' | 'none' | undefined;
52
54
  }
53
- type BiomeRuleSeverityValue = 'off' | 'on' | 'info' | 'warn' | 'error';
54
- type BiomeRuleSeverity = BiomeRuleSeverityValue | {
55
+ export type BiomeRuleSeverityValue = 'off' | 'on' | 'info' | 'warn' | 'error';
56
+ export type BiomeRuleSeverity = BiomeRuleSeverityValue | {
55
57
  level: BiomeRuleSeverityValue;
56
58
  options?: unknown;
57
59
  };
58
- interface BiomeFormatterConfig {
60
+ /**
61
+ * Biome's assist actions. They are code actions rather than lint rules, and the two Oxc
62
+ * surfaces split them: the sorting actions belong to Oxfmt, everything else has no home.
63
+ */
64
+ export interface BiomeAssistConfig {
65
+ enabled?: boolean;
66
+ includes?: string[];
67
+ actions?: BiomeAssistActions;
68
+ }
69
+ export interface BiomeAssistActions {
70
+ recommended?: boolean;
71
+ source?: BiomeAssistSource;
72
+ }
73
+ export interface BiomeAssistSource {
74
+ recommended?: boolean;
75
+ preset?: 'recommended' | 'all' | 'none';
76
+ [action: string]: BiomeAssistAction | boolean | 'recommended' | 'all' | 'none' | undefined;
77
+ }
78
+ export type BiomeAssistActionLevel = 'off' | 'on';
79
+ export type BiomeAssistAction = BiomeAssistActionLevel | {
80
+ level: BiomeAssistActionLevel;
81
+ options?: unknown;
82
+ };
83
+ export interface BiomeFormatterConfig {
59
84
  enabled?: boolean;
60
85
  include?: string[];
61
86
  includes?: string[];
@@ -74,7 +99,7 @@ interface BiomeFormatterConfig {
74
99
  trailingNewline?: boolean;
75
100
  [key: string]: unknown;
76
101
  }
77
- interface BiomeJavaScriptConfig {
102
+ export interface BiomeJavaScriptConfig {
78
103
  parser?: {
79
104
  unsafeParameterDecoratorsEnabled?: boolean;
80
105
  jsxEverywhere?: boolean;
@@ -104,7 +129,7 @@ interface BiomeJavaScriptConfig {
104
129
  globals?: string[];
105
130
  [key: string]: unknown;
106
131
  }
107
- interface BiomeJsonConfig {
132
+ export interface BiomeJsonConfig {
108
133
  parser?: {
109
134
  allowComments?: boolean;
110
135
  allowTrailingCommas?: boolean;
@@ -123,7 +148,7 @@ interface BiomeJsonConfig {
123
148
  enabled?: boolean;
124
149
  };
125
150
  }
126
- interface BiomeCssConfig {
151
+ export interface BiomeCssConfig {
127
152
  parser?: {
128
153
  cssModules?: boolean;
129
154
  };
@@ -140,7 +165,22 @@ interface BiomeCssConfig {
140
165
  enabled?: boolean;
141
166
  };
142
167
  }
143
- interface BiomeOverride {
168
+ export interface BiomeGraphqlConfig {
169
+ formatter?: {
170
+ enabled?: boolean;
171
+ bracketSpacing?: boolean;
172
+ indentStyle?: 'tab' | 'space';
173
+ indentWidth?: number;
174
+ lineEnding?: 'lf' | 'crlf' | 'cr' | 'auto';
175
+ lineWidth?: number;
176
+ quoteStyle?: 'single' | 'double';
177
+ trailingNewline?: boolean;
178
+ };
179
+ linter?: {
180
+ enabled?: boolean;
181
+ };
182
+ }
183
+ export interface BiomeOverride {
144
184
  include?: string[];
145
185
  includes?: string[];
146
186
  /** Negated `includes` entries (`!pattern`), split out during normalization. */
@@ -148,18 +188,20 @@ interface BiomeOverride {
148
188
  ignore?: string[];
149
189
  linter?: BiomeLinterConfig;
150
190
  formatter?: BiomeFormatterConfig;
191
+ assist?: BiomeAssistConfig;
151
192
  javascript?: BiomeJavaScriptConfig;
152
193
  json?: BiomeJsonConfig;
153
194
  css?: BiomeCssConfig;
195
+ graphql?: BiomeGraphqlConfig;
154
196
  }
155
- type FixStrategy = 'safe' | 'suggestions' | 'dangerous';
156
- type TypeAwareProfile = 'standard' | 'strict';
157
- type OxlintBuiltinPlugin = 'eslint' | 'react' | 'unicorn' | 'typescript' | 'oxc' | 'import' | 'jsdoc' | 'jest' | 'vitest' | 'jsx-a11y' | 'nextjs' | 'react-perf' | 'promise' | 'node' | 'vue';
158
- type OxlintJsPlugin = string | {
197
+ export type FixStrategy = 'safe' | 'suggestions' | 'dangerous';
198
+ export type TypeAwareProfile = 'standard' | 'strict';
199
+ export type OxlintBuiltinPlugin = 'eslint' | 'react' | 'unicorn' | 'typescript' | 'oxc' | 'import' | 'jsdoc' | 'jest' | 'vitest' | 'jsx-a11y' | 'nextjs' | 'react-perf' | 'promise' | 'node' | 'vue';
200
+ export type OxlintJsPlugin = string | {
159
201
  name: string;
160
202
  specifier: string;
161
203
  };
162
- interface OxlintSettings {
204
+ export interface OxlintSettings {
163
205
  jsdoc?: {
164
206
  augmentsExtendsReplacesDocs?: boolean;
165
207
  exemptDestructuredRootsFromChecks?: boolean;
@@ -195,7 +237,7 @@ interface OxlintSettings {
195
237
  };
196
238
  [key: string]: unknown;
197
239
  }
198
- interface OxlintConfig {
240
+ export interface OxlintConfig {
199
241
  $schema?: string;
200
242
  options?: {
201
243
  typeAware?: boolean;
@@ -211,7 +253,7 @@ interface OxlintConfig {
211
253
  ignorePatterns?: string[];
212
254
  settings?: OxlintSettings;
213
255
  }
214
- interface OxlintOverride {
256
+ export interface OxlintOverride {
215
257
  files: string[];
216
258
  excludeFiles?: string[];
217
259
  env?: Record<string, boolean>;
@@ -220,13 +262,13 @@ interface OxlintOverride {
220
262
  jsPlugins?: OxlintJsPlugin[];
221
263
  rules?: Record<string, OxlintRuleSeverity>;
222
264
  }
223
- type OxlintRuleSeverity = 'off' | 'warn' | 'error' | ['off' | 'warn' | 'error', ...unknown[]];
224
- interface OxfmtOverride {
265
+ export type OxlintRuleSeverity = 'off' | 'warn' | 'error' | ['off' | 'warn' | 'error', ...unknown[]];
266
+ export interface OxfmtOverride {
225
267
  files: string[];
226
268
  excludeFiles?: string[];
227
269
  options?: Partial<Omit<OxfmtConfig, '$schema' | 'ignorePatterns' | 'overrides'>>;
228
270
  }
229
- interface OxfmtConfig {
271
+ export interface OxfmtConfig {
230
272
  $schema?: string;
231
273
  printWidth?: number;
232
274
  tabWidth?: number;
@@ -309,7 +351,7 @@ interface OxfmtConfig {
309
351
  ignorePatterns?: string[];
310
352
  overrides?: OxfmtOverride[];
311
353
  }
312
- interface MigrationOptions {
354
+ export interface MigrationOptions {
313
355
  configPath?: string;
314
356
  outputDir?: string;
315
357
  dryRun?: boolean;
@@ -332,23 +374,23 @@ interface MigrationOptions {
332
374
  report?: string;
333
375
  signal?: AbortSignal;
334
376
  }
335
- interface PackageScriptUpdate {
377
+ export interface PackageScriptUpdate {
336
378
  name: string;
337
379
  before: string;
338
380
  after: string;
339
381
  }
340
- interface PackageDependencyRemoval {
382
+ export interface PackageDependencyRemoval {
341
383
  name: string;
342
384
  dependencyType: 'dependencies' | 'devDependencies';
343
385
  version?: string;
344
386
  }
345
- interface PackageDevDependencyChange {
387
+ export interface PackageDevDependencyChange {
346
388
  name: string;
347
389
  action: 'added' | 'already-present' | 'updated';
348
390
  from?: string;
349
391
  to: string;
350
392
  }
351
- interface PackageUpdateSummary {
393
+ export interface PackageUpdateSummary {
352
394
  packageJsonPath: string;
353
395
  found: boolean;
354
396
  dryRun: boolean;
@@ -359,14 +401,14 @@ interface PackageUpdateSummary {
359
401
  /** Lockfile that dependency changes invalidated, when one was detected. */
360
402
  lockfile?: LockfileStatus;
361
403
  }
362
- interface LockfileStatus {
404
+ export interface LockfileStatus {
363
405
  path: string;
364
406
  /** Command that regenerates the lockfile from the updated manifest. */
365
407
  installCommand: string;
366
408
  /** True when this run changed dependencies, so the lockfile no longer matches. */
367
409
  stale: boolean;
368
410
  }
369
- interface MigrationReport {
411
+ export interface MigrationReport {
370
412
  /**
371
413
  * True when the migration wrote every requested change without errors AND the
372
414
  * generated configuration is a complete replacement for the Biome setup.
@@ -404,7 +446,7 @@ interface MigrationReport {
404
446
  };
405
447
  cleanup?: CleanupOutcome;
406
448
  }
407
- interface CleanupOutcome {
449
+ export interface CleanupOutcome {
408
450
  /** Whether `--delete` was requested. */
409
451
  requested: boolean;
410
452
  /** Whether legacy Biome files were (or in dry-run, would be) removed. */
@@ -414,7 +456,7 @@ interface CleanupOutcome {
414
456
  /** Legacy Biome files removed, or that would be removed in dry-run. */
415
457
  files: string[];
416
458
  }
417
- interface Reporter {
459
+ export interface Reporter {
418
460
  warn(message: string): void;
419
461
  error(message: string): void;
420
462
  info(message: string): void;
@@ -434,6 +476,5 @@ interface Reporter {
434
476
  }
435
477
  //#endregion
436
478
  //#region src/index.d.ts
437
- declare function migrate(options?: MigrationOptions, reporter?: Reporter): Promise<MigrationReport>;
438
- //#endregion
439
- export { BiomeConfig, BiomeCssConfig, BiomeFilesConfig, BiomeFormatterConfig, BiomeJavaScriptConfig, BiomeJsonConfig, BiomeLinterConfig, BiomeLinterRules, BiomeOverride, BiomeRuleGroup, BiomeRuleSeverity, BiomeRuleSeverityValue, BiomeVcsConfig, CleanupOutcome, FixStrategy, LockfileStatus, MigrationOptions, MigrationReport, OxfmtConfig, OxfmtOverride, OxlintBuiltinPlugin, OxlintConfig, OxlintJsPlugin, OxlintOverride, OxlintRuleSeverity, OxlintSettings, PackageDependencyRemoval, PackageDevDependencyChange, PackageScriptUpdate, PackageUpdateSummary, Reporter, TypeAwareProfile, migrate };
479
+ export declare function migrate(options?: MigrationOptions, reporter?: Reporter): Promise<MigrationReport>;
480
+ //#endregion
@@ -1,2 +1,2 @@
1
- import { t as migrate } from "../src-C66EI3xm.mjs";
1
+ import { t as migrate } from "../src-BN3SXk7r.mjs";
2
2
  export { migrate };
@@ -147,8 +147,8 @@ function generateFeatureSpecificSuggestions(features) {
147
147
  if (features.hasGraphQL) {
148
148
  suggestions.push("");
149
149
  suggestions.push("GraphQL detected:");
150
- suggestions.push(" - Consider using graphql-prettier for GraphQL formatting");
151
- suggestions.push(" - Oxfmt may support GraphQL in future releases");
150
+ suggestions.push(" - Oxfmt formats .graphql and .gql files natively");
151
+ suggestions.push(" - Biome graphql.formatter settings are migrated to an Oxfmt override");
152
152
  }
153
153
  if (features.hasMonorepo) {
154
154
  suggestions.push("");
@@ -306,9 +306,11 @@ const SUPPORTED_TOP_LEVEL_FIELDS = /* @__PURE__ */ new Set([
306
306
  "vcs",
307
307
  "linter",
308
308
  "formatter",
309
+ "assist",
309
310
  "javascript",
310
311
  "json",
311
312
  "css",
313
+ "graphql",
312
314
  "html",
313
315
  "overrides"
314
316
  ]);
@@ -466,20 +468,59 @@ const BiomeCssConfigSchema = z.object({
466
468
  }).passthrough().optional(),
467
469
  linter: z.object({ enabled: z.boolean().optional() }).passthrough().optional()
468
470
  }).passthrough();
471
+ const BiomeGraphqlConfigSchema = z.object({
472
+ formatter: z.object({
473
+ enabled: z.boolean().optional(),
474
+ bracketSpacing: z.boolean().optional(),
475
+ indentStyle: z.enum(["tab", "space"]).optional(),
476
+ indentWidth: z.number().int().optional(),
477
+ lineEnding: z.enum([
478
+ "lf",
479
+ "crlf",
480
+ "cr",
481
+ "auto"
482
+ ]).optional(),
483
+ lineWidth: z.number().int().optional(),
484
+ quoteStyle: z.enum(["single", "double"]).optional(),
485
+ trailingNewline: z.boolean().optional()
486
+ }).passthrough().optional(),
487
+ linter: z.object({ enabled: z.boolean().optional() }).passthrough().optional()
488
+ }).passthrough();
469
489
  const BiomeLinterConfigSchema = IncludeFieldsSchema.extend({
470
490
  enabled: z.boolean().optional(),
471
491
  ignore: z.array(z.string()).optional(),
472
492
  rules: BiomeLinterRulesSchema.optional()
473
493
  }).passthrough();
494
+ const BiomeAssistActionSchema = z.union([z.enum(["off", "on"]), z.object({
495
+ level: z.enum(["off", "on"]),
496
+ options: z.unknown().optional()
497
+ }).passthrough()]);
498
+ const BiomeAssistConfigSchema = z.object({
499
+ enabled: z.boolean().optional(),
500
+ includes: z.array(z.string()).optional(),
501
+ actions: z.object({
502
+ recommended: z.boolean().optional(),
503
+ source: z.object({
504
+ recommended: z.boolean().optional(),
505
+ preset: z.enum([
506
+ "recommended",
507
+ "all",
508
+ "none"
509
+ ]).optional()
510
+ }).catchall(z.union([BiomeAssistActionSchema, z.boolean()])).optional()
511
+ }).passthrough().optional()
512
+ }).passthrough();
474
513
  const BiomeOverrideSchema = z.object({
475
514
  include: z.array(z.string()).optional(),
476
515
  includes: z.array(z.string()).optional(),
477
516
  ignore: z.array(z.string()).optional(),
478
517
  linter: BiomeLinterConfigSchema.optional(),
479
518
  formatter: BiomeFormatterConfigSchema.optional(),
519
+ assist: BiomeAssistConfigSchema.optional(),
480
520
  javascript: BiomeJavaScriptConfigSchema.optional(),
481
521
  json: BiomeJsonConfigSchema.optional(),
482
- css: BiomeCssConfigSchema.optional()
522
+ css: BiomeCssConfigSchema.optional(),
523
+ graphql: BiomeGraphqlConfigSchema.optional()
483
524
  }).passthrough();
484
525
  const BiomeConfigSchema = z.object({
485
526
  $schema: z.string().optional(),
@@ -501,9 +542,11 @@ const BiomeConfigSchema = z.object({
501
542
  }).passthrough().optional(),
502
543
  linter: BiomeLinterConfigSchema.optional(),
503
544
  formatter: BiomeFormatterConfigSchema.optional(),
545
+ assist: BiomeAssistConfigSchema.optional(),
504
546
  javascript: BiomeJavaScriptConfigSchema.optional(),
505
547
  json: BiomeJsonConfigSchema.optional(),
506
548
  css: BiomeCssConfigSchema.optional(),
549
+ graphql: BiomeGraphqlConfigSchema.optional(),
507
550
  html: z.record(z.string(), z.unknown()).optional(),
508
551
  overrides: z.array(BiomeOverrideSchema).optional()
509
552
  }).passthrough();
@@ -751,6 +794,7 @@ const CSS_EXTENSIONS = [
751
794
  "sass",
752
795
  "less"
753
796
  ];
797
+ const GRAPHQL_EXTENSIONS = ["graphql", "gql"];
754
798
  const JSON_FORMATTER_KEYS = /* @__PURE__ */ new Set([
755
799
  "enabled",
756
800
  "expand",
@@ -774,6 +818,18 @@ const CSS_FORMATTER_KEYS = /* @__PURE__ */ new Set([
774
818
  ...EXPLICIT_OXFMT_OPTION_KEYS$1,
775
819
  ...Object.keys(LEGACY_EXPLICIT_OXFMT_OPTION_ALIASES$1)
776
820
  ]);
821
+ const GRAPHQL_FORMATTER_KEYS = /* @__PURE__ */ new Set([
822
+ "enabled",
823
+ "bracketSpacing",
824
+ "indentStyle",
825
+ "indentWidth",
826
+ "lineEnding",
827
+ "lineWidth",
828
+ "quoteStyle",
829
+ "trailingNewline",
830
+ ...EXPLICIT_OXFMT_OPTION_KEYS$1,
831
+ ...Object.keys(LEGACY_EXPLICIT_OXFMT_OPTION_ALIASES$1)
832
+ ]);
777
833
  /**
778
834
  * Biome's `expand` and Oxfmt's `objectWrap` agree on two of three modes: Biome's `auto`
779
835
  * (expand when the literal already has a newline after `{`) is Oxfmt's `preserve`, and
@@ -797,10 +853,12 @@ function generateOxfmtOverrides(biomeOverrides, reporter) {
797
853
  if (!files || files.length === 0) continue;
798
854
  const excludePatterns = [.../* @__PURE__ */ new Set([...override.ignore ?? [], ...override.exclude ?? []])];
799
855
  const excludeFiles = excludePatterns.length > 0 ? excludePatterns : void 0;
856
+ if (override.assist) reporter.loss(`Biome override for ${files.join(", ")} configures assist actions; per-override assist is not migrated, so only the top-level assist configuration reached the Oxfmt config.`);
800
857
  pushOverride(oxfmtOverrides, files, excludeFiles, mapBaseFormatterOptions(override.formatter, reporter));
801
858
  pushScopedOverride(oxfmtOverrides, files, excludeFiles, mapJavaScriptFormatterOptions(override.javascript?.formatter, reporter), reporter, "javascript.formatter", JAVASCRIPT_EXTENSIONS);
802
859
  pushScopedOverride(oxfmtOverrides, files, excludeFiles, mapJsonFormatterOptions(override.json?.formatter, reporter), reporter, "json.formatter", JSON_EXTENSIONS);
803
860
  pushScopedOverride(oxfmtOverrides, files, excludeFiles, mapCssFormatterOptions(override.css?.formatter, reporter), reporter, "css.formatter", CSS_EXTENSIONS);
861
+ pushScopedOverride(oxfmtOverrides, files, excludeFiles, mapGraphqlFormatterOptions(override.graphql?.formatter, reporter), reporter, "graphql.formatter", GRAPHQL_EXTENSIONS);
804
862
  }
805
863
  return oxfmtOverrides;
806
864
  }
@@ -893,6 +951,19 @@ function mapCssFormatterOptions(cssFormatter, reporter) {
893
951
  applyExplicitFormatterOptionPassThrough$1([cssFormatter], options, reporter);
894
952
  return options;
895
953
  }
954
+ function mapGraphqlFormatterOptions(graphqlFormatter, reporter) {
955
+ const options = {};
956
+ if (!graphqlFormatter) return options;
957
+ warnAboutUnsupportedKeys(graphqlFormatter, GRAPHQL_FORMATTER_KEYS, "graphql.formatter", reporter);
958
+ if (graphqlFormatter.lineWidth !== void 0) options.printWidth = graphqlFormatter.lineWidth;
959
+ if (graphqlFormatter.indentStyle !== void 0) options.useTabs = graphqlFormatter.indentStyle === "tab";
960
+ if (graphqlFormatter.indentWidth !== void 0) options.tabWidth = graphqlFormatter.indentWidth;
961
+ if (graphqlFormatter.lineEnding !== void 0) options.endOfLine = graphqlFormatter.lineEnding;
962
+ if (graphqlFormatter.bracketSpacing !== void 0) options.bracketSpacing = graphqlFormatter.bracketSpacing;
963
+ if (graphqlFormatter.trailingNewline !== void 0) options.insertFinalNewline = graphqlFormatter.trailingNewline;
964
+ applyExplicitFormatterOptionPassThrough$1([graphqlFormatter], options, reporter);
965
+ return options;
966
+ }
896
967
  function warnAboutUnsupportedKeys(formatter, supportedKeys, label, reporter) {
897
968
  for (const [key, value] of Object.entries(formatter)) {
898
969
  if (value === void 0) continue;
@@ -996,6 +1067,13 @@ const OXFMT_ONLY_IGNORE_PATTERNS = [
996
1067
  "**/*.toml",
997
1068
  "**/*.{md,mdx}"
998
1069
  ];
1070
+ /**
1071
+ * The two Biome assist actions Oxfmt implements. Every other `assist.actions.source` entry
1072
+ * is a code action with no formatter equivalent, so enabling one is a migration loss.
1073
+ */
1074
+ const OXFMT_BACKED_ASSIST_ACTIONS = /* @__PURE__ */ new Set(["organizeImports", "useSortedPackageJson"]);
1075
+ /** Group-level switches that sit alongside the actions and are not actions themselves. */
1076
+ const ASSIST_GROUP_LEVEL_KEYS = /* @__PURE__ */ new Set(["recommended", "preset"]);
999
1077
  function generateOxfmtConfig(biomeConfig, reporter, options = {}) {
1000
1078
  const oxfmtConfig = { $schema: "./node_modules/oxfmt/configuration_schema.json" };
1001
1079
  const { formatter } = biomeConfig;
@@ -1003,7 +1081,7 @@ function generateOxfmtConfig(biomeConfig, reporter, options = {}) {
1003
1081
  warnAboutUnsupportedFormatterKeys(formatter, GLOBAL_FORMATTER_KEYS, "formatter", reporter);
1004
1082
  warnAboutUnsupportedFormatterKeys(jsFormatter, JAVASCRIPT_FORMATTER_KEYS, "javascript.formatter", reporter);
1005
1083
  if (formatter?.enabled === false) {
1006
- if (!(jsFormatter?.enabled === true || biomeConfig.json?.formatter?.enabled === true || biomeConfig.css?.formatter?.enabled === true)) oxfmtConfig.ignorePatterns = ["**/*"];
1084
+ if (!(jsFormatter?.enabled === true || biomeConfig.json?.formatter?.enabled === true || biomeConfig.css?.formatter?.enabled === true || biomeConfig.graphql?.formatter?.enabled === true)) oxfmtConfig.ignorePatterns = ["**/*"];
1007
1085
  else reporter.loss("Biome global formatting is disabled with a language formatter re-enabled; Oxfmt cannot represent that enable-only selection, so more files will be formatted than Biome formatted.");
1008
1086
  }
1009
1087
  const positiveSelectors = [...biomeConfig.files?.include ?? [], ...formatter?.include ?? []];
@@ -1011,6 +1089,7 @@ function generateOxfmtConfig(biomeConfig, reporter, options = {}) {
1011
1089
  mapFormatterOptions(formatter, oxfmtConfig, reporter);
1012
1090
  mapIgnorePatterns$1(biomeConfig, oxfmtConfig, options.biomeIgnorePatterns ?? []);
1013
1091
  mapDisabledLanguageFormatters(biomeConfig, oxfmtConfig);
1092
+ mapAssistActions(biomeConfig.assist, oxfmtConfig, reporter);
1014
1093
  applyExplicitFormatterOptionPassThrough([formatter], oxfmtConfig, reporter);
1015
1094
  oxfmtConfig.sortPackageJson ??= false;
1016
1095
  const languageOverrides = generateOxfmtOverrides([
@@ -1025,6 +1104,10 @@ function generateOxfmtConfig(biomeConfig, reporter, options = {}) {
1025
1104
  {
1026
1105
  include: ["**/*.{css,scss,sass,less}"],
1027
1106
  css: biomeConfig.css
1107
+ },
1108
+ {
1109
+ include: ["**/*.{graphql,gql}"],
1110
+ graphql: biomeConfig.graphql
1028
1111
  }
1029
1112
  ], reporter);
1030
1113
  if (languageOverrides.length > 0) oxfmtConfig.overrides = languageOverrides;
@@ -1047,6 +1130,7 @@ function mapDisabledLanguageFormatters(biomeConfig, oxfmtConfig) {
1047
1130
  if (biomeConfig.javascript?.formatter?.enabled === false) disabledPatterns.push("**/*.{js,jsx,ts,tsx,mjs,mts,cjs,cts}");
1048
1131
  if (biomeConfig.json?.formatter?.enabled === false) disabledPatterns.push("**/*.{json,jsonc,json5}");
1049
1132
  if (biomeConfig.css?.formatter?.enabled === false) disabledPatterns.push("**/*.{css,scss,sass,less}");
1133
+ if (biomeConfig.graphql?.formatter?.enabled === false) disabledPatterns.push("**/*.{graphql,gql}");
1050
1134
  if (disabledPatterns.length > 0) oxfmtConfig.ignorePatterns = [.../* @__PURE__ */ new Set([...oxfmtConfig.ignorePatterns ?? [], ...disabledPatterns])];
1051
1135
  }
1052
1136
  function warnAboutUnsupportedFormatterKeys(formatter, supportedKeys, label, reporter) {
@@ -1080,6 +1164,67 @@ function mapFormatterOptions(formatter, oxfmtConfig, reporter) {
1080
1164
  if (formatter?.trailingNewline !== void 0) oxfmtConfig.insertFinalNewline = formatter.trailingNewline;
1081
1165
  if (formatter?.formatWithErrors) reporter.warn("Biome's formatWithErrors option is not supported in Oxfmt");
1082
1166
  }
1167
+ /**
1168
+ * Maps Biome's assist actions onto the Oxfmt options that implement them.
1169
+ *
1170
+ * Only actions the config turns on explicitly are migrated. Biome enables `organizeImports`
1171
+ * through its recommended set even when the config never mentions it, but both Oxfmt options
1172
+ * default to off here, so deriving them from a preset would start rewriting imports in
1173
+ * projects whose Biome config never asked for it.
1174
+ */
1175
+ function mapAssistActions(assist, oxfmtConfig, reporter) {
1176
+ if (assist?.includes && assist.includes.length > 0) reporter.loss(`Biome assist.includes (${assist.includes.join(", ")}) cannot be represented in an Oxfmt config; Oxfmt applies its sorting options to every file it formats.`);
1177
+ if (assist?.enabled === false) return;
1178
+ const source = assist?.actions?.source;
1179
+ if (assist?.actions?.recommended || source?.recommended || source?.preset) reporter.loss("Biome assist actions were enabled through a preset rather than named individually; only the actions the config names explicitly were migrated to Oxfmt.");
1180
+ for (const [action, value] of Object.entries(source ?? {})) {
1181
+ if (ASSIST_GROUP_LEVEL_KEYS.has(action) || value === void 0) continue;
1182
+ if (readAssistActionLevel(value) !== "on") continue;
1183
+ if (!OXFMT_BACKED_ASSIST_ACTIONS.has(action)) {
1184
+ reporter.loss(`Biome assist action "${action}" has no Oxfmt equivalent and was not migrated; its code action is lost.`);
1185
+ continue;
1186
+ }
1187
+ if (action === "useSortedPackageJson") {
1188
+ oxfmtConfig.sortPackageJson = true;
1189
+ reporter.warn("Biome assist action useSortedPackageJson became Oxfmt's sortPackageJson; the two use different key orders, so the first Oxfmt run will reorder package.json once.");
1190
+ continue;
1191
+ }
1192
+ oxfmtConfig.sortImports = mapOrganizeImportsOptions(value, reporter);
1193
+ reporter.warn("Biome assist action organizeImports became Oxfmt's sortImports; the two group and order imports differently, so the first Oxfmt run will reorder imports once.");
1194
+ }
1195
+ warnAboutDefaultOrganizeImports(source, oxfmtConfig, reporter);
1196
+ }
1197
+ /**
1198
+ * Biome runs `organizeImports` through its recommended assist set even when the config never
1199
+ * mentions it, so `biome check` sorts imports in essentially every project. Oxfmt's
1200
+ * `sortImports` defaults to off and deriving it from that default would start rewriting
1201
+ * imports in projects that never asked for it, so the difference is reported instead.
1202
+ *
1203
+ * Reported as a warning rather than a loss because it applies to nearly every migration, the
1204
+ * way the linter's category-preset approximation does.
1205
+ */
1206
+ function warnAboutDefaultOrganizeImports(source, oxfmtConfig, reporter) {
1207
+ const organizeImports = source?.organizeImports;
1208
+ if (oxfmtConfig.sortImports !== void 0 || organizeImports !== void 0) return;
1209
+ reporter.warn("Biome sorts imports by default through its recommended assist set. Oxfmt does not sort imports unless asked, so the generated config leaves them alone; set \"sortImports\" in the Oxfmt config to keep sorting them.");
1210
+ }
1211
+ function readAssistActionLevel(value) {
1212
+ if (typeof value === "boolean") return value ? "on" : "off";
1213
+ if (typeof value === "string") return value === "on" ? "on" : "off";
1214
+ return value.level === "on" ? "on" : "off";
1215
+ }
1216
+ /**
1217
+ * Translates the one `organizeImports` option Oxfmt has a counterpart for. Biome's `groups`
1218
+ * are matcher predicates (`:NODE:`, source globs) while Oxfmt's are a fixed list of group
1219
+ * names, so a faithful translation is not available and the ordering is reported as lost.
1220
+ */
1221
+ function mapOrganizeImportsOptions(value, reporter) {
1222
+ const options = typeof value === "object" && value.options && typeof value.options === "object" ? value.options : void 0;
1223
+ if (!options) return {};
1224
+ if (options.groups) reporter.loss("Biome assist action organizeImports option \"groups\" was not migrated; Biome's groups are matcher predicates while Oxfmt's are a fixed set of group names, so Oxfmt will sort imports into its default groups.");
1225
+ if (options.identifierOrder !== void 0 && options.identifierOrder !== "natural") reporter.loss(`Biome assist action organizeImports option "identifierOrder": ${JSON.stringify(options.identifierOrder)} has no Oxfmt equivalent; Oxfmt sorts identifiers with its own order.`);
1226
+ return typeof options.sortBareImports === "boolean" ? { sortSideEffects: options.sortBareImports } : {};
1227
+ }
1083
1228
  function mapIgnorePatterns$1(biomeConfig, oxfmtConfig, additionalIgnorePatterns) {
1084
1229
  const ignorePatterns = [
1085
1230
  ...oxfmtConfig.ignorePatterns ?? [],
@@ -1192,8 +1337,7 @@ const UNSUPPORTED_BIOME_RULE_GUIDANCE = {
1192
1337
  noRedundantUseStrict: {
1193
1338
  nativeAlternatives: ["unicorn/prefer-module"],
1194
1339
  notes: "No native Oxlint equivalent for redundant `\"use strict\"` directives. For ESM, strict mode is already implicit."
1195
- },
1196
- useSingleVarDeclarator: { notes: "No native Oxlint equivalent for enforcing single declarators (ESLint `one-var`). Consider custom JS plugin policy if this is mandatory." }
1340
+ }
1197
1341
  };
1198
1342
  function collectUnsupportedBiomeRules(warnings) {
1199
1343
  const unsupported = /* @__PURE__ */ new Set();
@@ -1356,28 +1500,40 @@ const OXLINT_CATEGORIES = [
1356
1500
  "suspicious"
1357
1501
  ];
1358
1502
  /**
1359
- * The React Compiler rules Oxlint activates for ESLint's `react-hooks/recommended` preset.
1503
+ * Every React Compiler rule Oxlint implements.
1360
1504
  *
1361
- * Oxlint 1.79 replaced the single `react/react-compiler` rule with 22 per-diagnostic rules;
1362
- * Biome's `useReactCompiler` runs the compiler and reports every bailout it finds, so the
1363
- * `recommended` slice is the closest equivalent. The remaining rules default to off upstream
1364
- * and would fire on code Biome never flagged.
1505
+ * Oxlint 1.79 replaced the single `react/react-compiler` rule with 22 per-diagnostic rules,
1506
+ * spread across the `correctness`, `perf`, `restriction`, and `suspicious` categories. Biome's
1507
+ * `useReactCompiler` runs the compiler and reports every bailout it finds, so the whole set is
1508
+ * the equivalent: mapping only the rules Oxlint enables by default would drop diagnostics the
1509
+ * Biome rule did produce. Oxlint does not implement `gating` or `memoized-effect-dependencies`,
1510
+ * so there is nothing to enable for those two.
1365
1511
  *
1366
1512
  * @see https://oxc.rs/blog/2026-08-18-react-compiler-support
1367
1513
  */
1368
- const REACT_COMPILER_RECOMMENDED_RULES = [
1514
+ const REACT_COMPILER_RULES = [
1515
+ "react/capitalized-calls",
1369
1516
  "react/error-boundaries",
1517
+ "react/exhaustive-effect-dependencies",
1370
1518
  "react/globals",
1519
+ "react/hooks",
1371
1520
  "react/immutability",
1372
1521
  "react/incompatible-library",
1522
+ "react/invariant",
1523
+ "react/memo-dependencies",
1524
+ "react/no-deriving-state-in-effects",
1373
1525
  "react/preserve-manual-memoization",
1374
1526
  "react/purity",
1375
1527
  "react/refs",
1528
+ "react/rule-suppression",
1376
1529
  "react/set-state-in-effect",
1377
1530
  "react/set-state-in-render",
1378
1531
  "react/static-components",
1532
+ "react/syntax",
1533
+ "react/todo",
1379
1534
  "react/unsupported-syntax",
1380
- "react/use-memo"
1535
+ "react/use-memo",
1536
+ "react/void-use-memo"
1381
1537
  ];
1382
1538
  /**
1383
1539
  * Mappings where the Oxlint rule covers only part of what the Biome rule checked. The note
@@ -1385,10 +1541,31 @@ const REACT_COMPILER_RECOMMENDED_RULES = [
1385
1541
  */
1386
1542
  const PARTIAL_RULE_MAPPING_NOTES = {
1387
1543
  noComponentHookFactories: "Biome rule noComponentHookFactories was mapped to react/no-unstable-nested-components, which reports nested component definitions but not nested custom hook definitions.",
1544
+ 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.",
1388
1545
  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
+ 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
+ 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.",
1389
1548
  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.",
1390
- useReactCompiler: `Biome rule useReactCompiler was mapped to Oxlint's React Compiler rules for ESLint's recommended preset (${REACT_COMPILER_RECOMMENDED_RULES.join(", ")}); the compiler rules that are off by default upstream were not enabled. See https://oxc.rs/blog/2026-08-18-react-compiler-support`
1549
+ 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()."
1391
1550
  };
1551
+ /**
1552
+ * Biome rules whose only Oxlint counterpart is a type-aware rule, even though the Biome rule
1553
+ * itself needs no type information. Without type-aware linting the type-aware rule never runs,
1554
+ * so the migration would silently drop diagnostics the source config did produce. These
1555
+ * non-type-aware Oxlint rules take its place whenever the migration is not enabling type-aware
1556
+ * linting; they replace rather than accompany it, so switching the profile on later can never
1557
+ * report the same line twice.
1558
+ *
1559
+ * Every pair was checked by running both binaries over the same fixture.
1560
+ */
1561
+ const TYPE_AWARE_RULE_FALLBACKS = {
1562
+ useArrayFind: "unicorn/prefer-array-find",
1563
+ useFind: "unicorn/prefer-array-find",
1564
+ useIncludes: "unicorn/prefer-includes",
1565
+ useStringStartsEndsWith: "unicorn/prefer-string-starts-ends-with"
1566
+ };
1567
+ /** Fallbacks that cover less than the type-aware rule they stand in for. */
1568
+ 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." };
1392
1569
  const BIOME_TO_OXLINT_RULE_MAP = {
1393
1570
  noAccessKey: "jsx-a11y/no-access-key",
1394
1571
  noAdjacentSpacesInRegex: "no-regex-spaces",
@@ -1486,6 +1663,7 @@ const BIOME_TO_OXLINT_RULE_MAP = {
1486
1663
  noImportCycles: "import/no-cycle",
1487
1664
  noImplicitCoercions: "no-implicit-coercion",
1488
1665
  noInnerDeclarations: "no-inner-declarations",
1666
+ noInlineStyles: "react/forbid-dom-props",
1489
1667
  noImplicitBoolean: "no-implicit-coercion",
1490
1668
  noImpliedEval: "no-implied-eval",
1491
1669
  noIncrementDecrement: "no-plusplus",
@@ -1499,6 +1677,7 @@ const BIOME_TO_OXLINT_RULE_MAP = {
1499
1677
  noJsRestrictedProperties: "no-restricted-properties",
1500
1678
  noJsxLiterals: "react/jsx-no-literals",
1501
1679
  noJsxNamespace: "react/no-namespace",
1680
+ noJsxPropsBind: "react-perf/jsx-no-new-function-as-prop",
1502
1681
  noLabelWithoutControl: "jsx-a11y/label-has-associated-control",
1503
1682
  noLabelVar: "no-label-var",
1504
1683
  noLoopFunc: "no-loop-func",
@@ -1551,6 +1730,7 @@ const BIOME_TO_OXLINT_RULE_MAP = {
1551
1730
  noRestrictedImports: "no-restricted-imports",
1552
1731
  noRestrictedTypes: "typescript/no-restricted-types",
1553
1732
  noReturnAssign: "no-return-assign",
1733
+ noReturnInFinally: "promise/no-return-in-finally",
1554
1734
  noScriptUrl: "no-script-url",
1555
1735
  noSelfCompare: "no-self-compare",
1556
1736
  noSelfAssign: "no-self-assign",
@@ -1575,13 +1755,16 @@ const BIOME_TO_OXLINT_RULE_MAP = {
1575
1755
  noUnusedFunctionParameters: "no-unused-vars",
1576
1756
  noUnusedImports: "no-unused-vars",
1577
1757
  noUnnecessaryContinue: "no-continue",
1758
+ noUnmodifiedLoopCondition: "no-unmodified-loop-condition",
1578
1759
  noUnnecessaryConditions: "typescript/no-unnecessary-condition",
1579
1760
  noUnnecessaryTemplateExpression: "typescript/no-unnecessary-template-expression",
1580
1761
  noUnreachable: "no-unreachable",
1581
1762
  noUnreachableSuper: "constructor-super",
1763
+ noUnresolvedImports: "import/named",
1582
1764
  noUnsafeDeclarationMerging: "typescript/no-unsafe-declaration-merging",
1583
1765
  noUnsafeTypeAssertion: "typescript/no-unsafe-type-assertion",
1584
1766
  noUnsafeFinally: "no-unsafe-finally",
1767
+ noUnsafeIframeSandbox: "react/iframe-missing-sandbox",
1585
1768
  noUnsafeNegation: "no-unsafe-negation",
1586
1769
  noUnsafeOptionalChaining: "no-unsafe-optional-chaining",
1587
1770
  noUnsafePlusOperands: "typescript/restrict-plus-operands",
@@ -1649,12 +1832,14 @@ const BIOME_TO_OXLINT_RULE_MAP = {
1649
1832
  useConsistentBuiltinInstantiation: "no-new-wrappers",
1650
1833
  useConsistentCurlyBraces: "curly",
1651
1834
  useConsistentEnumValueType: "typescript/no-mixed-enums",
1835
+ useConsistentFunctionStyle: "func-style",
1652
1836
  useConsistentMethodSignatures: "typescript/method-signature-style",
1653
1837
  useConsistentTypeDefinitions: "typescript/consistent-type-definitions",
1654
1838
  useConsistentMemberAccessibility: "typescript/explicit-member-accessibility",
1655
1839
  useConsistentObjectDefinitions: "object-shorthand",
1656
1840
  useConsistentTestIt: ["jest/consistent-test-it", "vitest/consistent-test-it"],
1657
1841
  useConst: "prefer-const",
1842
+ useControlLabel: "jsx-a11y/control-has-associated-label",
1658
1843
  useDateNow: "unicorn/prefer-date-now",
1659
1844
  useDefaultParameterLast: "default-param-last",
1660
1845
  useDefaultSwitchClause: "default-case",
@@ -1708,6 +1893,7 @@ const BIOME_TO_OXLINT_RULE_MAP = {
1708
1893
  useMathMinMax: "unicorn/prefer-math-min-max",
1709
1894
  useMaxParams: "max-params",
1710
1895
  useMediaCaption: "jsx-a11y/media-has-caption",
1896
+ useModernMathApis: "unicorn/prefer-modern-math-apis",
1711
1897
  useNamedCaptureGroup: "prefer-named-capture-group",
1712
1898
  useNamespaceKeyword: "typescript/prefer-namespace-keyword",
1713
1899
  useNodejsImportProtocol: "unicorn/prefer-node-protocol",
@@ -1721,7 +1907,7 @@ const BIOME_TO_OXLINT_RULE_MAP = {
1721
1907
  useParseIntRadix: "radix",
1722
1908
  useReadonlyClassProperties: "typescript/prefer-readonly",
1723
1909
  useReactFunctionComponentDefinition: "react/function-component-definition",
1724
- useReactCompiler: REACT_COMPILER_RECOMMENDED_RULES,
1910
+ useReactCompiler: REACT_COMPILER_RULES,
1725
1911
  useReactFunctionComponents: "react/prefer-function-component",
1726
1912
  useReduceTypeParameter: "typescript/prefer-reduce-type-parameter",
1727
1913
  useRegexLiterals: "prefer-regex-literals",
@@ -1735,6 +1921,7 @@ const BIOME_TO_OXLINT_RULE_MAP = {
1735
1921
  useSimplifiedLogicExpression: "unicorn/prefer-logical-operator-over-ternary",
1736
1922
  useSpread: "prefer-spread",
1737
1923
  useSpreadOverApply: "prefer-spread",
1924
+ useStaticResponseMethods: "unicorn/prefer-response-static-json",
1738
1925
  useStringStartsEndsWith: "typescript/prefer-string-starts-ends-with",
1739
1926
  useSymbolDescription: "symbol-description",
1740
1927
  useTemplate: "prefer-template",
@@ -1753,8 +1940,10 @@ const BIOME_TO_OXLINT_RULE_MAP = {
1753
1940
  useValidAutocomplete: "jsx-a11y/autocomplete-valid",
1754
1941
  useValidForDirection: "for-direction",
1755
1942
  useValidLang: "jsx-a11y/lang",
1943
+ useValidTestTitle: ["jest/valid-title", "vitest/valid-title"],
1756
1944
  useValidTypeof: "valid-typeof",
1757
1945
  useVarsOnTop: "vars-on-top",
1946
+ useVueBaseImport: "vue/prefer-import-from-vue",
1758
1947
  useVueConsistentDefinePropsDeclaration: "vue/define-props-declaration",
1759
1948
  useVueNextTickPromise: "vue/next-tick-style",
1760
1949
  useYield: "require-yield"
@@ -1795,6 +1984,7 @@ function mapBiomeRuleOptionsToOxlintSeverity(biomeName, severity, biomeSeverity,
1795
1984
  IIFEs: options?.skipIifes !== true
1796
1985
  }];
1797
1986
  if (biomeName === "useSingleVarDeclarator") return [severity, "never"];
1987
+ if (biomeName === "noInlineStyles") return [severity, { forbid: ["style"] }];
1798
1988
  if (biomeName === "useImportExtensions") {
1799
1989
  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.");
1800
1990
  return [severity, "ignorePackages"];
@@ -1805,6 +1995,11 @@ function mapBiomeRuleOptionsToOxlintSeverity(biomeName, severity, biomeSeverity,
1805
1995
  "ts-nocheck": false,
1806
1996
  "ts-check": false
1807
1997
  }];
1998
+ if (biomeName === "useReactCompiler") {
1999
+ if (options?.compilationMode === "annotation") reporter.loss("Biome rule useReactCompiler option \"compilationMode\": \"annotation\" is not supported by Oxlint, which analyses every component and hook rather than only the functions carrying a \"use memo\" directive.");
2000
+ else if (options?.compilationMode === "all") reporter.loss("Biome rule useReactCompiler option \"compilationMode\": \"all\" is not supported by Oxlint, which analyses components and hooks only; React Compiler diagnostics in other functions are no longer reported.");
2001
+ return severity;
2002
+ }
1808
2003
  if (!options) return severity;
1809
2004
  if (biomeName === "noAmbiguousAnchorText") {
1810
2005
  const { words } = options;
@@ -1870,6 +2065,17 @@ function mapBiomeRuleOptionsToOxlintSeverity(biomeName, severity, biomeSeverity,
1870
2065
  if (Array.isArray(options.allowedNames) && options.allowedNames.every((value) => typeof value === "string")) oxlintOptions.allowedNames = options.allowedNames;
1871
2066
  return Object.keys(oxlintOptions).length > 0 ? [severity, oxlintOptions] : severity;
1872
2067
  }
2068
+ if (biomeName === "useConsistentFunctionStyle") {
2069
+ if (options.style !== "declaration") return severity;
2070
+ return [
2071
+ severity,
2072
+ "declaration",
2073
+ {
2074
+ allowArrowFunctions: options.allowArrowFunctions === true,
2075
+ allowTypeAnnotation: true
2076
+ }
2077
+ ];
2078
+ }
1873
2079
  if (biomeName === "useConsistentMethodSignatures") {
1874
2080
  const { style } = options;
1875
2081
  return style === "method" || style === "property" ? [severity, style] : severity;
@@ -1895,6 +2101,12 @@ function mapBiomeRuleOptionsToOxlintSeverity(biomeName, severity, biomeSeverity,
1895
2101
  const namedComponents = options.namedComponents === "functionDeclaration" ? "function-declaration" : options.namedComponents === "functionExpression" ? "function-expression" : options.namedComponents === "arrowFunction" ? "arrow-function" : null;
1896
2102
  return namedComponents ? [severity, { namedComponents }] : severity;
1897
2103
  }
2104
+ if (biomeName === "useValidTestTitle") {
2105
+ const { disallowedWords } = options;
2106
+ if (!Array.isArray(disallowedWords) || disallowedWords.length === 0 || !disallowedWords.every((value) => typeof value === "string")) return severity;
2107
+ reporter.loss("Biome rule useValidTestTitle option \"disallowedWords\" was migrated, but Oxlint valid-title stops reporting titles with leading or trailing whitespace once disallowedWords is set.");
2108
+ return [severity, { disallowedWords }];
2109
+ }
1898
2110
  if (biomeName === "useThisInClassMethods") {
1899
2111
  const oxlintOptions = {};
1900
2112
  if (options.ignoreClassesWithImplements === "all" || options.ignoreClassesWithImplements === "public-fields") oxlintOptions.ignoreClassesWithImplements = options.ignoreClassesWithImplements;
@@ -1934,7 +2146,13 @@ function mapBiomeRuleSeverity(severity, reporter, biomeRuleName) {
1934
2146
  warnUnsupportedSeverityOnce(rawSeverity, reporter, biomeRuleName);
1935
2147
  return "warn";
1936
2148
  }
1937
- function mapBiomeRuleToOxlintRules(biomeName, reporter) {
2149
+ function mapBiomeRuleToOxlintRules(biomeName, reporter, typeAware) {
2150
+ const fallback = typeAware ? void 0 : TYPE_AWARE_RULE_FALLBACKS[biomeName];
2151
+ if (fallback) {
2152
+ const fallbackNote = TYPE_AWARE_FALLBACK_NOTES[biomeName];
2153
+ if (fallbackNote) warnPartialMappingOnce(biomeName, fallbackNote, reporter);
2154
+ return [normalizeOxlintRuleName(fallback)];
2155
+ }
1938
2156
  const mapped = BIOME_TO_OXLINT_RULE_MAP[biomeName];
1939
2157
  if (mapped) {
1940
2158
  const partialMappingNote = PARTIAL_RULE_MAPPING_NOTES[biomeName];
@@ -1984,7 +2202,8 @@ function warnUnsupportedSeverityOnce(rawSeverity, reporter, biomeRuleName) {
1984
2202
  function mapBiomeCategoryToOxlint(biomeCategory) {
1985
2203
  return BIOME_TO_OXLINT_CATEGORY_MAP[biomeCategory] || null;
1986
2204
  }
1987
- function extractRulesFromBiomeConfig(linterRules, reporter, applyImplicitRecommended = false) {
2205
+ function extractRulesFromBiomeConfig(linterRules, reporter, options = {}) {
2206
+ const { applyImplicitRecommended = false, typeAware = false } = options;
1988
2207
  const rules = {};
1989
2208
  const categories = {};
1990
2209
  const sourceRulesConverted = /* @__PURE__ */ new Set();
@@ -2014,7 +2233,7 @@ function extractRulesFromBiomeConfig(linterRules, reporter, applyImplicitRecomme
2014
2233
  if (ruleName === "recommended" || ruleName === "all" || ruleName === "preset") continue;
2015
2234
  if (typeof ruleSeverity === "boolean" || ruleSeverity === void 0) continue;
2016
2235
  if (!isBiomeRuleSeverity(ruleSeverity)) continue;
2017
- const oxlintRuleNames = mapBiomeRuleToOxlintRules(ruleName, reporter);
2236
+ const oxlintRuleNames = mapBiomeRuleToOxlintRules(ruleName, reporter, typeAware);
2018
2237
  if (oxlintRuleNames.length > 0) {
2019
2238
  sourceRulesConverted.add(ruleName);
2020
2239
  const oxlintSeverity = mapBiomeRuleOptionsToOxlintSeverity(ruleName, mapBiomeRuleSeverity(ruleSeverity, reporter, ruleName), ruleSeverity, reporter);
@@ -2066,7 +2285,7 @@ function isBiomeRuleSeverity(value) {
2066
2285
  }
2067
2286
  //#endregion
2068
2287
  //#region src/overrides-transformer.ts
2069
- function transformOverridesToOxlint(biomeOverrides, reporter) {
2288
+ function transformOverridesToOxlint(biomeOverrides, reporter, options = {}) {
2070
2289
  const oxlintOverrides = [];
2071
2290
  const sourceRulesConverted = /* @__PURE__ */ new Set();
2072
2291
  const sourceRulesSkipped = /* @__PURE__ */ new Set();
@@ -2085,7 +2304,7 @@ function transformOverridesToOxlint(biomeOverrides, reporter) {
2085
2304
  const excludeFiles = [.../* @__PURE__ */ new Set([...override.ignore ?? [], ...override.exclude ?? []])];
2086
2305
  if (excludeFiles.length > 0) oxlintOverride.excludeFiles = excludeFiles;
2087
2306
  if (override.linter?.rules) {
2088
- const { rules, categories, ...ruleStats } = extractRulesFromBiomeConfig(override.linter.rules, reporter);
2307
+ const { rules, categories, ...ruleStats } = extractRulesFromBiomeConfig(override.linter.rules, reporter, { typeAware: options.typeAware ?? false });
2089
2308
  for (const ruleName of ruleStats.sourceRulesConverted) sourceRulesConverted.add(ruleName);
2090
2309
  for (const ruleName of ruleStats.sourceRulesSkipped) sourceRulesSkipped.add(ruleName);
2091
2310
  if (Object.keys(rules).length > 0) oxlintOverride.rules = rules;
@@ -2148,7 +2367,10 @@ function generateOxlintConfig(biomeConfig, reporter, options = {}) {
2148
2367
  categories: { ...DISABLED_CATEGORIES },
2149
2368
  sourceRulesConverted: /* @__PURE__ */ new Set(),
2150
2369
  sourceRulesSkipped: /* @__PURE__ */ new Set()
2151
- } : extractRulesFromBiomeConfig(biomeConfig.linter?.rules ?? {}, reporter, true);
2370
+ } : extractRulesFromBiomeConfig(biomeConfig.linter?.rules ?? {}, reporter, {
2371
+ applyImplicitRecommended: true,
2372
+ typeAware: options.typeAware ?? false
2373
+ });
2152
2374
  if (Object.keys(categories).length > 0) oxlintConfig.categories = categories;
2153
2375
  if (Object.keys(rules).length > 0) oxlintConfig.rules = rules;
2154
2376
  warnAboutUnrepresentableIncludes(biomeConfig, reporter);
@@ -2920,7 +3142,7 @@ async function runMigration(options, reporter, biomeConfigPath) {
2920
3142
  if (biomeConfig.overrides) {
2921
3143
  appendUniquePatterns(oxlintConfig, "ignorePatterns", collectDisabledOxlintOverridePatterns(biomeConfig.overrides));
2922
3144
  appendUniquePatterns(oxfmtConfig, "ignorePatterns", collectDisabledOxfmtOverridePatterns(biomeConfig.overrides));
2923
- const oxlintOverrideResult = transformOverridesToOxlint(biomeConfig.overrides, reporter);
3145
+ const oxlintOverrideResult = transformOverridesToOxlint(biomeConfig.overrides, reporter, { typeAware: typeAwareEnabled });
2924
3146
  if (oxlintOverrideResult.overrides.length > 0) oxlintConfig.overrides = oxlintOverrideResult.overrides;
2925
3147
  for (const ruleName of oxlintOverrideResult.sourceRulesConverted) convertedSourceRules.add(ruleName);
2926
3148
  for (const ruleName of oxlintOverrideResult.sourceRulesSkipped) skippedSourceRules.add(ruleName);
@@ -3147,7 +3369,7 @@ async function buildSuggestions({ biomeIgnorePatterns, detectedIntegrations, opt
3147
3369
  suggestions.push(" pnpm add -D oxlint-tsgolint@latest");
3148
3370
  suggestions.push(` ${typeAwareCommand}`);
3149
3371
  suggestions.push("Type-aware mode uses the tsgolint backend.");
3150
- suggestions.push("Type-aware linting is stable as of tsgolint v7; it covers 59 of typescript-eslint’s 61 type-aware rules, so check the remaining two if you relied on them.");
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.");
3151
3373
  suggestions.push("TypeScript compatibility caveat: migrate deprecated tsconfig options if needed (typescript-go / TS7+ behavior).");
3152
3374
  suggestions.push(`Resolved type-aware profile: ${typeAwareProfile}`);
3153
3375
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "biome-to-oxc",
3
- "version": "3.0.1",
3
+ "version": "3.6.0",
4
4
  "description": "Migrate from Biome to Oxc ecosystem (oxlint + oxfmt + oxlint-tsgolint)",
5
5
  "keywords": [
6
6
  "biome",
@@ -54,24 +54,24 @@
54
54
  "commander": "15.0.0",
55
55
  "jsonc-parser": "next",
56
56
  "picocolors": "^1.1.1",
57
- "zod": "^4.4.3"
57
+ "zod": "^4.6.5"
58
58
  },
59
59
  "devDependencies": {
60
60
  "@arethetypeswrong/core": "^0.18.5",
61
- "@biomejs/biome": "^2.5.9",
62
- "@types/node": "^26.2.0",
63
- "@vitest/coverage-v8": "^4.1.11",
64
- "oxfmt": "^0.64.0",
65
- "oxlint": "^1.79.0",
66
- "oxlint-tsgolint": "^7.0.2001",
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",
66
+ "oxlint-tsgolint": "^7.0.2003",
67
67
  "publint": "^0.3.24",
68
- "tsdown": "^0.22.14",
68
+ "tsdown": "rc",
69
69
  "typescript": "next",
70
- "vitest": "^4.1.11"
70
+ "vitest": "^5.0.2"
71
71
  },
72
72
  "engines": {
73
73
  "node": ">=24.19.0",
74
- "pnpm": ">=11.22.0"
74
+ "pnpm": ">=11.25.0"
75
75
  },
76
- "packageManager": "pnpm@11.22.0"
76
+ "packageManager": "pnpm@11.25.0"
77
77
  }