@letstri/oxc-config 0.3.1 → 0.4.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
@@ -10,9 +10,19 @@ Opinionated, shared [oxlint](https://oxc.rs/docs/guide/usage/linter.html) and [o
10
10
  ## Install
11
11
 
12
12
  ```bash
13
- pnpm add -D @letstri/oxc-config oxlint oxfmt
13
+ npm i -D @letstri/oxc-config oxlint oxfmt
14
14
  ```
15
15
 
16
+ Then scaffold the config files and editor settings with the `oxc-config` CLI:
17
+
18
+ ```bash
19
+ npx oxc-config config # create oxlint.config.ts + oxfmt.config.ts
20
+ npx oxc-config editors # write .vscode + .zed settings
21
+ ```
22
+
23
+ Run `oxc-config --help` for all commands. Details: [Usage](#usage) and
24
+ [Editor setup](#editor-setup).
25
+
16
26
  ## AI setup prompt
17
27
 
18
28
  Paste this into Claude Code, Cursor, or any coding agent to wire everything up:
@@ -121,9 +131,20 @@ export default oxlintConfig(
121
131
  ```
122
132
 
123
133
  Because arguments are merged (not spread), Tailwind's plugins combine with the
124
- ones above rather than overwriting them. `entryPoint` (required) is your Tailwind
125
- entry CSS, so the plugin can resolve class names. The plugin is an **optional
126
- peer dependency** — install it yourself:
134
+ ones above rather than overwriting them.
135
+
136
+ Options:
137
+
138
+ - `entryPoint` (required) — your Tailwind entry CSS, so the plugin can resolve
139
+ class names.
140
+ - `ignoreClasses` — class names to exempt from `no-unknown-classes` (e.g. classes
141
+ a component library generates that the plugin can't resolve):
142
+
143
+ ```ts
144
+ tailwind({ entryPoint: 'app/globals.css', ignoreClasses: ['toaster'] })
145
+ ```
146
+
147
+ The plugin is an **optional peer dependency** — install it yourself:
127
148
 
128
149
  ```bash
129
150
  pnpm add -D eslint-plugin-better-tailwindcss
@@ -136,119 +157,30 @@ If the plugin is missing, `tailwind()` throws with an install hint.
136
157
  Both editors use the official [oxc](https://oxc.rs) tooling — `oxlint` for
137
158
  linting and `oxfmt` for formatting — replacing ESLint and Prettier.
138
159
 
139
- ### VS Code
140
-
141
- Install the [`oxc.oxc-vscode`](https://marketplace.visualstudio.com/items?itemName=oxc.oxc-vscode)
142
- extension.
143
-
144
- <details>
145
- <summary>Show VS Code config</summary>
146
-
147
- `.vscode/extensions.json`:
160
+ The `oxc-config editors` command writes (or updates) the editor configs for you,
161
+ deep-merging into any existing files so your other settings are kept:
148
162
 
149
- ```json
150
- {
151
- "recommendations": ["oxc.oxc-vscode"]
152
- }
153
- ```
163
+ ```bash
164
+ # both editors
165
+ pnpm exec oxc-config editors
154
166
 
155
- `.vscode/settings.json`:
156
-
157
- ```jsonc
158
- {
159
- "oxc.configPath": "oxlint.config.ts",
160
- "oxc.fmt.configPath": "oxfmt.config.ts",
161
- "oxc.typeAware": true,
162
- "oxc.unusedDisableDirectives": "deny",
163
- "oxc.enable": true,
164
- "prettier.enable": false,
165
- "editor.defaultFormatter": "oxc.oxc-vscode",
166
- "editor.formatOnSave": true,
167
- "editor.formatOnSaveMode": "file", // oxfmt can only format whole files
168
- "editor.codeActionsOnSave": {
169
- "source.fixAll.oxc": "explicit",
170
- "source.organizeImports": "never", // let oxfmt handle import organization
171
- },
172
- }
167
+ # or just one
168
+ pnpm exec oxc-config editors --vscode
169
+ pnpm exec oxc-config editors --zed
173
170
  ```
174
171
 
175
- </details>
176
-
177
- ### Zed
172
+ It's idempotent — safe to re-run to pull the latest recommended settings.
178
173
 
179
- Zed ships with the oxc language servers built in.
174
+ ### VS Code
180
175
 
181
- <details>
182
- <summary>Show Zed config</summary>
183
-
184
- `.zed/settings.json`:
185
-
186
- ```jsonc
187
- {
188
- "lsp": {
189
- "oxlint": {
190
- "initialization_options": {
191
- "settings": {
192
- "configPath": null,
193
- "run": "onType",
194
- "disableNestedConfig": false,
195
- "fixKind": "safe_fix",
196
- "unusedDisableDirectives": "deny",
197
- },
198
- },
199
- },
200
- "oxfmt": {
201
- "initialization_options": {
202
- "settings": {
203
- "fmt.configPath": null,
204
- "run": "onSave",
205
- },
206
- },
207
- },
208
- },
209
- "languages": {
210
- "TypeScript": {
211
- "format_on_save": "on",
212
- "prettier": { "allowed": false },
213
- "language_servers": ["...", "oxlint", "oxfmt"],
214
- "formatter": [
215
- { "language_server": { "name": "oxfmt" } },
216
- { "code_action": "source.fixAll.oxc" },
217
- ],
218
- },
219
- "TSX": {
220
- "format_on_save": "on",
221
- "prettier": { "allowed": false },
222
- "language_servers": ["...", "oxlint", "oxfmt"],
223
- "formatter": [
224
- { "language_server": { "name": "oxfmt" } },
225
- { "code_action": "source.fixAll.oxc" },
226
- ],
227
- },
228
- "JavaScript": {
229
- "format_on_save": "on",
230
- "prettier": { "allowed": false },
231
- "language_servers": ["...", "oxlint", "oxfmt"],
232
- "formatter": [
233
- { "language_server": { "name": "oxfmt" } },
234
- { "code_action": "source.fixAll.oxc" },
235
- ],
236
- },
237
- "JSON": {
238
- "format_on_save": "on",
239
- "prettier": { "allowed": false },
240
- "formatter": [{ "language_server": { "name": "oxfmt" } }],
241
- },
242
- "Markdown": {
243
- "format_on_save": "on",
244
- "prettier": { "allowed": false },
245
- "formatter": [{ "language_server": { "name": "oxfmt" } }],
246
- },
247
- },
248
- }
249
- ```
176
+ Install the [`oxc.oxc-vscode`](https://marketplace.visualstudio.com/items?itemName=oxc.oxc-vscode)
177
+ extension (`oxc-config editors --vscode` also adds it to `.vscode/extensions.json`). The
178
+ CLI writes `.vscode/settings.json` — oxlint as linter, oxfmt as the default
179
+ formatter with format-on-save, and Prettier's import organization turned off.
250
180
 
251
- Apply the same `languages` block to any other file types you want oxfmt to
252
- format (`JSONC`, `CSS`, `HTML`, `YAML`, …).
181
+ ### Zed
253
182
 
254
- </details>
183
+ Zed ships with the oxc language servers built in, so no extension is needed.
184
+ `oxc-config editors --zed` writes `.zed/settings.json` — oxfmt as the formatter (with
185
+ `source.fixAll.oxc` on save for JS/TS) and Prettier disabled, across the file
186
+ types oxfmt supports.
package/dist/cli.d.mts ADDED
@@ -0,0 +1 @@
1
+ export { };
package/dist/cli.mjs ADDED
@@ -0,0 +1,165 @@
1
+ #!/usr/bin/env node
2
+ import process from "node:process";
3
+ import { createDefu } from "defu";
4
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
5
+ import { dirname, resolve } from "node:path";
6
+ import { boolean, command, run } from "@drizzle-team/brocli";
7
+ import { parse } from "jsonc-parser";
8
+ //#region src/editors.ts
9
+ /**
10
+ * Editor config templates, adapted from the official oxc examples:
11
+ * - https://github.com/oxc-project/oxc-vscode/blob/main/.vscode/settings.json
12
+ * - https://github.com/oxc-project/oxc-zed/blob/main/examples/both/.zed/settings.json
13
+ *
14
+ * `configPath` values point at this library's TypeScript config files.
15
+ */
16
+ const vscodeExtensions = { recommendations: ["oxc.oxc-vscode"] };
17
+ const vscodeSettings = {
18
+ "oxc.configPath": "oxlint.config.ts",
19
+ "oxc.fmt.configPath": "oxfmt.config.ts",
20
+ "oxc.typeAware": true,
21
+ "oxc.unusedDisableDirectives": "deny",
22
+ "editor.defaultFormatter": "oxc.oxc-vscode",
23
+ "editor.formatOnSave": true,
24
+ "editor.formatOnSaveMode": "file",
25
+ "editor.codeActionsOnSave": { "source.organizeImports": "never" }
26
+ };
27
+ const oxfmtFormatter = { language_server: { name: "oxfmt" } };
28
+ function formatOnSave(withFix = false) {
29
+ return {
30
+ format_on_save: "on",
31
+ prettier: { allowed: false },
32
+ formatter: withFix ? [oxfmtFormatter, { code_action: "source.fixAll.oxc" }] : [oxfmtFormatter]
33
+ };
34
+ }
35
+ const cssLike = [
36
+ "CSS",
37
+ "GraphQL",
38
+ "Handlebars",
39
+ "HTML",
40
+ "JSON",
41
+ "JSON5",
42
+ "JSONC",
43
+ "Less",
44
+ "Markdown",
45
+ "MDX",
46
+ "SCSS",
47
+ "YAML"
48
+ ];
49
+ const codeLike = [
50
+ "JavaScript",
51
+ "TypeScript",
52
+ "TSX",
53
+ "Vue.js"
54
+ ];
55
+ const zedSettings = {
56
+ lsp: {
57
+ oxlint: { initialization_options: { settings: {
58
+ configPath: null,
59
+ run: "onType",
60
+ disableNestedConfig: false,
61
+ fixKind: "safe_fix",
62
+ unusedDisableDirectives: "deny"
63
+ } } },
64
+ oxfmt: { initialization_options: { settings: {
65
+ "fmt.configPath": null,
66
+ "run": "onSave"
67
+ } } }
68
+ },
69
+ languages: {
70
+ ...Object.fromEntries(cssLike.map((lang) => [lang, formatOnSave()])),
71
+ ...Object.fromEntries(codeLike.map((lang) => [lang, formatOnSave(true)]))
72
+ }
73
+ };
74
+ //#endregion
75
+ //#region src/cli.ts
76
+ /**
77
+ * Like defu, but arrays are merged as a de-duplicated union instead of being
78
+ * concatenated — keeps updates idempotent and preserves user-added entries.
79
+ */
80
+ const mergeConfig = createDefu((obj, key, value) => {
81
+ const current = obj[key];
82
+ if (Array.isArray(current) && Array.isArray(value)) {
83
+ const seen = /* @__PURE__ */ new Set();
84
+ obj[key] = [...value, ...current].filter((item) => {
85
+ const id = JSON.stringify(item);
86
+ if (seen.has(id)) return false;
87
+ seen.add(id);
88
+ return true;
89
+ });
90
+ return true;
91
+ }
92
+ return false;
93
+ });
94
+ function readJsonc(path) {
95
+ if (!existsSync(path)) return {};
96
+ return parse(readFileSync(path, "utf-8"), [], { allowTrailingComma: true }) ?? {};
97
+ }
98
+ /** Deep-merge `base` into the JSON(C) file at `path`, creating it if absent. */
99
+ function mergeJsonFile(path, base) {
100
+ const existed = existsSync(path);
101
+ const merged = mergeConfig(base, readJsonc(path));
102
+ mkdirSync(dirname(path), { recursive: true });
103
+ writeFileSync(path, `${JSON.stringify(merged, null, 2)}\n`);
104
+ return existed ? "updated" : "created";
105
+ }
106
+ /** Write `content` to `path`, skipping an existing file unless `force`. */
107
+ function writeFile(path, content, force) {
108
+ const existed = existsSync(path);
109
+ if (existed && !force) return "skipped";
110
+ mkdirSync(dirname(path), { recursive: true });
111
+ writeFileSync(path, content);
112
+ return existed ? "overwritten" : "created";
113
+ }
114
+ const OXLINT_CONFIG = `import { oxlintConfig } from '@letstri/oxc-config'
115
+
116
+ export default oxlintConfig()
117
+ `;
118
+ const OXFMT_CONFIG = `import { oxfmtConfig } from '@letstri/oxc-config'
119
+
120
+ export default oxfmtConfig()
121
+ `;
122
+ function version() {
123
+ try {
124
+ return JSON.parse(readFileSync(new URL("../package.json", import.meta.url), "utf-8")).version ?? "0.0.0";
125
+ } catch {
126
+ return "0.0.0";
127
+ }
128
+ }
129
+ run([command({
130
+ name: "config",
131
+ desc: "Create oxlint.config.ts and oxfmt.config.ts",
132
+ options: { force: boolean().alias("f").desc("Overwrite existing files").default(false) },
133
+ handler: ({ force }) => {
134
+ const cwd = process.cwd();
135
+ const lint = writeFile(resolve(cwd, "oxlint.config.ts"), OXLINT_CONFIG, force);
136
+ const fmt = writeFile(resolve(cwd, "oxfmt.config.ts"), OXFMT_CONFIG, force);
137
+ process.stdout.write(`config: ${lint} oxlint.config.ts, ${fmt} oxfmt.config.ts\n`);
138
+ }
139
+ }), command({
140
+ name: "editors",
141
+ desc: "Write/update VS Code and Zed editor configs (deep-merged into existing files)",
142
+ options: {
143
+ vscode: boolean().desc("Only VS Code").default(false),
144
+ zed: boolean().desc("Only Zed").default(false)
145
+ },
146
+ handler: ({ vscode, zed }) => {
147
+ const both = !vscode && !zed;
148
+ const cwd = process.cwd();
149
+ if (both || vscode) {
150
+ const s = mergeJsonFile(resolve(cwd, ".vscode/settings.json"), vscodeSettings);
151
+ const e = mergeJsonFile(resolve(cwd, ".vscode/extensions.json"), vscodeExtensions);
152
+ process.stdout.write(`vscode: ${s} .vscode/settings.json, ${e} .vscode/extensions.json\n`);
153
+ }
154
+ if (both || zed) {
155
+ const s = mergeJsonFile(resolve(cwd, ".zed/settings.json"), zedSettings);
156
+ process.stdout.write(`zed: ${s} .zed/settings.json\n`);
157
+ }
158
+ }
159
+ })], {
160
+ name: "oxc-config",
161
+ description: "Set up @letstri/oxc-config in your project",
162
+ version: version()
163
+ });
164
+ //#endregion
165
+ export {};
package/dist/index.d.mts CHANGED
@@ -50,6 +50,11 @@ interface TailwindOptions {
50
50
  * class names.
51
51
  */
52
52
  entryPoint: string;
53
+ /**
54
+ * Class names to exempt from `no-unknown-classes` — e.g. classes generated by
55
+ * component libraries that the plugin can't resolve from the entry CSS.
56
+ */
57
+ ignoreClasses?: string[];
53
58
  /**
54
59
  * Directory scanned to check the plugin is installed.
55
60
  *
@@ -62,13 +67,16 @@ interface TailwindOptions {
62
67
  * Pass the result as an argument to `oxlintConfig`:
63
68
  *
64
69
  * ```ts
65
- * export default oxlintConfig(tailwind({ entryPoint: 'app/globals.css' }))
70
+ * export default oxlintConfig(tailwind({
71
+ * entryPoint: 'app/globals.css',
72
+ * ignoreClasses: ['toaster'],
73
+ * }))
66
74
  * ```
67
75
  *
68
76
  * The plugin is an optional peer dependency — install it yourself
69
77
  * (`pnpm add -D eslint-plugin-better-tailwindcss`). If it is missing, an error
70
78
  * is thrown.
71
79
  */
72
- declare function tailwind({ entryPoint, cwd }: TailwindOptions): OxlintOptions;
80
+ declare function tailwind({ entryPoint, ignoreClasses, cwd }: TailwindOptions): OxlintOptions;
73
81
  //#endregion
74
82
  export { oxfmtConfig, oxlintConfig, tailwind };
package/dist/index.mjs CHANGED
@@ -586,14 +586,17 @@ const TAILWIND_PLUGIN = "eslint-plugin-better-tailwindcss";
586
586
  * Pass the result as an argument to `oxlintConfig`:
587
587
  *
588
588
  * ```ts
589
- * export default oxlintConfig(tailwind({ entryPoint: 'app/globals.css' }))
589
+ * export default oxlintConfig(tailwind({
590
+ * entryPoint: 'app/globals.css',
591
+ * ignoreClasses: ['toaster'],
592
+ * }))
590
593
  * ```
591
594
  *
592
595
  * The plugin is an optional peer dependency — install it yourself
593
596
  * (`pnpm add -D eslint-plugin-better-tailwindcss`). If it is missing, an error
594
597
  * is thrown.
595
598
  */
596
- function tailwind({ entryPoint, cwd = process.cwd() }) {
599
+ function tailwind({ entryPoint, ignoreClasses = [], cwd = process.cwd() }) {
597
600
  if (!getInstalledPackages(cwd).has(TAILWIND_PLUGIN)) throw new Error(`[@letstri/oxc-config] Tailwind linting needs "${TAILWIND_PLUGIN}". Install it: pnpm add -D ${TAILWIND_PLUGIN}`);
598
601
  return defineConfig({
599
602
  jsPlugins: [TAILWIND_PLUGIN],
@@ -606,7 +609,7 @@ function tailwind({ entryPoint, cwd = process.cwd() }) {
606
609
  "better-tailwindcss/no-conflicting-classes": "error",
607
610
  "better-tailwindcss/no-deprecated-classes": "error",
608
611
  "better-tailwindcss/no-duplicate-classes": "error",
609
- "better-tailwindcss/no-unknown-classes": "error"
612
+ "better-tailwindcss/no-unknown-classes": ["error", { ignore: ignoreClasses }]
610
613
  }
611
614
  });
612
615
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@letstri/oxc-config",
3
- "version": "0.3.1",
3
+ "version": "0.4.0",
4
4
  "description": "Opinionated Oxlint and Oxfmt configs",
5
5
  "homepage": "https://github.com/letstri/oxc-config#readme",
6
6
  "bugs": {
@@ -12,6 +12,9 @@
12
12
  "type": "git",
13
13
  "url": "git+https://github.com/letstri/oxc-config.git"
14
14
  },
15
+ "bin": {
16
+ "oxc-config": "./dist/cli.mjs"
17
+ },
15
18
  "files": [
16
19
  "dist"
17
20
  ],
@@ -28,7 +31,9 @@
28
31
  "access": "public"
29
32
  },
30
33
  "dependencies": {
31
- "defu": "^6.1.7"
34
+ "@drizzle-team/brocli": "^0.12.0",
35
+ "defu": "^6.1.7",
36
+ "jsonc-parser": "^3.3.1"
32
37
  },
33
38
  "devDependencies": {
34
39
  "@types/node": "^26.1.1",