@st1ggy/linter-config 5.0.0 → 6.3.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 (50) hide show
  1. package/README.md +38 -44
  2. package/{packages/eslint/data → data}/linter-config-inventory.json +3 -3
  3. package/docs/RULE_MAPPING.md +38 -0
  4. package/index.d.ts +0 -32
  5. package/package.json +43 -80
  6. package/scripts/README.md +23 -18
  7. package/scripts/init-common.sh +6 -0
  8. package/scripts/init-next.sh +5 -0
  9. package/scripts/init-react.sh +5 -0
  10. package/scripts/init-svelte.sh +5 -0
  11. package/{packages/eslint/scripts → scripts}/inventory.mjs +5 -5
  12. package/scripts/linter-init-core.mjs +317 -0
  13. package/scripts/linter-init.mjs +243 -0
  14. package/scripts/remove-current.sh +17 -0
  15. package/packages/biome/LICENSE +0 -201
  16. package/packages/biome/biome.json +0 -5
  17. package/packages/biome/configs/biome.common.json +0 -44
  18. package/packages/biome/configs/biome.next.json +0 -10
  19. package/packages/biome/configs/biome.react.json +0 -12
  20. package/packages/biome/configs/biome.svelte.json +0 -23
  21. package/packages/biome/docs/README.md +0 -29
  22. package/packages/eslint/docs/RULE_MAPPING.md +0 -50
  23. package/packages/eslint/scripts/init-common.sh +0 -4
  24. package/packages/eslint/scripts/init-next.sh +0 -4
  25. package/packages/eslint/scripts/init-react.sh +0 -4
  26. package/packages/eslint/scripts/init-svelte.sh +0 -4
  27. package/packages/eslint/scripts/linter-init.mjs +0 -209
  28. package/packages/eslint/scripts/remove-current.sh +0 -12
  29. /package/{packages/eslint/src → src}/eslint/configs/config.common.js +0 -0
  30. /package/{packages/eslint/src → src}/eslint/configs/config.gitignore.js +0 -0
  31. /package/{packages/eslint/src → src}/eslint/configs/config.import.js +0 -0
  32. /package/{packages/eslint/src → src}/eslint/configs/config.next.js +0 -0
  33. /package/{packages/eslint/src → src}/eslint/configs/config.prettier.js +0 -0
  34. /package/{packages/eslint/src → src}/eslint/configs/config.react.js +0 -0
  35. /package/{packages/eslint/src → src}/eslint/configs/config.sonarjs.js +0 -0
  36. /package/{packages/eslint/src → src}/eslint/configs/config.typescript.js +0 -0
  37. /package/{packages/eslint/src → src}/eslint/configs/config.unicorn.js +0 -0
  38. /package/{packages/eslint/src → src}/eslint/configs/constants.js +0 -0
  39. /package/{packages/eslint/src → src}/eslint/configs/svelte-stack.js +0 -0
  40. /package/{packages/eslint/src → src}/eslint/eslint-svelte.config.js +0 -0
  41. /package/{packages/eslint/src → src}/eslint/eslint.config.common.js +0 -0
  42. /package/{packages/eslint/src → src}/eslint/eslint.config.next.js +0 -0
  43. /package/{packages/eslint/src → src}/eslint/eslint.config.react.js +0 -0
  44. /package/{packages/eslint/src → src}/examples/example.scss +0 -0
  45. /package/{packages/eslint/src → src}/examples/example.svelte +0 -0
  46. /package/{packages/eslint/src → src}/examples/example.ts +0 -0
  47. /package/{packages/eslint/src → src}/index.js +0 -0
  48. /package/{packages/eslint/src → src}/prettier/prettier-svelte.config.js +0 -0
  49. /package/{packages/eslint/src → src}/prettier/prettier.config.common.js +0 -0
  50. /package/{packages/eslint/src → src}/stylelint/stylelint.config.scss.js +0 -0
