@st1ggy/linter-config 7.3.1 → 8.0.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.
Files changed (49) hide show
  1. package/README.md +127 -8
  2. package/data/oxlint-config-inventory.json +2268 -0
  3. package/data/oxlint-rule-map.json +91896 -0
  4. package/data/oxlint-source-inventory.json +107728 -0
  5. package/docs/OXLINT_RULE_MAPPING.md +2972 -0
  6. package/docs/RULE_MAPPING.md +15 -0
  7. package/package.json +68 -3
  8. package/scripts/README.md +37 -1
  9. package/scripts/init-astro-ox.sh +4 -0
  10. package/scripts/init-common-ox.sh +4 -0
  11. package/scripts/init-next-ox.sh +4 -0
  12. package/scripts/init-react-ox.sh +4 -0
  13. package/scripts/init-solid-ox.sh +4 -0
  14. package/scripts/init-svelte-ox.sh +4 -0
  15. package/scripts/linter-init-core.mjs +104 -24
  16. package/scripts/linter-init.mjs +24 -32
  17. package/src/oxfmt/index.d.ts +8 -0
  18. package/src/oxfmt/oxfmt.config.common.js +16 -0
  19. package/src/oxlint/configs/config.astro.js +4093 -0
  20. package/src/oxlint/configs/config.common.js +3206 -0
  21. package/src/oxlint/configs/config.next.js +3588 -0
  22. package/src/oxlint/configs/config.react.js +3499 -0
  23. package/src/oxlint/configs/config.solid.js +4972 -0
  24. package/src/oxlint/configs/config.svelte.js +3302 -0
  25. package/src/oxlint/create-config.js +40 -0
  26. package/src/oxlint/index.d.ts +49 -0
  27. package/src/oxlint/oxlint.config.astro.js +4 -0
  28. package/src/oxlint/oxlint.config.common.js +4 -0
  29. package/src/oxlint/oxlint.config.next.js +4 -0
  30. package/src/oxlint/oxlint.config.react.js +4 -0
  31. package/src/oxlint/oxlint.config.solid.js +4 -0
  32. package/src/oxlint/oxlint.config.svelte.js +4 -0
  33. package/src/oxlint/plugins/astro.js +41 -0
  34. package/src/oxlint/plugins/eslint-core.js +5 -0
  35. package/src/oxlint/plugins/fresh-source-code.js +30 -0
  36. package/src/oxlint/plugins/import-x.js +38 -0
  37. package/src/oxlint/plugins/next.js +1 -0
  38. package/src/oxlint/plugins/prettier.js +1 -0
  39. package/src/oxlint/plugins/react-hooks.js +1 -0
  40. package/src/oxlint/plugins/react.js +1 -0
  41. package/src/oxlint/plugins/solid.js +1 -0
  42. package/src/oxlint/plugins/sonarjs.js +5 -0
  43. package/src/oxlint/plugins/stylistic.js +1 -0
  44. package/src/oxlint/plugins/svelte.js +42 -0
  45. package/src/oxlint/plugins/typescript.js +1 -0
  46. package/src/oxlint/plugins/unicorn.js +5 -0
  47. package/src/oxlint/stacks.js +17 -0
  48. package/scripts/package-smoke.mjs +0 -56
  49. package/scripts/solid-config.test.mjs +0 -134
@@ -33,6 +33,21 @@ Inventory rule counts (sample files in this repository):
33
33
  - `eslint-svelte`: effective rules on `src/examples/example.svelte`
34
34
  - `eslint-astro`: effective rules on `src/examples/example.astro`
35
35
 
36
+ ## Oxlint counterparts
37
+
38
+ The package also exports `common-ox`, `react-ox`, `solid-ox`, `next-ox`, `svelte-ox` and `astro-ox`. Each is one `OxlintConfig` object, used through `oxlint.config.ts`, with `options.typeAware: true` and the native TS7 tsgolint engine. The original ESLint presets remain available.
39
+
40
+ The new variants use explicit migrated rules plus compatible JS adapters. Common/React/Solid/Next pair with `oxfmt-common` and a separate `oxfmt --check`; their source `prettier/prettier` check is recorded as `formatter` in the audit instead of running a Prettier JS plugin. Svelte/Astro keep their existing Prettier selections. Every stack retains Stylelint. See [OXLINT_RULE_MAPPING.md](OXLINT_RULE_MAPPING.md) for source/target correspondence, per-scope counts, partial mappings and unsupported rules. Native script extraction does not provide Svelte/Astro template linting.
41
+
42
+ Regenerate and verify the Oxlint inventory with:
43
+
44
+ ```bash
45
+ npm run generate:ox
46
+ npm run inventory:ox
47
+ npm run generate:ox:check
48
+ npm run inventory:ox:check
49
+ ```
50
+
36
51
  ## Stylelint (SCSS preset)
