eslint-plugin-oxfmt 0.27.0 → 0.28.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
@@ -24,24 +24,18 @@ Maintained in the [eslint-plugin-oxfmt monorepo](https://github.com/ntnyq/eslint
24
24
 
25
25
  - **ESLint**: `^9.5.0 || ^10.0.0` (Only supports ESLint flat config)
26
26
  - **Node.js**: `^22.13.0 || >=24`
27
- - **oxfmt**: `>= 0.72.0`
28
-
29
- Upgrading from oxfmt 0.71.0 to 0.72.0 switches Markdown to the native formatter.
30
- JavaScript and TypeScript code fences now use Oxc formatting, including
31
- `sortImports` and `sortTailwindcss` when enabled. Markdown block directives are
32
- supported, Chinese/Japanese line breaks are preserved across `proseWrap`
33
- settings, and list/container prefixes remain spaces with `useTabs`. Review
34
- Markdown and embedded code formatting changes after upgrading; YAML frontmatter
35
- formatting remains supported.
36
-
37
- The release also fixes `quoteProps: consistent` in destructuring patterns and
38
- around computed keys, comments between callees and their opening delimiters,
39
- assignment-target comments, embedded template layouts, and CSS/JSON formatting.
40
- No formatting options were added, renamed, or removed, and config merge
41
- precedence is unchanged. The loader already respects excluded parent
42
- directories and checks global ignores before loading nested configs; these
43
- upstream fixes require no loader API changes.
44
- See the upstream [0.72.0 release notes](https://github.com/oxc-project/oxc/releases/tag/oxfmt_v0.72.0).
27
+ - **oxfmt**: `>= 0.73.0`
28
+
29
+ Upgrading from oxfmt 0.72.0 to 0.73.0 adds language `associations` and opt-in
30
+ Astro formatting. Install `@astrojs/compiler-rs` to use `astro: true` or an Astro
31
+ options object. No existing options were renamed or removed.
32
+
33
+ TOML now supports TOML 1.1 syntax and formats TOML frontmatter and Markdown code
34
+ fences. Review these outputs after upgrading. Upstream also fixes CSS-in-JS
35
+ interpolations, TypeScript instantiation parentheses, export wrapping, JSDoc
36
+ paragraphs, and nested Svelte embeds. Config merge precedence is unchanged, and
37
+ the loader already follows symlinked config files.
38
+ See the upstream [0.73.0 release notes](https://github.com/oxc-project/oxc/releases/tag/oxfmt_v0.73.0).
45
39
 
46
40
  The monorepo migration aligns the Node.js requirement with `load-oxfmt-config`; Node.js 20 is no longer supported.
47
41
 
@@ -324,6 +318,7 @@ Tip: `jsdoc: true` is equivalent to enabling JSDoc with default settings.
324
318
  | `sortImports` | `boolean \| object` | disabled | Experimental import sorting configuration |
325
319
  | `sortPackageJson` | `boolean \| object` | `true` | Experimental package.json sorting (object form: `{ sortScripts?: boolean }`) |
326
320
  | `sortTailwindcss` | `boolean \| object` | disabled | Experimental Tailwind CSS class sorting (enable with `{}` for defaults) |
321
+ | `astro` | `boolean \| object` | disabled | Enable/configure Astro formatting; requires `@astrojs/compiler-rs` |
327
322
  | `svelte` | `boolean \| object` | disabled | Enable/configure `.svelte` formatting via `prettier-plugin-svelte` |
328
323
 
329
324
  #### Import sorting (`sortImports`)
@@ -388,6 +383,68 @@ You can pass the Tailwind plugin options to control which attributes/functions a
388
383
 
389
384
  Paths in `sortTailwindcss.config` and `sortTailwindcss.stylesheet` are resolved relative to the oxfmt config file when set in that file or its overrides. Paths set in ESLint rule options or rule-level overrides are resolved relative to ESLint `cwd`. Absolute paths are used directly.
390
385
 
386
+ ## Language associations
387
+
388
+ Use `associations` in your oxfmt config or inline rule options to format files
389
+ as a language regardless of their extension:
390
+
391
+ ```js
392
+ // eslint.config.mjs
393
+ import pluginOxfmt from 'eslint-plugin-oxfmt'
394
+
395
+ export default [
396
+ {
397
+ ...pluginOxfmt.configs.recommended,
398
+ files: ['**/*.{html,wxml,conf}'],
399
+ rules: {
400
+ 'oxfmt/oxfmt': [
401
+ 'error',
402
+ {
403
+ associations: [
404
+ {
405
+ files: ['**/*.html'],
406
+ excludeFiles: ['index.html'],
407
+ language: 'angular-html',
408
+ },
409
+ { files: ['**/*.wxml'], language: 'html' },
410
+ { files: ['**/*.conf'], language: 'json' },
411
+ ],
412
+ },
413
+ ],
414
+ },
415
+ },
416
+ ]
417
+ ```
418
+
419
+ Each entry requires `files: string[]` and `language`; `excludeFiles: string[]`
420
+ is optional. Later matching entries win. Languages are `javascript`, `jsx`,
421
+ `typescript`, `tsx`, `json`, `jsonc`, `json5`, `css`, `scss`, `less`, `graphql`,
422
+ `yaml`, `markdown`, `toml`, `html`, `angular-html`, `vue`, `svelte`, `astro`,
423
+ `handlebars`, and `mdx`.
424
+
425
+ Config associations match paths relative to the resolved config directory.
426
+ Inline associations match relative to ESLint `cwd` and replace config
427
+ associations entirely; `associations: []` clears them. `useConfig: false` keeps
428
+ inline associations active. Ignores, EditorConfig, and overrides use the original
429
+ file path. Upstream's excluded lockfiles remain excluded. Associations do not
430
+ change ESLint file discovery or enable optional formatters automatically.
431
+
432
+ ### Astro options
433
+
434
+ Install `@astrojs/compiler-rs` alongside this plugin, then set `astro: true`
435
+ or an object. The compiler is an optional peer dependency.
436
+
437
+ | Option | Type | Default | Description |
438
+ | ----------------------- | --------------------------- | ------- | -------------------------------------------------------------------------------------------------------- |
439
+ | `astro.allowShorthand` | `boolean` | unset | Normalize matching identifier attributes to shorthand or explicit form; unset preserves the written form |
440
+ | `astro.compressHTML` | `'jsx' \| 'html' \| 'none'` | `'jsx'` | Match Astro compiler whitespace collapsing |
441
+ | `astro.skipFrontmatter` | `boolean` | `false` | Preserve frontmatter without formatting |
442
+
443
+ `astro: false` disables Astro formatting, including in overrides. `astro: true`
444
+ resets inherited Astro options to defaults. With
445
+ `embeddedLanguageFormatting: 'off'`, Astro frontmatter is still formatted by
446
+ Prettier; use `astro.skipFrontmatter` to preserve it.
447
+
391
448
  ## File-specific overrides
392
449
 
393
450
  Use `overrides` to apply different oxfmt options per file glob (later entries win on conflicts):
package/dist/index.d.mts CHANGED
@@ -1,4 +1,4 @@
1
- import { Linter, Rule as Rule$1 } from "eslint";
1
+ import { Linter, Rule } from "eslint";
2
2
  import "load-oxfmt-config";
3
3
  import "oxfmt";
4
4
  //#region src/types.d.ts
@@ -13,7 +13,7 @@ interface PluginOxfmt {
13
13
  version: string;
14
14
  };
15
15
  rules: {
16
- oxfmt: Rule$1.RuleModule;
16
+ oxfmt: Rule.RuleModule;
17
17
  };
18
18
  }
19
19
  //#endregion
package/dist/index.mjs CHANGED
@@ -80,7 +80,7 @@ const configs = {
80
80
  //#region src/meta.ts
81
81
  const meta = {
82
82
  name: "eslint-plugin-oxfmt",
83
- version: "0.27.0"
83
+ version: "0.28.0"
84
84
  };
85
85
  //#endregion
86
86
  //#region src/dir.ts
@@ -164,6 +164,43 @@ const oxfmtOptionsSchema = {
164
164
  enum: ["always", "avoid"],
165
165
  type: "string"
166
166
  },
167
+ astro: {
168
+ anyOf: [{ type: "boolean" }, {
169
+ additionalProperties: false,
170
+ type: "object",
171
+ properties: {
172
+ allowShorthand: {
173
+ type: "boolean",
174
+ description: "Whether to normalize matching identifier attributes to shorthand or explicit form.\nWhen unset, the form that was written stays as-is.\n\n- Default: Unset"
175
+ },
176
+ compressHTML: {
177
+ description: "Mirror of Astro's `compressHTML` config.\nTells the formatter which whitespace the compiler will collapse.\n\n- Default: `\"jsx\"`",
178
+ oneOf: [
179
+ {
180
+ enum: ["jsx"],
181
+ type: "string",
182
+ description: "Whitespace runs containing a newline are dropped; same-line spaces are content."
183
+ },
184
+ {
185
+ description: "Browser HTML whitespace rules.",
186
+ enum: ["html"],
187
+ type: "string"
188
+ },
189
+ {
190
+ description: "No collapsing.",
191
+ enum: ["none"],
192
+ type: "string"
193
+ }
194
+ ]
195
+ },
196
+ skipFrontmatter: {
197
+ type: "boolean",
198
+ description: "Whether to skip formatting the frontmatter.\n\n- Default: `false`"
199
+ }
200
+ }
201
+ }],
202
+ description: "Options for `prettier-plugin-astro`.\n\nPass `true` or an object to enable `.astro` file formatting, `false` to disable (handy in overrides).\nSetting `true` resets to defaults, dropping options inherited from a parent scope.\n\nNOTE: `prettier-plugin-astro` requires the `@astrojs/compiler-rs` package at runtime.\nOxfmt does NOT bundle it, so install it in your project, otherwise formatting fails.\n\n- Languages: Astro\n- Default: Disabled"
203
+ },
167
204
  bracketSameLine: {
168
205
  description: `Put the > of a multi-line JSX element at the end of the last line\ninstead of being alone on the next line. (Default: false)`,
169
206
  type: "boolean"
@@ -550,6 +587,54 @@ const oxfmtRuleSchema = {
550
587
  properties: {
551
588
  ...oxfmtOptionsSchema.properties,
552
589
  ...oxfmtConfigSchema.properties,
590
+ associations: {
591
+ type: "array",
592
+ description: "Format matched files as the given language, instead of detecting it from the file name.\ne.g. Angular templates named `*.html`, or `*.wxml` files formatted as HTML.\nWhen a file matches multiple associations, the later one takes precedence (array order matters).\n\n- Default: `[]`",
593
+ items: {
594
+ additionalProperties: false,
595
+ required: ["files", "language"],
596
+ type: "object",
597
+ properties: {
598
+ excludeFiles: {
599
+ type: "array",
600
+ description: "Glob patterns excluded from this association, relative to the config directory or ESLint cwd for inline associations.",
601
+ items: { type: "string" }
602
+ },
603
+ files: {
604
+ type: "array",
605
+ description: "Glob patterns matched relative to the config directory, or ESLint cwd for inline associations.",
606
+ items: { type: "string" }
607
+ },
608
+ language: {
609
+ type: "string",
610
+ description: "The language to format matched files as, named by its Shiki id (as in Markdown code fences).",
611
+ enum: [
612
+ "javascript",
613
+ "jsx",
614
+ "typescript",
615
+ "tsx",
616
+ "json",
617
+ "jsonc",
618
+ "json5",
619
+ "css",
620
+ "scss",
621
+ "less",
622
+ "graphql",
623
+ "yaml",
624
+ "markdown",
625
+ "toml",
626
+ "html",
627
+ "angular-html",
628
+ "vue",
629
+ "svelte",
630
+ "astro",
631
+ "handlebars",
632
+ "mdx"
633
+ ]
634
+ }
635
+ }
636
+ }
637
+ },
553
638
  overrides: {
554
639
  description: `File-specific overrides.\nWhen a file matches multiple overrides, the later override takes precedence (array order matters).\n\n- (Default: [])`,
555
640
  type: "array",
@@ -16,6 +16,15 @@ export type OxfmtOxfmt = []|[{
16
16
 
17
17
  arrowParens?: ("always" | "avoid")
18
18
 
19
+ astro?: (boolean | {
20
+
21
+ allowShorthand?: boolean
22
+
23
+ compressHTML?: ("jsx" | "html" | "none")
24
+
25
+ skipFrontmatter?: boolean
26
+ })
27
+
19
28
  bracketSameLine?: boolean
20
29
 
21
30
  bracketSpacing?: boolean
@@ -162,6 +171,15 @@ export type OxfmtOxfmt = []|[{
162
171
 
163
172
  withNodeModules?: boolean
164
173
 
174
+ associations?: {
175
+
176
+ excludeFiles?: string[]
177
+
178
+ files: string[]
179
+
180
+ language: ("javascript" | "jsx" | "typescript" | "tsx" | "json" | "jsonc" | "json5" | "css" | "scss" | "less" | "graphql" | "yaml" | "markdown" | "toml" | "html" | "angular-html" | "vue" | "svelte" | "astro" | "handlebars" | "mdx")
181
+ }[]
182
+
165
183
  overrides?: {
166
184
 
167
185
  excludeFiles?: string[]
@@ -172,6 +190,15 @@ export type OxfmtOxfmt = []|[{
172
190
 
173
191
  arrowParens?: ("always" | "avoid")
174
192
 
193
+ astro?: (boolean | {
194
+
195
+ allowShorthand?: boolean
196
+
197
+ compressHTML?: ("jsx" | "html" | "none")
198
+
199
+ skipFrontmatter?: boolean
200
+ })
201
+
175
202
  bracketSameLine?: boolean
176
203
 
177
204
  bracketSpacing?: boolean
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "eslint-plugin-oxfmt",
3
3
  "type": "module",
4
- "version": "0.27.0",
4
+ "version": "0.28.0",
5
5
  "description": "An ESLint plugin for formatting code with oxfmt.",
6
6
  "keywords": [
7
7
  "eslint",
@@ -47,11 +47,15 @@
47
47
  },
48
48
  "sideEffects": false,
49
49
  "peerDependencies": {
50
+ "@astrojs/compiler-rs": "*",
50
51
  "eslint": "^9.5.0 || ^10.0.0",
51
- "oxfmt": ">=0.72.0",
52
+ "oxfmt": ">=0.73.0",
52
53
  "svelte": "*"
53
54
  },
54
55
  "peerDependenciesMeta": {
56
+ "@astrojs/compiler-rs": {
57
+ "optional": true
58
+ },
55
59
  "svelte": {
56
60
  "optional": true
57
61
  }
@@ -59,14 +63,15 @@
59
63
  "dependencies": {
60
64
  "generate-differences": "^0.1.1",
61
65
  "ignore": "^7.0.12",
62
- "load-oxfmt-config": "^0.27.0",
66
+ "load-oxfmt-config": "^0.28.0",
63
67
  "picomatch": "^4.0.7",
64
- "synckit": "^0.11.13"
68
+ "synckit": "^0.12.3"
65
69
  },
66
70
  "devDependencies": {
71
+ "@astrojs/compiler-rs": "^0.5.1",
67
72
  "@types/json-schema": "^7.0.15",
68
73
  "eslint-parser-plain": "^0.1.1",
69
- "eslint-plugin-oxfmt": "0.27.0",
74
+ "eslint-plugin-oxfmt": "0.28.0",
70
75
  "eslint-typegen": "^2.3.1",
71
76
  "eslint-vitest-rule-tester": "^3.2.0",
72
77
  "jsonc-eslint-parser": "^3.3.0",
package/workers/oxfmt.mjs CHANGED
@@ -1,6 +1,6 @@
1
1
  // @ts-check
2
2
 
3
- import { dirname, extname, relative, resolve } from 'node:path'
3
+ import { basename, dirname, extname, join, relative, resolve } from 'node:path'
4
4
  import ignore from 'ignore'
5
5
  import { isOxfmtIgnored, loadOxfmtConfigForFile } from 'load-oxfmt-config'
6
6
  import { format } from 'oxfmt'
@@ -96,6 +96,59 @@ const overrideMatcherCache = new Map()
96
96
  */
97
97
  const ignoreMatcherCache = new Map()
98
98
 
99
+ /**
100
+ * Filename suffixes selecting the same formatter as upstream Language routes.
101
+ * The format() API does not accept a language, unlike the CLI.
102
+ * @type {Record<import('oxfmt').Language, string>}
103
+ */
104
+ const LANGUAGE_SUFFIXES = {
105
+ 'angular-html': 'component.html',
106
+ astro: 'astro',
107
+ css: 'css',
108
+ graphql: 'graphql',
109
+ handlebars: 'hbs',
110
+ html: 'html',
111
+ javascript: 'js',
112
+ json: 'json',
113
+ json5: 'json5',
114
+ jsonc: 'jsonc',
115
+ jsx: 'jsx',
116
+ less: 'less',
117
+ markdown: 'md',
118
+ mdx: 'mdx',
119
+ scss: 'scss',
120
+ svelte: 'svelte',
121
+ toml: 'toml',
122
+ tsx: 'tsx',
123
+ typescript: 'ts',
124
+ vue: 'vue',
125
+ yaml: 'yaml',
126
+ }
127
+
128
+ /**
129
+ * Upstream classify_as excludes these even when associations explicitly match.
130
+ * Keep this guard before substituting a formatter filename.
131
+ * @type {Set<string>}
132
+ */
133
+ const ASSOCIATION_EXCLUDED_FILENAMES = new Set([
134
+ 'bun.lock',
135
+ 'Cargo.lock',
136
+ 'composer.lock',
137
+ 'deno.lock',
138
+ 'flake.lock',
139
+ 'Gopkg.lock',
140
+ 'mcmod.info',
141
+ 'MODULE.bazel.lock',
142
+ 'package-lock.json',
143
+ 'Package.resolved',
144
+ 'pdm.lock',
145
+ 'Pipfile.lock',
146
+ 'pnpm-lock.yaml',
147
+ 'poetry.lock',
148
+ 'uv.lock',
149
+ 'yarn.lock',
150
+ ])
151
+
99
152
  /**
100
153
  * Apply override entries to a base options object.
101
154
  * @param {string} relativePath - Relative file path used for matching.
@@ -215,8 +268,13 @@ async function formatViaOxfmt(filename, sourceText, options = {}) {
215
268
  * Inline overrides separated from root options to preserve merge order.
216
269
  * @type {OxfmtOptions}
217
270
  */
218
- const { overrides: ruleOverrides, ...inlineOptionsWithoutOverrides } =
219
- inlineFormatOptions
271
+ const {
272
+ associations: ruleAssociations,
273
+ overrides: ruleOverrides,
274
+ ...inlineOptionsWithoutOverrides
275
+ } = inlineFormatOptions
276
+ let associations = ruleAssociations
277
+ let associationBaseDir = inlineBaseDir
220
278
  /**
221
279
  * Effective formatter options with paths resolved against their source.
222
280
  * @type {import('oxfmt').FormatConfig}
@@ -235,8 +293,16 @@ async function formatViaOxfmt(filename, sourceText, options = {}) {
235
293
  filepath: filename,
236
294
  useCache,
237
295
  })
238
- const { overrides: configOverrides, ...loadedConfig } = loaded.config
296
+ const {
297
+ associations: configAssociations,
298
+ overrides: configOverrides,
299
+ ...loadedConfig
300
+ } = loaded.config
239
301
  overrideBaseDir = loaded.dirname ?? overrideBaseDir
302
+ if (ruleAssociations === undefined) {
303
+ associations = configAssociations
304
+ associationBaseDir = overrideBaseDir
305
+ }
240
306
 
241
307
  finalOptions = {
242
308
  ...resolveTailwindPaths(loadedConfig, overrideBaseDir),
@@ -258,7 +324,27 @@ async function formatViaOxfmt(filename, sourceText, options = {}) {
258
324
  inlineBaseDir,
259
325
  )
260
326
 
261
- return format(filename, sourceText, finalOptions)
327
+ const formatFilename = resolveAssociationFilename(
328
+ filename,
329
+ associations,
330
+ associationBaseDir,
331
+ )
332
+ if (
333
+ formatFilename !== filename &&
334
+ ASSOCIATION_EXCLUDED_FILENAMES.has(basename(filename))
335
+ ) {
336
+ return {
337
+ code: sourceText,
338
+ ignored: true,
339
+ }
340
+ }
341
+ const result = await format(formatFilename, sourceText, finalOptions)
342
+ if (formatFilename !== filename) {
343
+ for (const error of result.errors) {
344
+ error.message = error.message.replaceAll(formatFilename, () => filename)
345
+ }
346
+ }
347
+ return result
262
348
  }
263
349
 
264
350
  /**
@@ -354,6 +440,52 @@ function isStringArray(value) {
354
440
  return Array.isArray(value) && value.every(item => typeof item === 'string')
355
441
  }
356
442
 
443
+ /**
444
+ * Resolve the last matching association, validating entries even if unmatched.
445
+ * @param {string} filename - Original file path.
446
+ * @param {OxfmtOptions['associations']} associations - Ordered associations.
447
+ * @param {string} baseDir - Directory that owns the association patterns.
448
+ * @returns {string} Filename used only for selecting the formatter.
449
+ */
450
+ function resolveAssociationFilename(filename, associations, baseDir) {
451
+ if (associations === undefined) {
452
+ return filename
453
+ }
454
+ if (!Array.isArray(associations)) {
455
+ throw new TypeError('oxfmt associations must be an array.')
456
+ }
457
+
458
+ const relativePath = getRelativePath(baseDir, filename)
459
+ let formatFilename = filename
460
+ for (const association of associations) {
461
+ if (
462
+ !association ||
463
+ !isStringArray(association.files) ||
464
+ (association.excludeFiles !== undefined &&
465
+ !isStringArray(association.excludeFiles)) ||
466
+ !Object.hasOwn(LANGUAGE_SUFFIXES, association.language)
467
+ ) {
468
+ throw new TypeError(
469
+ 'Invalid oxfmt association: expected files, optional excludeFiles, and a supported language.',
470
+ )
471
+ }
472
+
473
+ const matches = getCachedOverrideMatcher(association.files)(relativePath)
474
+ const excluded = association.excludeFiles
475
+ ? getCachedOverrideMatcher(association.excludeFiles)(relativePath)
476
+ : false
477
+ if (matches && !excluded) {
478
+ // Use a neutral basename to avoid package.json sorting and .component.html
479
+ // detection overriding the explicitly selected language.
480
+ formatFilename = join(
481
+ dirname(filename),
482
+ `__oxfmt__.${LANGUAGE_SUFFIXES[association.language]}`,
483
+ )
484
+ }
485
+ }
486
+ return formatFilename
487
+ }
488
+
357
489
  /**
358
490
  * Resolve Tailwind paths before merging options from different sources.
359
491
  * @param {import('oxfmt').FormatConfig} options - Format options to normalize.
@@ -378,7 +510,10 @@ function resolveTailwindPaths(options, baseDir) {
378
510
  resolved.stylesheet = resolve(baseDir, resolved.stylesheet)
379
511
  }
380
512
 
381
- return { ...options, sortTailwindcss: resolved }
513
+ return {
514
+ ...options,
515
+ sortTailwindcss: resolved,
516
+ }
382
517
  }
383
518
 
384
519
  /**