@@ -0,0 +1,317 @@
1
+ // Shared logic for @st1ggy/linter-config CLI (no prompts).
2
+
3
+ import { spawnSync } from 'node:child_process'
4
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs'
5
+ import path from 'node:path'
6
+
7
+ export const PACKAGE = '@st1ggy/linter-config'
8
+
9
+ export const STACK_KEYS = ['common', 'react', 'next', 'svelte']
10
+
11
+ export const STACKS = {
12
+ common: {
13
+ eslint: 'eslint-common',
14
+ prettier: 'prettier-common',
15
+ stylelint: 'stylelint-scss',
16
+ },
17
+ react: {
18
+ eslint: 'eslint-react',
19
+ prettier: 'prettier-common',
20
+ stylelint: 'stylelint-scss',
21
+ },
22
+ next: {
23
+ eslint: 'eslint-next',
24
+ prettier: 'prettier-common',
25
+ stylelint: 'stylelint-scss',
26
+ },
27
+ svelte: {
28
+ eslint: 'eslint-svelte',
29
+ prettier: 'prettier-svelte',
30
+ stylelint: 'stylelint-scss',
31
+ },
32
+ }
33
+
34
+ export const STACK_CHOICES = [
35
+ {
36
+ value: 'common',
37
+ name: 'common — TypeScript/JS base + Stylelint (default)',
38
+ },
39
+ {
40
+ value: 'react',
41
+ name: 'react — React + hooks on top of common',
42
+ },
43
+ {
44
+ value: 'next',
45
+ name: 'next — Next.js App Router on top of react',
46
+ },
47
+ {
48
+ value: 'svelte',
49
+ name: 'svelte — Svelte + Prettier plugin for Svelte',
50
+ },
51
+ ]
52
+
53
+ export function readPackageJson(directory) {
54
+ const filePath = path.join(directory, 'package.json')
55
+
56
+ if (!existsSync(filePath)) {
57
+ return null
58
+ }
59
+
60
+ try {
61
+ return JSON.parse(readFileSync(filePath, 'utf8'))
62
+ } catch {
63
+ return null
64
+ }
65
+ }
66
+
67
+ export function mergedDeps(packageJson) {
68
+ return {
69
+ ...packageJson.optionalDependencies,
70
+ ...packageJson.peerDependencies,
71
+ ...packageJson.dependencies,
72
+ ...packageJson.devDependencies,
73
+ }
74
+ }
75
+
76
+ export function isSelfPackageRoot(directory) {
77
+ const packageJson = readPackageJson(directory)
78
+
79
+ return packageJson?.name === PACKAGE
80
+ }
81
+
82
+ export function hasResolvablePackage(startDirectory) {
83
+ let currentDirectory = path.resolve(startDirectory)
84
+
85
+ while (true) {
86
+ const marker = path.join(currentDirectory, 'node_modules', '@st1ggy', 'linter-config', 'package.json')
87
+
88
+ if (existsSync(marker)) {
89
+ return true
90
+ }
91
+
92
+ const parentDirectory = path.dirname(currentDirectory)
93
+
94
+ if (parentDirectory === currentDirectory) {
95
+ break
96
+ }
97
+
98
+ currentDirectory = parentDirectory
99
+ }
100
+
101
+ return false
102
+ }
103
+
104
+ export function detectPackageManager(startDirectory) {
105
+ let currentDirectory = path.resolve(startDirectory)
106
+
107
+ while (true) {
108
+ if (existsSync(path.join(currentDirectory, 'pnpm-lock.yaml'))) {
109
+ return 'pnpm'
110
+ }
111
+
112
+ if (existsSync(path.join(currentDirectory, 'yarn.lock'))) {
113
+ return 'yarn'
114
+ }
115
+
116
+ if (existsSync(path.join(currentDirectory, 'bun.lock'))) {
117
+ return 'bun'
118
+ }
119
+
120
+ if (existsSync(path.join(currentDirectory, 'package-lock.json'))) {
121
+ return 'npm'
122
+ }
123
+
124
+ const parentDirectory = path.dirname(currentDirectory)
125
+
126
+ if (parentDirectory === currentDirectory) {
127
+ return 'npm'
128
+ }
129
+
130
+ currentDirectory = parentDirectory
131
+ }
132
+ }
133
+
134
+ export function runPmSync(command, args, cwd) {
135
+ const result = spawnSync(command, args, { cwd, env: process.env, stdio: 'inherit' })
136
+
137
+ if (result.error) {
138
+ throw result.error
139
+ }
140
+
141
+ if (result.status !== 0) {
142
+ throw new Error(`${command} ${args.join(' ')} exited with ${result.status}`)
143
+ }
144
+ }
145
+
146
+ // skipInstall: skip npm install; options.quiet: suppress stderr warnings
147
+ export function ensureDevDependency(targetDirectory, skipInstall, options = {}) {
148
+ const { quiet = false } = options
149
+
150
+ if (skipInstall) {
151
+ return
152
+ }
153
+
154
+ const directory = path.resolve(targetDirectory)
155
+
156
+ const packageJsonPath = path.join(directory, 'package.json')
157
+
158
+ if (!existsSync(packageJsonPath)) {
159
+ if (!quiet) {
160
+ process.stderr.write(
161
+ `warning: no package.json in ${directory}; add the dependency manually: npm i -D ${PACKAGE}\n`,
162
+ )
163
+ }
164
+
165
+ return
166
+ }
167
+
168
+ if (hasResolvablePackage(directory)) {
169
+ return
170
+ }
171
+
172
+ const packageJson = readPackageJson(directory)
173
+
174
+ if (!packageJson) {
175
+ return
176
+ }
177
+
178
+ const pm = detectPackageManager(directory)
179
+ const listed = Boolean(mergedDeps(packageJson)[PACKAGE])
180
+
181
+ if (listed) {
182
+ if (!quiet) {
183
+ process.stdout.write(`install: ${pm} install (${directory}) — dependency already listed\n`)
184
+ }
185
+
186
+ const sync = {
187
+ npm: () => runPmSync('npm', ['install'], directory),
188
+ pnpm: () => runPmSync('pnpm', ['install'], directory),
189
+ yarn: () => runPmSync('yarn', ['install'], directory),
190
+ bun: () => runPmSync('bun', ['install'], directory),
191
+ }
192
+
193
+ sync[pm]()
194
+
195
+ return
196
+ }
197
+
198
+ if (!quiet) {
199
+ process.stdout.write(`install: ${pm} add -D ${PACKAGE} (${directory})\n`)
200
+ }
201
+
202
+ const add = {
203
+ npm: () => runPmSync('npm', ['install', '-D', PACKAGE], directory),
204
+ pnpm: () => runPmSync('pnpm', ['add', '-D', PACKAGE], directory),
205
+ yarn: () => runPmSync('yarn', ['add', '-D', PACKAGE], directory),
206
+ bun: () => runPmSync('bun', ['add', '-d', PACKAGE], directory),
207
+ }
208
+
209
+ add[pm]()
210
+ }
211
+
212
+ export function printHelp() {
213
+ process.stdout.write(`\
214
+ ${PACKAGE} — generate local wrapper configs in a consumer project.
215
+
216
+ With no command in an interactive terminal, opens a guided menu (or pass -i / --interactive).
217
+
218
+ Commands:
219
+ init Create missing files only (skip existing).
220
+ reinit Overwrite selected files.
221
+ create Same as init.
222
+
223
+ Writes (each command):
224
+ eslint.config.js, prettier.config.js, stylelint.config.js — re-exports for one stack.
225
+
226
+ Unless --skip-install: if package.json exists and ${PACKAGE} is not yet resolvable from node_modules
227
+ (walking up to the filesystem root), runs the detected package manager to add or sync the dependency.
228
+
229
+ Stack (at most one; default: common):
230
+ --common | --react | --next | --svelte
231
+
232
+ Options:
233
+ --dir, -d Target directory (default: current working directory).
234
+ --skip-install Do not run npm/pnpm/yarn/bun (only write wrapper files).
235
+ -i, --interactive Open the menu even when a command is passed (experimental).
236
+
237
+ Examples (after: npm i -D ${PACKAGE}):
238
+ npx ${PACKAGE}
239
+ npx ${PACKAGE} init
240
+ npx ${PACKAGE} init --react
241
+ npx ${PACKAGE} reinit --svelte --dir ./apps/web
242
+ npm exec ${PACKAGE} -- init --common
243
+
244
+ Without prior install (downloads this package; then runs the same CLI):
245
+ npx --yes ${PACKAGE} init --common
246
+
247
+ Repo (paths from root):
248
+ node scripts/linter-init.mjs init --common
249
+ `)
250
+ }
251
+
252
+ export function jsReExport(subpath) {
253
+ return `export { default } from '${PACKAGE}/${subpath}';\n`
254
+ }
255
+
256
+ export function resolveTargetDirectory(raw) {
257
+ return path.resolve(raw)
258
+ }
259
+
260
+ export function writeFile(targetDirectory, name, content, overwrite, options = {}) {
261
+ const { quiet = false } = options
262
+ const filePath = path.join(targetDirectory, name)
263
+
264
+ if (!overwrite && existsSync(filePath)) {
265
+ if (!quiet) {
266
+ process.stdout.write(`skip (exists): ${name}\n`)
267
+ }
268
+
269
+ return false
270
+ }
271
+
272
+ mkdirSync(targetDirectory, { recursive: true })
273
+ writeFileSync(filePath, content, 'utf8')
274
+
275
+ if (!quiet) {
276
+ process.stdout.write(`write: ${name}\n`)
277
+ }
278
+
279
+ return true
280
+ }
281
+
282
+ export function resolveStackKey(values) {
283
+ const chosen = []
284
+
285
+ for (const key of STACK_KEYS) {
286
+ if (values[key] === true) {
287
+ chosen.push(key)
288
+ }
289
+ }
290
+
291
+ if (chosen.length === 0) {
292
+ return 'common'
293
+ }
294
+
295
+ if (chosen.length > 1) {
296
+ const flags = STACK_KEYS.map((key) => `--${key}`).join(', ')
297
+
298
+ throw new Error(`Pick at most one stack flag: ${flags}`)
299
+ }
300
+
301
+ return chosen[0]
302
+ }
303
+
304
+ export function run(mode, targetDirectory, stackKey, options = {}) {
305
+ const { quiet = false } = options
306
+ const stack = STACKS[stackKey]
307
+
308
+ if (!stack) {
309
+ throw new Error(`Unknown stack "${stackKey}". Use: ${STACK_KEYS.join(', ')}`)
310
+ }
311
+
312
+ const overwrite = mode === 'reinit'
313
+
314
+ writeFile(targetDirectory, 'eslint.config.js', jsReExport(stack.eslint), overwrite, { quiet })
315
+ writeFile(targetDirectory, 'prettier.config.js', jsReExport(stack.prettier), overwrite, { quiet })
316
+ writeFile(targetDirectory, 'stylelint.config.js', jsReExport(stack.stylelint), overwrite, { quiet })
317
+ }
@@ -0,0 +1,243 @@
1
+ #!/usr/bin/env node
2
+ // @st1ggy/linter-config — interactive menu (TTY) or minimist CLI.
3
+
4
+ import { CancelPromptError, ExitPromptError } from '@inquirer/core'
5
+ import { confirm, input, select } from '@inquirer/prompts'
6
+ import minimist from 'minimist'
7
+ import process from 'node:process'
8
+ import ora from 'ora'
9
+
10
+ import {
11
+ PACKAGE,
12
+ STACK_CHOICES,
13
+ ensureDevDependency,
14
+ isSelfPackageRoot,
15
+ printHelp,
16
+ resolveStackKey,
17
+ resolveTargetDirectory,
18
+ run,
19
+ } from './linter-init-core.mjs'
20
+
21
+ function parseArgv(argv) {
22
+ return minimist(argv, {
23
+ string: ['dir', 'd'],
24
+ boolean: ['help', 'h', 'skip-install', 'eslint', 'biome', 'common', 'react', 'next', 'svelte', 'interactive', 'i'],
25
+ alias: {
26
+ h: 'help',
27
+ d: 'dir',
28
+ i: 'interactive',
29
+ },
30
+ default: {},
31
+ })
32
+ }
33
+
34
+ function argvToValues(argv) {
35
+ const directory = argv.dir ?? argv.d ?? process.cwd()
36
+
37
+ return {
38
+ dir: directory,
39
+ 'skip-install': Boolean(argv['skip-install']),
40
+ eslint: Boolean(argv.eslint),
41
+ biome: Boolean(argv.biome),
42
+ common: Boolean(argv.common),
43
+ react: Boolean(argv.react),
44
+ next: Boolean(argv.next),
45
+ svelte: Boolean(argv.svelte),
46
+ }
47
+ }
48
+
49
+ async function runInteractive() {
50
+ const command = await select({
51
+ message: 'Command',
52
+ choices: [
53
+ {
54
+ value: 'init',
55
+ name: 'init — create eslint / prettier / stylelint wrappers (skip if files exist)',
56
+ },
57
+ {
58
+ value: 'reinit',
59
+ name: 'reinit — overwrite the three wrapper files',
60
+ },
61
+ {
62
+ value: 'create',
63
+ name: 'create — same as init',
64
+ },
65
+ ],
66
+ default: 'init',
67
+ })
68
+
69
+ const stackKey = await select({
70
+ message: 'Stack',
71
+ choices: STACK_CHOICES,
72
+ default: 'common',
73
+ })
74
+
75
+ const targetDirectory = await input({
76
+ message: 'Target directory',
77
+ default: process.cwd(),
78
+ })
79
+
80
+ const skipInstall = await confirm({
81
+ message: `Skip installing ${PACKAGE} (only write config files)?`,
82
+ default: false,
83
+ })
84
+
85
+ const resolved = resolveTargetDirectory(targetDirectory)
86
+
87
+ process.stdout.write('\n')
88
+ process.stdout.write(` Command: ${command}\n`)
89
+ process.stdout.write(` Stack: ${stackKey}\n`)
90
+ process.stdout.write(` Dir: ${resolved}\n`)
91
+ process.stdout.write(` Skip install: ${skipInstall ? 'yes' : 'no'}\n`)
92
+ process.stdout.write('\n')
93
+
94
+ const proceed = await confirm({
95
+ message: 'Proceed?',
96
+ default: true,
97
+ })
98
+
99
+ if (!proceed) {
100
+ process.stdout.write('Cancelled.\n')
101
+ process.exitCode = 0
102
+
103
+ return
104
+ }
105
+
106
+ if (isSelfPackageRoot(resolved)) {
107
+ throw new Error(
108
+ `Refusing to write consumer wrapper configs in the ${PACKAGE} source tree (that would replace this repository’s dev configs). Run from another project, or pass a different directory.`,
109
+ )
110
+ }
111
+
112
+ const cmd = command === 'create' ? 'init' : command
113
+
114
+ if (skipInstall) {
115
+ ora({ color: 'gray' }).info(`Skipped installing ${PACKAGE} (--skip-install)`)
116
+ } else {
117
+ const spinner = ora({
118
+ color: 'cyan',
119
+ text: `Ensuring ${PACKAGE} is installed…`,
120
+ }).start()
121
+
122
+ try {
123
+ ensureDevDependency(resolved, false, { quiet: true })
124
+ spinner.succeed(`${PACKAGE} is available`)
125
+ } catch (error) {
126
+ spinner.fail('Dependency step failed')
127
+ throw error
128
+ }
129
+ }
130
+
131
+ const writeSpinner = ora({
132
+ color: 'green',
133
+ text: 'Writing eslint.config.js, prettier.config.js, stylelint.config.js…',
134
+ }).start()
135
+
136
+ try {
137
+ run(cmd, resolved, stackKey, { quiet: true })
138
+ writeSpinner.succeed('Wrapper configs written')
139
+ } catch (error) {
140
+ writeSpinner.fail('Failed to write configs')
141
+ throw error
142
+ }
143
+ }
144
+
145
+ function runCli(command, values) {
146
+ if (values.biome === true) {
147
+ process.stderr.write(
148
+ `warning: --biome is ignored; Biome presets were removed from ${PACKAGE}. Only ESLint, Prettier, and Stylelint wrappers are written.\n`,
149
+ )
150
+ }
151
+
152
+ const targetDirectory = resolveTargetDirectory(values.dir)
153
+ const stackKey = resolveStackKey(values)
154
+ const skipInstall = values['skip-install'] === true
155
+
156
+ if (isSelfPackageRoot(targetDirectory)) {
157
+ throw new Error(
158
+ `Refusing to write consumer wrapper configs in the ${PACKAGE} source tree (that would replace this repository’s dev configs). Run from another project, or pass --dir <path> to a folder with its own package.json.`,
159
+ )
160
+ }
161
+
162
+ const cmd = command === 'create' ? 'init' : command
163
+
164
+ ensureDevDependency(targetDirectory, skipInstall)
165
+ run(cmd, targetDirectory, stackKey)
166
+ }
167
+
168
+ async function runInteractiveOrHandleErrors() {
169
+ try {
170
+ await runInteractive()
171
+ } catch (error) {
172
+ if (error instanceof ExitPromptError || error instanceof CancelPromptError) {
173
+ process.stdout.write('\nCancelled.\n')
174
+ process.exitCode = 0
175
+
176
+ return
177
+ }
178
+
179
+ const message = error instanceof Error ? error.message : String(error)
180
+
181
+ process.stderr.write(`${message}\n`)
182
+ process.exitCode = 1
183
+ }
184
+ }
185
+
186
+ function runSubcommandCli(command, argv) {
187
+ const values = argvToValues(argv)
188
+
189
+ try {
190
+ runCli(command, values)
191
+ } catch (error) {
192
+ const message = error instanceof Error ? error.message : String(error)
193
+
194
+ process.stderr.write(`${message}\n`)
195
+ process.exitCode = 1
196
+ }
197
+ }
198
+
199
+ async function main() {
200
+ const raw = process.argv.slice(2)
201
+ const argv = parseArgv(raw)
202
+ const positional = argv._
203
+ const first = positional[0]
204
+
205
+ if (argv.help) {
206
+ printHelp()
207
+ process.exitCode = 0
208
+
209
+ return
210
+ }
211
+
212
+ const tty = Boolean(process.stdin.isTTY && process.stdout.isTTY)
213
+ const forceMenu = Boolean(argv.interactive || argv.i)
214
+ const noSubcommand = positional.length === 0
215
+ const useInteractiveMenu = noSubcommand && (forceMenu || tty)
216
+
217
+ if (useInteractiveMenu) {
218
+ await runInteractiveOrHandleErrors()
219
+
220
+ return
221
+ }
222
+
223
+ if (noSubcommand) {
224
+ printHelp()
225
+ process.exitCode = 1
226
+
227
+ return
228
+ }
229
+
230
+ const command = first
231
+
232
+ if (command !== 'init' && command !== 'reinit' && command !== 'create') {
233
+ process.stderr.write(`Unknown command "${String(command)}".\n\n`)
234
+ printHelp()
235
+ process.exitCode = 1
236
+
237
+ return
238
+ }
239
+
240
+ runSubcommandCli(command, argv)
241
+ }
242
+
243
+ await main()
@@ -0,0 +1,17 @@
1
+ #!/usr/bin/env bash
2
+ # Optional migration helper: interactively delete files in the *current directory* whose names
3
+ # contain eslint, stylelint, or prettier (maxdepth 1). Use before switching to the three standard
4
+ # wrappers from @st1ggy/linter-config — it does NOT replace `reinit` (reinit overwrites the standard
5
+ # filenames without prompts). Not invoked by the CLI; run manually when cleaning legacy configs.
6
+
7
+ set -euo pipefail
8
+
9
+ for tool in eslint stylelint prettier; do
10
+ while IFS= read -r -d '' file; do
11
+ read -r -p "Delete ${file#./}? y/n " -n 1 -r
12
+ echo
13
+ if [[ $REPLY =~ ^[Yy]$ ]]; then
14
+ rm -f "$file"
15
+ fi
16
+ done < <(find . -maxdepth 1 -type f -name "*${tool}*" -print0)
17
+ done