37
52
 
38
53
  | Layer | `@st1ggy/linter-config` | Notes |
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@st1ggy/linter-config",
3
- "version": "7.3.1",
4
- "description": "ESLint, Prettier, and Stylelint shareable configs (sources under src/).",
3
+ "version": "8.0.0",
4
+ "description": "Shareable ESLint, Oxlint, Oxfmt, Prettier, and Stylelint configs.",
5
5
  "private": false,
6
6
  "repository": {
7
7
  "type": "git",
@@ -93,11 +93,46 @@
93
93
  "import": "./src/eslint/eslint.config.common.js",
94
94
  "default": "./src/eslint/eslint.config.common.js"
95
95
  },
96
+ "./common-ox": {
97
+ "types": "./src/oxlint/index.d.ts",
98
+ "import": "./src/oxlint/oxlint.config.common.js",
99
+ "default": "./src/oxlint/oxlint.config.common.js"
100
+ },
101
+ "./react-ox": {
102
+ "types": "./src/oxlint/index.d.ts",
103
+ "import": "./src/oxlint/oxlint.config.react.js",
104
+ "default": "./src/oxlint/oxlint.config.react.js"
105
+ },
106
+ "./solid-ox": {
107
+ "types": "./src/oxlint/index.d.ts",
108
+ "import": "./src/oxlint/oxlint.config.solid.js",
109
+ "default": "./src/oxlint/oxlint.config.solid.js"
110
+ },
111
+ "./next-ox": {
112
+ "types": "./src/oxlint/index.d.ts",
113
+ "import": "./src/oxlint/oxlint.config.next.js",
114
+ "default": "./src/oxlint/oxlint.config.next.js"
115
+ },
116
+ "./svelte-ox": {
117
+ "types": "./src/oxlint/index.d.ts",
118
+ "import": "./src/oxlint/oxlint.config.svelte.js",
119
+ "default": "./src/oxlint/oxlint.config.svelte.js"
120
+ },
121
+ "./astro-ox": {
122
+ "types": "./src/oxlint/index.d.ts",
123
+ "import": "./src/oxlint/oxlint.config.astro.js",
124
+ "default": "./src/oxlint/oxlint.config.astro.js"
125
+ },
96
126
  "./prettier-common": {
97
127
  "types": "./index.d.ts",
98
128
  "import": "./src/prettier/prettier.config.common.js",
99
129
  "default": "./src/prettier/prettier.config.common.js"
100
130
  },
131
+ "./oxfmt-common": {
132
+ "types": "./src/oxfmt/index.d.ts",
133
+ "import": "./src/oxfmt/oxfmt.config.common.js",
134
+ "default": "./src/oxfmt/oxfmt.config.common.js"
135
+ },
101
136
  "./prettier-svelte": {
102
137
  "types": "./index.d.ts",
103
138
  "import": "./src/prettier/prettier-svelte.config.js",
@@ -119,7 +154,12 @@
119
154
  "src",
120
155
  "scripts",
121
156
  "data",
122
- "docs"
157
+ "docs",
158
+ "!scripts/fixtures",
159
+ "!scripts/*.test.mjs",
160
+ "!scripts/oxlint-*.mjs",
161
+ "!scripts/oxfmt-*.mjs",
162
+ "!scripts/package-smoke.mjs"
123
163
  ],
