@letstri/oxc-config 0.4.0 → 0.6.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
@@ -13,53 +13,24 @@ Opinionated, shared [oxlint](https://oxc.rs/docs/guide/usage/linter.html) and [o
13
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:
16
+ Then scaffold everything with the `oxc-config` CLI:
17
17
 
18
18
  ```bash
19
- npx oxc-config config # create oxlint.config.ts + oxfmt.config.ts
20
- npx oxc-config editors # write .vscode + .zed settings
19
+ npx oxc-config init
21
20
  ```
22
21
 
23
- Run `oxc-config --help` for all commands. Details: [Usage](#usage) and
24
- [Editor setup](#editor-setup).
25
-
26
- ## AI setup prompt
27
-
28
- Paste this into Claude Code, Cursor, or any coding agent to wire everything up:
29
-
30
- <details>
31
- <summary>Show prompt</summary>
32
-
33
- ````text
34
- Set up @letstri/oxc-config (oxlint + oxfmt) in this project:
35
-
36
- 1. Install dev deps: `@letstri/oxc-config oxlint oxfmt`.
37
- 2. Remove ESLint and Prettier: their configs, deps, and scripts.
38
- 3. Create `oxlint.config.ts`:
39
- ```ts
40
- import { oxlintConfig } from '@letstri/oxc-config'
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):
41
24
 
42
- export default oxlintConfig()
43
- ```
44
- 4. Create `oxfmt.config.ts`:
45
- ```ts
46
- import { oxfmtConfig } from '@letstri/oxc-config'
47
-
48
- export default oxfmtConfig()
49
- ```
50
- 5. Add package.json scripts:
51
- - "lint": "oxlint"
52
- - "lint:fix": "oxlint --fix"
53
- - "format": "oxfmt"
54
- - "format:check": "oxfmt --check"
55
- 6. Framework plugins auto-enable from the nearest package.json. If a framework
56
- dep (react/vue/next/vitest/jest/typescript) lives in a nested workspace,
57
- enable it manually, e.g. `oxlintConfig({ plugins: ['vue'] })`.
58
- 7. Add the VS Code and Zed editor settings from the @letstri/oxc-config README.
59
- 8. Run `pnpm lint` and `pnpm format` and fix anything reported.
60
- ````
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
+ ```
61
29
 
62
- </details>
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).
63
34
 
64
35
  ## Usage
65
36
 
@@ -117,16 +88,16 @@ export default oxlintConfig({ rules: { 'no-console': 'off' } }, { plugins: ['vue
117
88
 
118
89
  ### Tailwind
119
90
 
120
- `tailwind({ entryPoint })` returns a config chunk for
91
+ `tailwindPlugin({ entryPoint })` returns a config chunk for
121
92
  [`eslint-plugin-better-tailwindcss`](https://github.com/schoero/eslint-plugin-better-tailwindcss).
122
93
  Pass it as an argument to `oxlintConfig`:
123
94
 
124
95
  ```ts
125
- import { oxlintConfig, tailwind } from '@letstri/oxc-config'
96
+ import { oxlintConfig, tailwindPlugin } from '@letstri/oxc-config'
126
97
 
127
98
  export default oxlintConfig(
128
99
  { plugins: ['react', 'jsx-a11y'] },
129
- tailwind({ entryPoint: 'app/globals.css' }),
100
+ tailwindPlugin({ entryPoint: 'app/globals.css' }),
130
101
  )
131
102
  ```
132
103
 
@@ -141,7 +112,7 @@ Options:
141
112
  a component library generates that the plugin can't resolve):
142
113
 
143
114
  ```ts
144
- tailwind({ entryPoint: 'app/globals.css', ignoreClasses: ['toaster'] })
115
+ tailwindPlugin({ entryPoint: 'app/globals.css', ignoreClasses: ['toaster'] })
145
116
  ```
146
117
 
147
118
  The plugin is an **optional peer dependency** — install it yourself:
@@ -150,23 +121,19 @@ The plugin is an **optional peer dependency** — install it yourself:
150
121
  pnpm add -D eslint-plugin-better-tailwindcss
151
122
  ```
152
123
 
153
- If the plugin is missing, `tailwind()` throws with an install hint.
124
+ If the plugin is missing, `tailwindPlugin()` throws with an install hint.
154
125
 
155
126
  ## Editor setup
156
127
 
157
128
  Both editors use the official [oxc](https://oxc.rs) tooling — `oxlint` for
158
129
  linting and `oxfmt` for formatting — replacing ESLint and Prettier.
159
130
 
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:
131
+ `oxc-config init` writes (or updates) the editor configs for you, deep-merging
132
+ into any existing files so your other settings are kept:
162
133
 
163
134
  ```bash
164
135
  # both editors
165
- pnpm exec oxc-config editors
166
-
167
- # or just one
168
- pnpm exec oxc-config editors --vscode
169
- pnpm exec oxc-config editors --zed
136
+ pnpm exec oxc-config init --vscode --zed
170
137
  ```
171
138
 
172
139
  It's idempotent — safe to re-run to pull the latest recommended settings.
@@ -174,13 +141,13 @@ It's idempotent — safe to re-run to pull the latest recommended settings.
174
141
  ### VS Code
175
142
 
176
143
  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.
144
+ extension (`init --vscode` also adds it to `.vscode/extensions.json`). It writes
145
+ `.vscode/settings.json` — oxlint as linter, oxfmt as the default formatter with
146
+ format-on-save, and Prettier's import organization turned off.
180
147
 
181
148
  ### Zed
182
149
 
183
150
  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
151
+ `init --zed` writes `.zed/settings.json` — oxfmt as the formatter (with
185
152
  `source.fixAll.oxc` on save for JS/TS) and Prettier disabled, across the file
186
153
  types oxfmt supports.
package/dist/cli.mjs CHANGED
@@ -2,8 +2,8 @@
2
2
  import process from "node:process";
3
3
  import { createDefu } from "defu";
4
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";
5
+ import { dirname } from "node:path";
6
+ import { cancel, isCI, isCancel, multiselect } from "@clack/prompts";
7
7
  import { parse } from "jsonc-parser";
8
8
  //#region src/editors.ts
9
9
  /**
@@ -73,93 +73,93 @@ const zedSettings = {
73
73
  };
74
74
  //#endregion
75
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) => {
76
+ const log = (message) => process.stdout.write(`${message}\n`);
77
+ const merge = createDefu((obj, key, value) => {
81
78
  const current = obj[key];
82
79
  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
- });
80
+ const union = [...value, ...current].map((item) => JSON.stringify(item));
81
+ obj[key] = [...new Set(union)].map((item) => JSON.parse(item));
90
82
  return true;
91
83
  }
92
84
  return false;
93
85
  });
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) {
86
+ function mergeJson(path, base) {
100
87
  const existed = existsSync(path);
101
- const merged = mergeConfig(base, readJsonc(path));
88
+ const current = existed ? parse(readFileSync(path, "utf-8"), [], { allowTrailingComma: true }) : {};
102
89
  mkdirSync(dirname(path), { recursive: true });
103
- writeFileSync(path, `${JSON.stringify(merged, null, 2)}\n`);
90
+ writeFileSync(path, `${JSON.stringify(merge(base, current ?? {}), null, 2)}\n`);
104
91
  return existed ? "updated" : "created";
105
92
  }
106
- /** Write `content` to `path`, skipping an existing file unless `force`. */
107
- function writeFile(path, content, force) {
93
+ function writeConfig(path, content, force) {
108
94
  const existed = existsSync(path);
109
95
  if (existed && !force) return "skipped";
110
- mkdirSync(dirname(path), { recursive: true });
111
96
  writeFileSync(path, content);
112
97
  return existed ? "overwritten" : "created";
113
98
  }
114
- const OXLINT_CONFIG = `import { oxlintConfig } from '@letstri/oxc-config'
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
115
143
 
116
- export default oxlintConfig()
117
- `;
118
- const OXFMT_CONFIG = `import { oxfmtConfig } from '@letstri/oxc-config'
144
+ Usage:
145
+ oxc-config [flags]
119
146
 
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
- }
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));
128
163
  }
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
164
  //#endregion
165
165
  export {};
package/dist/index.d.mts CHANGED
@@ -1,7 +1,21 @@
1
1
  import { defineConfig } from "oxlint";
2
2
  import { defineConfig as defineConfig$1 } from "oxfmt";
3
3
  //#region src/oxlint.d.ts
4
- type OxlintOptions$1 = Parameters<typeof defineConfig>[0];
4
+ type OxlintOptions = Parameters<typeof defineConfig>[0];
5
+ type OxlintPlugin = NonNullable<NonNullable<OxlintOptions>['plugins']>[number];
6
+ declare const basePlugins: readonly ["import", "unicorn", "jsdoc", "node", "promise", "oxc"];
7
+ /**
8
+ * Plugins always enabled by the base config — excluded from the `plugins` you
9
+ * can add, since listing them again is redundant.
10
+ */
11
+ type BasePlugin = (typeof basePlugins)[number];
12
+ /**
13
+ * Oxlint config accepted by {@link oxlintConfig}. Same as oxlint's own config,
14
+ * but `plugins` can't include the always-on base plugins.
15
+ */
16
+ type OxlintConfig = Omit<NonNullable<OxlintOptions>, 'plugins'> & {
17
+ plugins?: Exclude<OxlintPlugin, BasePlugin>[];
18
+ };
5
19
  /**
6
20
  * Build an oxlint config.
7
21
  *
@@ -10,8 +24,9 @@ type OxlintOptions$1 = Parameters<typeof defineConfig>[0];
10
24
  * plugin whose dependency lives elsewhere — e.g. a nested workspace like
11
25
  * `apps/web/package.json` — add it through `plugins`.
12
26
  *
13
- * Pass any number of config objects; they are deep-merged (arrays concatenated),
14
- * so pieces like {@link tailwind} compose without clobbering each other.
27
+ * Pass any number of config objects; they are deep-merged (arrays concatenated,
28
+ * with `plugins` de-duplicated), so pieces like {@link tailwindPlugin} compose
29
+ * without clobbering each other.
15
30
  *
16
31
  * @example
17
32
  * ```ts
@@ -28,11 +43,11 @@ type OxlintOptions$1 = Parameters<typeof defineConfig>[0];
28
43
  * // compose Tailwind — its plugins merge with the ones above, not overwrite
29
44
  * export default oxlintConfig(
30
45
  * { plugins: ['react', 'jsx-a11y'] },
31
- * tailwind({ entryPoint: 'app/globals.css' }),
46
+ * tailwindPlugin({ entryPoint: 'app/globals.css' }),
32
47
  * )
33
48
  * ```
34
49
  */
35
- declare function oxlintConfig(...overrides: OxlintOptions$1[]): OxlintOptions$1;
50
+ declare function oxlintConfig(...overrides: OxlintConfig[]): OxlintOptions;
36
51
  //#endregion
37
52
  //#region src/oxfmt.d.ts
38
53
  type OxfmtOptions = Parameters<typeof defineConfig$1>[0];
@@ -43,7 +58,6 @@ type OxfmtOptions = Parameters<typeof defineConfig$1>[0];
43
58
  declare function oxfmtConfig(...overrides: OxfmtOptions[]): OxfmtOptions;
44
59
  //#endregion
45
60
  //#region src/tailwind.d.ts
46
- type OxlintOptions = Parameters<typeof defineConfig>[0];
47
61
  interface TailwindOptions {
48
62
  /**
49
63
  * Path to the Tailwind entry CSS. Required — the plugin needs it to resolve
@@ -67,7 +81,7 @@ interface TailwindOptions {
67
81
  * Pass the result as an argument to `oxlintConfig`:
68
82
  *
69
83
  * ```ts
70
- * export default oxlintConfig(tailwind({
84
+ * export default oxlintConfig(tailwindPlugin({
71
85
  * entryPoint: 'app/globals.css',
72
86
  * ignoreClasses: ['toaster'],
73
87
  * }))
@@ -77,6 +91,6 @@ interface TailwindOptions {
77
91
  * (`pnpm add -D eslint-plugin-better-tailwindcss`). If it is missing, an error
78
92
  * is thrown.
79
93
  */
80
- declare function tailwind({ entryPoint, ignoreClasses, cwd }: TailwindOptions): OxlintOptions;
94
+ declare function tailwindPlugin({ entryPoint, ignoreClasses, cwd }: TailwindOptions): OxlintConfig;
81
95
  //#endregion
82
- export { oxfmtConfig, oxlintConfig, tailwind };
96
+ export { oxfmtConfig, oxlintConfig, tailwindPlugin };
package/dist/index.mjs CHANGED
@@ -536,8 +536,9 @@ function resolvePlugins(cwd) {
536
536
  * plugin whose dependency lives elsewhere — e.g. a nested workspace like
537
537
  * `apps/web/package.json` — add it through `plugins`.
538
538
  *
539
- * Pass any number of config objects; they are deep-merged (arrays concatenated),
540
- * so pieces like {@link tailwind} compose without clobbering each other.
539
+ * Pass any number of config objects; they are deep-merged (arrays concatenated,
540
+ * with `plugins` de-duplicated), so pieces like {@link tailwindPlugin} compose
541
+ * without clobbering each other.
541
542
  *
542
543
  * @example
543
544
  * ```ts
@@ -554,12 +555,14 @@ function resolvePlugins(cwd) {
554
555
  * // compose Tailwind — its plugins merge with the ones above, not overwrite
555
556
  * export default oxlintConfig(
556
557
  * { plugins: ['react', 'jsx-a11y'] },
557
- * tailwind({ entryPoint: 'app/globals.css' }),
558
+ * tailwindPlugin({ entryPoint: 'app/globals.css' }),
558
559
  * )
559
560
  * ```
560
561
  */
561
562
  function oxlintConfig(...overrides) {
562
- return defu({}, ...overrides, { plugins: resolvePlugins(process.cwd()) }, baseOxlintConfig);
563
+ const merged = defu({}, ...overrides, { plugins: resolvePlugins(process.cwd()) }, baseOxlintConfig);
564
+ if (merged?.plugins) merged.plugins = [...new Set(merged.plugins)];
565
+ return merged;
563
566
  }
564
567
  //#endregion
565
568
  //#region src/oxfmt.ts
@@ -586,7 +589,7 @@ const TAILWIND_PLUGIN = "eslint-plugin-better-tailwindcss";
586
589
  * Pass the result as an argument to `oxlintConfig`:
587
590
  *
588
591
  * ```ts
589
- * export default oxlintConfig(tailwind({
592
+ * export default oxlintConfig(tailwindPlugin({
590
593
  * entryPoint: 'app/globals.css',
591
594
  * ignoreClasses: ['toaster'],
592
595
  * }))
@@ -596,7 +599,7 @@ const TAILWIND_PLUGIN = "eslint-plugin-better-tailwindcss";
596
599
  * (`pnpm add -D eslint-plugin-better-tailwindcss`). If it is missing, an error
597
600
  * is thrown.
598
601
  */
599
- function tailwind({ entryPoint, ignoreClasses = [], cwd = process.cwd() }) {
602
+ function tailwindPlugin({ entryPoint, ignoreClasses = [], cwd = process.cwd() }) {
600
603
  if (!getInstalledPackages(cwd).has(TAILWIND_PLUGIN)) throw new Error(`[@letstri/oxc-config] Tailwind linting needs "${TAILWIND_PLUGIN}". Install it: pnpm add -D ${TAILWIND_PLUGIN}`);
601
604
  return defineConfig({
602
605
  jsPlugins: [TAILWIND_PLUGIN],
@@ -614,4 +617,4 @@ function tailwind({ entryPoint, ignoreClasses = [], cwd = process.cwd() }) {
614
617
  });
615
618
  }
616
619
  //#endregion
617
- export { oxfmtConfig, oxlintConfig, tailwind };
620
+ export { oxfmtConfig, oxlintConfig, tailwindPlugin };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@letstri/oxc-config",
3
- "version": "0.4.0",
3
+ "version": "0.6.0",
4
4
  "description": "Opinionated Oxlint and Oxfmt configs",
5
5
  "homepage": "https://github.com/letstri/oxc-config#readme",
6
6
  "bugs": {
@@ -31,7 +31,7 @@
31
31
  "access": "public"
32
32
  },
33
33
  "dependencies": {
34
- "@drizzle-team/brocli": "^0.12.0",
34
+ "@clack/prompts": "^1.7.0",
35
35
  "defu": "^6.1.7",
36
36
  "jsonc-parser": "^3.3.1"
37
37
  },