lintmax 0.0.11 → 0.0.12
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 +23 -79
- package/dist/cli.js +9 -484
- package/dist/cli.js.map +1 -1
- package/dist/compact.d.ts +7 -0
- package/dist/compact.d.ts.map +1 -0
- package/dist/compact.js +101 -0
- package/dist/compact.js.map +1 -0
- package/dist/constants.d.ts +11 -0
- package/dist/constants.d.ts.map +1 -0
- package/dist/constants.js +94 -0
- package/dist/constants.js.map +1 -0
- package/dist/core.d.ts +50 -0
- package/dist/core.d.ts.map +1 -0
- package/dist/core.js +107 -0
- package/dist/core.js.map +1 -0
- package/dist/eslint.d.ts +3 -7
- package/dist/eslint.d.ts.map +1 -1
- package/dist/eslint.js +141 -10
- package/dist/eslint.js.map +1 -1
- package/dist/index.d.ts +4 -34
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +462 -143
- package/dist/index.js.map +1 -1
- package/dist/init.d.ts +3 -0
- package/dist/init.d.ts.map +1 -0
- package/dist/init.js +105 -0
- package/dist/init.js.map +1 -0
- package/dist/lintmax-types.d.ts +59 -0
- package/dist/lintmax-types.d.ts.map +1 -0
- package/dist/lintmax-types.js +1 -0
- package/dist/lintmax-types.js.map +1 -0
- package/dist/normalize.d.ts +39 -0
- package/dist/normalize.d.ts.map +1 -0
- package/dist/normalize.js +138 -0
- package/dist/normalize.js.map +1 -0
- package/dist/pipeline.d.ts +5 -0
- package/dist/pipeline.d.ts.map +1 -0
- package/dist/pipeline.js +200 -0
- package/dist/pipeline.js.map +1 -0
- package/oxlintrc.json +0 -2
- package/package.json +7 -4
package/README.md
CHANGED
|
@@ -1,105 +1,49 @@
|
|
|
1
1
|
# lintmax
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Opinionated max-strict lint/format/typecheck with a minimal config surface.
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
## Quick start
|
|
6
6
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
## Install
|
|
7
|
+
Install and initialize:
|
|
10
8
|
|
|
11
9
|
```bash
|
|
12
10
|
bun add -d lintmax
|
|
13
11
|
bunx lintmax init
|
|
14
12
|
```
|
|
15
13
|
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
- `tsconfig.json` extending `lintmax/tsconfig`
|
|
19
|
-
- `package.json` scripts (`fix`, `check`)
|
|
20
|
-
- `.gitignore` entries
|
|
21
|
-
- `.vscode/settings.json` (biome formatter, eslint, codeActionsOnSave)
|
|
22
|
-
- `.vscode/extensions.json`
|
|
23
|
-
|
|
24
|
-
## Usage
|
|
14
|
+
Run checks:
|
|
25
15
|
|
|
26
16
|
```bash
|
|
27
|
-
bun fix
|
|
28
|
-
bun check
|
|
17
|
+
bun fix
|
|
18
|
+
bun check
|
|
29
19
|
```
|
|
30
20
|
|
|
31
|
-
|
|
21
|
+
`lintmax` works without configuration. Add `lintmax.config.ts` only when you need to tune behavior.
|
|
22
|
+
|
|
23
|
+
## Canonical knobs
|
|
32
24
|
|
|
33
|
-
|
|
25
|
+
- `off`: disable rules
|
|
26
|
+
- `ignores`: ignore file globs
|
|
27
|
+
- `overrides`: map file globs to per-linter `off` arrays
|
|
34
28
|
|
|
35
|
-
|
|
29
|
+
Example:
|
|
36
30
|
|
|
37
31
|
```ts
|
|
38
32
|
import { defineConfig } from 'lintmax'
|
|
39
33
|
|
|
40
34
|
export default defineConfig({
|
|
41
|
-
compact: true,
|
|
42
|
-
globalIgnorePatterns: ['packages/ui/**', '.intlayer/cache/**'],
|
|
43
|
-
biome: {
|
|
44
|
-
rules: { noBarrelFile: 'off' },
|
|
45
|
-
overrides: [{ disableLinter: true, includes: ['packages/ui/**'] }]
|
|
46
|
-
},
|
|
47
|
-
oxlint: {
|
|
48
|
-
ignorePatterns: ['_generated/'],
|
|
49
|
-
rules: { 'unicorn/filename-case': 'off' }
|
|
50
|
-
}
|
|
51
|
-
})
|
|
52
|
-
```
|
|
53
|
-
|
|
54
|
-
- `globalIgnorePatterns` appends patterns to Biome, ESLint, and oxlint ignore sets.
|
|
55
|
-
- `compact` runs a whitespace compaction pass before linting (`false` by default).
|
|
56
|
-
- `lintmax fix`: rewrites tracked/untracked text files by collapsing 2+ blank lines to 1.
|
|
57
|
-
- `lintmax check`: verifies compaction state without modifying files.
|
|
58
|
-
|
|
59
|
-
### ESLint overrides
|
|
60
|
-
|
|
61
|
-
Create `eslint.config.ts`:
|
|
62
|
-
|
|
63
|
-
```ts
|
|
64
|
-
import { eslint } from 'lintmax/eslint'
|
|
65
|
-
|
|
66
|
-
export default eslint({
|
|
67
|
-
rules: { '@typescript-eslint/no-magic-numbers': 'off' },
|
|
68
35
|
ignores: ['vendor/**'],
|
|
69
|
-
|
|
70
|
-
|
|
36
|
+
eslint: { off: ['@typescript-eslint/no-magic-numbers'] },
|
|
37
|
+
overrides: {
|
|
38
|
+
'**/*.test.ts': {
|
|
39
|
+
eslint: ['no-console'],
|
|
40
|
+
oxlint: ['no-console'],
|
|
41
|
+
biome: ['noConsole']
|
|
42
|
+
}
|
|
43
|
+
}
|
|
71
44
|
})
|
|
72
45
|
```
|
|
73
46
|
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
### TypeScript
|
|
77
|
-
|
|
78
|
-
`tsconfig.json`:
|
|
79
|
-
|
|
80
|
-
```json
|
|
81
|
-
{ "extends": "lintmax/tsconfig" }
|
|
82
|
-
```
|
|
83
|
-
|
|
84
|
-
Strict mode, bundler resolution, ESNext target, JSX preserve. One preset for all.
|
|
85
|
-
|
|
86
|
-
## How it stays up to date
|
|
87
|
-
|
|
88
|
-
Biome config is built **dynamically** from the installed `@biomejs/biome` schema at runtime. New rules, category changes, and nursery promotions are picked up automatically.
|
|
89
|
-
|
|
90
|
-
ESLint plugins and oxlint are dependencies with auto-updating versions. Run `bun update` to get the latest rules without waiting for a lintmax release.
|
|
91
|
-
|
|
92
|
-
## What each tool handles
|
|
93
|
-
|
|
94
|
-
| Tool | Scope |
|
|
95
|
-
| ----------------- | ------------------------------------------------------------------------------- |
|
|
96
|
-
| Biome | Formatting (JS/TS/JSX/TSX/CSS/JSON) + linting |
|
|
97
|
-
| oxlint | Fast linting (correctness, perf, style, pedantic) |
|
|
98
|
-
| ESLint | Type-aware linting (typescript-eslint, React, Next.js, Tailwind, Perfectionist) |
|
|
99
|
-
| Prettier | Markdown formatting |
|
|
100
|
-
| sort-package-json | package.json field ordering |
|
|
101
|
-
| Flowmark | Markdown prose wrapping (optional, uses system install) |
|
|
102
|
-
|
|
103
|
-
## License
|
|
47
|
+
Advanced options: `doc/advanced-configuration.md`.
|
|
104
48
|
|
|
105
|
-
MIT
|
|
49
|
+
License: MIT
|
package/dist/cli.js
CHANGED
|
@@ -1,499 +1,24 @@
|
|
|
1
1
|
#!/usr/bin/env bun
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
|
|
6
|
-
code;
|
|
7
|
-
constructor({ code, message }) {
|
|
8
|
-
super(message ?? '');
|
|
9
|
-
this.code = code;
|
|
10
|
-
}
|
|
11
|
-
}
|
|
12
|
-
const COMPACT_REGEX = /(?:\r?\n){2,}/gu, compactBasenames = new Set(['.env.example', '.gitignore', '.npmrc', '.prettierignore', 'Dockerfile', 'Makefile']), compactExtensions = new Set([
|
|
13
|
-
'.cjs',
|
|
14
|
-
'.css',
|
|
15
|
-
'.gql',
|
|
16
|
-
'.graphql',
|
|
17
|
-
'.html',
|
|
18
|
-
'.js',
|
|
19
|
-
'.json',
|
|
20
|
-
'.jsonc',
|
|
21
|
-
'.jsx',
|
|
22
|
-
'.md',
|
|
23
|
-
'.mjs',
|
|
24
|
-
'.mts',
|
|
25
|
-
'.scss',
|
|
26
|
-
'.sql',
|
|
27
|
-
'.ts',
|
|
28
|
-
'.tsx',
|
|
29
|
-
'.txt',
|
|
30
|
-
'.yaml',
|
|
31
|
-
'.yml'
|
|
32
|
-
]), decoder = new TextDecoder(), cwd = process.cwd(), cmd = process.argv[2], ignoreEntries = ['.cache/', '.eslintcache'], lintmaxRoot = dirnamePath(dirnamePath(fromFileUrl(import.meta.url))), decodeText = (bytes) => decoder.decode(bytes ?? new Uint8Array()), pathExists = async ({ path }) => file(path).exists(), readJson = async ({ path }) => {
|
|
33
|
-
if (!(await pathExists({ path })))
|
|
34
|
-
return {};
|
|
35
|
-
try {
|
|
36
|
-
const text = await file(path).text();
|
|
37
|
-
return JSON.parse(text);
|
|
38
|
-
}
|
|
39
|
-
catch {
|
|
40
|
-
return {};
|
|
41
|
-
}
|
|
42
|
-
}, readRequiredJson = async ({ path }) => {
|
|
43
|
-
const text = await file(path).text();
|
|
44
|
-
return JSON.parse(text);
|
|
45
|
-
}, writeJson = async ({ data, path }) => write(path, `${JSON.stringify(data, null, 2)}\n`), basename = ({ path }) => {
|
|
46
|
-
const index = path.lastIndexOf('/');
|
|
47
|
-
if (index === -1)
|
|
48
|
-
return path;
|
|
49
|
-
return path.slice(index + 1);
|
|
50
|
-
}, extension = ({ path }) => {
|
|
51
|
-
const slashIndex = path.lastIndexOf('/'), dotIndex = path.lastIndexOf('.');
|
|
52
|
-
return dotIndex > slashIndex ? path.slice(dotIndex) : '';
|
|
53
|
-
}, compactContent = ({ content }) => content.replace(COMPACT_REGEX, '\n'), isCompactCandidate = ({ relativePath }) => {
|
|
54
|
-
const fileName = basename({ path: relativePath });
|
|
55
|
-
if (compactBasenames.has(fileName))
|
|
56
|
-
return true;
|
|
57
|
-
return compactExtensions.has(extension({ path: relativePath }));
|
|
58
|
-
}, isBinary = ({ bytes }) => {
|
|
59
|
-
for (const byte of bytes)
|
|
60
|
-
if (byte === 0)
|
|
61
|
-
return true;
|
|
62
|
-
return false;
|
|
63
|
-
}, listCompactFiles = ({ env, root }) => {
|
|
64
|
-
const result = spawnSync({
|
|
65
|
-
cmd: ['git', '-C', root, 'ls-files', '-z', '--cached', '--others', '--exclude-standard'],
|
|
66
|
-
env,
|
|
67
|
-
stderr: 'pipe',
|
|
68
|
-
stdout: 'pipe'
|
|
69
|
-
});
|
|
70
|
-
if (result.exitCode !== 0) {
|
|
71
|
-
const stderr = decodeText(result.stderr).trim();
|
|
72
|
-
throw new CliExitError({
|
|
73
|
-
code: result.exitCode,
|
|
74
|
-
message: stderr.length > 0 ? stderr : 'Failed to list files for compact step'
|
|
75
|
-
});
|
|
76
|
-
}
|
|
77
|
-
const entries = decodeText(result.stdout).split('\0'), files = [];
|
|
78
|
-
for (const entry of entries)
|
|
79
|
-
if (entry.length > 0 && entry !== 'bun.lock')
|
|
80
|
-
files.push(entry);
|
|
81
|
-
return files;
|
|
82
|
-
}, runCompact = async ({ env, mode, root }) => {
|
|
83
|
-
const files = listCompactFiles({ env, root }), results = await Promise.all(files.map(async (relativePath) => {
|
|
84
|
-
if (!isCompactCandidate({ relativePath }))
|
|
85
|
-
return { changed: false, relativePath, scanned: false };
|
|
86
|
-
const absolutePath = joinPath(root, relativePath), source = file(absolutePath);
|
|
87
|
-
if (!(await source.exists()))
|
|
88
|
-
return { changed: false, relativePath, scanned: false };
|
|
89
|
-
const bytes = new Uint8Array(await source.arrayBuffer());
|
|
90
|
-
if (isBinary({ bytes }))
|
|
91
|
-
return { changed: false, relativePath, scanned: true };
|
|
92
|
-
const content = decodeText(bytes), compacted = compactContent({ content });
|
|
93
|
-
if (content === compacted)
|
|
94
|
-
return { changed: false, relativePath, scanned: true };
|
|
95
|
-
if (mode === 'fix')
|
|
96
|
-
await write(absolutePath, compacted);
|
|
97
|
-
return { changed: true, relativePath, scanned: true };
|
|
98
|
-
})), changed = [];
|
|
99
|
-
let scanned = 0;
|
|
100
|
-
for (const result of results) {
|
|
101
|
-
if (result.scanned)
|
|
102
|
-
scanned += 1;
|
|
103
|
-
if (result.changed)
|
|
104
|
-
changed.push(result.relativePath);
|
|
105
|
-
}
|
|
106
|
-
if (mode === 'fix') {
|
|
107
|
-
process.stdout.write(`[compact] Scanned ${scanned} files\n`);
|
|
108
|
-
process.stdout.write(`[compact] Updated ${changed.length} files\n`);
|
|
109
|
-
return;
|
|
110
|
-
}
|
|
111
|
-
if (changed.length === 0)
|
|
112
|
-
return;
|
|
113
|
-
const shown = changed.slice(0, 10), suffix = changed.length > shown.length ? `\n...and ${changed.length - shown.length} more` : '';
|
|
114
|
-
throw new CliExitError({
|
|
115
|
-
code: 1,
|
|
116
|
-
message: `[compact]\nFiles requiring compaction:\n${shown.join('\n')}${suffix}\nRun: lintmax fix`
|
|
117
|
-
});
|
|
118
|
-
}, ensureDirectory = ({ directory }) => {
|
|
119
|
-
const result = spawnSync({
|
|
120
|
-
cmd: ['mkdir', '-p', directory],
|
|
121
|
-
stderr: 'pipe',
|
|
122
|
-
stdout: 'pipe'
|
|
123
|
-
});
|
|
124
|
-
if (result.exitCode === 0)
|
|
125
|
-
return;
|
|
126
|
-
const stderr = decodeText(result.stderr).trim();
|
|
127
|
-
throw new CliExitError({
|
|
128
|
-
code: result.exitCode,
|
|
129
|
-
message: stderr.length > 0 ? stderr : `Failed to create directory: ${directory}`
|
|
130
|
-
});
|
|
131
|
-
}, sortKeys = (obj) => {
|
|
132
|
-
const sorted = {}, keys = Object.keys(obj).toSorted();
|
|
133
|
-
for (const key of keys)
|
|
134
|
-
sorted[key] = obj[key];
|
|
135
|
-
return sorted;
|
|
136
|
-
}, initScripts = async ({ pkg, pkgPath }) => {
|
|
137
|
-
const scripts = pkg.scripts ?? {};
|
|
138
|
-
let changed = false;
|
|
139
|
-
if (!scripts.fix) {
|
|
140
|
-
scripts.fix = 'lintmax fix';
|
|
141
|
-
changed = true;
|
|
142
|
-
}
|
|
143
|
-
if (!scripts.check) {
|
|
144
|
-
scripts.check = 'lintmax check';
|
|
145
|
-
changed = true;
|
|
146
|
-
}
|
|
147
|
-
if (!changed)
|
|
148
|
-
return;
|
|
149
|
-
pkg.scripts = scripts;
|
|
150
|
-
await writeJson({ data: pkg, path: pkgPath });
|
|
151
|
-
}, initTsconfig = async ({ configFiles }) => {
|
|
152
|
-
const tsconfigPath = joinPath(cwd, 'tsconfig.json');
|
|
153
|
-
if (!(await pathExists({ path: tsconfigPath }))) {
|
|
154
|
-
const tsconfig = { extends: 'lintmax/tsconfig' };
|
|
155
|
-
if (configFiles.length > 0)
|
|
156
|
-
tsconfig.include = configFiles;
|
|
157
|
-
await writeJson({ data: tsconfig, path: tsconfigPath });
|
|
158
|
-
return;
|
|
159
|
-
}
|
|
160
|
-
try {
|
|
161
|
-
const tsconfig = await readRequiredJson({ path: tsconfigPath });
|
|
162
|
-
let changed = false;
|
|
163
|
-
if (tsconfig.extends !== 'lintmax/tsconfig') {
|
|
164
|
-
tsconfig.extends = 'lintmax/tsconfig';
|
|
165
|
-
changed = true;
|
|
166
|
-
}
|
|
167
|
-
const toAdd = configFiles.filter(f => !(tsconfig.include ?? []).includes(f));
|
|
168
|
-
if (toAdd.length > 0) {
|
|
169
|
-
tsconfig.include = [...(tsconfig.include ?? []), ...toAdd];
|
|
170
|
-
changed = true;
|
|
171
|
-
}
|
|
172
|
-
if (changed)
|
|
173
|
-
await writeJson({ data: tsconfig, path: tsconfigPath });
|
|
174
|
-
}
|
|
175
|
-
catch {
|
|
176
|
-
process.stderr.write('tsconfig.json: could not parse, add "extends": "lintmax/tsconfig" manually\n');
|
|
177
|
-
}
|
|
178
|
-
}, initGitignore = async () => {
|
|
179
|
-
const gitignorePath = joinPath(cwd, '.gitignore');
|
|
180
|
-
if (await pathExists({ path: gitignorePath })) {
|
|
181
|
-
const content = await file(gitignorePath).text(), toAdd = [];
|
|
182
|
-
for (const entry of ignoreEntries)
|
|
183
|
-
if (!content.includes(entry))
|
|
184
|
-
toAdd.push(entry);
|
|
185
|
-
if (toAdd.length > 0)
|
|
186
|
-
await write(gitignorePath, `${content.trimEnd()}\n${toAdd.join('\n')}\n`);
|
|
187
|
-
return;
|
|
188
|
-
}
|
|
189
|
-
await write(gitignorePath, `${ignoreEntries.join('\n')}\n`);
|
|
190
|
-
}, initVscodeSettings = async ({ pkg }) => {
|
|
191
|
-
const vscodePath = joinPath(cwd, '.vscode');
|
|
192
|
-
ensureDirectory({ directory: vscodePath });
|
|
193
|
-
const settingsPath = joinPath(vscodePath, 'settings.json'), settings = await readJson({ path: settingsPath });
|
|
194
|
-
settings['biome.configPath'] = 'node_modules/.cache/lintmax/biome.json';
|
|
195
|
-
const formatterLangs = [
|
|
196
|
-
'css',
|
|
197
|
-
'graphql',
|
|
198
|
-
'javascript',
|
|
199
|
-
'javascriptreact',
|
|
200
|
-
'json',
|
|
201
|
-
'jsonc',
|
|
202
|
-
'typescript',
|
|
203
|
-
'typescriptreact'
|
|
204
|
-
];
|
|
205
|
-
for (const lang of formatterLangs) {
|
|
206
|
-
const key = `[${lang}]`, existing = (settings[key] ?? {});
|
|
207
|
-
settings[key] = {
|
|
208
|
-
...existing,
|
|
209
|
-
'editor.defaultFormatter': 'biomejs.biome'
|
|
210
|
-
};
|
|
211
|
-
}
|
|
212
|
-
const existingActions = (settings['editor.codeActionsOnSave'] ?? {});
|
|
213
|
-
settings['editor.codeActionsOnSave'] = {
|
|
214
|
-
...existingActions,
|
|
215
|
-
'source.fixAll.biome': 'always',
|
|
216
|
-
'source.fixAll.eslint': 'always',
|
|
217
|
-
'source.organizeImports.biome': 'always'
|
|
218
|
-
};
|
|
219
|
-
settings['editor.formatOnSave'] = true;
|
|
220
|
-
settings['eslint.rules.customizations'] = [{ rule: '*', severity: 'warn' }];
|
|
221
|
-
if (pkg.workspaces && !settings['eslint.workingDirectories']) {
|
|
222
|
-
const dirs = [];
|
|
223
|
-
for (const ws of pkg.workspaces) {
|
|
224
|
-
const pattern = ws.endsWith('/') ? ws : `${ws}/`;
|
|
225
|
-
dirs.push({ pattern });
|
|
226
|
-
}
|
|
227
|
-
if (dirs.length > 0)
|
|
228
|
-
settings['eslint.workingDirectories'] = dirs;
|
|
229
|
-
}
|
|
230
|
-
await writeJson({ data: sortKeys(settings), path: settingsPath });
|
|
231
|
-
}, initVscodeExtensions = async () => {
|
|
232
|
-
const extensionsPath = joinPath(cwd, '.vscode', 'extensions.json'), extJson = (await readJson({ path: extensionsPath })), recommendations = ['biomejs.biome', 'dbaeumer.vscode-eslint'], currentRecs = extJson.recommendations ?? [], recsToAdd = [];
|
|
233
|
-
for (const rec of recommendations)
|
|
234
|
-
if (!currentRecs.includes(rec))
|
|
235
|
-
recsToAdd.push(rec);
|
|
236
|
-
if (recsToAdd.length > 0 || !extJson.recommendations) {
|
|
237
|
-
extJson.recommendations = [...currentRecs, ...recsToAdd];
|
|
238
|
-
await writeJson({ data: extJson, path: extensionsPath });
|
|
239
|
-
}
|
|
240
|
-
}, findLegacyConfigs = async () => {
|
|
241
|
-
const legacyConfigs = [
|
|
242
|
-
'.eslintrc',
|
|
243
|
-
'.eslintrc.json',
|
|
244
|
-
'.eslintrc.js',
|
|
245
|
-
'.eslintrc.cjs',
|
|
246
|
-
'.eslintrc.yml',
|
|
247
|
-
'.eslintrc.yaml',
|
|
248
|
-
'.prettierrc',
|
|
249
|
-
'.prettierrc.json',
|
|
250
|
-
'.prettierrc.js',
|
|
251
|
-
'.prettierrc.yml',
|
|
252
|
-
'.prettierrc.yaml',
|
|
253
|
-
'.prettierrc.toml',
|
|
254
|
-
'biome.json',
|
|
255
|
-
'biome.jsonc',
|
|
256
|
-
'.oxlintrc.json'
|
|
257
|
-
], checks = legacyConfigs.map(async (configFile) => ({
|
|
258
|
-
configFile,
|
|
259
|
-
exists: await pathExists({ path: joinPath(cwd, configFile) })
|
|
260
|
-
})), resolved = await Promise.all(checks), found = [];
|
|
261
|
-
for (const item of resolved)
|
|
262
|
-
if (item.exists)
|
|
263
|
-
found.push(item.configFile);
|
|
264
|
-
return found;
|
|
265
|
-
}, readVersion = async () => {
|
|
266
|
-
const pkg = await readRequiredJson({
|
|
267
|
-
path: joinPath(lintmaxRoot, 'package.json')
|
|
268
|
-
});
|
|
269
|
-
return pkg.version;
|
|
270
|
-
}, usage = ({ version }) => {
|
|
271
|
-
process.stdout.write(`lintmax v${version}\n\n`);
|
|
272
|
-
process.stdout.write('Usage: lintmax <command>\n\n');
|
|
273
|
-
process.stdout.write('Commands:\n');
|
|
274
|
-
process.stdout.write(' init Scaffold config files for a new project\n');
|
|
275
|
-
process.stdout.write(' fix Auto-fix and format all files\n');
|
|
276
|
-
process.stdout.write(' check Check all files without modifying\n');
|
|
277
|
-
process.stdout.write(' --version Show version\n');
|
|
278
|
-
}, resolvePackageJsonPath = async ({ pkg }) => {
|
|
279
|
-
try {
|
|
280
|
-
return fromFileUrl(import.meta.resolve(`${pkg}/package.json`));
|
|
281
|
-
}
|
|
282
|
-
catch {
|
|
283
|
-
const consumerCandidate = joinPath(cwd, 'node_modules', pkg, 'package.json');
|
|
284
|
-
if (await pathExists({ path: consumerCandidate }))
|
|
285
|
-
return consumerCandidate;
|
|
286
|
-
return null;
|
|
287
|
-
}
|
|
288
|
-
}, resolveBin = async ({ bin, pkg }) => {
|
|
289
|
-
const packageJsonPath = await resolvePackageJsonPath({ pkg });
|
|
290
|
-
if (!packageJsonPath)
|
|
291
|
-
throw new CliExitError({
|
|
292
|
-
code: 1,
|
|
293
|
-
message: `Cannot find ${pkg} — run: bun add -d lintmax`
|
|
294
|
-
});
|
|
295
|
-
const pkgJson = await readRequiredJson({ path: packageJsonPath }), pkgDir = dirnamePath(packageJsonPath), binPath = typeof pkgJson.bin === 'string' ? pkgJson.bin : (pkgJson.bin?.[bin] ?? '');
|
|
296
|
-
return joinPath(pkgDir, binPath);
|
|
297
|
-
}, run = ({ args, command, env, label, silent = false }) => {
|
|
298
|
-
const result = spawnSync({
|
|
299
|
-
cmd: [command, ...args],
|
|
300
|
-
cwd,
|
|
301
|
-
env,
|
|
302
|
-
stderr: silent ? 'pipe' : 'inherit',
|
|
303
|
-
stdout: silent ? 'pipe' : 'inherit'
|
|
304
|
-
});
|
|
305
|
-
if (result.exitCode === 0)
|
|
306
|
-
return;
|
|
307
|
-
if (silent) {
|
|
308
|
-
process.stderr.write(`[${label}]\n`);
|
|
309
|
-
const stdout = decodeText(result.stdout);
|
|
310
|
-
if (stdout.length > 0)
|
|
311
|
-
process.stderr.write(stdout);
|
|
312
|
-
const stderr = decodeText(result.stderr);
|
|
313
|
-
if (stderr.length > 0)
|
|
314
|
-
process.stderr.write(stderr);
|
|
315
|
-
}
|
|
316
|
-
throw new CliExitError({ code: result.exitCode });
|
|
317
|
-
}, runInit = async () => {
|
|
318
|
-
const pkgPath = joinPath(cwd, 'package.json');
|
|
319
|
-
if (!(await pathExists({ path: pkgPath })))
|
|
320
|
-
throw new CliExitError({ code: 1, message: 'No package.json found' });
|
|
321
|
-
const pkg = await readRequiredJson({ path: pkgPath }), configFiles = [];
|
|
322
|
-
if (await pathExists({ path: joinPath(cwd, 'eslint.config.ts') }))
|
|
323
|
-
configFiles.push('eslint.config.ts');
|
|
324
|
-
if (await pathExists({ path: joinPath(cwd, 'lintmax.config.ts') }))
|
|
325
|
-
configFiles.push('lintmax.config.ts');
|
|
326
|
-
await initScripts({ pkg, pkgPath });
|
|
327
|
-
await initTsconfig({ configFiles });
|
|
328
|
-
await initGitignore();
|
|
329
|
-
await initVscodeSettings({ pkg });
|
|
330
|
-
await initVscodeExtensions();
|
|
331
|
-
const foundLegacy = await findLegacyConfigs();
|
|
332
|
-
process.stdout.write('tsconfig.json extends lintmax/tsconfig');
|
|
333
|
-
if (configFiles.length > 0)
|
|
334
|
-
process.stdout.write(`, include: ${configFiles.join(', ')}`);
|
|
335
|
-
process.stdout.write('\n');
|
|
336
|
-
process.stdout.write('package.json "fix": "lintmax fix", "check": "lintmax check"\n');
|
|
337
|
-
process.stdout.write(`.gitignore ${ignoreEntries.join(', ')}\n`);
|
|
338
|
-
process.stdout.write('.vscode/settings biome formatter, codeActionsOnSave, eslint\n');
|
|
339
|
-
process.stdout.write('.vscode/ext biomejs.biome, dbaeumer.vscode-eslint\n');
|
|
340
|
-
if (foundLegacy.length > 0)
|
|
341
|
-
process.stdout.write(`\nLegacy configs found (can be removed): ${foundLegacy.join(', ')}\n`);
|
|
342
|
-
process.stdout.write('\nRun: bun fix\n');
|
|
343
|
-
}, runLint = async () => {
|
|
344
|
-
const dir = joinPath(cwd, cacheDir);
|
|
345
|
-
ensureDirectory({ directory: dir });
|
|
346
|
-
const configPath = joinPath(cwd, 'lintmax.config.ts'), hasConfig = await pathExists({ path: configPath }), bundledBinA = joinPath(lintmaxRoot, 'node_modules', '.bin'), bundledBinB = joinPath(dirnamePath(lintmaxRoot), '.bin'), cwdBinDir = joinPath(cwd, 'node_modules', '.bin'), runtimePath = joinPath(dir, 'lintmax.json'), env = {
|
|
347
|
-
...bunEnv,
|
|
348
|
-
PATH: `${bundledBinA}:${bundledBinB}:${cwdBinDir}:${bunEnv.PATH ?? ''}`
|
|
349
|
-
};
|
|
350
|
-
if (hasConfig)
|
|
351
|
-
run({
|
|
352
|
-
args: [
|
|
353
|
-
'-e',
|
|
354
|
-
`const m = await import('${configPath}'); const { sync: s } = await import('lintmax'); await s(m.default);`
|
|
355
|
-
],
|
|
356
|
-
command: 'bun',
|
|
357
|
-
env,
|
|
358
|
-
label: 'config',
|
|
359
|
-
silent: true
|
|
360
|
-
});
|
|
361
|
-
else
|
|
362
|
-
await sync();
|
|
363
|
-
const runtime = (await readJson({ path: runtimePath }));
|
|
364
|
-
if (runtime.compact === true)
|
|
365
|
-
await runCompact({
|
|
366
|
-
env,
|
|
367
|
-
mode: cmd === 'fix' ? 'fix' : 'check',
|
|
368
|
-
root: cwd
|
|
369
|
-
});
|
|
370
|
-
const hasEslintConfig = (await pathExists({ path: joinPath(cwd, 'eslint.config.ts') })) ||
|
|
371
|
-
(await pathExists({ path: joinPath(cwd, 'eslint.config.js') })) ||
|
|
372
|
-
(await pathExists({ path: joinPath(cwd, 'eslint.config.mjs') })), eslintArgs = hasEslintConfig ? [] : ['--config', joinPath(dir, 'eslint.config.mjs')], [sortPkgJson, biomeBin, oxlintBin, eslintBin, prettierBin] = await Promise.all([
|
|
373
|
-
resolveBin({ bin: 'sort-package-json', pkg: 'sort-package-json' }),
|
|
374
|
-
resolveBin({ bin: 'biome', pkg: '@biomejs/biome' }),
|
|
375
|
-
resolveBin({ bin: 'oxlint', pkg: 'oxlint' }),
|
|
376
|
-
resolveBin({ bin: 'eslint', pkg: 'eslint' }),
|
|
377
|
-
resolveBin({ bin: 'prettier', pkg: 'prettier' })
|
|
378
|
-
]), prettierMd = [
|
|
379
|
-
'--single-quote',
|
|
380
|
-
'--no-semi',
|
|
381
|
-
'--trailing-comma',
|
|
382
|
-
'none',
|
|
383
|
-
'--print-width',
|
|
384
|
-
'80',
|
|
385
|
-
'--arrow-parens',
|
|
386
|
-
'avoid',
|
|
387
|
-
'--tab-width',
|
|
388
|
-
'2',
|
|
389
|
-
'--prose-wrap',
|
|
390
|
-
'preserve'
|
|
391
|
-
], hasFlowmark = spawnSync({
|
|
392
|
-
cmd: ['which', 'flowmark'],
|
|
393
|
-
env,
|
|
394
|
-
stderr: 'pipe',
|
|
395
|
-
stdout: 'pipe'
|
|
396
|
-
}).exitCode === 0;
|
|
397
|
-
if (cmd === 'fix') {
|
|
398
|
-
run({
|
|
399
|
-
args: [sortPkgJson, '**/package.json', '--ignore', '**/node_modules/**'],
|
|
400
|
-
command: 'bun',
|
|
401
|
-
env,
|
|
402
|
-
label: 'sort-package-json',
|
|
403
|
-
silent: true
|
|
404
|
-
});
|
|
405
|
-
run({
|
|
406
|
-
args: [biomeBin, 'check', '--config-path', dir, '--fix', '--diagnostic-level=error'],
|
|
407
|
-
command: 'bun',
|
|
408
|
-
env,
|
|
409
|
-
label: 'biome',
|
|
410
|
-
silent: true
|
|
411
|
-
});
|
|
412
|
-
run({
|
|
413
|
-
args: [oxlintBin, '-c', joinPath(dir, '.oxlintrc.json'), '--fix', '--fix-suggestions', '--quiet'],
|
|
414
|
-
command: 'bun',
|
|
415
|
-
env,
|
|
416
|
-
label: 'oxlint',
|
|
417
|
-
silent: true
|
|
418
|
-
});
|
|
419
|
-
run({
|
|
420
|
-
args: [eslintBin, ...eslintArgs, '--fix', '--cache', '--cache-location', joinPath(cwd, '.cache', '.eslintcache')],
|
|
421
|
-
command: 'bun',
|
|
422
|
-
env,
|
|
423
|
-
label: 'eslint',
|
|
424
|
-
silent: true
|
|
425
|
-
});
|
|
426
|
-
run({
|
|
427
|
-
args: [biomeBin, 'check', '--config-path', dir, '--fix', '--diagnostic-level=error'],
|
|
428
|
-
command: 'bun',
|
|
429
|
-
env,
|
|
430
|
-
label: 'biome',
|
|
431
|
-
silent: true
|
|
432
|
-
});
|
|
433
|
-
if (hasFlowmark)
|
|
434
|
-
run({
|
|
435
|
-
args: ['-w', '0', '--auto', '.'],
|
|
436
|
-
command: 'flowmark',
|
|
437
|
-
env,
|
|
438
|
-
label: 'flowmark',
|
|
439
|
-
silent: true
|
|
440
|
-
});
|
|
441
|
-
run({
|
|
442
|
-
args: [prettierBin, ...prettierMd, '--write', '--no-error-on-unmatched-pattern', '**/*.md'],
|
|
443
|
-
command: 'bun',
|
|
444
|
-
env,
|
|
445
|
-
label: 'prettier',
|
|
446
|
-
silent: true
|
|
447
|
-
});
|
|
448
|
-
return;
|
|
449
|
-
}
|
|
450
|
-
run({
|
|
451
|
-
args: [sortPkgJson, '--check', '**/package.json', '--ignore', '**/node_modules/**'],
|
|
452
|
-
command: 'bun',
|
|
453
|
-
env,
|
|
454
|
-
label: 'sort-package-json'
|
|
455
|
-
});
|
|
456
|
-
run({
|
|
457
|
-
args: [biomeBin, 'ci', '--config-path', dir, '--diagnostic-level=error'],
|
|
458
|
-
command: 'bun',
|
|
459
|
-
env,
|
|
460
|
-
label: 'biome'
|
|
461
|
-
});
|
|
462
|
-
run({
|
|
463
|
-
args: [oxlintBin, '-c', joinPath(dir, '.oxlintrc.json'), '--quiet'],
|
|
464
|
-
command: 'bun',
|
|
465
|
-
env,
|
|
466
|
-
label: 'oxlint'
|
|
467
|
-
});
|
|
468
|
-
run({
|
|
469
|
-
args: [eslintBin, ...eslintArgs, '--cache', '--cache-location', joinPath(cwd, '.cache', '.eslintcache')],
|
|
470
|
-
command: 'bun',
|
|
471
|
-
env,
|
|
472
|
-
label: 'eslint'
|
|
473
|
-
});
|
|
474
|
-
run({
|
|
475
|
-
args: [prettierBin, ...prettierMd, '--check', '--no-error-on-unmatched-pattern', '**/*.md'],
|
|
476
|
-
command: 'bun',
|
|
477
|
-
env,
|
|
478
|
-
label: 'prettier'
|
|
479
|
-
});
|
|
480
|
-
}, main = async () => {
|
|
2
|
+
import { CliExitError, readVersion, usage } from './core.js';
|
|
3
|
+
import { runInit } from './init.js';
|
|
4
|
+
import { runLint } from './pipeline.js';
|
|
5
|
+
const command = process.argv[2], main = async () => {
|
|
481
6
|
const version = await readVersion();
|
|
482
|
-
if (
|
|
7
|
+
if (command === 'init') {
|
|
483
8
|
await runInit();
|
|
484
9
|
return;
|
|
485
10
|
}
|
|
486
|
-
if (
|
|
11
|
+
if (command === '--version' || command === '-v') {
|
|
487
12
|
process.stdout.write(`${version}\n`);
|
|
488
13
|
return;
|
|
489
14
|
}
|
|
490
|
-
if (
|
|
15
|
+
if (command !== 'fix' && command !== 'check') {
|
|
491
16
|
usage({ version });
|
|
492
|
-
if (
|
|
17
|
+
if (command === '--help' || command === '-h')
|
|
493
18
|
return;
|
|
494
19
|
throw new CliExitError({ code: 1 });
|
|
495
20
|
}
|
|
496
|
-
await runLint();
|
|
21
|
+
await runLint({ command });
|
|
497
22
|
};
|
|
498
23
|
try {
|
|
499
24
|
await main();
|