@letstri/oxc-config 0.3.1 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -10,9 +10,28 @@ 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 everything with the `oxc-config` CLI:
17
+
18
+ ```bash
19
+ npx oxc-config init
20
+ ```
21
+
22
+ `init` prompts you to pick what to set up — oxlint config, oxfmt config, VS Code,
23
+ Zed. Pass flags to skip the prompt (useful in CI/scripts):
24
+
25
+ ```bash
26
+ npx oxc-config init --oxlint --oxfmt # just the config files
27
+ npx oxc-config init --vscode --zed # just the editor settings
28
+ ```
29
+
30
+ With flags, only the chosen targets run; with none in a non-interactive shell,
31
+ all four run. Config files are skipped if they already exist (`--force` to
32
+ overwrite); editor settings are deep-merged into any existing files. See
33
+ [Editor setup](#editor-setup).
34
+
16
35
  ## AI setup prompt
17
36
 
18
37
  Paste this into Claude Code, Cursor, or any coding agent to wire everything up:
@@ -121,9 +140,20 @@ export default oxlintConfig(
121
140
  ```
122
141
 
123
142
  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:
143
+ ones above rather than overwriting them.
144
+
145
+ Options:
146
+
147
+ - `entryPoint` (required) — your Tailwind entry CSS, so the plugin can resolve
148
+ class names.
149
+ - `ignoreClasses` — class names to exempt from `no-unknown-classes` (e.g. classes
150
+ a component library generates that the plugin can't resolve):
151
+
152
+ ```ts
153
+ tailwind({ entryPoint: 'app/globals.css', ignoreClasses: ['toaster'] })
154
+ ```
155
+
156
+ The plugin is an **optional peer dependency** — install it yourself:
127
157
 
128
158
  ```bash
129
159
  pnpm add -D eslint-plugin-better-tailwindcss
@@ -136,119 +166,26 @@ If the plugin is missing, `tailwind()` throws with an install hint.
136
166
  Both editors use the official [oxc](https://oxc.rs) tooling — `oxlint` for
137
167
  linting and `oxfmt` for formatting — replacing ESLint and Prettier.
138
168
 
139
- ### VS Code
140
-
141
- Install the [`oxc.oxc-vscode`](https://marketplace.visualstudio.com/items?itemName=oxc.oxc-vscode)
142
- extension.
169
+ `oxc-config init` writes (or updates) the editor configs for you, deep-merging
170
+ into any existing files so your other settings are kept:
143
171
 
144
- <details>
145
- <summary>Show VS Code config</summary>
146
-
147
- `.vscode/extensions.json`:
148
-
149
- ```json
150
- {
151
- "recommendations": ["oxc.oxc-vscode"]
152
- }
153
- ```
154
-
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
- }
172
+ ```bash
173
+ # both editors
174
+ pnpm exec oxc-config init --vscode --zed
173
175
  ```
174
176
 
175
- </details>
177
+ It's idempotent — safe to re-run to pull the latest recommended settings.
176
178
 
177
- ### Zed
178
-
179
- Zed ships with the oxc language servers built in.
179
+ ### VS Code
180
180
 
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
- ```
181
+ Install the [`oxc.oxc-vscode`](https://marketplace.visualstudio.com/items?itemName=oxc.oxc-vscode)
182
+ extension (`init --vscode` also adds it to `.vscode/extensions.json`). It writes
183
+ `.vscode/settings.json` — oxlint as linter, oxfmt as the default formatter with
184
+ format-on-save, and Prettier's import organization turned off.
250
185
 
251
- Apply the same `languages` block to any other file types you want oxfmt to
252
- format (`JSONC`, `CSS`, `HTML`, `YAML`, …).
186
+ ### Zed
253
187
 
254
- </details>
188
+ Zed ships with the oxc language servers built in, so no extension is needed.
189
+ `init --zed` writes `.zed/settings.json` — oxfmt as the formatter (with
190
+ `source.fixAll.oxc` on save for JS/TS) and Prettier disabled, across the file
191
+ 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 } from "node:path";
6
+ import { cancel, isCI, isCancel, multiselect } from "@clack/prompts";
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
+ const log = (message) => process.stdout.write(`${message}\n`);
77
+ const merge = createDefu((obj, key, value) => {
78
+ const current = obj[key];
79
+ if (Array.isArray(current) && Array.isArray(value)) {
80
+ const union = [...value, ...current].map((item) => JSON.stringify(item));
81
+ obj[key] = [...new Set(union)].map((item) => JSON.parse(item));
82
+ return true;
83
+ }
84
+ return false;
85
+ });
86
+ function mergeJson(path, base) {
87
+ const existed = existsSync(path);
88
+ const current = existed ? parse(readFileSync(path, "utf-8"), [], { allowTrailingComma: true }) : {};
89
+ mkdirSync(dirname(path), { recursive: true });
90
+ writeFileSync(path, `${JSON.stringify(merge(base, current ?? {}), null, 2)}\n`);
91
+ return existed ? "updated" : "created";
92
+ }
93
+ function writeConfig(path, content, force) {
94
+ const existed = existsSync(path);
95
+ if (existed && !force) return "skipped";
96
+ writeFileSync(path, content);
97
+ return existed ? "overwritten" : "created";
98
+ }
99
+ const template = (fn) => `import { ${fn} } from '@letstri/oxc-config'\n\nexport default ${fn}()\n`;
100
+ const TARGETS = {
101
+ oxlint: {
102
+ label: "oxlint.config.ts",
103
+ run: (force) => `oxlint: ${writeConfig("oxlint.config.ts", template("oxlintConfig"), force)}`
104
+ },
105
+ oxfmt: {
106
+ label: "oxfmt.config.ts",
107
+ run: (force) => `oxfmt: ${writeConfig("oxfmt.config.ts", template("oxfmtConfig"), force)}`
108
+ },
109
+ vscode: {
110
+ label: "VS Code settings (.vscode)",
111
+ run: () => `vscode: ${mergeJson(".vscode/settings.json", vscodeSettings)} settings, ${mergeJson(".vscode/extensions.json", vscodeExtensions)} extensions`
112
+ },
113
+ zed: {
114
+ label: "Zed settings (.zed)",
115
+ run: () => `zed: ${mergeJson(".zed/settings.json", zedSettings)}`
116
+ }
117
+ };
118
+ const ALL = Object.keys(TARGETS);
119
+ async function pickTargets(flags) {
120
+ const flagged = ALL.filter((target) => flags.has(`--${target}`));
121
+ if (flagged.length > 0) return flagged;
122
+ if (isCI() || !process.stdout.isTTY) return ALL;
123
+ const selected = await multiselect({
124
+ message: "What do you want to set up?",
125
+ options: ALL.map((value) => ({
126
+ value,
127
+ label: TARGETS[value].label
128
+ })),
129
+ initialValues: ALL,
130
+ required: true
131
+ });
132
+ if (isCancel(selected)) {
133
+ cancel("Cancelled.");
134
+ return null;
135
+ }
136
+ return selected;
137
+ }
138
+ function version() {
139
+ const url = new URL("../package.json", import.meta.url);
140
+ return JSON.parse(readFileSync(url, "utf-8")).version;
141
+ }
142
+ const HELP = `oxc-config — set up @letstri/oxc-config in your project
143
+
144
+ Usage:
145
+ oxc-config [flags]
146
+
147
+ Flags (default: prompt for what to set up):
148
+ --oxlint create oxlint.config.ts
149
+ --oxfmt create oxfmt.config.ts
150
+ --vscode write .vscode settings
151
+ --zed write .zed settings
152
+ -f, --force overwrite existing config files
153
+ -h, --help show this help
154
+ -v, --version show version`;
155
+ const argv = process.argv.slice(2);
156
+ if (argv.includes("--help") || argv.includes("-h")) log(HELP);
157
+ else if (argv.includes("--version") || argv.includes("-v")) log(version());
158
+ else {
159
+ const flags = new Set(argv);
160
+ const force = flags.has("--force") || flags.has("-f");
161
+ const targets = await pickTargets(flags);
162
+ for (const target of targets ?? []) log(TARGETS[target].run(force));
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.5.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
+ "@clack/prompts": "^1.7.0",
35
+ "defu": "^6.1.7",
36
+ "jsonc-parser": "^3.3.1"
32
37
  },
33
38
  "devDependencies": {
34
39
  "@types/node": "^26.1.1",