@letstri/oxc-config 0.1.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 ADDED
@@ -0,0 +1,247 @@
1
+ # Oxlint and Oxfmt configs
2
+
3
+ Opinionated, shared [oxlint](https://oxc.rs/docs/guide/usage/linter.html) and [oxfmt](https://oxc.rs) config, in the spirit of [@antfu/eslint-config](https://github.com/antfu/eslint-config).
4
+
5
+ > [!NOTE]
6
+ > This is an **opinionated** config — it ships a curated set of rules and
7
+ > formatting defaults meant to work out of the box. Override anything you
8
+ > disagree with via the [`override`](#overrides) option.
9
+
10
+ ## Install
11
+
12
+ ```bash
13
+ pnpm add -D @letstri/oxc-config oxlint oxfmt
14
+ ```
15
+
16
+ ## AI setup prompt
17
+
18
+ Paste this into Claude Code, Cursor, or any coding agent to wire everything up:
19
+
20
+ <details>
21
+ <summary>Show prompt</summary>
22
+
23
+ ````text
24
+ Set up @letstri/oxc-config (oxlint + oxfmt) in this project:
25
+
26
+ 1. Install dev deps: `@letstri/oxc-config oxlint oxfmt`.
27
+ 2. Remove ESLint and Prettier: their configs, deps, and scripts.
28
+ 3. Create `oxlint.config.ts`:
29
+ ```ts
30
+ import { oxlintConfig } from '@letstri/oxc-config'
31
+
32
+ export default oxlintConfig()
33
+ ```
34
+ 4. Create `oxfmt.config.ts`:
35
+ ```ts
36
+ import { oxfmtConfig } from '@letstri/oxc-config'
37
+
38
+ export default oxfmtConfig()
39
+ ```
40
+ 5. Add package.json scripts:
41
+ - "lint": "oxlint"
42
+ - "lint:fix": "oxlint --fix"
43
+ - "format": "oxfmt"
44
+ - "format:check": "oxfmt --check"
45
+ 6. Framework plugins auto-enable from the nearest package.json. If a framework
46
+ dep (react/vue/next/vitest/jest/typescript) lives in a nested workspace,
47
+ enable it manually, e.g. `oxlintConfig({ vue: true })`.
48
+ 7. Add the VS Code and Zed editor settings from the @letstri/oxc-config README.
49
+ 8. Run `pnpm lint` and `pnpm format` and fix anything reported.
50
+ ````
51
+
52
+ </details>
53
+
54
+ ## Usage
55
+
56
+ `oxlint.config.ts`:
57
+
58
+ ```ts
59
+ import { oxlintConfig } from '@letstri/oxc-config'
60
+
61
+ export default oxlintConfig()
62
+ ```
63
+
64
+ `oxfmt.config.ts`:
65
+
66
+ ```ts
67
+ import { oxfmtConfig } from '@letstri/oxc-config'
68
+
69
+ export default oxfmtConfig()
70
+ ```
71
+
72
+ ### Plugins
73
+
74
+ Framework plugins are enabled automatically by detecting dependencies in the
75
+ nearest `package.json` (`typescript`, `react`, `vue`, `next`, `vitest`, `jest`,
76
+ `tailwind`). Toggle any of them manually — useful when the dependency lives in a
77
+ nested workspace such as `apps/web/package.json`:
78
+
79
+ ```ts
80
+ export default oxlintConfig({
81
+ vue: true, // force on
82
+ jest: false, // force off
83
+ })
84
+ ```
85
+
86
+ ### Tailwind
87
+
88
+ `tailwind: true` enables [`eslint-plugin-better-tailwindcss`](https://github.com/schoero/eslint-plugin-better-tailwindcss)
89
+ (auto-detected from a `tailwindcss` dependency). The plugin is an **optional peer
90
+ dependency** — install it yourself:
91
+
92
+ ```bash
93
+ pnpm add -D eslint-plugin-better-tailwindcss
94
+ ```
95
+
96
+ If Tailwind is enabled but the plugin is missing, the config logs a reminder and
97
+ skips Tailwind linting instead of crashing. Point the plugin at your Tailwind
98
+ entry CSS so it can resolve class names:
99
+
100
+ ```ts
101
+ export default oxlintConfig({
102
+ tailwind: true,
103
+ override: {
104
+ settings: {
105
+ 'better-tailwindcss': {
106
+ entryPoint: 'src/styles/globals.css',
107
+ },
108
+ },
109
+ },
110
+ })
111
+ ```
112
+
113
+ ### Overrides
114
+
115
+ `override` is deep-merged over the base config via [defu](https://github.com/unjs/defu):
116
+
117
+ ```ts
118
+ export default oxlintConfig({
119
+ override: {
120
+ rules: {
121
+ 'no-console': 'off',
122
+ },
123
+ },
124
+ })
125
+ ```
126
+
127
+ ## Editor setup
128
+
129
+ Both editors use the official [oxc](https://oxc.rs) tooling — `oxlint` for
130
+ linting and `oxfmt` for formatting — replacing ESLint and Prettier.
131
+
132
+ ### VS Code
133
+
134
+ Install the [`oxc.oxc-vscode`](https://marketplace.visualstudio.com/items?itemName=oxc.oxc-vscode)
135
+ extension.
136
+
137
+ <details>
138
+ <summary>Show VS Code config</summary>
139
+
140
+ `.vscode/extensions.json`:
141
+
142
+ ```json
143
+ {
144
+ "recommendations": ["oxc.oxc-vscode"]
145
+ }
146
+ ```
147
+
148
+ `.vscode/settings.json`:
149
+
150
+ ```jsonc
151
+ {
152
+ "oxc.configPath": "oxlint.config.ts",
153
+ "oxc.fmt.configPath": "oxfmt.config.ts",
154
+ "oxc.typeAware": true,
155
+ "oxc.unusedDisableDirectives": "deny",
156
+ "oxc.enable": true,
157
+ "prettier.enable": false,
158
+ "editor.defaultFormatter": "oxc.oxc-vscode",
159
+ "editor.formatOnSave": true,
160
+ "editor.formatOnSaveMode": "file", // oxfmt can only format whole files
161
+ "editor.codeActionsOnSave": {
162
+ "source.fixAll.oxc": "explicit",
163
+ "source.organizeImports": "never", // let oxfmt handle import organization
164
+ },
165
+ }
166
+ ```
167
+
168
+ </details>
169
+
170
+ ### Zed
171
+
172
+ Zed ships with the oxc language servers built in.
173
+
174
+ <details>
175
+ <summary>Show Zed config</summary>
176
+
177
+ `.zed/settings.json`:
178
+
179
+ ```jsonc
180
+ {
181
+ "lsp": {
182
+ "oxlint": {
183
+ "initialization_options": {
184
+ "settings": {
185
+ "configPath": null,
186
+ "run": "onType",
187
+ "disableNestedConfig": false,
188
+ "fixKind": "safe_fix",
189
+ "unusedDisableDirectives": "deny",
190
+ },
191
+ },
192
+ },
193
+ "oxfmt": {
194
+ "initialization_options": {
195
+ "settings": {
196
+ "fmt.configPath": null,
197
+ "run": "onSave",
198
+ },
199
+ },
200
+ },
201
+ },
202
+ "languages": {
203
+ "TypeScript": {
204
+ "format_on_save": "on",
205
+ "prettier": { "allowed": false },
206
+ "language_servers": ["...", "oxlint", "oxfmt"],
207
+ "formatter": [
208
+ { "language_server": { "name": "oxfmt" } },
209
+ { "code_action": "source.fixAll.oxc" },
210
+ ],
211
+ },
212
+ "TSX": {
213
+ "format_on_save": "on",
214
+ "prettier": { "allowed": false },
215
+ "language_servers": ["...", "oxlint", "oxfmt"],
216
+ "formatter": [
217
+ { "language_server": { "name": "oxfmt" } },
218
+ { "code_action": "source.fixAll.oxc" },
219
+ ],
220
+ },
221
+ "JavaScript": {
222
+ "format_on_save": "on",
223
+ "prettier": { "allowed": false },
224
+ "language_servers": ["...", "oxlint", "oxfmt"],
225
+ "formatter": [
226
+ { "language_server": { "name": "oxfmt" } },
227
+ { "code_action": "source.fixAll.oxc" },
228
+ ],
229
+ },
230
+ "JSON": {
231
+ "format_on_save": "on",
232
+ "prettier": { "allowed": false },
233
+ "formatter": [{ "language_server": { "name": "oxfmt" } }],
234
+ },
235
+ "Markdown": {
236
+ "format_on_save": "on",
237
+ "prettier": { "allowed": false },
238
+ "formatter": [{ "language_server": { "name": "oxfmt" } }],
239
+ },
240
+ },
241
+ }
242
+ ```
243
+
244
+ Apply the same `languages` block to any other file types you want oxfmt to
245
+ format (`JSONC`, `CSS`, `HTML`, `YAML`, …).
246
+
247
+ </details>
@@ -0,0 +1,61 @@
1
+ import { defineConfig } from "oxfmt";
2
+ import { defineConfig as defineConfig$1 } from "oxlint";
3
+ //#region src/index.d.ts
4
+ type OxlintOptions = Parameters<typeof defineConfig$1>[0];
5
+ type OxfmtOptions = Parameters<typeof defineConfig>[0];
6
+ declare const pluginToggles: {
7
+ typescript: {
8
+ packages: string[];
9
+ plugins: "typescript"[];
10
+ };
11
+ react: {
12
+ packages: string[];
13
+ plugins: ("react" | "jsx-a11y" | "react-perf")[];
14
+ };
15
+ vue: {
16
+ packages: string[];
17
+ plugins: "vue"[];
18
+ };
19
+ next: {
20
+ packages: string[];
21
+ plugins: "nextjs"[];
22
+ };
23
+ vitest: {
24
+ packages: string[];
25
+ plugins: "vitest"[];
26
+ };
27
+ jest: {
28
+ packages: string[];
29
+ plugins: "jest"[];
30
+ };
31
+ };
32
+ type ToggleName = keyof typeof pluginToggles;
33
+ interface OxlintConfigOptions extends Partial<Record<ToggleName, boolean>> {
34
+ /**
35
+ * Enable `eslint-plugin-better-tailwindcss`. Auto-detected from a `tailwindcss`
36
+ * dependency; set it explicitly when Tailwind lives in a nested workspace.
37
+ *
38
+ * The plugin is an optional peer dependency — install it yourself
39
+ * (`pnpm add -D eslint-plugin-better-tailwindcss`). If it is missing, a warning
40
+ * is logged and Tailwind linting is skipped. Point the plugin at your entry
41
+ * CSS via `override.settings`.
42
+ */
43
+ tailwind?: boolean;
44
+ /**
45
+ * Deep-merged over the base config via defu.
46
+ */
47
+ override?: OxlintOptions;
48
+ /**
49
+ * Directory whose `package.json` is scanned to auto-detect plugins.
50
+ *
51
+ * @default process.cwd()
52
+ */
53
+ cwd?: string;
54
+ }
55
+ interface OxfmtConfigOptions {
56
+ override?: OxfmtOptions;
57
+ }
58
+ declare function oxlintConfig(options?: OxlintConfigOptions): OxlintOptions;
59
+ declare function oxfmtConfig({ override }?: OxfmtConfigOptions): OxfmtOptions;
60
+ //#endregion
61
+ export { oxfmtConfig, oxlintConfig };
package/dist/index.mjs ADDED
@@ -0,0 +1,588 @@
1
+ import { readFileSync } from "node:fs";
2
+ import { resolve } from "node:path";
3
+ import process from "node:process";
4
+ import { defu } from "defu";
5
+ import { defineConfig } from "oxfmt";
6
+ import { defineConfig as defineConfig$1 } from "oxlint";
7
+ //#region src/index.ts
8
+ const baseOxlintConfig = defineConfig$1({
9
+ categories: {
10
+ correctness: "error",
11
+ suspicious: "warn",
12
+ perf: "warn"
13
+ },
14
+ env: {
15
+ builtin: true,
16
+ es2026: true,
17
+ browser: true,
18
+ node: true
19
+ },
20
+ globals: {
21
+ AudioPlaybackStats: "readonly",
22
+ CSSFontFaceDescriptors: "readonly",
23
+ CSSPseudoElement: "readonly",
24
+ LanguageModel: "readonly",
25
+ ModelContext: "readonly",
26
+ Sanitizer: "readonly",
27
+ TimelineTrigger: "readonly",
28
+ TimelineTriggerRange: "readonly",
29
+ TimelineTriggerRangeList: "readonly",
30
+ WebMCPEvent: "readonly",
31
+ XRCompositionLayer: "readonly",
32
+ XRCubeLayer: "readonly",
33
+ XRCylinderLayer: "readonly",
34
+ XREquirectLayer: "readonly",
35
+ XRLayerEvent: "readonly",
36
+ XRPlane: "readonly",
37
+ XRPlaneSet: "readonly",
38
+ XRProjectionLayer: "readonly",
39
+ XRQuadLayer: "readonly",
40
+ XRSubImage: "readonly",
41
+ XRWebGLSubImage: "readonly"
42
+ },
43
+ ignorePatterns: [
44
+ "**/.DS_Store",
45
+ "**/node_modules",
46
+ "**/.turbo",
47
+ "**/*.tsbuildinfo",
48
+ "**/.idea",
49
+ "**/.node-modules-inspector",
50
+ "**/.pnpm-store",
51
+ "**/dist",
52
+ "**/package-lock.json",
53
+ "**/yarn.lock",
54
+ "**/pnpm-lock.yaml",
55
+ "**/bun.lockb",
56
+ "**/output",
57
+ "**/coverage",
58
+ "**/temp",
59
+ "**/.temp",
60
+ "**/tmp",
61
+ "**/.tmp",
62
+ "**/.history",
63
+ "**/.vitepress/cache",
64
+ "**/.nuxt",
65
+ "**/.next",
66
+ "**/.svelte-kit",
67
+ "**/.vercel",
68
+ "**/.changeset",
69
+ "**/.cache",
70
+ "**/.output",
71
+ "**/.vite-inspect",
72
+ "**/.yarn",
73
+ "**/CHANGELOG*.md",
74
+ "**/LICENSE*",
75
+ "**/*.min.*",
76
+ "**/__snapshots__",
77
+ "**/vite.config.*.timestamp-*",
78
+ "**/auto-import?(s).d.ts",
79
+ "**/components.d.ts",
80
+ "**/.context",
81
+ "**/.claude",
82
+ "**/.agents",
83
+ "**/.*/skills",
84
+ "**/*.md",
85
+ "**/routeTree.gen.ts",
86
+ "**/release/**/*",
87
+ "**/.tanstack/**/*",
88
+ "**/.nitro/**/*",
89
+ "**/.output/**/*",
90
+ "**/.source/**/*",
91
+ "**/dist-electron/**/*",
92
+ "**/dist-desktop/**/*",
93
+ "**/playwright-report/**/*",
94
+ "**/test-results/**/*",
95
+ "**/.types/**/*",
96
+ "**/migrations/**/*",
97
+ "**/migrations.json"
98
+ ],
99
+ rules: {
100
+ "accessor-pairs": ["error", {
101
+ enforceForClassMembers: true,
102
+ setWithoutGet: true
103
+ }],
104
+ "array-callback-return": "error",
105
+ "block-scoped-var": "error",
106
+ "constructor-super": "error",
107
+ "default-case-last": "error",
108
+ "eqeqeq": ["error", "smart"],
109
+ "new-cap": ["error", {
110
+ capIsNew: false,
111
+ newIsCap: true,
112
+ properties: true
113
+ }],
114
+ "no-alert": "error",
115
+ "no-array-constructor": "error",
116
+ "no-async-promise-executor": "error",
117
+ "no-caller": "error",
118
+ "no-case-declarations": "error",
119
+ "no-class-assign": "error",
120
+ "no-compare-neg-zero": "error",
121
+ "no-cond-assign": ["error", "always"],
122
+ "no-console": ["warn", { allow: ["warn", "error"] }],
123
+ "no-const-assign": "error",
124
+ "no-control-regex": "error",
125
+ "no-debugger": "error",
126
+ "no-delete-var": "error",
127
+ "no-dupe-class-members": "error",
128
+ "no-dupe-keys": "error",
129
+ "no-duplicate-case": "error",
130
+ "no-empty": ["error", { allowEmptyCatch: true }],
131
+ "no-empty-pattern": "error",
132
+ "no-eval": "error",
133
+ "no-ex-assign": "error",
134
+ "no-extend-native": "error",
135
+ "no-extra-bind": "error",
136
+ "no-extra-boolean-cast": "error",
137
+ "no-fallthrough": "error",
138
+ "no-func-assign": "error",
139
+ "no-global-assign": "error",
140
+ "no-implied-eval": "error",
141
+ "no-import-assign": "error",
142
+ "no-irregular-whitespace": "error",
143
+ "no-iterator": "error",
144
+ "no-labels": ["error", {
145
+ allowLoop: false,
146
+ allowSwitch: false
147
+ }],
148
+ "no-lone-blocks": "error",
149
+ "no-loss-of-precision": "error",
150
+ "no-misleading-character-class": "error",
151
+ "no-multi-str": "error",
152
+ "no-new": "error",
153
+ "no-new-func": "error",
154
+ "no-new-native-nonconstructor": "error",
155
+ "no-new-wrappers": "error",
156
+ "no-obj-calls": "error",
157
+ "no-proto": "error",
158
+ "no-prototype-builtins": "error",
159
+ "no-redeclare": ["error", { builtinGlobals: false }],
160
+ "no-regex-spaces": "error",
161
+ "no-restricted-globals": [
162
+ "error",
163
+ {
164
+ message: "Use `globalThis` instead.",
165
+ name: "global"
166
+ },
167
+ {
168
+ message: "Use `globalThis` instead.",
169
+ name: "self"
170
+ }
171
+ ],
172
+ "no-restricted-properties": [
173
+ "error",
174
+ {
175
+ message: "Use `Object.getPrototypeOf` or `Object.setPrototypeOf` instead.",
176
+ property: "__proto__"
177
+ },
178
+ {
179
+ message: "Use `Object.defineProperty` instead.",
180
+ property: "__defineGetter__"
181
+ },
182
+ {
183
+ message: "Use `Object.defineProperty` instead.",
184
+ property: "__defineSetter__"
185
+ },
186
+ {
187
+ message: "Use `Object.getOwnPropertyDescriptor` instead.",
188
+ property: "__lookupGetter__"
189
+ },
190
+ {
191
+ message: "Use `Object.getOwnPropertyDescriptor` instead.",
192
+ property: "__lookupSetter__"
193
+ }
194
+ ],
195
+ "no-self-assign": ["error", { props: true }],
196
+ "no-self-compare": "error",
197
+ "no-sequences": "error",
198
+ "no-shadow": "off",
199
+ "no-shadow-restricted-names": "error",
200
+ "no-sparse-arrays": "error",
201
+ "no-template-curly-in-string": "error",
202
+ "no-this-before-super": "error",
203
+ "no-throw-literal": "error",
204
+ "no-underscore-dangle": ["warn", { allow: [
205
+ "__filename",
206
+ "__dirname",
207
+ "_url",
208
+ "_URL"
209
+ ] }],
210
+ "no-unexpected-multiline": "error",
211
+ "no-unmodified-loop-condition": "error",
212
+ "no-unneeded-ternary": ["error", { defaultAssignment: false }],
213
+ "no-unreachable": "error",
214
+ "no-unsafe-finally": "error",
215
+ "no-unsafe-negation": "error",
216
+ "no-unused-expressions": ["error", {
217
+ allowShortCircuit: true,
218
+ allowTaggedTemplates: true,
219
+ allowTernary: true
220
+ }],
221
+ "no-unused-vars": ["error", {
222
+ args: "none",
223
+ argsIgnorePattern: "^_",
224
+ caughtErrors: "none",
225
+ ignoreRestSiblings: true,
226
+ vars: "all",
227
+ varsIgnorePattern: "^_"
228
+ }],
229
+ "no-use-before-define": ["error", {
230
+ classes: false,
231
+ functions: false,
232
+ variables: true
233
+ }],
234
+ "no-useless-call": "error",
235
+ "no-useless-catch": "error",
236
+ "no-useless-computed-key": "error",
237
+ "no-useless-constructor": "error",
238
+ "no-useless-rename": "error",
239
+ "no-useless-return": "error",
240
+ "no-var": "error",
241
+ "no-with": "error",
242
+ "object-shorthand": [
243
+ "error",
244
+ "always",
245
+ {
246
+ avoidQuotes: true,
247
+ ignoreConstructors: false
248
+ }
249
+ ],
250
+ "prefer-const": ["error", {
251
+ destructuring: "all",
252
+ ignoreReadBeforeAssign: true
253
+ }],
254
+ "prefer-exponentiation-operator": "error",
255
+ "prefer-promise-reject-errors": "error",
256
+ "prefer-regex-literals": ["error", { disallowRedundantWrapping: true }],
257
+ "prefer-rest-params": "error",
258
+ "prefer-spread": "error",
259
+ "prefer-template": "error",
260
+ "symbol-description": "error",
261
+ "unicode-bom": ["error", "never"],
262
+ "use-isnan": ["error", {
263
+ enforceForIndexOf: true,
264
+ enforceForSwitchCase: true
265
+ }],
266
+ "valid-typeof": ["error", { requireStringLiterals: true }],
267
+ "vars-on-top": "error",
268
+ "yoda": ["error", "never"],
269
+ "import/consistent-type-specifier-style": ["error", "prefer-top-level"],
270
+ "import/first": "error",
271
+ "import/no-duplicates": "error",
272
+ "import/no-mutable-exports": "error",
273
+ "import/no-named-default": "error",
274
+ "import/no-unassigned-import": "off",
275
+ "import/newline-after-import": ["error", { count: 1 }],
276
+ "no-unassigned-vars": "warn",
277
+ "preserve-caught-error": "warn",
278
+ "react/react-in-jsx-scope": "off",
279
+ "jsx-a11y/anchor-has-content": "warn",
280
+ "jsx-a11y/click-events-have-key-events": "warn",
281
+ "jsx-a11y/control-has-associated-label": "warn",
282
+ "jsx-a11y/interactive-supports-focus": "warn",
283
+ "jsx-a11y/label-has-associated-control": "warn",
284
+ "jsx-a11y/mouse-events-have-key-events": "warn",
285
+ "jsx-a11y/no-autofocus": "off",
286
+ "jsx-a11y/no-noninteractive-element-interactions": "warn",
287
+ "jsx-a11y/no-noninteractive-tabindex": "warn",
288
+ "jsx-a11y/no-redundant-roles": "warn",
289
+ "jsx-a11y/no-static-element-interactions": "warn",
290
+ "jsx-a11y/prefer-tag-over-role": "warn",
291
+ "react/rules-of-hooks": "error",
292
+ "react/exhaustive-deps": ["warn", { additionalHooks: "(useMountedEffect)" }],
293
+ "typescript/no-explicit-any": "error"
294
+ },
295
+ overrides: [
296
+ {
297
+ files: ["**/*.?([cm])[jt]s?(x)"],
298
+ rules: {
299
+ "node/handle-callback-err": ["error", "^(err|error)$"],
300
+ "node/no-exports-assign": "error",
301
+ "node/no-new-require": "error",
302
+ "node/no-path-concat": "error",
303
+ "jsdoc/check-access": "warn",
304
+ "jsdoc/check-property-names": "warn",
305
+ "jsdoc/empty-tags": "warn",
306
+ "jsdoc/implements-on-classes": "warn",
307
+ "jsdoc/no-defaults": "warn",
308
+ "jsdoc/require-param-name": "warn",
309
+ "jsdoc/require-property": "warn",
310
+ "jsdoc/require-property-description": "warn",
311
+ "jsdoc/require-property-name": "warn",
312
+ "jsdoc/require-returns-description": "warn",
313
+ "unicorn/consistent-empty-array-spread": "error",
314
+ "unicorn/error-message": "error",
315
+ "unicorn/escape-case": "error",
316
+ "unicorn/new-for-builtins": "error",
317
+ "unicorn/no-instanceof-builtins": "error",
318
+ "unicorn/no-new-array": "error",
319
+ "unicorn/no-new-buffer": "error",
320
+ "unicorn/number-literal-case": "error",
321
+ "unicorn/prefer-dom-node-text-content": "error",
322
+ "unicorn/prefer-includes": "error",
323
+ "unicorn/prefer-node-protocol": "error",
324
+ "unicorn/prefer-number-properties": "error",
325
+ "unicorn/prefer-string-starts-ends-with": "error",
326
+ "unicorn/prefer-type-error": "error",
327
+ "unicorn/throw-new-error": "error"
328
+ },
329
+ plugins: [
330
+ "node",
331
+ "jsdoc",
332
+ "unicorn"
333
+ ]
334
+ },
335
+ {
336
+ files: ["**/*.?([cm])ts", "**/*.?([cm])tsx"],
337
+ rules: {
338
+ "constructor-super": "off",
339
+ "getter-return": "off",
340
+ "no-class-assign": "off",
341
+ "no-const-assign": "off",
342
+ "no-dupe-keys": "off",
343
+ "no-func-assign": "off",
344
+ "no-import-assign": "off",
345
+ "no-new-native-nonconstructor": "off",
346
+ "no-obj-calls": "off",
347
+ "no-redeclare": ["error", { builtinGlobals: false }],
348
+ "no-setter-return": "off",
349
+ "no-this-before-super": "off",
350
+ "no-unreachable": "off",
351
+ "no-unsafe-negation": "off",
352
+ "no-with": "off",
353
+ "prefer-const": ["error", {
354
+ destructuring: "all",
355
+ ignoreReadBeforeAssign: true
356
+ }],
357
+ "no-unused-expressions": ["error", {
358
+ allowShortCircuit: true,
359
+ allowTaggedTemplates: true,
360
+ allowTernary: true
361
+ }],
362
+ "no-unused-vars": "off",
363
+ "no-useless-constructor": "off",
364
+ "no-use-before-define": ["error", {
365
+ classes: false,
366
+ functions: false,
367
+ variables: true
368
+ }],
369
+ "typescript/ban-ts-comment": ["error", { "ts-expect-error": "allow-with-description" }],
370
+ "typescript/no-duplicate-enum-values": "error",
371
+ "typescript/no-dynamic-delete": "off",
372
+ "typescript/no-empty-object-type": ["error", { allowInterfaces: "always" }],
373
+ "typescript/no-extra-non-null-assertion": "error",
374
+ "typescript/no-extraneous-class": "off",
375
+ "typescript/no-invalid-void-type": "off",
376
+ "typescript/no-misused-new": "error",
377
+ "typescript/no-namespace": "error",
378
+ "typescript/no-non-null-asserted-nullish-coalescing": "error",
379
+ "typescript/no-non-null-asserted-optional-chain": "error",
380
+ "typescript/no-non-null-assertion": "off",
381
+ "typescript/no-require-imports": "error",
382
+ "typescript/no-this-alias": "error",
383
+ "typescript/no-unnecessary-type-constraint": "error",
384
+ "typescript/no-unsafe-declaration-merging": "error",
385
+ "typescript/no-unsafe-function-type": "error",
386
+ "typescript/no-wrapper-object-types": "error",
387
+ "typescript/prefer-as-const": "error",
388
+ "typescript/prefer-literal-enum-member": "error",
389
+ "typescript/prefer-namespace-keyword": "error",
390
+ "typescript/triple-slash-reference": "off",
391
+ "typescript/unified-signatures": "off",
392
+ "typescript/consistent-type-definitions": ["error", "interface"],
393
+ "typescript/consistent-type-imports": ["error", {
394
+ disallowTypeAnnotations: false,
395
+ fixStyle: "separate-type-imports",
396
+ prefer: "type-imports"
397
+ }],
398
+ "typescript/method-signature-style": ["error", "property"],
399
+ "typescript/no-import-type-side-effects": "error"
400
+ }
401
+ },
402
+ {
403
+ files: [
404
+ "**/__tests__/**/*.?([cm])[jt]s?(x)",
405
+ "**/*.spec.?([cm])[jt]s?(x)",
406
+ "**/*.test.?([cm])[jt]s?(x)",
407
+ "**/*.bench.?([cm])[jt]s?(x)",
408
+ "**/*.benchmark.?([cm])[jt]s?(x)"
409
+ ],
410
+ rules: {
411
+ "vitest/consistent-test-it": ["error", {
412
+ fn: "it",
413
+ withinDescribe: "it"
414
+ }],
415
+ "vitest/no-identical-title": "error",
416
+ "vitest/no-import-node-test": "error",
417
+ "vitest/prefer-hooks-in-order": "error",
418
+ "vitest/prefer-lowercase-title": "error",
419
+ "no-unused-expressions": "off",
420
+ "typescript/explicit-function-return-type": "off"
421
+ },
422
+ plugins: ["vitest"]
423
+ },
424
+ {
425
+ files: ["**/*.?([cm])[jt]s?(x)"],
426
+ rules: {
427
+ "react/no-array-index-key": "warn",
428
+ "react/no-clone-element": "warn",
429
+ "react/no-direct-mutation-state": "error",
430
+ "react/jsx-no-comment-textnodes": "warn",
431
+ "react/only-export-components": ["error", {
432
+ allowConstantExport: false,
433
+ allowExportNames: []
434
+ }]
435
+ }
436
+ },
437
+ {
438
+ files: ["**/*.md/**/*.?([cm])[jt]s?(x)"],
439
+ rules: {
440
+ "no-alert": "off",
441
+ "no-labels": "off",
442
+ "no-lone-blocks": "off",
443
+ "no-unused-expressions": "off",
444
+ "no-unused-labels": "off",
445
+ "no-unused-vars": "off",
446
+ "unicode-bom": "off",
447
+ "no-redeclare": "off",
448
+ "no-use-before-define": "off",
449
+ "typescript/consistent-type-imports": "off",
450
+ "typescript/explicit-function-return-type": "off",
451
+ "typescript/no-namespace": "off",
452
+ "typescript/no-require-imports": "off"
453
+ }
454
+ },
455
+ {
456
+ files: ["**/scripts/**/*.?([cm])[jt]s?(x)"],
457
+ rules: { "typescript/explicit-function-return-type": "off" }
458
+ },
459
+ {
460
+ files: ["**/*.js", "**/*.cjs"],
461
+ rules: { "typescript/no-require-imports": "off" }
462
+ },
463
+ {
464
+ files: ["**/*.config.?([cm])[jt]s?(x)", "**/*.config.*.?([cm])[jt]s?(x)"],
465
+ rules: { "typescript/explicit-function-return-type": "off" }
466
+ },
467
+ {
468
+ files: ["**/e2e/**/*"],
469
+ rules: { "react/rules-of-hooks": "off" }
470
+ },
471
+ {
472
+ files: ["**/monaco-worker.ts"],
473
+ rules: { "import/default": "off" }
474
+ }
475
+ ]
476
+ });
477
+ const baseOxfmtConfig = defineConfig({
478
+ singleQuote: true,
479
+ semi: false,
480
+ arrowParens: "avoid",
481
+ sortImports: true,
482
+ quoteProps: "consistent"
483
+ });
484
+ const basePlugins = [
485
+ "import",
486
+ "unicorn",
487
+ "jsdoc",
488
+ "node",
489
+ "promise",
490
+ "oxc"
491
+ ];
492
+ const pluginToggles = {
493
+ typescript: {
494
+ packages: ["typescript"],
495
+ plugins: ["typescript"]
496
+ },
497
+ react: {
498
+ packages: ["react"],
499
+ plugins: [
500
+ "react",
501
+ "jsx-a11y",
502
+ "react-perf"
503
+ ]
504
+ },
505
+ vue: {
506
+ packages: ["vue"],
507
+ plugins: ["vue"]
508
+ },
509
+ next: {
510
+ packages: ["next"],
511
+ plugins: ["nextjs"]
512
+ },
513
+ vitest: {
514
+ packages: ["vitest"],
515
+ plugins: ["vitest"]
516
+ },
517
+ jest: {
518
+ packages: ["jest"],
519
+ plugins: ["jest"]
520
+ }
521
+ };
522
+ const tailwindConfig = defineConfig$1({ jsPlugins: ["eslint-plugin-better-tailwindcss"] });
523
+ function getInstalledPackages(cwd) {
524
+ const names = /* @__PURE__ */ new Set();
525
+ try {
526
+ const pkg = JSON.parse(readFileSync(resolve(cwd, "package.json"), "utf-8"));
527
+ for (const field of [
528
+ pkg.dependencies,
529
+ pkg.devDependencies,
530
+ pkg.peerDependencies
531
+ ]) for (const name of Object.keys(field ?? {})) names.add(name);
532
+ } catch {}
533
+ return names;
534
+ }
535
+ function resolvePlugins(options, installed) {
536
+ const plugins = [...basePlugins];
537
+ for (const name of Object.keys(pluginToggles)) {
538
+ const toggle = pluginToggles[name];
539
+ if (options[name] ?? toggle.packages.some((pkg) => installed.has(pkg))) plugins.push(...toggle.plugins);
540
+ }
541
+ return [...new Set(plugins)];
542
+ }
543
+ /**
544
+ * Build an oxlint config.
545
+ *
546
+ * Framework plugins (`typescript`, `react`, `vue`, `next`, `vitest`, `jest`,
547
+ * `tailwind`) are auto-enabled by detecting their package in the nearest
548
+ * `package.json`. If a dependency is not found there — e.g. it lives in a nested
549
+ * workspace like `apps/web/package.json` — its plugin stays off, so enable it
550
+ * manually:
551
+ *
552
+ * @example
553
+ * ```ts
554
+ * // auto-detect from the current package.json
555
+ * export default oxlintConfig()
556
+ * ```
557
+ * @example
558
+ * ```ts
559
+ * // dep lives in a nested workspace — turn the plugin on by hand
560
+ * export default oxlintConfig({ vue: true })
561
+ * ```
562
+ * @example
563
+ * ```ts
564
+ * // Tailwind, pointed at the entry CSS so classes can be resolved
565
+ * export default oxlintConfig({
566
+ * tailwind: true,
567
+ * override: {
568
+ * settings: { 'better-tailwindcss': { entryPoint: 'src/styles/globals.css' } },
569
+ * },
570
+ * })
571
+ * ```
572
+ */
573
+ const TAILWIND_PLUGIN = "eslint-plugin-better-tailwindcss";
574
+ function oxlintConfig(options = {}) {
575
+ const { override = {} } = options;
576
+ const installed = getInstalledPackages(options.cwd ?? process.cwd());
577
+ let tailwind = options.tailwind ?? installed.has("tailwindcss");
578
+ if (tailwind && !installed.has(TAILWIND_PLUGIN)) {
579
+ console.warn(`[@letstri/oxc-config] Tailwind linting needs "${TAILWIND_PLUGIN}". Install it: pnpm add -D ${TAILWIND_PLUGIN}`);
580
+ tailwind = false;
581
+ }
582
+ return defu(override, { plugins: resolvePlugins(options, installed) }, tailwind ? tailwindConfig : {}, baseOxlintConfig);
583
+ }
584
+ function oxfmtConfig({ override = {} } = {}) {
585
+ return defu(override, baseOxfmtConfig);
586
+ }
587
+ //#endregion
588
+ export { oxfmtConfig, oxlintConfig };
package/package.json ADDED
@@ -0,0 +1,70 @@
1
+ {
2
+ "name": "@letstri/oxc-config",
3
+ "version": "0.1.0",
4
+ "description": "Opinionated Oxlint and Oxfmt configs",
5
+ "homepage": "https://github.com/letstri/oxc-config#readme",
6
+ "bugs": {
7
+ "url": "https://github.com/letstri/oxc-config/issues"
8
+ },
9
+ "license": "MIT",
10
+ "author": "Valerii Strilets",
11
+ "repository": {
12
+ "type": "git",
13
+ "url": "git+https://github.com/letstri/oxc-config.git"
14
+ },
15
+ "files": [
16
+ "dist"
17
+ ],
18
+ "type": "module",
19
+ "main": "./dist/index.mjs",
20
+ "types": "./dist/index.d.mts",
21
+ "exports": {
22
+ ".": {
23
+ "types": "./dist/index.d.mts",
24
+ "import": "./dist/index.mjs"
25
+ }
26
+ },
27
+ "publishConfig": {
28
+ "access": "public"
29
+ },
30
+ "scripts": {
31
+ "build": "tsdown",
32
+ "dev": "tsdown --watch",
33
+ "check-types": "tsc --noEmit",
34
+ "lint": "oxlint -c oxlint.config.ts",
35
+ "lint:fix": "oxlint -c oxlint.config.ts --fix",
36
+ "format": "oxfmt -c oxfmt.config.ts",
37
+ "format:check": "oxfmt -c oxfmt.config.ts --check",
38
+ "check": "run-p lint check-types format:check",
39
+ "update": "taze -r -I major",
40
+ "prepare": "husky",
41
+ "prepublishOnly": "pnpm run build"
42
+ },
43
+ "dependencies": {
44
+ "defu": "^6.1.7"
45
+ },
46
+ "devDependencies": {
47
+ "@types/node": "^26.1.1",
48
+ "husky": "^9.1.7",
49
+ "npm-run-all2": "^9.0.2",
50
+ "oxfmt": "^0.58.0",
51
+ "oxlint": "^1.73.0",
52
+ "taze": "^19.14.1",
53
+ "tsdown": "^0.22.4",
54
+ "typescript": "^6.0.3"
55
+ },
56
+ "peerDependencies": {
57
+ "eslint-plugin-better-tailwindcss": ">=4",
58
+ "oxfmt": ">=0.58.0",
59
+ "oxlint": ">=1.73.0"
60
+ },
61
+ "peerDependenciesMeta": {
62
+ "eslint-plugin-better-tailwindcss": {
63
+ "optional": true
64
+ }
65
+ },
66
+ "engines": {
67
+ "node": ">=22"
68
+ },
69
+ "packageManager": "pnpm@11.11.0"
70
+ }