eslint-plugin-oxfmt 0.1.1 → 0.2.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 +48 -6
- package/dist/index.d.mts +2 -2
- package/dist/index.mjs +71 -35
- package/dts/rule-options.d.ts +50 -0
- package/package.json +14 -14
- package/workers/oxfmt.mjs +342 -22
package/README.md
CHANGED
|
@@ -13,7 +13,9 @@
|
|
|
13
13
|
- 🔧 **Auto-fix** - Automatically format code on save or via ESLint's fix command
|
|
14
14
|
- 🎯 **ESLint Integration** - Seamlessly integrates with ESLint v9+ flat config
|
|
15
15
|
- 📦 **Zero Config** - Works out of the box with sensible defaults
|
|
16
|
-
-
|
|
16
|
+
- 🧩 **Config File Discovery** - Supports `.oxfmtrc.json`, `.oxfmtrc.jsonc`, and `oxfmt.config.ts`
|
|
17
|
+
- 📝 **EditorConfig Integration** - Respects a subset of `.editorconfig` options via [oxfmt's strategy](https://oxc.rs/docs/guide/usage/formatter/config#editorconfig)
|
|
18
|
+
- 🎨 **Highly Configurable** - Supports all oxfmt formatting options
|
|
17
19
|
- 🌐 **Multi-language Support** - JavaScript, TypeScript, JSX, TSX and [more](https://oxc.rs/docs/guide/usage/formatter.html#supported-languages)
|
|
18
20
|
|
|
19
21
|
## Requirements
|
|
@@ -104,10 +106,17 @@ export default [
|
|
|
104
106
|
htmlWhitespaceSensitivity: 'css',
|
|
105
107
|
proseWrap: 'preserve',
|
|
106
108
|
|
|
109
|
+
// JSDoc
|
|
110
|
+
jsdoc: {
|
|
111
|
+
commentLineStrategy: 'singleLine',
|
|
112
|
+
lineWrappingStyle: 'greedy',
|
|
113
|
+
separateTagGroups: false,
|
|
114
|
+
},
|
|
115
|
+
|
|
107
116
|
// Vue
|
|
108
117
|
vueIndentScriptAndStyle: false,
|
|
109
118
|
|
|
110
|
-
//
|
|
119
|
+
// Advanced
|
|
111
120
|
sortImports: {
|
|
112
121
|
order: 'asc',
|
|
113
122
|
newlinesBetween: true,
|
|
@@ -132,12 +141,27 @@ All options are optional and default to sensible values.
|
|
|
132
141
|
|
|
133
142
|
### Plugin Options
|
|
134
143
|
|
|
135
|
-
| Option | Type | Default | Description
|
|
136
|
-
| ------------ | --------- | ------- |
|
|
137
|
-
| `useConfig` | `boolean` | `true` | Load `.oxfmtrc
|
|
138
|
-
| `configPath` | `string` | — | Custom path to an oxfmt config file. Resolved from ESLint `cwd` when set.
|
|
144
|
+
| Option | Type | Default | Description |
|
|
145
|
+
| ------------ | --------- | ------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
146
|
+
| `useConfig` | `boolean` | `true` | Load `.oxfmtrc.json`, `.oxfmtrc.jsonc`, or `oxfmt.config.ts` via `load-oxfmt-config` (with `.editorconfig` merge support). Set to `false` to rely only on inline options. |
|
|
147
|
+
| `configPath` | `string` | — | Custom path to an oxfmt config file. Resolved from ESLint `cwd` when set. |
|
|
139
148
|
|
|
140
149
|
> Note: `cwd` is taken from ESLint automatically; you usually do not need to set it manually.
|
|
150
|
+
> `.editorconfig` merge behavior follows oxfmt's documented strategy: https://oxc.rs/docs/guide/usage/formatter/config#editorconfig
|
|
151
|
+
|
|
152
|
+
### Config Discovery & Precedence
|
|
153
|
+
|
|
154
|
+
When `useConfig` is `true`, the plugin loads config using `load-oxfmt-config`.
|
|
155
|
+
|
|
156
|
+
- Config discovery order (from `cwd`, walking upward): `.oxfmtrc.json` → `.oxfmtrc.jsonc` → `oxfmt.config.ts`
|
|
157
|
+
- `.editorconfig` support: nearest `.editorconfig` (including section overrides) is merged into the final options
|
|
158
|
+
- `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).
|
|
160
|
+
|
|
161
|
+
For detailed behavior, see:
|
|
162
|
+
|
|
163
|
+
- [Oxfmt configuration](https://oxc.rs/docs/guide/usage/formatter/config)
|
|
164
|
+
- [Oxfmt `.editorconfig` strategy](https://oxc.rs/docs/guide/usage/formatter/config#editorconfig)
|
|
141
165
|
|
|
142
166
|
### Basic Options
|
|
143
167
|
|
|
@@ -199,6 +223,24 @@ All options are optional and default to sensible values.
|
|
|
199
223
|
| `htmlWhitespaceSensitivity` | `'css' \| 'ignore' \| 'strict'` | `'css'` | Global whitespace sensitivity for HTML-like languages |
|
|
200
224
|
| `proseWrap` | `'always' \| 'never' \| 'preserve'` | `'preserve'` | Control prose wrapping in Markdown/MDX |
|
|
201
225
|
|
|
226
|
+
### JSDoc Options
|
|
227
|
+
|
|
228
|
+
Use `jsdoc` to enable and configure JSDoc comment formatting. Pass an object to enable it (for example `jsdoc: {}`).
|
|
229
|
+
|
|
230
|
+
| Option | Type | Default | Description |
|
|
231
|
+
| ----------------------------------- | --------------------------------------- | -------------- | ------------------------------------------------------ |
|
|
232
|
+
| `jsdoc.commentLineStrategy` | `'singleLine' \| 'multiline' \| 'keep'` | `'singleLine'` | Comment block style policy |
|
|
233
|
+
| `jsdoc.lineWrappingStyle` | `'greedy' \| 'balance'` | `'greedy'` | Wrapping strategy for long descriptions |
|
|
234
|
+
| `jsdoc.addDefaultToDescription` | `boolean` | `true` | Append default values to `@param` descriptions |
|
|
235
|
+
| `jsdoc.bracketSpacing` | `boolean` | `false` | Add spaces inside JSDoc type braces |
|
|
236
|
+
| `jsdoc.capitalizeDescriptions` | `boolean` | `true` | Capitalize the first letter of tag descriptions |
|
|
237
|
+
| `jsdoc.descriptionTag` | `boolean` | `false` | Emit `@description` tag instead of inline description |
|
|
238
|
+
| `jsdoc.descriptionWithDot` | `boolean` | `false` | Add a trailing dot to the end of descriptions |
|
|
239
|
+
| `jsdoc.keepUnparsableExampleIndent` | `boolean` | `false` | Preserve indentation in unparsable `@example` code |
|
|
240
|
+
| `jsdoc.preferCodeFences` | `boolean` | `false` | Prefer fenced code blocks over 4-space indentation |
|
|
241
|
+
| `jsdoc.separateReturnsFromParam` | `boolean` | `false` | Add a blank line between final `@param` and `@returns` |
|
|
242
|
+
| `jsdoc.separateTagGroups` | `boolean` | `false` | Add blank lines between different tag groups |
|
|
243
|
+
|
|
202
244
|
### Advanced Options
|
|
203
245
|
|
|
204
246
|
| Option | Type | Default | Description |
|
package/dist/index.d.mts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import * as eslint from "eslint";
|
|
1
|
+
import * as _$eslint from "eslint";
|
|
2
2
|
import { Linter, Rule } from "eslint";
|
|
3
3
|
|
|
4
4
|
//#region src/types.d.ts
|
|
@@ -23,7 +23,7 @@ declare const meta: {
|
|
|
23
23
|
//#endregion
|
|
24
24
|
//#region src/rules/index.d.ts
|
|
25
25
|
declare const rules: {
|
|
26
|
-
oxfmt: eslint.Rule.RuleModule;
|
|
26
|
+
oxfmt: _$eslint.Rule.RuleModule;
|
|
27
27
|
};
|
|
28
28
|
//#endregion
|
|
29
29
|
//#region src/parser.d.ts
|
package/dist/index.mjs
CHANGED
|
@@ -2,23 +2,17 @@ import { join } from "node:path";
|
|
|
2
2
|
import { createSyncFn } from "synckit";
|
|
3
3
|
import { URL, fileURLToPath } from "node:url";
|
|
4
4
|
import { DIFFERENCE, generateDifferences } from "generate-differences";
|
|
5
|
-
|
|
6
5
|
//#region \0rolldown/runtime.js
|
|
7
6
|
var __defProp = Object.defineProperty;
|
|
8
7
|
var __exportAll = (all, no_symbols) => {
|
|
9
8
|
let target = {};
|
|
10
|
-
for (var name in all) {
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
}
|
|
16
|
-
if (!no_symbols) {
|
|
17
|
-
__defProp(target, Symbol.toStringTag, { value: "Module" });
|
|
18
|
-
}
|
|
9
|
+
for (var name in all) __defProp(target, name, {
|
|
10
|
+
get: all[name],
|
|
11
|
+
enumerable: true
|
|
12
|
+
});
|
|
13
|
+
if (!no_symbols) __defProp(target, Symbol.toStringTag, { value: "Module" });
|
|
19
14
|
return target;
|
|
20
15
|
};
|
|
21
|
-
|
|
22
16
|
//#endregion
|
|
23
17
|
//#region node_modules/.pnpm/eslint-parser-plain@0.1.1/node_modules/eslint-parser-plain/dist/index.mjs
|
|
24
18
|
var dist_exports = /* @__PURE__ */ __exportAll({
|
|
@@ -47,11 +41,9 @@ const meta$1 = {
|
|
|
47
41
|
name: name$1,
|
|
48
42
|
version: version$1
|
|
49
43
|
};
|
|
50
|
-
|
|
51
44
|
//#endregion
|
|
52
45
|
//#region src/parser.ts
|
|
53
46
|
const parserPlain = dist_exports;
|
|
54
|
-
|
|
55
47
|
//#endregion
|
|
56
48
|
//#region src/configs.ts
|
|
57
49
|
const recommended = {
|
|
@@ -63,23 +55,15 @@ const recommended = {
|
|
|
63
55
|
rules: { "oxfmt/oxfmt": "error" }
|
|
64
56
|
};
|
|
65
57
|
const configs = { recommended };
|
|
66
|
-
|
|
67
|
-
//#endregion
|
|
68
|
-
//#region package.json
|
|
69
|
-
var name = "eslint-plugin-oxfmt";
|
|
70
|
-
var version = "0.1.1";
|
|
71
|
-
|
|
72
58
|
//#endregion
|
|
73
59
|
//#region src/meta.ts
|
|
74
60
|
const meta = {
|
|
75
|
-
name,
|
|
76
|
-
version
|
|
61
|
+
name: "eslint-plugin-oxfmt",
|
|
62
|
+
version: "0.2.0"
|
|
77
63
|
};
|
|
78
|
-
|
|
79
64
|
//#endregion
|
|
80
65
|
//#region src/dir.ts
|
|
81
66
|
const dirWorkers = fileURLToPath(new URL("../workers", import.meta.url));
|
|
82
|
-
|
|
83
67
|
//#endregion
|
|
84
68
|
//#region node_modules/.pnpm/show-invisibles@0.0.2/node_modules/show-invisibles/dist/index.js
|
|
85
69
|
const DEFAULT_MAPPINGS = new Map([
|
|
@@ -112,7 +96,6 @@ function showInvisibles(input, options = {}) {
|
|
|
112
96
|
if (typeof input !== "string") throw new TypeError(`Expected input to type string, got ${typeof input}`);
|
|
113
97
|
return input.split("").map((c) => mappings.get(c) || c).join("");
|
|
114
98
|
}
|
|
115
|
-
|
|
116
99
|
//#endregion
|
|
117
100
|
//#region src/reporter.ts
|
|
118
101
|
/**
|
|
@@ -149,7 +132,6 @@ function _reportDifference(context, difference, rangeOffset = 0) {
|
|
|
149
132
|
fix: (fixer) => fixer.replaceTextRange(range, insertText)
|
|
150
133
|
});
|
|
151
134
|
}
|
|
152
|
-
|
|
153
135
|
//#endregion
|
|
154
136
|
//#region src/schema.ts
|
|
155
137
|
const oxfmtOptionsSchema = {
|
|
@@ -201,6 +183,63 @@ const oxfmtOptionsSchema = {
|
|
|
201
183
|
description: `Whether to insert a final newline at the end of the file. (Default: true)`,
|
|
202
184
|
type: "boolean"
|
|
203
185
|
},
|
|
186
|
+
jsdoc: {
|
|
187
|
+
additionalProperties: false,
|
|
188
|
+
description: `Enable JSDoc comment formatting.\n\nWhen enabled, JSDoc comments are normalized and reformatted:\ntag aliases are canonicalized, descriptions are capitalized,\nlong lines are wrapped, and short comments are collapsed to single-line.\n\nPass an object (\`jsdoc: {}\`) to enable with defaults, or omit to disable.\n\n- (Default: Disabled)`,
|
|
189
|
+
type: "object",
|
|
190
|
+
properties: {
|
|
191
|
+
addDefaultToDescription: {
|
|
192
|
+
description: `Append default values to \`@param\` descriptions (e.g. "Default is \`value\`").\n\n- (Default: true)`,
|
|
193
|
+
type: "boolean"
|
|
194
|
+
},
|
|
195
|
+
bracketSpacing: {
|
|
196
|
+
description: `Add spaces inside JSDoc type braces: \`{string}\` → \`{ string }\`.\n\n- (Default: false)`,
|
|
197
|
+
type: "boolean"
|
|
198
|
+
},
|
|
199
|
+
capitalizeDescriptions: {
|
|
200
|
+
description: `Capitalize the first letter of tag descriptions.\n\n- (Default: true)`,
|
|
201
|
+
type: "boolean"
|
|
202
|
+
},
|
|
203
|
+
commentLineStrategy: {
|
|
204
|
+
description: `How to format comment blocks.\n\n- \`"singleLine"\` — Convert to single-line \`/** content */\` when possible.\n- \`"multiline"\` — Always use multi-line format.\n- \`"keep"\` — Preserve original formatting.\n\n- (Default: "singleLine")`,
|
|
205
|
+
enum: [
|
|
206
|
+
"singleLine",
|
|
207
|
+
"multiline",
|
|
208
|
+
"keep"
|
|
209
|
+
],
|
|
210
|
+
type: "string"
|
|
211
|
+
},
|
|
212
|
+
descriptionTag: {
|
|
213
|
+
description: `Emit \`@description\` tag instead of inline description.\n\n- (Default: false)`,
|
|
214
|
+
type: "boolean"
|
|
215
|
+
},
|
|
216
|
+
descriptionWithDot: {
|
|
217
|
+
description: `Add a trailing dot to the end of descriptions.\n\n- (Default: false)`,
|
|
218
|
+
type: "boolean"
|
|
219
|
+
},
|
|
220
|
+
keepUnparsableExampleIndent: {
|
|
221
|
+
description: `Preserve indentation in unparsable \`@example\` code.\n\n- (Default: false)`,
|
|
222
|
+
type: "boolean"
|
|
223
|
+
},
|
|
224
|
+
lineWrappingStyle: {
|
|
225
|
+
description: `Strategy for wrapping description lines at print width.\n\n- \`"greedy"\` — Always re-wrap text to fit within print width.\n- \`"balance"\` — Preserve original line breaks if all lines fit within print width.\n\n- (Default: "greedy")`,
|
|
226
|
+
enum: ["greedy", "balance"],
|
|
227
|
+
type: "string"
|
|
228
|
+
},
|
|
229
|
+
preferCodeFences: {
|
|
230
|
+
description: `Use fenced code blocks (\`\`\`) instead of 4-space indentation for code without a language tag.\n\n- (Default: false)`,
|
|
231
|
+
type: "boolean"
|
|
232
|
+
},
|
|
233
|
+
separateReturnsFromParam: {
|
|
234
|
+
description: `Add a blank line between the last \`@param\` and \`@returns\`.\n\n- (Default: false)`,
|
|
235
|
+
type: "boolean"
|
|
236
|
+
},
|
|
237
|
+
separateTagGroups: {
|
|
238
|
+
description: `Add blank lines between different tag groups (e.g. between \`@param\` and \`@returns\`).\n\n- (Default: false)`,
|
|
239
|
+
type: "boolean"
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
},
|
|
204
243
|
jsxSingleQuote: {
|
|
205
244
|
description: `Use single quotes instead of double quotes in JSX. (Default: false)`,
|
|
206
245
|
type: "boolean"
|
|
@@ -449,11 +488,12 @@ const oxfmtRuleSchema = {
|
|
|
449
488
|
}
|
|
450
489
|
}
|
|
451
490
|
};
|
|
452
|
-
|
|
453
491
|
//#endregion
|
|
454
492
|
//#region src/rules/oxfmt.ts
|
|
455
493
|
let formatViaOxfmt;
|
|
456
|
-
|
|
494
|
+
//#endregion
|
|
495
|
+
//#region src/rules/index.ts
|
|
496
|
+
const rules = { oxfmt: {
|
|
457
497
|
meta: {
|
|
458
498
|
defaultOptions: [],
|
|
459
499
|
fixable: "whitespace",
|
|
@@ -467,6 +507,8 @@ const oxfmt = {
|
|
|
467
507
|
}
|
|
468
508
|
},
|
|
469
509
|
create(context) {
|
|
510
|
+
const physicalFilename = context.physicalFilename ?? context.filename;
|
|
511
|
+
if (context.filename !== physicalFilename) return {};
|
|
470
512
|
if (!formatViaOxfmt) formatViaOxfmt = createSyncFn(join(dirWorkers, "oxfmt.mjs"));
|
|
471
513
|
const sourceText = context.sourceCode.text;
|
|
472
514
|
return { [context.sourceCode.ast.type || "Program"]() {
|
|
@@ -519,12 +561,7 @@ const oxfmt = {
|
|
|
519
561
|
}
|
|
520
562
|
} };
|
|
521
563
|
}
|
|
522
|
-
};
|
|
523
|
-
|
|
524
|
-
//#endregion
|
|
525
|
-
//#region src/rules/index.ts
|
|
526
|
-
const rules = { oxfmt };
|
|
527
|
-
|
|
564
|
+
} };
|
|
528
565
|
//#endregion
|
|
529
566
|
//#region src/index.ts
|
|
530
567
|
const plugin = {
|
|
@@ -532,6 +569,5 @@ const plugin = {
|
|
|
532
569
|
meta,
|
|
533
570
|
rules
|
|
534
571
|
};
|
|
535
|
-
|
|
536
572
|
//#endregion
|
|
537
|
-
export { configs, plugin as default, plugin, meta, parserPlain, recommended, rules };
|
|
573
|
+
export { configs, plugin as default, plugin, meta, parserPlain, recommended, rules };
|
package/dts/rule-options.d.ts
CHANGED
|
@@ -30,6 +30,31 @@ export type OxfmtOxfmt = []|[{
|
|
|
30
30
|
|
|
31
31
|
insertFinalNewline?: boolean
|
|
32
32
|
|
|
33
|
+
jsdoc?: {
|
|
34
|
+
|
|
35
|
+
addDefaultToDescription?: boolean
|
|
36
|
+
|
|
37
|
+
bracketSpacing?: boolean
|
|
38
|
+
|
|
39
|
+
capitalizeDescriptions?: boolean
|
|
40
|
+
|
|
41
|
+
commentLineStrategy?: ("singleLine" | "multiline" | "keep")
|
|
42
|
+
|
|
43
|
+
descriptionTag?: boolean
|
|
44
|
+
|
|
45
|
+
descriptionWithDot?: boolean
|
|
46
|
+
|
|
47
|
+
keepUnparsableExampleIndent?: boolean
|
|
48
|
+
|
|
49
|
+
lineWrappingStyle?: ("greedy" | "balance")
|
|
50
|
+
|
|
51
|
+
preferCodeFences?: boolean
|
|
52
|
+
|
|
53
|
+
separateReturnsFromParam?: boolean
|
|
54
|
+
|
|
55
|
+
separateTagGroups?: boolean
|
|
56
|
+
}
|
|
57
|
+
|
|
33
58
|
jsxSingleQuote?: boolean
|
|
34
59
|
|
|
35
60
|
objectWrap?: ("preserve" | "collapse" | "always")
|
|
@@ -135,6 +160,31 @@ export type OxfmtOxfmt = []|[{
|
|
|
135
160
|
|
|
136
161
|
insertFinalNewline?: boolean
|
|
137
162
|
|
|
163
|
+
jsdoc?: {
|
|
164
|
+
|
|
165
|
+
addDefaultToDescription?: boolean
|
|
166
|
+
|
|
167
|
+
bracketSpacing?: boolean
|
|
168
|
+
|
|
169
|
+
capitalizeDescriptions?: boolean
|
|
170
|
+
|
|
171
|
+
commentLineStrategy?: ("singleLine" | "multiline" | "keep")
|
|
172
|
+
|
|
173
|
+
descriptionTag?: boolean
|
|
174
|
+
|
|
175
|
+
descriptionWithDot?: boolean
|
|
176
|
+
|
|
177
|
+
keepUnparsableExampleIndent?: boolean
|
|
178
|
+
|
|
179
|
+
lineWrappingStyle?: ("greedy" | "balance")
|
|
180
|
+
|
|
181
|
+
preferCodeFences?: boolean
|
|
182
|
+
|
|
183
|
+
separateReturnsFromParam?: boolean
|
|
184
|
+
|
|
185
|
+
separateTagGroups?: boolean
|
|
186
|
+
}
|
|
187
|
+
|
|
138
188
|
jsxSingleQuote?: boolean
|
|
139
189
|
|
|
140
190
|
objectWrap?: ("preserve" | "collapse" | "always")
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "eslint-plugin-oxfmt",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "0.
|
|
4
|
+
"version": "0.2.0",
|
|
5
5
|
"description": "An ESLint plugin for formatting code with oxfmt.",
|
|
6
6
|
"keywords": [
|
|
7
7
|
"eslint",
|
|
@@ -44,35 +44,35 @@
|
|
|
44
44
|
"sideEffects": false,
|
|
45
45
|
"peerDependencies": {
|
|
46
46
|
"eslint": "^9.5.0 || ^10.0.0",
|
|
47
|
-
"oxfmt": ">=0.
|
|
47
|
+
"oxfmt": ">=0.42.0"
|
|
48
48
|
},
|
|
49
49
|
"dependencies": {
|
|
50
50
|
"generate-differences": "^0.1.1",
|
|
51
|
-
"load-oxfmt-config": "^0.
|
|
52
|
-
"picomatch": "^4.0.
|
|
51
|
+
"load-oxfmt-config": "^0.3.0",
|
|
52
|
+
"picomatch": "^4.0.4",
|
|
53
53
|
"synckit": "^0.11.12"
|
|
54
54
|
},
|
|
55
55
|
"devDependencies": {
|
|
56
|
-
"@ntnyq/eslint-config": "^6.0.0-beta.
|
|
56
|
+
"@ntnyq/eslint-config": "^6.0.0-beta.15",
|
|
57
57
|
"@types/json-schema": "^7.0.15",
|
|
58
|
-
"@types/node": "^25.
|
|
59
|
-
"@typescript/native-preview": "^7.0.0-dev.
|
|
60
|
-
"bumpp": "^
|
|
61
|
-
"eslint": "^10.0
|
|
58
|
+
"@types/node": "^25.5.0",
|
|
59
|
+
"@typescript/native-preview": "^7.0.0-dev.20260328.1",
|
|
60
|
+
"bumpp": "^11.0.1",
|
|
61
|
+
"eslint": "^10.1.0",
|
|
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
66
|
"nano-staged": "^0.9.0",
|
|
67
67
|
"npm-run-all2": "^8.0.4",
|
|
68
|
-
"oxfmt": "^0.
|
|
68
|
+
"oxfmt": "^0.42.0",
|
|
69
69
|
"show-invisibles": "^0.0.2",
|
|
70
70
|
"tinyglobby": "^0.2.15",
|
|
71
|
-
"tsdown": "^0.
|
|
71
|
+
"tsdown": "^0.21.7",
|
|
72
72
|
"tsx": "^4.21.0",
|
|
73
|
-
"typescript": "^
|
|
74
|
-
"vitest": "^4.
|
|
75
|
-
"eslint-plugin-oxfmt": "0.
|
|
73
|
+
"typescript": "^6.0.2",
|
|
74
|
+
"vitest": "^4.1.2",
|
|
75
|
+
"eslint-plugin-oxfmt": "0.2.0"
|
|
76
76
|
},
|
|
77
77
|
"engines": {
|
|
78
78
|
"node": "^20.19.0 || >=22.12.0"
|
package/workers/oxfmt.mjs
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// @ts-check
|
|
2
2
|
|
|
3
|
-
import { relative } from 'node:path'
|
|
4
|
-
import { loadOxfmtConfig } from 'load-oxfmt-config'
|
|
3
|
+
import { dirname, isAbsolute, join, relative } from 'node:path'
|
|
4
|
+
import { loadOxfmtConfig, resolveOxfmtrcPath } from 'load-oxfmt-config'
|
|
5
5
|
import { format } from 'oxfmt'
|
|
6
6
|
import picomatch from 'picomatch'
|
|
7
7
|
import { runAsWorker } from 'synckit'
|
|
@@ -12,17 +12,75 @@ import { runAsWorker } from 'synckit'
|
|
|
12
12
|
* @property {string} cwd - Current working directory for resolving configuration
|
|
13
13
|
* @property {string} [configPath] - Custom path to oxfmt configuration file
|
|
14
14
|
*/
|
|
15
|
+
|
|
15
16
|
/**
|
|
16
|
-
* @typedef {import('load-oxfmt-config').
|
|
17
|
+
* @typedef {import('load-oxfmt-config').OxfmtConfigOverride} Override
|
|
17
18
|
*/
|
|
18
19
|
/**
|
|
19
20
|
* @typedef {import('load-oxfmt-config').OxfmtOptions & PluginOptions} Options
|
|
20
21
|
*/
|
|
21
22
|
|
|
23
|
+
/**
|
|
24
|
+
* @typedef {object} ResolvedBaseOptions
|
|
25
|
+
* @property {import('oxfmt').FormatConfig} baseOptions Resolved base formatter options.
|
|
26
|
+
* @property {string} configDir Directory of the resolved config file, used as base for config-derived glob patterns.
|
|
27
|
+
*/
|
|
28
|
+
|
|
29
|
+
const MAX_CACHE_SIZE = 1000
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Evict oldest entries when the cache exceeds MAX_CACHE_SIZE.
|
|
33
|
+
* @param cache - The cache map to evict entries from
|
|
34
|
+
*/
|
|
35
|
+
function evictCache(
|
|
36
|
+
/** @type {Map<string, unknown>} */
|
|
37
|
+
cache,
|
|
38
|
+
) {
|
|
39
|
+
if (cache.size <= MAX_CACHE_SIZE) {
|
|
40
|
+
return
|
|
41
|
+
}
|
|
42
|
+
const keysToDelete = [...cache.keys()].slice(0, cache.size - MAX_CACHE_SIZE)
|
|
43
|
+
for (const key of keysToDelete) {
|
|
44
|
+
cache.delete(key)
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* JSON.stringify replacer that sorts object keys for stable serialization.
|
|
50
|
+
* @param key - The key of the property being processed
|
|
51
|
+
* @param value - The value of the property being processed
|
|
52
|
+
* @returns - The value to be serialized, with object keys sorted if it's an object
|
|
53
|
+
*/
|
|
54
|
+
function stableReplacer(
|
|
55
|
+
/** @type {string} */
|
|
56
|
+
key,
|
|
57
|
+
/** @type {unknown} */
|
|
58
|
+
value,
|
|
59
|
+
) {
|
|
60
|
+
if (value && typeof value === 'object' && !Array.isArray(value)) {
|
|
61
|
+
return Object.keys(value)
|
|
62
|
+
.sort()
|
|
63
|
+
.reduce((sorted, k) => {
|
|
64
|
+
sorted[k] = /** @type {Record<string, unknown>} */ (value)[k]
|
|
65
|
+
return sorted
|
|
66
|
+
}, /** @type {Record<string, unknown>} */ ({}))
|
|
67
|
+
}
|
|
68
|
+
return value
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/** @type {Map<string, Promise<ResolvedBaseOptions>>} */
|
|
72
|
+
const resolvedBaseOptionsCache = new Map()
|
|
73
|
+
|
|
74
|
+
/** @type {Map<string, import('oxfmt').FormatConfig>} */
|
|
75
|
+
const mergedOptionsCache = new Map()
|
|
76
|
+
|
|
77
|
+
/** @type {Map<string, ReturnType<typeof picomatch>>} */
|
|
78
|
+
const picomatchCache = new Map()
|
|
79
|
+
|
|
22
80
|
/**
|
|
23
81
|
* Apply overrides to the base options based on the filename
|
|
24
82
|
* @param filename - The file path
|
|
25
|
-
* @param cwd -
|
|
83
|
+
* @param cwd - Base directory for glob matching
|
|
26
84
|
* @param baseOptions - Base format options
|
|
27
85
|
* @param [overrides] - Override configurations
|
|
28
86
|
* @returns - Merged options
|
|
@@ -32,7 +90,7 @@ function applyOverrides(
|
|
|
32
90
|
filename,
|
|
33
91
|
/** @type {string} */
|
|
34
92
|
cwd,
|
|
35
|
-
/** @type {import('oxfmt').
|
|
93
|
+
/** @type {import('oxfmt').FormatConfig} */
|
|
36
94
|
baseOptions,
|
|
37
95
|
/** @type {Override[] | undefined} */
|
|
38
96
|
overrides,
|
|
@@ -52,12 +110,12 @@ function applyOverrides(
|
|
|
52
110
|
const { excludeFiles, files, options: overrideOptions } = override
|
|
53
111
|
|
|
54
112
|
// Check if file matches the files patterns
|
|
55
|
-
const matches =
|
|
113
|
+
const matches = getCachedMatcher(files)(relativePath)
|
|
56
114
|
|
|
57
115
|
// Check if file is excluded
|
|
58
116
|
const excluded =
|
|
59
117
|
excludeFiles && excludeFiles.length > 0
|
|
60
|
-
?
|
|
118
|
+
? getCachedMatcher(excludeFiles)(relativePath)
|
|
61
119
|
: false
|
|
62
120
|
|
|
63
121
|
if (matches && !excluded && overrideOptions) {
|
|
@@ -69,6 +127,231 @@ function applyOverrides(
|
|
|
69
127
|
return hasOverrides ? mergedOptions : baseOptions
|
|
70
128
|
}
|
|
71
129
|
|
|
130
|
+
/**
|
|
131
|
+
* Get or create a cached picomatch matcher for the given patterns.
|
|
132
|
+
* @param patterns - Glob patterns to match against file paths
|
|
133
|
+
* @returns - Picomatch matcher function
|
|
134
|
+
*/
|
|
135
|
+
function getCachedMatcher(
|
|
136
|
+
/** @type {string | string[]} */
|
|
137
|
+
patterns,
|
|
138
|
+
) {
|
|
139
|
+
const key = Array.isArray(patterns) ? patterns.join('\0') : patterns
|
|
140
|
+
const cached = picomatchCache.get(key)
|
|
141
|
+
if (cached) {
|
|
142
|
+
return cached
|
|
143
|
+
}
|
|
144
|
+
const matcher = picomatch(patterns)
|
|
145
|
+
picomatchCache.set(key, matcher)
|
|
146
|
+
evictCache(picomatchCache)
|
|
147
|
+
return matcher
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Resolve an effective config path for a file.
|
|
152
|
+
* - Absolute paths are returned as-is.
|
|
153
|
+
* - Relative paths are resolved from cwd.
|
|
154
|
+
*
|
|
155
|
+
* @param cwd - Current working directory from ESLint context.
|
|
156
|
+
* @param [configPath] - Optional user-provided config path.
|
|
157
|
+
* @returns Absolute config path when provided, otherwise undefined.
|
|
158
|
+
*/
|
|
159
|
+
function getConfigPathForFile(
|
|
160
|
+
/** @type {string} */
|
|
161
|
+
cwd,
|
|
162
|
+
/** @type {string | undefined} */
|
|
163
|
+
configPath,
|
|
164
|
+
) {
|
|
165
|
+
if (!configPath) {
|
|
166
|
+
return undefined
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
return isAbsolute(configPath) ? configPath : join(cwd, configPath)
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* 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.
|
|
176
|
+
*
|
|
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
|
+
* @param baseOptions - Resolved base options used for formatting.
|
|
181
|
+
* @param ignorePatterns - Rule-level ignore patterns.
|
|
182
|
+
* @param overrides - Rule-level override entries.
|
|
183
|
+
* @param useConfig - Whether config file loading is enabled.
|
|
184
|
+
* @returns Serialized cache key.
|
|
185
|
+
*/
|
|
186
|
+
function getMergedOptionsCacheKey(
|
|
187
|
+
/** @type {string} */
|
|
188
|
+
filename,
|
|
189
|
+
/** @type {string} */
|
|
190
|
+
cwd,
|
|
191
|
+
/** @type {string} */
|
|
192
|
+
configDir,
|
|
193
|
+
/** @type {import('oxfmt').FormatConfig} */
|
|
194
|
+
baseOptions,
|
|
195
|
+
/** @type {string[] | undefined} */
|
|
196
|
+
ignorePatterns,
|
|
197
|
+
/** @type {Override[] | undefined} */
|
|
198
|
+
overrides,
|
|
199
|
+
/** @type {boolean} */
|
|
200
|
+
useConfig,
|
|
201
|
+
) {
|
|
202
|
+
return JSON.stringify(
|
|
203
|
+
{
|
|
204
|
+
baseOptions,
|
|
205
|
+
configDir,
|
|
206
|
+
cwd,
|
|
207
|
+
filename,
|
|
208
|
+
ignorePatterns,
|
|
209
|
+
overrides,
|
|
210
|
+
useConfig,
|
|
211
|
+
},
|
|
212
|
+
stableReplacer,
|
|
213
|
+
)
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/**
|
|
217
|
+
* Build cache key for resolving base formatter options.
|
|
218
|
+
* The key includes file directory and all resolution inputs.
|
|
219
|
+
*
|
|
220
|
+
* @param filename - Current file path.
|
|
221
|
+
* @param cwd - Current working directory from ESLint context.
|
|
222
|
+
* @param configPath - Optional user-provided config path.
|
|
223
|
+
* @param useConfig - Whether config file loading is enabled.
|
|
224
|
+
* @param formatOptions - Rule-level format options.
|
|
225
|
+
* @returns Serialized cache key.
|
|
226
|
+
*/
|
|
227
|
+
function getResolvedBaseOptionsCacheKey(
|
|
228
|
+
/** @type {string} */
|
|
229
|
+
filename,
|
|
230
|
+
/** @type {string} */
|
|
231
|
+
cwd,
|
|
232
|
+
/** @type {string | undefined} */
|
|
233
|
+
configPath,
|
|
234
|
+
/** @type {boolean} */
|
|
235
|
+
useConfig,
|
|
236
|
+
/** @type {import('oxfmt').FormatConfig} */
|
|
237
|
+
formatOptions,
|
|
238
|
+
) {
|
|
239
|
+
return JSON.stringify(
|
|
240
|
+
{
|
|
241
|
+
configPath: configPath || '',
|
|
242
|
+
cwd,
|
|
243
|
+
fileDir: dirname(filename),
|
|
244
|
+
formatOptions,
|
|
245
|
+
useConfig,
|
|
246
|
+
},
|
|
247
|
+
stableReplacer,
|
|
248
|
+
)
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
/**
|
|
252
|
+
* Resolve base formatter options for a file and cache the async result.
|
|
253
|
+
*
|
|
254
|
+
* @param filename - Current file path.
|
|
255
|
+
* @param cwd - Current working directory from ESLint context.
|
|
256
|
+
* @param configPath - Optional user-provided config path.
|
|
257
|
+
* @param useConfig - Whether config file loading is enabled.
|
|
258
|
+
* @param formatOptions - Rule-level format options.
|
|
259
|
+
* @returns Base options after config loading and inline option merge.
|
|
260
|
+
*/
|
|
261
|
+
async function resolveBaseOptions(
|
|
262
|
+
/** @type {string} */
|
|
263
|
+
filename,
|
|
264
|
+
/** @type {string} */
|
|
265
|
+
cwd,
|
|
266
|
+
/** @type {string | undefined} */
|
|
267
|
+
configPath,
|
|
268
|
+
/** @type {boolean} */
|
|
269
|
+
useConfig,
|
|
270
|
+
/** @type {import('oxfmt').FormatConfig} */
|
|
271
|
+
formatOptions,
|
|
272
|
+
) {
|
|
273
|
+
const cacheKey = getResolvedBaseOptionsCacheKey(
|
|
274
|
+
filename,
|
|
275
|
+
cwd,
|
|
276
|
+
configPath,
|
|
277
|
+
useConfig,
|
|
278
|
+
formatOptions,
|
|
279
|
+
)
|
|
280
|
+
|
|
281
|
+
const cachedTask = resolvedBaseOptionsCache.get(cacheKey)
|
|
282
|
+
if (cachedTask) {
|
|
283
|
+
return cachedTask
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
const task = (async () => {
|
|
287
|
+
const resolveFromDir = dirname(filename)
|
|
288
|
+
|
|
289
|
+
if (!useConfig) {
|
|
290
|
+
return {
|
|
291
|
+
configDir: cwd,
|
|
292
|
+
baseOptions: {
|
|
293
|
+
...formatOptions,
|
|
294
|
+
},
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
const resolvedConfigPath = getConfigPathForFile(cwd, configPath)
|
|
299
|
+
const resolvedPath = await resolveOxfmtrcPath(
|
|
300
|
+
resolveFromDir,
|
|
301
|
+
resolvedConfigPath,
|
|
302
|
+
)
|
|
303
|
+
const configDir = resolvedPath ? dirname(resolvedPath) : cwd
|
|
304
|
+
const configOptions = await loadOxfmtConfig({
|
|
305
|
+
configPath: resolvedConfigPath,
|
|
306
|
+
cwd: resolveFromDir,
|
|
307
|
+
})
|
|
308
|
+
|
|
309
|
+
return {
|
|
310
|
+
configDir,
|
|
311
|
+
baseOptions: {
|
|
312
|
+
...configOptions,
|
|
313
|
+
...formatOptions,
|
|
314
|
+
},
|
|
315
|
+
}
|
|
316
|
+
})()
|
|
317
|
+
|
|
318
|
+
resolvedBaseOptionsCache.set(cacheKey, task)
|
|
319
|
+
evictCache(resolvedBaseOptionsCache)
|
|
320
|
+
|
|
321
|
+
try {
|
|
322
|
+
return await task
|
|
323
|
+
} catch (err) {
|
|
324
|
+
resolvedBaseOptionsCache.delete(cacheKey)
|
|
325
|
+
throw err
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
/**
|
|
330
|
+
* Check if a file should be ignored based on ignorePatterns
|
|
331
|
+
* @param filename - The file path
|
|
332
|
+
* @param cwd - Base directory for glob matching
|
|
333
|
+
* @param [ignorePatterns] - Ignore patterns
|
|
334
|
+
* @returns - Whether the file should be ignored
|
|
335
|
+
*/
|
|
336
|
+
function shouldIgnoreFile(
|
|
337
|
+
/** @type {string} */
|
|
338
|
+
filename,
|
|
339
|
+
/** @type {string} */
|
|
340
|
+
cwd,
|
|
341
|
+
/** @type {string[] | undefined} */
|
|
342
|
+
ignorePatterns,
|
|
343
|
+
) {
|
|
344
|
+
if (!ignorePatterns || ignorePatterns.length === 0) {
|
|
345
|
+
return false
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
// Get relative path from cwd and normalize to forward slashes for cross-platform compatibility
|
|
349
|
+
const relativePath = relative(cwd, filename).replace(/\\/g, '/')
|
|
350
|
+
|
|
351
|
+
// Check if file matches any ignore pattern
|
|
352
|
+
return getCachedMatcher(ignorePatterns)(relativePath)
|
|
353
|
+
}
|
|
354
|
+
|
|
72
355
|
runAsWorker(
|
|
73
356
|
async (
|
|
74
357
|
/**
|
|
@@ -87,31 +370,68 @@ runAsWorker(
|
|
|
87
370
|
const {
|
|
88
371
|
configPath,
|
|
89
372
|
cwd,
|
|
373
|
+
ignorePatterns,
|
|
90
374
|
overrides,
|
|
91
375
|
useConfig = true,
|
|
92
376
|
...formatOptions
|
|
93
377
|
} = options
|
|
94
378
|
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
: {}),
|
|
103
|
-
...formatOptions,
|
|
104
|
-
}
|
|
379
|
+
const { baseOptions, configDir } = await resolveBaseOptions(
|
|
380
|
+
filename,
|
|
381
|
+
cwd,
|
|
382
|
+
configPath,
|
|
383
|
+
useConfig,
|
|
384
|
+
formatOptions,
|
|
385
|
+
)
|
|
105
386
|
|
|
106
|
-
|
|
107
|
-
const mergedOptions = applyOverrides(
|
|
387
|
+
const mergedOptionsCacheKey = getMergedOptionsCacheKey(
|
|
108
388
|
filename,
|
|
109
389
|
cwd,
|
|
390
|
+
configDir,
|
|
110
391
|
baseOptions,
|
|
111
|
-
|
|
392
|
+
ignorePatterns,
|
|
393
|
+
overrides,
|
|
394
|
+
useConfig,
|
|
395
|
+
)
|
|
396
|
+
|
|
397
|
+
const baseIgnorePatterns = /** @type {string[] | undefined} */ (
|
|
398
|
+
baseOptions.ignorePatterns
|
|
112
399
|
)
|
|
400
|
+
const effectiveIgnorePatterns = ignorePatterns ?? baseIgnorePatterns
|
|
401
|
+
const ignoreBase = ignorePatterns == null ? configDir : cwd
|
|
402
|
+
|
|
403
|
+
if (shouldIgnoreFile(filename, ignoreBase, effectiveIgnorePatterns)) {
|
|
404
|
+
return { code: sourceText }
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
const cachedMergedOptions = mergedOptionsCache.get(mergedOptionsCacheKey)
|
|
408
|
+
if (cachedMergedOptions) {
|
|
409
|
+
return format(filename, sourceText, cachedMergedOptions)
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
const baseOverrides = /** @type {Override[] | undefined} */ (
|
|
413
|
+
baseOptions.overrides
|
|
414
|
+
)
|
|
415
|
+
|
|
416
|
+
// Apply config-level overrides (relative to config directory)
|
|
417
|
+
let mergedOptions = baseOptions
|
|
418
|
+
if (useConfig && baseOverrides && baseOverrides.length > 0) {
|
|
419
|
+
mergedOptions = applyOverrides(
|
|
420
|
+
filename,
|
|
421
|
+
configDir,
|
|
422
|
+
mergedOptions,
|
|
423
|
+
baseOverrides,
|
|
424
|
+
)
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
// Apply rule-level overrides (relative to ESLint cwd)
|
|
428
|
+
if (overrides && overrides.length > 0) {
|
|
429
|
+
mergedOptions = applyOverrides(filename, cwd, mergedOptions, overrides)
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
mergedOptionsCache.set(mergedOptionsCacheKey, mergedOptions)
|
|
433
|
+
evictCache(mergedOptionsCache)
|
|
113
434
|
|
|
114
|
-
|
|
115
|
-
return formatResult
|
|
435
|
+
return format(filename, sourceText, mergedOptions)
|
|
116
436
|
},
|
|
117
437
|
)
|