eslint-plugin-oxfmt 0.4.0 → 0.5.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
@@ -55,6 +55,22 @@ export default [
55
55
  ]
56
56
  ```
57
57
 
58
+ `recommended` includes `eslint-parser-plain` as `languageOptions.parser` for convenience.
59
+
60
+ If you need parser-agnostic composition (for example to keep `jsonc-eslint-parser` from another config), use the dedicated preset:
61
+
62
+ ```js
63
+ // eslint.config.mjs
64
+ import pluginOxfmt from 'eslint-plugin-oxfmt'
65
+
66
+ export default [
67
+ {
68
+ ...pluginOxfmt.configs.recommendedWithoutParser,
69
+ files: ['**/*.{js,ts,mjs,cjs,jsx,tsx,json,jsonc}'],
70
+ },
71
+ ]
72
+ ```
73
+
58
74
  ### Custom Configuration
59
75
 
60
76
  You can customize the formatting options by configuring the rule:
@@ -156,7 +172,9 @@ When `useConfig` is `true`, the plugin loads config using `load-oxfmt-config`.
156
172
  - Config discovery order (from `cwd`, walking upward): `.oxfmtrc.json` → `.oxfmtrc.jsonc` → `oxfmt.config.ts`
157
173
  - `.editorconfig` support: nearest `.editorconfig` (including section overrides) is merged into the final options
158
174
  - `configPath` overrides discovery and directly targets the specified config file
159
- - ESLint rule options generally take highest priority because inline rule options are merged after loaded config. However, when `useConfig` is `true`, `overrides` are taken from the loaded config (not from rule options).
175
+ - ESLint rule options generally take highest priority because inline rule options are merged after loaded config.
176
+ - When `useConfig` is `true`, rule-level `overrides` are ignored. Only `overrides` loaded from the resolved oxfmt config file are applied.
177
+ - Rule-level `ignorePatterns` still override config-derived `ignorePatterns` when provided.
160
178
 
161
179
  For detailed behavior, see:
162
180
 
@@ -365,6 +383,13 @@ This plugin provides a single rule that formats your code using oxfmt.
365
383
 
366
384
  ## Integration
367
385
 
386
+ ### Parser Compatibility
387
+
388
+ - `recommended`: forces `eslint-parser-plain` for matched files
389
+ - `recommendedWithoutParser`: parser-agnostic (safe to compose with language-specific parsers)
390
+
391
+ When composing shareable configs, prefer `recommendedWithoutParser` if parser ownership belongs to another preset.
392
+
368
393
  ### Format on Save in VS Code
369
394
 
370
395
  Add this to your `.vscode/settings.json`:
package/dist/index.d.mts CHANGED
@@ -5,6 +5,7 @@ import { Linter, Rule } from "eslint";
5
5
  interface PluginOxfmt {
6
6
  configs: {
7
7
  recommended: Linter.Config<Linter.RulesRecord>;
8
+ recommendedWithoutParser: Linter.Config<Linter.RulesRecord>;
8
9
  };
9
10
  meta: {
10
11
  name: string;
@@ -30,10 +31,11 @@ declare const rules: {
30
31
  declare const parserPlain: Linter.Parser;
31
32
  //#endregion
32
33
  //#region src/configs.d.ts
34
+ declare const recommendedWithoutParser: Linter.Config<Linter.RulesRecord>;
33
35
  declare const recommended: Linter.Config<Linter.RulesRecord>;
34
36
  declare const configs: PluginOxfmt['configs'];
35
37
  //#endregion
36
38
  //#region src/index.d.ts
37
39
  declare const plugin: PluginOxfmt;
38
40
  //#endregion
39
- export { configs, plugin as default, plugin, meta, parserPlain, recommended, rules };
41
+ export { configs, plugin as default, plugin, meta, parserPlain, recommended, recommendedWithoutParser, rules };
package/dist/index.mjs CHANGED
@@ -46,20 +46,29 @@ const meta$1 = {
46
46
  const parserPlain = dist_exports;
47
47
  //#endregion
48
48
  //#region src/configs.ts
49
- const recommended = {
50
- name: "oxfmt/recommended",
51
- languageOptions: { parser: parserPlain },
52
- plugins: { get oxfmt() {
49
+ const recommendedWithoutParser = {
50
+ name: "oxfmt/recommended-without-parser",
51
+ plugins: {
52
+ /* v8 ignore start */
53
+ get oxfmt() {
53
54
  return plugin;
54
55
  } },
55
56
  rules: { "oxfmt/oxfmt": "error" }
56
57
  };
57
- const configs = { recommended };
58
+ const recommended = {
59
+ ...recommendedWithoutParser,
60
+ name: "oxfmt/recommended",
61
+ languageOptions: { parser: parserPlain }
62
+ };
63
+ const configs = {
64
+ recommended,
65
+ recommendedWithoutParser
66
+ };
58
67
  //#endregion
59
68
  //#region src/meta.ts
60
69
  const meta = {
61
70
  name: "eslint-plugin-oxfmt",
62
- version: "0.4.0"
71
+ version: "0.5.0"
63
72
  };
64
73
  //#endregion
65
74
  //#region src/dir.ts
@@ -550,9 +559,10 @@ const rules = { oxfmt: {
550
559
  });
551
560
  }
552
561
  else reportDifferences(context, sourceText, formatResult.code);
553
- } catch {
562
+ } catch (err) {
563
+ const details = err instanceof Error ? `: ${err.message}` : "";
554
564
  context.report({
555
- message: `Failed to format file: ${context.filename}`,
565
+ message: `Failed to format file: ${context.filename}${details}`,
556
566
  loc: {
557
567
  end: {
558
568
  column: 0,
@@ -576,4 +586,4 @@ const plugin = {
576
586
  rules
577
587
  };
578
588
  //#endregion
579
- export { configs, plugin as default, plugin, meta, parserPlain, recommended, rules };
589
+ export { configs, plugin as default, plugin, meta, parserPlain, recommended, recommendedWithoutParser, rules };
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "eslint-plugin-oxfmt",
3
3
  "type": "module",
4
- "version": "0.4.0",
4
+ "version": "0.5.0",
5
5
  "description": "An ESLint plugin for formatting code with oxfmt.",
6
6
  "keywords": [
7
7
  "eslint",
@@ -48,31 +48,32 @@
48
48
  },
49
49
  "dependencies": {
50
50
  "generate-differences": "^0.1.1",
51
- "load-oxfmt-config": "^0.4.0",
51
+ "load-oxfmt-config": "^0.4.1",
52
52
  "picomatch": "^4.0.4",
53
53
  "synckit": "^0.11.12"
54
54
  },
55
55
  "devDependencies": {
56
- "@ntnyq/eslint-config": "^6.0.1",
56
+ "@ntnyq/eslint-config": "^6.1.0",
57
57
  "@types/json-schema": "^7.0.15",
58
58
  "@types/node": "^25.6.0",
59
- "@typescript/native-preview": "^7.0.0-dev.20260413.1",
59
+ "@typescript/native-preview": "^7.0.0-dev.20260429.1",
60
60
  "bumpp": "^11.0.1",
61
- "eslint": "^10.2.0",
61
+ "eslint": "^10.2.1",
62
62
  "eslint-parser-plain": "^0.1.1",
63
63
  "eslint-typegen": "^2.3.1",
64
64
  "eslint-vitest-rule-tester": "^3.1.0",
65
65
  "husky": "^9.1.7",
66
+ "jsonc-eslint-parser": "^3.1.0",
66
67
  "nano-staged": "^1.0.2",
67
68
  "npm-run-all2": "^8.0.4",
68
- "oxfmt": "^0.45.0",
69
+ "oxfmt": "^0.47.0",
69
70
  "show-invisibles": "^0.0.2",
70
71
  "tinyglobby": "^0.2.16",
71
- "tsdown": "^0.21.8",
72
+ "tsdown": "^0.21.10",
72
73
  "tsx": "^4.21.0",
73
- "typescript": "^6.0.2",
74
- "vitest": "^4.1.4",
75
- "eslint-plugin-oxfmt": "0.4.0"
74
+ "typescript": "^6.0.3",
75
+ "vitest": "^4.1.5",
76
+ "eslint-plugin-oxfmt": "0.5.0"
76
77
  },
77
78
  "engines": {
78
79
  "node": "^20.19.0 || >=22.12.0"
package/workers/oxfmt.mjs CHANGED
@@ -20,9 +20,21 @@ import { runAsWorker } from 'synckit'
20
20
  * @typedef {import('load-oxfmt-config').OxfmtOptions & PluginOptions} Options
21
21
  */
22
22
 
23
+ /**
24
+ * @typedef {object} WorkerOptions
25
+ * @property {string} cwd Current working directory for resolving rule-relative inputs.
26
+ * @property {string | undefined} configPath Custom path to an oxfmt configuration file.
27
+ * @property {string[] | undefined} ignorePatterns Rule-level ignore patterns.
28
+ * @property {Override[] | undefined} overrides Rule-level override entries.
29
+ * @property {boolean} useConfig Whether config loading is enabled.
30
+ * @property {import('oxfmt').FormatConfig} formatOptions Pure formatter options passed to oxfmt.
31
+ */
32
+
23
33
  /**
24
34
  * @typedef {object} ResolvedBaseOptions
25
- * @property {import('oxfmt').FormatConfig} baseOptions Resolved base formatter options.
35
+ * @property {import('oxfmt').FormatConfig} formatOptions Resolved base formatter options.
36
+ * @property {string[] | undefined} ignorePatterns Resolved ignore patterns from config loading.
37
+ * @property {Override[] | undefined} overrides Resolved overrides from config loading.
26
38
  * @property {string} configDir Directory of the resolved config file, used as base for config-derived glob patterns.
27
39
  */
28
40
 
@@ -79,17 +91,14 @@ const picomatchCache = new Map()
79
91
 
80
92
  /**
81
93
  * Apply overrides to the base options based on the filename
82
- * @param filename - The file path
83
- * @param cwd - Base directory for glob matching
94
+ * @param relativePath - The file path relative to the glob base directory
84
95
  * @param baseOptions - Base format options
85
96
  * @param [overrides] - Override configurations
86
97
  * @returns - Merged options
87
98
  */
88
99
  function applyOverrides(
89
100
  /** @type {string} */
90
- filename,
91
- /** @type {string} */
92
- cwd,
101
+ relativePath,
93
102
  /** @type {import('oxfmt').FormatConfig} */
94
103
  baseOptions,
95
104
  /** @type {Override[] | undefined} */
@@ -99,9 +108,6 @@ function applyOverrides(
99
108
  return baseOptions
100
109
  }
101
110
 
102
- // Get relative path from cwd and normalize to forward slashes for cross-platform compatibility
103
- const relativePath = relative(cwd, filename).replace(/\\/g, '/')
104
-
105
111
  let mergedOptions = baseOptions
106
112
  let hasOverrides = false
107
113
 
@@ -171,48 +177,48 @@ function getConfigPathForFile(
171
177
 
172
178
  /**
173
179
  * Build cache key for merged options per file invocation.
174
- * The key includes filename, cwd, configDir, resolved base options, and rule-level
175
- * override inputs to avoid stale cache hits.
180
+ * The key only includes data that can affect the final formatter options.
176
181
  *
177
- * @param filename - Current file path.
178
- * @param cwd - Base directory used for rule-level glob matching.
179
- * @param configDir - Directory of the resolved config file, used for config-derived glob matching.
180
182
  * @param baseOptions - Resolved base options used for formatting.
181
- * @param ignorePatterns - Rule-level ignore patterns.
183
+ * @param relativePath - File path relative to the override base directory.
182
184
  * @param overrides - Rule-level override entries.
183
- * @param useConfig - Whether config file loading is enabled.
184
185
  * @returns Serialized cache key.
185
186
  */
186
187
  function getMergedOptionsCacheKey(
187
- /** @type {string} */
188
- filename,
189
- /** @type {string} */
190
- cwd,
191
- /** @type {string} */
192
- configDir,
193
188
  /** @type {import('oxfmt').FormatConfig} */
194
189
  baseOptions,
195
- /** @type {string[] | undefined} */
196
- ignorePatterns,
190
+ /** @type {string | undefined} */
191
+ relativePath,
197
192
  /** @type {Override[] | undefined} */
198
193
  overrides,
199
- /** @type {boolean} */
200
- useConfig,
201
194
  ) {
195
+ const hasOverrides = !!(overrides && overrides.length > 0)
196
+
202
197
  return JSON.stringify(
203
198
  {
204
199
  baseOptions,
205
- configDir,
206
- cwd,
207
- filename,
208
- ignorePatterns,
209
- overrides,
210
- useConfig,
200
+ overrides: hasOverrides ? overrides : undefined,
201
+ relativePath: hasOverrides ? relativePath : undefined,
211
202
  },
212
203
  stableReplacer,
213
204
  )
214
205
  }
215
206
 
207
+ /**
208
+ * Normalize a file path relative to the provided base directory.
209
+ * @param baseDir - Base directory used for glob evaluation
210
+ * @param filename - Absolute file path
211
+ * @returns - Normalized relative path using forward slashes
212
+ */
213
+ function getRelativePath(
214
+ /** @type {string} */
215
+ baseDir,
216
+ /** @type {string} */
217
+ filename,
218
+ ) {
219
+ return relative(baseDir, filename).replace(/\\/g, '/')
220
+ }
221
+
216
222
  /**
217
223
  * Build cache key for resolving base formatter options.
218
224
  * The key includes file directory and all resolution inputs.
@@ -248,6 +254,74 @@ function getResolvedBaseOptionsCacheKey(
248
254
  )
249
255
  }
250
256
 
257
+ /**
258
+ * Validate and normalize worker invocation options.
259
+ * @param options - Raw worker options
260
+ * @returns - Validated worker options
261
+ */
262
+ function getWorkerOptions(
263
+ /** @type {Options | undefined} */
264
+ options,
265
+ ) {
266
+ if (!options || typeof options !== 'object') {
267
+ throw new TypeError('oxfmt worker expected an options object.')
268
+ }
269
+
270
+ const {
271
+ configPath,
272
+ cwd,
273
+ ignorePatterns,
274
+ overrides,
275
+ useConfig = true,
276
+ ...formatOptions
277
+ } = options
278
+
279
+ if (typeof cwd !== 'string' || cwd.length === 0) {
280
+ throw new TypeError('oxfmt worker requires a non-empty "cwd" option.')
281
+ }
282
+ if (configPath != null && typeof configPath !== 'string') {
283
+ throw new TypeError(
284
+ 'oxfmt worker requires "configPath" to be a string when provided.',
285
+ )
286
+ }
287
+ if (ignorePatterns != null && !isStringArray(ignorePatterns)) {
288
+ throw new TypeError(
289
+ 'oxfmt worker requires "ignorePatterns" to be an array of strings when provided.',
290
+ )
291
+ }
292
+ if (overrides != null && !Array.isArray(overrides)) {
293
+ throw new TypeError(
294
+ 'oxfmt worker requires "overrides" to be an array when provided.',
295
+ )
296
+ }
297
+ if (typeof useConfig !== 'boolean') {
298
+ throw new TypeError(
299
+ 'oxfmt worker requires "useConfig" to be a boolean when provided.',
300
+ )
301
+ }
302
+
303
+ return {
304
+ configPath,
305
+ cwd,
306
+ formatOptions,
307
+ ignorePatterns,
308
+ overrides,
309
+ useConfig,
310
+ }
311
+ }
312
+
313
+ /**
314
+ * Check whether a value is an array of strings.
315
+ * @param value - Value to validate
316
+ * @returns - Whether the value is a string array
317
+ */
318
+ function isStringArray(
319
+ /** @type {unknown} */
320
+ value,
321
+ ) {
322
+ return Array.isArray(value) && value.every(item => typeof item === 'string')
323
+ }
324
+
251
325
  /**
252
326
  * Resolve base formatter options for a file and cache the async result.
253
327
  *
@@ -290,7 +364,9 @@ async function resolveBaseOptions(
290
364
  if (!useConfig) {
291
365
  return {
292
366
  configDir: cwd,
293
- baseOptions: {
367
+ ignorePatterns: undefined,
368
+ overrides: undefined,
369
+ formatOptions: {
294
370
  ...formatOptions,
295
371
  },
296
372
  }
@@ -306,11 +382,15 @@ async function resolveBaseOptions(
306
382
  configPath: resolvedConfigPath,
307
383
  cwd: resolveFromDir,
308
384
  })
385
+ const { ignorePatterns, overrides, ...loadedFormatOptions } =
386
+ configOptions ?? {}
309
387
 
310
388
  return {
311
389
  configDir,
312
- baseOptions: {
313
- ...configOptions,
390
+ ignorePatterns,
391
+ overrides,
392
+ formatOptions: {
393
+ ...loadedFormatOptions,
314
394
  ...formatOptions,
315
395
  },
316
396
  }
@@ -329,26 +409,19 @@ async function resolveBaseOptions(
329
409
 
330
410
  /**
331
411
  * Check if a file should be ignored based on ignorePatterns
332
- * @param filename - The file path
333
- * @param cwd - Base directory for glob matching
412
+ * @param relativePath - The file path relative to the glob base directory
334
413
  * @param [ignorePatterns] - Ignore patterns
335
414
  * @returns - Whether the file should be ignored
336
415
  */
337
416
  function shouldIgnoreFile(
338
417
  /** @type {string} */
339
- filename,
340
- /** @type {string} */
341
- cwd,
342
- /** @type {string[] | undefined} */
343
- ignorePatterns,
418
+ relativePath,
419
+ /** @type {string[] | undefined} */ ignorePatterns,
344
420
  ) {
345
421
  if (!ignorePatterns || ignorePatterns.length === 0) {
346
422
  return false
347
423
  }
348
424
 
349
- // Get relative path from cwd and normalize to forward slashes for cross-platform compatibility
350
- const relativePath = relative(cwd, filename).replace(/\\/g, '/')
351
-
352
425
  // Check if file matches any ignore pattern
353
426
  return getCachedMatcher(ignorePatterns)(relativePath)
354
427
  }
@@ -371,65 +444,62 @@ runAsWorker(
371
444
  const {
372
445
  configPath,
373
446
  cwd,
447
+ formatOptions,
374
448
  ignorePatterns,
375
449
  overrides,
376
- useConfig = true,
377
- ...formatOptions
378
- } = options
450
+ useConfig,
451
+ } = getWorkerOptions(options)
379
452
 
380
- const { baseOptions, configDir } = await resolveBaseOptions(
453
+ const {
454
+ configDir,
455
+ formatOptions: baseFormatOptions,
456
+ ignorePatterns: baseIgnorePatterns,
457
+ overrides: baseOverrides,
458
+ } = await resolveBaseOptions(
381
459
  filename,
382
460
  cwd,
383
461
  configPath,
384
462
  useConfig,
385
463
  formatOptions,
386
464
  )
387
-
388
- const mergedOptionsCacheKey = getMergedOptionsCacheKey(
389
- filename,
390
- cwd,
391
- configDir,
392
- baseOptions,
393
- ignorePatterns,
394
- overrides,
395
- useConfig,
396
- )
397
-
398
- const baseIgnorePatterns = /** @type {string[] | undefined} */ (
399
- baseOptions.ignorePatterns
400
- )
401
465
  const effectiveIgnorePatterns = ignorePatterns ?? baseIgnorePatterns
402
466
  const ignoreBase = ignorePatterns == null ? configDir : cwd
403
-
404
- if (shouldIgnoreFile(filename, ignoreBase, effectiveIgnorePatterns)) {
467
+ const ignoreRelativePath = effectiveIgnorePatterns?.length
468
+ ? getRelativePath(ignoreBase, filename)
469
+ : undefined
470
+
471
+ if (
472
+ ignoreRelativePath &&
473
+ shouldIgnoreFile(ignoreRelativePath, effectiveIgnorePatterns)
474
+ ) {
405
475
  return { code: sourceText }
406
476
  }
407
477
 
478
+ const effectiveOverrides = useConfig ? baseOverrides : overrides
479
+ const overrideBase = useConfig ? configDir : cwd
480
+ const overrideRelativePath = effectiveOverrides?.length
481
+ ? getRelativePath(overrideBase, filename)
482
+ : undefined
483
+ const mergedOptionsCacheKey = getMergedOptionsCacheKey(
484
+ baseFormatOptions,
485
+ overrideRelativePath,
486
+ effectiveOverrides,
487
+ )
488
+
408
489
  const cachedMergedOptions = mergedOptionsCache.get(mergedOptionsCacheKey)
409
490
  if (cachedMergedOptions) {
410
491
  return format(filename, sourceText, cachedMergedOptions)
411
492
  }
412
493
 
413
- const baseOverrides = /** @type {Override[] | undefined} */ (
414
- baseOptions.overrides
415
- )
416
-
417
- // Apply config-level overrides (relative to config directory)
418
- let mergedOptions = baseOptions
419
- if (useConfig && baseOverrides && baseOverrides.length > 0) {
494
+ let mergedOptions = baseFormatOptions
495
+ if (overrideRelativePath && effectiveOverrides?.length) {
420
496
  mergedOptions = applyOverrides(
421
- filename,
422
- configDir,
497
+ overrideRelativePath,
423
498
  mergedOptions,
424
- baseOverrides,
499
+ effectiveOverrides,
425
500
  )
426
501
  }
427
502
 
428
- // Apply rule-level overrides (relative to ESLint cwd)
429
- if (overrides && overrides.length > 0) {
430
- mergedOptions = applyOverrides(filename, cwd, mergedOptions, overrides)
431
- }
432
-
433
503
  mergedOptionsCache.set(mergedOptionsCacheKey, mergedOptions)
434
504
  evictCache(mergedOptionsCache)
435
505