124
164
  "scripts": {
125
165
  "lint": "eslint . && stylelint \"src/**/*.{css,scss}\" --config ./src/stylelint/stylelint.config.scss.js --allow-empty-input && prettier --check .",
@@ -132,6 +172,14 @@
132
172
  "changelog:dry": "conventional-changelog -p conventionalcommits",
133
173
  "publish:npm": "npm publish --access public",
134
174
  "inventory": "node ./scripts/inventory.mjs",
175
+ "typecheck:ox": "node ./node_modules/@typescript/native/bin/tsc --noEmit",
176
+ "generate:ox": "node scripts/oxlint-generate.mjs",
177
+ "generate:ox:check": "node scripts/oxlint-generate.mjs --check",
178
+ "inventory:ox": "node scripts/oxlint-inventory.mjs --write",
179
+ "inventory:ox:check": "node scripts/oxlint-inventory.mjs --source-only --check && node scripts/oxlint-inventory.mjs --check",
180
+ "test:ox": "node --test --test-concurrency=2 scripts/oxlint-*.test.mjs",
181
+ "format:ox:compare": "node scripts/oxfmt-compare.mjs",
182
+ "format:ox:check": "oxfmt --config ./src/oxfmt/oxfmt.config.common.js --check .",
135
183
  "config:init": "node ./scripts/linter-init.mjs init --common --dir ./examples/init-smoke",
136
184
  "config:migrate": "node ./scripts/linter-init.mjs migrate --common --dir ./examples/init-smoke",
137
185
  "config:reinit": "node ./scripts/linter-init.mjs reinit --common --dir ./examples/init-smoke",
@@ -148,6 +196,9 @@
148
196
  "eslint-plugin-solid": "^0.18.0",
149
197
  "eslint-plugin-svelte": "^3.22.0",
150
198
  "next": "^16.3.0",
199
+ "oxlint": "1.86.0",
200
+ "oxfmt": "0.71.0",
201
+ "oxlint-tsgolint": "7.0.2003",
151
202
  "prettier": "^3.9.6",
152
203
  "prettier-plugin-astro": "^0.14.1",
153
204
  "prettier-plugin-svelte": "^4.1.1",
@@ -180,6 +231,15 @@
180
231
  "next": {
181
232
  "optional": true
182
233
  },
234
+ "oxlint": {
235
+ "optional": true
236
+ },
237
+ "oxfmt": {
238
+ "optional": true
239
+ },
240
+ "oxlint-tsgolint": {
241
+ "optional": true
242
+ },
183
243
  "astro": {
184
244
  "optional": true
185
245
  },
@@ -203,6 +263,8 @@
203
263
  }
204
264
  },
205
265
  "devDependencies": {
266
+ "@typescript/native": "npm:typescript@7.0.2",
267
+ "pnpm": "10.32.1",
206
268
  "@next/eslint-plugin-next": "^16.3.0",
207
269
  "@stylistic/eslint-plugin-migrate": "^4.4.1",
208
270
  "conventional-changelog": "^8.1.0",
@@ -213,6 +275,9 @@
213
275
  "eslint-plugin-react-hooks": "^7.1.1",
214
276
  "eslint-plugin-solid": "^0.18.0",
215
277
  "eslint-plugin-svelte": "^3.22.0",
278
+ "oxlint": "1.86.0",
279
+ "oxfmt": "0.71.0",
280
+ "oxlint-tsgolint": "7.0.2003",
216
281
  "prettier-plugin-astro": "^0.14.1",
217
282
  "prettier-plugin-svelte": "^4.1.1",
218
283
  "solid-js": "^1.9.15",
package/scripts/README.md CHANGED
@@ -41,10 +41,46 @@ npm exec @st1ggy/linter-config -- migrate --svelte
41
41
  npx --yes @st1ggy/linter-config init --astro
42
42
  ```
43
43
 
44
- - **Stack (optional, default common):** `--common` | `--react` | `--solid` | `--next` | `--svelte` | `--astro` (at most one).
44
+ - **ESLint stacks:** `--common` | `--react` | `--solid` | `--next` | `--svelte` | `--astro`.
45
+ - **Oxlint stacks:** `--common-ox` | `--react-ox` | `--solid-ox` | `--next-ox` | `--svelte-ox` | `--astro-ox`.
46
+ - Select at most one stack; the default is `common`.
45
47
  - **`--skip-install`:** only write wrapper files; do not run npm/pnpm/yarn/bun.
46
48
  - Every CLI command opens an interactive wizard. `init` creates missing wrappers; `migrate` can remove selected legacy configs and replaces wrappers.
47
49
 
48
50
  Shell shortcuts in this folder (`init-common.sh`, etc.) call **`node …/linter-init.mjs`** (same script as the published **`@st1ggy/linter-config`** CLI) with the matching stack; pass **`--skip-install`** through when needed.
49
51
 
50
52
  For SolidJS, `init-solid.sh` selects `eslint-solid`, `prettier-common`, and `stylelint-scss`, and installs `eslint-plugin-solid` alongside `@st1ggy/linter-config`.
53
+
54
+ ## Oxlint wrappers
55
+
56
+ The six `init-*-ox.sh` shortcuts select the corresponding `*-ox` stack. All write `oxlint.config.ts` and `stylelint.config.js`. Common/React/Solid/Next write `oxfmt.config.ts`; Svelte/Astro write `prettier.config.js`. The wizard pins `oxlint@1.86.0` and `oxlint-tsgolint@7.0.2003`, adds `oxfmt@0.71.0` to the four Oxfmt stacks, and installs only the selected framework integrations. Svelte projects provide their Svelte runtime; their formatter and ESLint-compatible script rules use it.
57
+
58
+ ```bash
59
+ npx @st1ggy/linter-config init --react-ox
60
+ npx @st1ggy/linter-config migrate --astro-ox
61
+ node ./scripts/linter-init.mjs init --solid-ox --dir ./my-app --skip-install
62
+ ```
63
+
64
+ `init` preserves existing wrappers, including the other linter/formatter config. `migrate` offers the opposite configs and old `.oxlintrc.*`/`.oxfmtrc.*` files for removal before generating the selected wrappers. It never deletes them without confirmation.
65
+
66
+ For the four Oxfmt stacks, run `oxlint .` and `oxfmt --check .` separately. Their lint presets do not run Prettier's JS rule. `oxfmt --write .` formats after lint fixes; import and package.json sorting are disabled. The shared formatter options are available through `@st1ggy/linter-config/oxfmt-common`.
67
+
68
+ Oxlint uses native TS7 for typed linting. The consumer tsconfig must be TS7-compatible; the generator does not modify it. Read the [usage guide](../README.md#oxlint-variants) and [coverage report](../docs/OXLINT_RULE_MAPPING.md) before switching a project with Svelte/Astro templates or TS parser-service-dependent JS rules.
69
+
70
+ ## Oxlint development checks
71
+
72
+ ```bash
73
+ node scripts/oxlint-inventory.mjs --source-only
74
+ npm run generate:ox
75
+ npm run inventory:ox
76
+ npm run generate:ox:check
77
+ npm run inventory:ox:check
78
+ npm run test:ox
79
+ npm run format:ox:check
80
+ npm run format:ox:compare
81
+ npm run typecheck:ox
82
+ node scripts/package-smoke.mjs
83
+ node scripts/package-smoke.mjs --pm pnpm
84
+ ```
85
+
86
+ The parity corpus asserts source and target diagnostics; guardrails reject missing mappings, option/severity drift, absent fixtures and unverified claims of complete SFC coverage. Inventory regeneration is deterministic and does not silently adopt new upstream rules: generated artifacts and fixtures must be reviewed together after dependency updates.
@@ -0,0 +1,4 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
4
+ exec node "${SCRIPT_DIR}/linter-init.mjs" init --astro-ox "$@"
@@ -0,0 +1,4 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
4
+ exec node "${SCRIPT_DIR}/linter-init.mjs" init --common-ox "$@"
@@ -0,0 +1,4 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
4
+ exec node "${SCRIPT_DIR}/linter-init.mjs" init --next-ox "$@"
@@ -0,0 +1,4 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
4
+ exec node "${SCRIPT_DIR}/linter-init.mjs" init --react-ox "$@"
@@ -0,0 +1,4 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
4
+ exec node "${SCRIPT_DIR}/linter-init.mjs" init --solid-ox "$@"
@@ -0,0 +1,4 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
4
+ exec node "${SCRIPT_DIR}/linter-init.mjs" init --svelte-ox "$@"
@@ -4,9 +4,13 @@ import { spawnSync } from 'node:child_process'
4
4
  import { existsSync, mkdirSync, readFileSync, readdirSync, rmSync, writeFileSync } from 'node:fs'
5
5
  import path from 'node:path'
6
6
 
7
+ import { OXFMT_STACKS, OX_FRAMEWORK_PACKAGES, oxStackToolVersions } from '../src/oxlint/stacks.js'
8
+
7
9
  export const PACKAGE = '@st1ggy/linter-config'
8
10
 
9
- export const STACK_KEYS = ['common', 'react', 'solid', 'next', 'svelte', 'astro']
11
+ const BASE_STACK_KEYS = ['common', 'react', 'solid', 'next', 'svelte', 'astro']
12
+
13
+ export const STACK_KEYS = [...BASE_STACK_KEYS, ...BASE_STACK_KEYS.map((key) => `${key}-ox`)]
10
14
 
11
15
  export const STACKS = {
12
16
  common: {
@@ -74,6 +78,30 @@ export const STACK_CHOICES = [
74
78
  },
75
79
  ]
76
80
 
81
+ for (const key of BASE_STACK_KEYS) {
82
+ const base = STACKS[key]
83
+
84
+ base.linterFile = 'eslint.config.js'
85
+ base.linterPreset = base.eslint
86
+ base.formatterFile = 'prettier.config.js'
87
+ base.formatterPreset = base.prettier
88
+ const hasOxfmt = OXFMT_STACKS.includes(key)
89
+
90
+ STACKS[`${key}-ox`] = {
91
+ linterFile: 'oxlint.config.ts',
92
+ linterPreset: `${key}-ox`,
93
+ prettier: base.prettier,
94
+ formatterFile: hasOxfmt ? 'oxfmt.config.ts' : 'prettier.config.js',
95
+ formatterPreset: hasOxfmt ? 'oxfmt-common' : base.prettier,
96
+ stylelint: base.stylelint,
97
+ packages: [...Object.keys(oxStackToolVersions(key)), ...OX_FRAMEWORK_PACKAGES[key]],
98
+ }
99
+ STACK_CHOICES.push({
100
+ value: `${key}-ox`,
101
+ name: `${key}-ox — Oxlint + native TypeScript 7 + ${hasOxfmt ? 'Oxfmt' : 'Prettier'}`,
102
+ })
103
+ }
104
+
77
105
  export function readPackageJson(directory) {
78
106
  const filePath = path.join(directory, 'package.json')
79
107
 
@@ -103,7 +131,7 @@ export function isSelfPackageRoot(directory) {
103
131
  return packageJson?.name === PACKAGE
104
132
  }
105
133
 
106
- export function hasResolvablePackage(startDirectory, packageName = PACKAGE) {
134
+ function packageManifestPath(startDirectory, packageName) {
107
135
  let currentDirectory = path.resolve(startDirectory)
108
136
  const packageParts = packageName.split('/')
109
137
 
@@ -111,7 +139,7 @@ export function hasResolvablePackage(startDirectory, packageName = PACKAGE) {
111
139
  const marker = path.join(currentDirectory, 'node_modules', ...packageParts, 'package.json')
112
140
 
113
141
  if (existsSync(marker)) {
114
- return true
142
+ return marker
115
143
  }
116
144
 
117
145
  const parentDirectory = path.dirname(currentDirectory)
@@ -123,7 +151,25 @@ export function hasResolvablePackage(startDirectory, packageName = PACKAGE) {
123
151
  currentDirectory = parentDirectory
124
152
  }
125
153
 
126
- return false
154
+ return null
155
+ }
156
+
157
+ export function hasResolvablePackage(startDirectory, packageName = PACKAGE) {
158
+ return packageManifestPath(startDirectory, packageName) !== null
159
+ }
160
+
161
+ function hasRequestedVersion(directory, packageName, spec) {
162
+ const marker = packageManifestPath(directory, packageName)
163
+
164
+ if (!marker) {
165
+ return false
166
+ }
167
+
168
+ try {
169
+ return JSON.parse(readFileSync(marker, 'utf8')).version === spec.split('@').at(-1)
170
+ } catch {
171
+ return false
172
+ }
127
173
  }
128
174
 
129
175
  export function detectPackageManager(startDirectory) {
@@ -170,7 +216,7 @@ export function runPmSync(command, args, cwd) {
170
216
 
171
217
  // skipInstall: skip npm install; options.quiet: suppress stderr warnings
172
218
  export function ensureDevDependencies(targetDirectory, packages, shouldSkipInstall, options = {}) {
173
- const { quiet = false } = options
219
+ const { quiet = false, specs = {} } = options
174
220
 
175
221
  if (shouldSkipInstall) {
176
222
  return
@@ -197,7 +243,11 @@ export function ensureDevDependencies(targetDirectory, packages, shouldSkipInsta
197
243
  }
198
244
 
199
245
  const dependencies = mergedDependencies(packageJson)
200
- const packagesToAdd = packages.filter((packageName) => !Object.hasOwn(dependencies, packageName))
246
+ const packagesToAdd = packages.filter(
247
+ (packageName) =>
248
+ !Object.hasOwn(dependencies, packageName) ||
249
+ (specs[packageName] && !hasRequestedVersion(directory, packageName, specs[packageName])),
250
+ )
201
251
  const hasUnresolvedPackage = packages.some((packageName) => !hasResolvablePackage(directory, packageName))
202
252
 
203
253
  if (!hasUnresolvedPackage && packagesToAdd.length === 0) {
@@ -223,15 +273,18 @@ export function ensureDevDependencies(targetDirectory, packages, shouldSkipInsta
223
273
  return
224
274
  }
225
275
 
276
+ const requested = packagesToAdd.map((name) => specs[name] ?? name)
277
+ const shouldPin = requested.some((name, index) => name !== packagesToAdd[index])
278
+
226
279
  if (!quiet) {
227
- process.stdout.write(`install: ${pm} add -D ${packagesToAdd.join(' ')} (${directory})\n`)
280
+ process.stdout.write(`install: ${pm} add -D ${requested.join(' ')} (${directory})\n`)
228
281
  }
229
282
 
230
283
  const add = {
231
- npm: () => runPmSync('npm', ['install', '-D', ...packagesToAdd], directory),
232
- pnpm: () => runPmSync('pnpm', ['add', '-D', ...packagesToAdd], directory),
233
- yarn: () => runPmSync('yarn', ['add', '-D', ...packagesToAdd], directory),
234
- bun: () => runPmSync('bun', ['add', '-d', ...packagesToAdd], directory),
284
+ npm: () => runPmSync('npm', ['install', '-D', ...(shouldPin ? ['--save-exact'] : []), ...requested], directory),
285
+ pnpm: () => runPmSync('pnpm', ['add', '-D', ...(shouldPin ? ['--save-exact'] : []), ...requested], directory),
286
+ yarn: () => runPmSync('yarn', ['add', '-D', ...(shouldPin ? ['--exact'] : []), ...requested], directory),
287
+ bun: () => runPmSync('bun', ['add', '-d', ...(shouldPin ? ['--exact'] : []), ...requested], directory),
235
288
  }
236
289
 
237
290
  add[pm]()
@@ -251,6 +304,18 @@ export function stackPackages(stackKey) {
251
304
  return [PACKAGE, ...stack.packages]
252
305
  }
253
306
 
307
+ export function stackPackageSpecs(stackKey) {
308
+ stackPackages(stackKey)
309
+
310
+ if (!stackKey.endsWith('-ox')) {
311
+ return {}
312
+ }
313
+
314
+ const versions = oxStackToolVersions(stackKey.slice(0, -3))
315
+
316
+ return Object.fromEntries(Object.entries(versions).map(([name, version]) => [name, `${name}@${version}`]))
317
+ }
318
+
254
319
  export function printHelp() {
255
320
  process.stdout.write(`\
256
321
  ${PACKAGE} — generate local wrapper configs in a consumer project.
@@ -264,13 +329,16 @@ Commands:
264
329
  create Alias for init.
265
330
 
266
331
  Writes (each command):
267
- eslint.config.js, prettier.config.js, stylelint.config.js — re-exports for one stack.
332
+ ESLint stacks: eslint.config.js, prettier.config.js, stylelint.config.js.
333
+ common/react/solid/next-ox: oxlint.config.ts, oxfmt.config.ts, stylelint.config.js.
334
+ svelte/astro-ox: oxlint.config.ts, prettier.config.js, stylelint.config.js.
268
335
 
269
336
  Unless --skip-install: if package.json exists, runs the detected package manager to add or sync
270
337
  ${PACKAGE} and the selected stack's integration plugins.
271
338
 
272
339
  Stack (at most one; default: common):
273
340
  --common | --react | --solid | --next | --svelte | --astro
341
+ --common-ox | --react-ox | --solid-ox | --next-ox | --svelte-ox | --astro-ox
274
342
 
275
343
  Options:
276
344
  --dir, -d Target directory (default: current working directory).
@@ -282,6 +350,7 @@ Examples (after: npm i -D ${PACKAGE}):
282
350
  npx ${PACKAGE} init
283
351
  npx ${PACKAGE} init --react
284
352
  npx ${PACKAGE} init --solid
353
+ npx ${PACKAGE} init --solid-ox
285
354
  npx ${PACKAGE} migrate --svelte --dir ./apps/web
286
355
  npx ${PACKAGE} init --astro
287
356
  npm exec ${PACKAGE} -- init --common
@@ -294,8 +363,8 @@ Repo (paths from root):
294
363
  `)
295
364
  }
296
365
 
297
- export function jsReExport(subpath) {
298
- return `export { default } from '${PACKAGE}/${subpath}';\n`
366
+ export function jsReExport(subpath, hasSemicolon = true) {
367
+ return `export { default } from '${PACKAGE}/${subpath}'${hasSemicolon ? ';' : ''}\n`
299
368
  }
300
369
 
301
370
  export function resolveTargetDirectory(raw) {
@@ -324,23 +393,28 @@ export function writeFile(targetDirectory, name, content, overwrite, options = {
324
393
  return true
325
394
  }
326
395
 
327
- export function wrapperFileNames() {
328
- return ['eslint.config.js', 'prettier.config.js', 'stylelint.config.js']
396
+ export function wrapperFileNames(stackKey = 'common') {
397
+ stackPackages(stackKey)
398
+
399
+ return [STACKS[stackKey].linterFile, STACKS[stackKey].formatterFile, 'stylelint.config.js']
329
400
  }
330
401
 
331
- export function existingWrapperFiles(targetDirectory) {
332
- return wrapperFileNames().filter((name) => existsSync(path.join(targetDirectory, name)))
402
+ export function existingWrapperFiles(targetDirectory, stackKey = 'common') {
403
+ return wrapperFileNames(stackKey).filter((name) => existsSync(path.join(targetDirectory, name)))
333
404
  }
334
405
 
335
- export function legacyConfigFiles(targetDirectory) {
406
+ export function legacyConfigFiles(targetDirectory, stackKey = 'common') {
336
407
  if (!existsSync(targetDirectory)) {
337
408
  return []
338
409
  }
339
410
 
340
- const wrappers = new Set(wrapperFileNames())
411
+ const wrappers = new Set(wrapperFileNames(stackKey))
341
412
 
342
413
  return readdirSync(targetDirectory, { withFileTypes: true })
343
- .filter((entry) => entry.isFile() && /(?:eslint|prettier|stylelint)/i.test(entry.name) && !wrappers.has(entry.name))
414
+ .filter(
415
+ (entry) =>
416
+ entry.isFile() && /(?:eslint|oxlint|oxfmt|prettier|stylelint)/i.test(entry.name) && !wrappers.has(entry.name),
417
+ )
344
418
  .map((entry) => entry.name)
345
419
  .toSorted((left, right) => left.localeCompare(right))
346
420
  }
@@ -383,7 +457,13 @@ export function run(mode, targetDirectory, stackKey, options = {}) {
383
457
 
384
458
  const shouldOverwrite = mode === 'migrate' || mode === 'reinit'
385
459
 
386
- writeFile(targetDirectory, 'eslint.config.js', jsReExport(stack.eslint), shouldOverwrite, { quiet })
387
- writeFile(targetDirectory, 'prettier.config.js', jsReExport(stack.prettier), shouldOverwrite, { quiet })
388
- writeFile(targetDirectory, 'stylelint.config.js', jsReExport(stack.stylelint), shouldOverwrite, { quiet })
460
+ const hasSemicolon = stack.formatterPreset !== 'oxfmt-common'
461
+
462
+ writeFile(targetDirectory, stack.linterFile, jsReExport(stack.linterPreset, hasSemicolon), shouldOverwrite, { quiet })
463
+ writeFile(targetDirectory, stack.formatterFile, jsReExport(stack.formatterPreset, hasSemicolon), shouldOverwrite, {
464
+ quiet,
465
+ })
466
+ writeFile(targetDirectory, 'stylelint.config.js', jsReExport(stack.stylelint, hasSemicolon), shouldOverwrite, {
467
+ quiet,
468
+ })
389
469
  }
@@ -20,27 +20,15 @@ import {
20
20
  resolveStackKey,
21
21
  resolveTargetDirectory,
22
22
  run,
23
+ stackPackageSpecs,
23
24
  stackPackages,
25
+ wrapperFileNames,
24
26
  } from './linter-init-core.mjs'
25
27
 
26
28
  function parseArgv(argv) {
27
29
  return minimist(argv, {
28
30
  string: ['dir', 'd'],
29
- boolean: [
30
- 'help',
31
- 'h',
32
- 'skip-install',
33
- 'eslint',
34
- 'biome',
35
- 'common',
36
- 'react',
37
- 'solid',
38
- 'next',
39
- 'svelte',
40
- 'astro',
41
- 'interactive',
42
- 'i',
43
- ],
31
+ boolean: ['help', 'h', 'skip-install', 'eslint', 'biome', ...STACK_KEYS, 'interactive', 'i'],
44
32
  alias: {
45
33
  h: 'help',
46
34
  d: 'dir',
@@ -58,12 +46,7 @@ function argvToValues(argv) {
58
46
  'skip-install': Boolean(argv['skip-install']),
59
47
  eslint: Boolean(argv.eslint),
60
48
  biome: Boolean(argv.biome),
61
- common: Boolean(argv.common),
62
- react: Boolean(argv.react),
63
- solid: Boolean(argv.solid),
64
- next: Boolean(argv.next),
65
- svelte: Boolean(argv.svelte),
66
- astro: Boolean(argv.astro),
49
+ ...Object.fromEntries(STACK_KEYS.map((key) => [key, argv[key] === true])),
67
50
  }
68
51
  }
69
52
 
@@ -77,7 +60,7 @@ async function selectCommand(command) {
77
60
  choices: [
78
61
  {
79
62
  value: 'init',
80
- name: 'init — create missing ESLint, Prettier and Stylelint wrappers',
63
+ name: 'init — create missing linter, formatter and Stylelint wrappers',
81
64
  },
82
65
  {
83
66
  value: 'migrate',
@@ -102,14 +85,14 @@ async function selectStack(values) {
102
85
  })
103
86
  }
104
87
 
105
- async function selectLegacyFiles(command, directory) {
88
+ async function selectLegacyFiles(command, directory, stack) {
106
89
  if (command !== 'migrate') {
107
90
  return []
108
91
  }
109
92
 
110
93
  const selected = []
111
94
 
112
- for (const file of legacyConfigFiles(directory)) {
95
+ for (const file of legacyConfigFiles(directory, stack)) {
113
96
  if (await confirm({ message: `Remove legacy config ${file}?`, default: false })) {
114
97
  selected.push(file)
115
98
  }
@@ -122,7 +105,7 @@ function printSummary(command, stack, directory, shouldSkipInstall, existing, fi
122
105
  const wrapperFiles =
123
106
  command === 'init' && existing.length > 0
124
107
  ? `create missing; keep ${existing.join(', ')}`
125
- : 'write eslint.config.js, prettier.config.js, stylelint.config.js'
108
+ : `write ${wrapperFileNames(stack).join(', ')}`
126
109
 
127
110
  process.stdout.write('\n')
128
111
  process.stdout.write(` Action: ${command}\n`)
@@ -148,11 +131,20 @@ async function runInteractive(command, values) {
148
131
  }
149
132
 
150
133
  const shouldSkipInstall = values['skip-install'] === true
151
- const existing = existingWrapperFiles(resolved)
152
- const filesToRemove = await selectLegacyFiles(selectedCommand, resolved)
134
+ const existing = existingWrapperFiles(resolved, selectedStack)
135
+ const filesToRemove = await selectLegacyFiles(selectedCommand, resolved, selectedStack)
153
136
  const packages = stackPackages(selectedStack)
154
-
155
- printSummary(selectedCommand, selectedStack, resolved, shouldSkipInstall, existing, filesToRemove, packages)
137
+ const specs = stackPackageSpecs(selectedStack)
138
+
139
+ printSummary(
140
+ selectedCommand,
141
+ selectedStack,
142
+ resolved,
143
+ shouldSkipInstall,
144
+ existing,
145
+ filesToRemove,
146
+ packages.map((name) => specs[name] ?? name),
147
+ )
156
148
 
157
149
  const shouldProceed = await confirm({
158
150
  message: 'Proceed?',
@@ -175,7 +167,7 @@ async function runInteractive(command, values) {
175
167
  }).start()
176
168
 
177
169
  try {
178
- ensureDevDependencies(resolved, packages, false, { quiet: true })
170
+ ensureDevDependencies(resolved, packages, false, { quiet: true, specs })
179
171
  spinner.succeed(`${selectedStack} linting packages are available`)
180
172
  } catch (error) {
181
173
  spinner.fail('Dependency step failed')
@@ -185,7 +177,7 @@ async function runInteractive(command, values) {
185
177
 
186
178
  const writeSpinner = ora({
187
179
  color: 'green',
188
- text: 'Writing eslint.config.js, prettier.config.js, stylelint.config.js…',
180
+ text: `Writing ${wrapperFileNames(selectedStack).join(', ')}…`,
189
181
  }).start()
190
182
 
191
183
  try {
@@ -213,7 +205,7 @@ function normalizeCommand(command) {
213
205
  function warnLegacyFlags(values) {
214
206
  if (values.biome === true) {
215
207
  process.stderr.write(
216
- `warning: --biome is ignored; Biome presets were removed from ${PACKAGE}. Only ESLint, Prettier, and Stylelint wrappers are written.\n`,
208
+ `warning: --biome is ignored; Biome presets were removed from ${PACKAGE}. Select an ESLint or Oxlint stack.\n`,
217
209
  )
218
210
  }
219
211
  }
@@ -0,0 +1,8 @@
1
+ // Kept separate from the legacy declarations to make the formatter an optional peer.
2
+ declare module '@st1ggy/linter-config/oxfmt-common' {
3
+ import type { OxfmtConfig } from 'oxfmt'
4
+
5
+ const config: OxfmtConfig
6
+
7
+ export default config
8
+ }
@@ -0,0 +1,16 @@
1
+ export default {
2
+ useTabs: false,
3
+ printWidth: 120,
4
+ tabWidth: 2,
5
+ semi: false,
6
+ singleQuote: true,
7
+ trailingComma: 'all',
8
+ bracketSpacing: true,
9
+ arrowParens: 'always',
10
+ proseWrap: 'never',
11
+ endOfLine: 'lf',
12
+ insertFinalNewline: true,
13
+ sortImports: false,
14
+ sortPackageJson: false,
15
+ ignorePatterns: ['**/node_modules/**', '**/.git/**'],
16
+ }