@eimerreis/linting 0.2.0 → 0.4.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 +95 -7
- package/bin/init.mjs +459 -173
- package/oxfmt.config.json +17 -17
- package/oxlint.config.json +36 -36
- package/package.json +57 -58
package/README.md
CHANGED
|
@@ -4,6 +4,8 @@ Personal linting and formatting defaults for new JavaScript/TypeScript projects.
|
|
|
4
4
|
|
|
5
5
|
Requires Node `^20.19.0 || >=22.12.0`.
|
|
6
6
|
|
|
7
|
+
Zero peer-dependency setup for consumers: install only `@eimerreis/linting`.
|
|
8
|
+
|
|
7
9
|
Built on:
|
|
8
10
|
|
|
9
11
|
- `oxlint` for linting
|
|
@@ -19,10 +21,10 @@ Default focus:
|
|
|
19
21
|
## Install
|
|
20
22
|
|
|
21
23
|
```bash
|
|
22
|
-
npm add -D @eimerreis/linting
|
|
24
|
+
npm add -D @eimerreis/linting
|
|
23
25
|
```
|
|
24
26
|
|
|
25
|
-
`lint` also executes
|
|
27
|
+
`lint` also executes react-doctor via:
|
|
26
28
|
|
|
27
29
|
```bash
|
|
28
30
|
npx react-doctor -y .
|
|
@@ -58,16 +60,51 @@ And updates `package.json` scripts (if present) so linting is executed through t
|
|
|
58
60
|
|
|
59
61
|
```bash
|
|
60
62
|
eimerreis-linting init [targetDir] [--force]
|
|
61
|
-
eimerreis-linting lint [targetDir] [--fix]
|
|
62
|
-
eimerreis-linting format [targetDir] [--check]
|
|
63
|
+
eimerreis-linting lint [targetDir] [--fix] [--ignore-path <path>] [--ignore-pattern <pattern>]
|
|
64
|
+
eimerreis-linting format [targetDir] [--check] [--ignore-path <path>] [--ignore-pattern <pattern>]
|
|
63
65
|
```
|
|
64
66
|
|
|
65
67
|
- `init --force`: overwrite existing `.oxlintrc.json` / `.oxfmtrc.json` and script values
|
|
66
|
-
- `lint --fix`: run `oxlint --fix .` and then
|
|
68
|
+
- `lint --fix`: run `oxlint --fix .` and then run react-doctor
|
|
67
69
|
- `format --check`: run `oxfmt --check .`
|
|
70
|
+
- `--ignore-path`: add one ignore file (repeatable) for lint/format
|
|
71
|
+
- `--ignore-pattern`: add one glob pattern (repeatable) for lint/format
|
|
68
72
|
|
|
69
73
|
`react-doctor` runs only when the target package has `react`, `react-dom`, or `next` in dependencies/devDependencies/peerDependencies.
|
|
70
74
|
|
|
75
|
+
### Ignoring files
|
|
76
|
+
|
|
77
|
+
Supported options:
|
|
78
|
+
|
|
79
|
+
```bash
|
|
80
|
+
eimerreis-linting lint --ignore-path .gitignore
|
|
81
|
+
eimerreis-linting format --check --ignore-path .gitignore --ignore-path .prettierignore
|
|
82
|
+
eimerreis-linting lint --ignore-pattern "dist/**" --ignore-pattern "coverage/**"
|
|
83
|
+
eimerreis-linting format --check --ignore-path .gitignore --ignore-pattern "**/*.generated.ts"
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
You can also add a dedicated ignore file at project root:
|
|
87
|
+
|
|
88
|
+
```text
|
|
89
|
+
.eimerreis-lintingignore
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
If present, it is picked up automatically by both lint and format.
|
|
93
|
+
|
|
94
|
+
### First-time one-shot usage
|
|
95
|
+
|
|
96
|
+
You do not need to run `init` first.
|
|
97
|
+
|
|
98
|
+
```bash
|
|
99
|
+
npx @eimerreis/linting lint
|
|
100
|
+
npx @eimerreis/linting format --check
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
Behavior:
|
|
104
|
+
|
|
105
|
+
- If project config exists (`.oxlintrc.*` / `oxlint.config.*`, `.oxfmtrc.*` / `oxfmt.config.*`), it uses that.
|
|
106
|
+
- If not, it falls back to the package's built-in defaults.
|
|
107
|
+
|
|
71
108
|
## Manual Setup
|
|
72
109
|
|
|
73
110
|
If you do not want to use the init script, create the config files manually.
|
|
@@ -76,7 +113,7 @@ If you do not want to use the init script, create the config files manually.
|
|
|
76
113
|
|
|
77
114
|
```json
|
|
78
115
|
{
|
|
79
|
-
|
|
116
|
+
"extends": ["./node_modules/@eimerreis/linting/oxlint.config.json"]
|
|
80
117
|
}
|
|
81
118
|
```
|
|
82
119
|
|
|
@@ -84,6 +121,57 @@ If you do not want to use the init script, create the config files manually.
|
|
|
84
121
|
|
|
85
122
|
```json
|
|
86
123
|
{
|
|
87
|
-
|
|
124
|
+
"extends": ["./node_modules/@eimerreis/linting/oxfmt.config.json"]
|
|
125
|
+
}
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
## Scripts
|
|
129
|
+
|
|
130
|
+
Generated project scripts:
|
|
131
|
+
|
|
132
|
+
```json
|
|
133
|
+
{
|
|
134
|
+
"scripts": {
|
|
135
|
+
"lint": "eimerreis-linting lint",
|
|
136
|
+
"lint:fix": "eimerreis-linting lint --fix",
|
|
137
|
+
"format": "eimerreis-linting format",
|
|
138
|
+
"format:check": "eimerreis-linting format --check",
|
|
139
|
+
"lint:ignore": "eimerreis-linting lint --ignore-path .eimerreis-lintingignore",
|
|
140
|
+
"format:check:ignore": "eimerreis-linting format --check --ignore-path .eimerreis-lintingignore"
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
`init` also adds this dev dependency automatically (if missing):
|
|
146
|
+
|
|
147
|
+
```json
|
|
148
|
+
{
|
|
149
|
+
"devDependencies": {
|
|
150
|
+
"@eimerreis/linting": "^<current-version>"
|
|
151
|
+
}
|
|
88
152
|
}
|
|
89
153
|
```
|
|
154
|
+
|
|
155
|
+
## Publish
|
|
156
|
+
|
|
157
|
+
### Git-based release workflow (Changesets + GitHub Actions)
|
|
158
|
+
|
|
159
|
+
1. Create a changeset:
|
|
160
|
+
|
|
161
|
+
```bash
|
|
162
|
+
npm run changeset
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
2. Commit `.changeset/*.md` and push to `main`.
|
|
166
|
+
3. Workflow `Release` checks for pending changesets.
|
|
167
|
+
4. If changesets exist, it versions, publishes via npm trusted publishing (OIDC), and commits `chore: version packages`.
|
|
168
|
+
|
|
169
|
+
Trusted publishing setup (one-time on npmjs.com):
|
|
170
|
+
|
|
171
|
+
1. Package `@eimerreis/linting` -> Settings -> Trusted Publisher
|
|
172
|
+
2. Provider: GitHub Actions
|
|
173
|
+
3. Organization/user: `eimerreis`
|
|
174
|
+
4. Repository: `eimerreis-linting`
|
|
175
|
+
5. Workflow filename: `release.yml`
|
|
176
|
+
|
|
177
|
+
No `NPM_TOKEN` secret is required for publishing.
|
package/bin/init.mjs
CHANGED
|
@@ -1,256 +1,542 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
import { spawn } from "node:child_process";
|
|
4
|
-
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
4
|
+
import { existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
|
5
|
+
import { createRequire } from "node:module";
|
|
6
|
+
import { tmpdir } from "node:os";
|
|
7
|
+
import { dirname, join, resolve } from "node:path";
|
|
7
8
|
import process from "node:process";
|
|
9
|
+
import { fileURLToPath } from "node:url";
|
|
8
10
|
|
|
9
11
|
const packageName = "@eimerreis/linting";
|
|
10
12
|
const packageRoot = resolve(dirname(fileURLToPath(import.meta.url)), "..");
|
|
13
|
+
const requireFromHere = createRequire(import.meta.url);
|
|
11
14
|
|
|
12
15
|
function ensureDirectory(filePath) {
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
16
|
+
const dir = dirname(filePath);
|
|
17
|
+
if (!existsSync(dir)) {
|
|
18
|
+
mkdirSync(dir, { recursive: true });
|
|
19
|
+
}
|
|
17
20
|
}
|
|
18
21
|
|
|
19
22
|
function writeJson(filePath, data) {
|
|
20
|
-
|
|
21
|
-
|
|
23
|
+
ensureDirectory(filePath);
|
|
24
|
+
writeFileSync(filePath, `${JSON.stringify(data, null, 2)}\n`, "utf8");
|
|
22
25
|
}
|
|
23
26
|
|
|
24
27
|
function readJson(filePath) {
|
|
25
|
-
|
|
28
|
+
return JSON.parse(readFileSync(filePath, "utf8"));
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function getSelfDependencyRange() {
|
|
32
|
+
try {
|
|
33
|
+
const selfPackageJsonPath = resolve(packageRoot, "package.json");
|
|
34
|
+
const selfPackageJson = readJson(selfPackageJsonPath);
|
|
35
|
+
if (selfPackageJson.version) {
|
|
36
|
+
return `^${selfPackageJson.version}`;
|
|
37
|
+
}
|
|
38
|
+
} catch {
|
|
39
|
+
// ignore
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
return "latest";
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function resolvePackageBin(dependencyName, binRelativePath) {
|
|
46
|
+
const entryPointPath = requireFromHere.resolve(dependencyName);
|
|
47
|
+
let cursor = dirname(entryPointPath);
|
|
48
|
+
|
|
49
|
+
while (cursor !== dirname(cursor)) {
|
|
50
|
+
const packageJsonPath = resolve(cursor, "package.json");
|
|
51
|
+
if (existsSync(packageJsonPath)) {
|
|
52
|
+
const packageJson = readJson(packageJsonPath);
|
|
53
|
+
if (packageJson.name === dependencyName) {
|
|
54
|
+
const binPath = resolve(cursor, binRelativePath);
|
|
55
|
+
if (!existsSync(binPath)) {
|
|
56
|
+
throw new Error(`Cannot find ${dependencyName} binary at ${binPath}`);
|
|
57
|
+
}
|
|
58
|
+
return binPath;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
cursor = dirname(cursor);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
throw new Error(`Unable to resolve package root for ${dependencyName}`);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function resolvePackageEntry(dependencyName) {
|
|
68
|
+
return requireFromHere.resolve(dependencyName);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function hasInstalledPackage(dependencyName) {
|
|
72
|
+
try {
|
|
73
|
+
resolvePackageEntry(dependencyName);
|
|
74
|
+
return true;
|
|
75
|
+
} catch {
|
|
76
|
+
return false;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function resolveExistingConfigPath(targetDir, configCandidates) {
|
|
81
|
+
for (const candidate of configCandidates) {
|
|
82
|
+
const candidatePath = resolve(targetDir, candidate);
|
|
83
|
+
if (existsSync(candidatePath)) {
|
|
84
|
+
return candidatePath;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
return null;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function resolveLintConfigPath(targetDir) {
|
|
92
|
+
const localConfigPath = resolveExistingConfigPath(targetDir, [
|
|
93
|
+
".oxlintrc.json",
|
|
94
|
+
".oxlintrc.jsonc",
|
|
95
|
+
".oxlintrc.js",
|
|
96
|
+
".oxlintrc.mjs",
|
|
97
|
+
".oxlintrc.cjs",
|
|
98
|
+
".oxlintrc.ts",
|
|
99
|
+
".oxlintrc.mts",
|
|
100
|
+
".oxlintrc.cts",
|
|
101
|
+
"oxlint.config.js",
|
|
102
|
+
"oxlint.config.mjs",
|
|
103
|
+
"oxlint.config.cjs",
|
|
104
|
+
"oxlint.config.ts",
|
|
105
|
+
"oxlint.config.mts",
|
|
106
|
+
"oxlint.config.cts",
|
|
107
|
+
]);
|
|
108
|
+
|
|
109
|
+
if (localConfigPath) {
|
|
110
|
+
return localConfigPath;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
return resolve(packageRoot, "oxlint.config.json");
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function resolveFormatConfigPath(targetDir) {
|
|
117
|
+
const localConfigPath = resolveExistingConfigPath(targetDir, [
|
|
118
|
+
".oxfmtrc.json",
|
|
119
|
+
".oxfmtrc.jsonc",
|
|
120
|
+
".oxfmtrc.js",
|
|
121
|
+
".oxfmtrc.mjs",
|
|
122
|
+
".oxfmtrc.cjs",
|
|
123
|
+
".oxfmtrc.ts",
|
|
124
|
+
".oxfmtrc.mts",
|
|
125
|
+
".oxfmtrc.cts",
|
|
126
|
+
"oxfmt.config.js",
|
|
127
|
+
"oxfmt.config.mjs",
|
|
128
|
+
"oxfmt.config.cjs",
|
|
129
|
+
"oxfmt.config.ts",
|
|
130
|
+
"oxfmt.config.mts",
|
|
131
|
+
"oxfmt.config.cts",
|
|
132
|
+
]);
|
|
133
|
+
|
|
134
|
+
if (localConfigPath) {
|
|
135
|
+
return localConfigPath;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
return resolve(packageRoot, "oxfmt.config.json");
|
|
26
139
|
}
|
|
27
140
|
|
|
28
141
|
function printUsage() {
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
142
|
+
console.log("Usage:");
|
|
143
|
+
console.log(" eimerreis-linting init [targetDir] [--force]");
|
|
144
|
+
console.log(" eimerreis-linting lint [targetDir] [--fix] [--ignore-path <path>] [--ignore-pattern <pattern>]");
|
|
145
|
+
console.log(" eimerreis-linting format [targetDir] [--check] [--ignore-path <path>] [--ignore-pattern <pattern>]");
|
|
33
146
|
}
|
|
34
147
|
|
|
35
148
|
function parseCommand(rawArgs) {
|
|
36
|
-
|
|
37
|
-
|
|
149
|
+
const firstArg = rawArgs[0];
|
|
150
|
+
if (!firstArg || firstArg === "init" || firstArg.startsWith("-")) {
|
|
151
|
+
return {
|
|
152
|
+
command: "init",
|
|
153
|
+
args: firstArg === "init" ? rawArgs.slice(1) : rawArgs,
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
|
|
38
157
|
return {
|
|
39
|
-
|
|
40
|
-
|
|
158
|
+
command: firstArg,
|
|
159
|
+
args: rawArgs.slice(1),
|
|
41
160
|
};
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
return {
|
|
45
|
-
command: firstArg,
|
|
46
|
-
args: rawArgs.slice(1),
|
|
47
|
-
};
|
|
48
161
|
}
|
|
49
162
|
|
|
50
163
|
function parsePathAndFlags(args, allowedFlags) {
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
164
|
+
let targetDirArg;
|
|
165
|
+
const flags = new Set();
|
|
166
|
+
|
|
167
|
+
for (const arg of args) {
|
|
168
|
+
if (arg.startsWith("-")) {
|
|
169
|
+
if (!allowedFlags.includes(arg)) {
|
|
170
|
+
throw new Error(`Unknown flag: ${arg}`);
|
|
171
|
+
}
|
|
172
|
+
flags.add(arg);
|
|
173
|
+
continue;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
if (targetDirArg) {
|
|
177
|
+
throw new Error(`Unexpected argument: ${arg}`);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
targetDirArg = arg;
|
|
61
181
|
}
|
|
62
182
|
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
183
|
+
return {
|
|
184
|
+
targetDir: resolve(process.cwd(), targetDirArg ?? "."),
|
|
185
|
+
flags,
|
|
186
|
+
};
|
|
187
|
+
}
|
|
66
188
|
|
|
67
|
-
|
|
68
|
-
|
|
189
|
+
function parseRunArgs(args, primaryFlag) {
|
|
190
|
+
let targetDirArg;
|
|
191
|
+
const ignorePaths = [];
|
|
192
|
+
const ignorePatterns = [];
|
|
193
|
+
let primaryEnabled = false;
|
|
194
|
+
|
|
195
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
196
|
+
const arg = args[index];
|
|
197
|
+
|
|
198
|
+
if (arg === primaryFlag) {
|
|
199
|
+
primaryEnabled = true;
|
|
200
|
+
continue;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
if (arg === "--ignore-path") {
|
|
204
|
+
const ignorePath = args[index + 1];
|
|
205
|
+
if (!ignorePath || ignorePath.startsWith("-")) {
|
|
206
|
+
throw new Error("Missing value for --ignore-path");
|
|
207
|
+
}
|
|
208
|
+
ignorePaths.push(ignorePath);
|
|
209
|
+
index += 1;
|
|
210
|
+
continue;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
if (arg.startsWith("--ignore-path=")) {
|
|
214
|
+
const ignorePath = arg.slice("--ignore-path=".length);
|
|
215
|
+
if (!ignorePath) {
|
|
216
|
+
throw new Error("Missing value for --ignore-path");
|
|
217
|
+
}
|
|
218
|
+
ignorePaths.push(ignorePath);
|
|
219
|
+
continue;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
if (arg === "--ignore-pattern") {
|
|
223
|
+
const ignorePattern = args[index + 1];
|
|
224
|
+
if (!ignorePattern || ignorePattern.startsWith("-")) {
|
|
225
|
+
throw new Error("Missing value for --ignore-pattern");
|
|
226
|
+
}
|
|
227
|
+
ignorePatterns.push(ignorePattern);
|
|
228
|
+
index += 1;
|
|
229
|
+
continue;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
if (arg.startsWith("--ignore-pattern=")) {
|
|
233
|
+
const ignorePattern = arg.slice("--ignore-pattern=".length);
|
|
234
|
+
if (!ignorePattern) {
|
|
235
|
+
throw new Error("Missing value for --ignore-pattern");
|
|
236
|
+
}
|
|
237
|
+
ignorePatterns.push(ignorePattern);
|
|
238
|
+
continue;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
if (arg.startsWith("-")) {
|
|
242
|
+
throw new Error(`Unknown flag: ${arg}`);
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
if (targetDirArg) {
|
|
246
|
+
throw new Error(`Unexpected argument: ${arg}`);
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
targetDirArg = arg;
|
|
250
|
+
}
|
|
69
251
|
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
252
|
+
return {
|
|
253
|
+
targetDir: resolve(process.cwd(), targetDirArg ?? "."),
|
|
254
|
+
primaryEnabled,
|
|
255
|
+
ignorePaths,
|
|
256
|
+
ignorePatterns,
|
|
257
|
+
};
|
|
74
258
|
}
|
|
75
259
|
|
|
76
260
|
function runCommand(command, commandArgs, cwd) {
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
261
|
+
return new Promise((resolvePromise) => {
|
|
262
|
+
const child = spawn(command, commandArgs, {
|
|
263
|
+
cwd,
|
|
264
|
+
stdio: "inherit",
|
|
265
|
+
shell: process.platform === "win32",
|
|
266
|
+
});
|
|
267
|
+
|
|
268
|
+
child.on("error", (error) => {
|
|
269
|
+
console.error(`failed to run ${command}: ${error.message}`);
|
|
270
|
+
resolvePromise(1);
|
|
271
|
+
});
|
|
272
|
+
|
|
273
|
+
child.on("close", (code) => {
|
|
274
|
+
resolvePromise(code ?? 1);
|
|
275
|
+
});
|
|
82
276
|
});
|
|
83
|
-
|
|
84
|
-
child.on("error", (error) => {
|
|
85
|
-
console.error(`failed to run ${command}: ${error.message}`);
|
|
86
|
-
resolvePromise(1);
|
|
87
|
-
});
|
|
88
|
-
|
|
89
|
-
child.on("close", (code) => {
|
|
90
|
-
resolvePromise(code ?? 1);
|
|
91
|
-
});
|
|
92
|
-
});
|
|
93
277
|
}
|
|
94
278
|
|
|
95
279
|
function hasReactProject(cwd) {
|
|
96
|
-
|
|
280
|
+
const packageJsonPath = resolve(cwd, "package.json");
|
|
97
281
|
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
282
|
+
if (!existsSync(packageJsonPath)) {
|
|
283
|
+
return false;
|
|
284
|
+
}
|
|
101
285
|
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
286
|
+
try {
|
|
287
|
+
const packageJson = readJson(packageJsonPath);
|
|
288
|
+
const dependencyFields = ["dependencies", "devDependencies", "peerDependencies"];
|
|
105
289
|
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
290
|
+
return dependencyFields.some((field) => {
|
|
291
|
+
const deps = packageJson[field];
|
|
292
|
+
return Boolean(deps?.react || deps?.["react-dom"] || deps?.next);
|
|
293
|
+
});
|
|
294
|
+
} catch {
|
|
295
|
+
return false;
|
|
296
|
+
}
|
|
113
297
|
}
|
|
114
298
|
|
|
115
299
|
function upsertScript(packageJson, name, command, force) {
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
300
|
+
if (!packageJson.scripts || typeof packageJson.scripts !== "object") {
|
|
301
|
+
packageJson.scripts = {};
|
|
302
|
+
}
|
|
303
|
+
if (!packageJson.scripts[name] || force) {
|
|
304
|
+
packageJson.scripts[name] = command;
|
|
305
|
+
}
|
|
122
306
|
}
|
|
123
307
|
|
|
124
308
|
function createExtendsConfig(targetPath, exportPath, force) {
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
309
|
+
if (existsSync(targetPath) && !force) {
|
|
310
|
+
console.log(`skip ${targetPath} (already exists)`);
|
|
311
|
+
return;
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
writeJson(targetPath, {
|
|
315
|
+
extends: [`./node_modules/${packageName}/${exportPath}`],
|
|
316
|
+
});
|
|
317
|
+
console.log(`write ${targetPath}`);
|
|
134
318
|
}
|
|
135
319
|
|
|
136
320
|
function maybeUpdatePackageJson(targetPackageJsonPath, force) {
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
321
|
+
if (!existsSync(targetPackageJsonPath)) {
|
|
322
|
+
console.log("skip package.json (not found)");
|
|
323
|
+
return;
|
|
324
|
+
}
|
|
141
325
|
|
|
142
|
-
|
|
326
|
+
const packageJson = readJson(targetPackageJsonPath);
|
|
143
327
|
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
328
|
+
if (!packageJson.devDependencies || typeof packageJson.devDependencies !== "object") {
|
|
329
|
+
packageJson.devDependencies = {};
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
const selfDependencyRange = getSelfDependencyRange();
|
|
333
|
+
if (!packageJson.devDependencies[packageName] || force) {
|
|
334
|
+
packageJson.devDependencies[packageName] = selfDependencyRange;
|
|
335
|
+
}
|
|
148
336
|
|
|
149
|
-
|
|
150
|
-
|
|
337
|
+
upsertScript(packageJson, "lint", "eimerreis-linting lint", force);
|
|
338
|
+
upsertScript(packageJson, "lint:fix", "eimerreis-linting lint --fix", force);
|
|
339
|
+
upsertScript(packageJson, "format", "eimerreis-linting format", force);
|
|
340
|
+
upsertScript(packageJson, "format:check", "eimerreis-linting format --check", force);
|
|
341
|
+
upsertScript(packageJson, "lint:ignore", "eimerreis-linting lint --ignore-path .eimerreis-lintingignore", force);
|
|
342
|
+
upsertScript(
|
|
343
|
+
packageJson,
|
|
344
|
+
"format:check:ignore",
|
|
345
|
+
"eimerreis-linting format --check --ignore-path .eimerreis-lintingignore",
|
|
346
|
+
force
|
|
347
|
+
);
|
|
348
|
+
|
|
349
|
+
writeJson(targetPackageJsonPath, packageJson);
|
|
350
|
+
console.log(`update ${targetPackageJsonPath}`);
|
|
151
351
|
}
|
|
152
352
|
|
|
153
353
|
function printNextSteps(targetDir) {
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
354
|
+
const relativePath = targetDir === process.cwd() ? "." : targetDir;
|
|
355
|
+
console.log("done");
|
|
356
|
+
console.log("next steps:");
|
|
357
|
+
console.log(`1) cd ${relativePath}`);
|
|
358
|
+
console.log("2) npm install");
|
|
359
|
+
console.log("3) npm run lint && npm run format:check");
|
|
160
360
|
}
|
|
161
361
|
|
|
162
|
-
function
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
362
|
+
function resolveIgnorePaths(targetDir, rawIgnorePaths) {
|
|
363
|
+
const collectedPaths = [];
|
|
364
|
+
const defaultIgnorePath = resolve(targetDir, ".eimerreis-lintingignore");
|
|
365
|
+
|
|
366
|
+
if (existsSync(defaultIgnorePath)) {
|
|
367
|
+
collectedPaths.push(defaultIgnorePath);
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
for (const rawIgnorePath of rawIgnorePaths) {
|
|
371
|
+
const resolvedPath = resolve(targetDir, rawIgnorePath);
|
|
372
|
+
if (!existsSync(resolvedPath)) {
|
|
373
|
+
throw new Error(`Ignore file not found: ${resolvedPath}`);
|
|
374
|
+
}
|
|
375
|
+
collectedPaths.push(resolvedPath);
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
return [...new Set(collectedPaths)];
|
|
178
379
|
}
|
|
179
380
|
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
381
|
+
function createMergedIgnoreFile(ignorePaths, ignorePatterns) {
|
|
382
|
+
if (ignorePaths.length === 0 && ignorePatterns.length === 0) {
|
|
383
|
+
return null;
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
const fileEntries = ignorePaths
|
|
387
|
+
.map((ignorePath) => readFileSync(ignorePath, "utf8").trim())
|
|
388
|
+
.filter((entry) => entry.length > 0);
|
|
183
389
|
|
|
184
|
-
|
|
185
|
-
lintArgs.push("--fix");
|
|
186
|
-
}
|
|
390
|
+
const patternEntries = ignorePatterns.map((pattern) => pattern.trim()).filter((pattern) => pattern.length > 0);
|
|
187
391
|
|
|
188
|
-
|
|
392
|
+
const mergedEntries = [...fileEntries, ...patternEntries];
|
|
189
393
|
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
}
|
|
394
|
+
if (mergedEntries.length === 0) {
|
|
395
|
+
return null;
|
|
396
|
+
}
|
|
194
397
|
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
}
|
|
398
|
+
const tempDir = mkdtempSync(join(tmpdir(), "eimerreis-linting-"));
|
|
399
|
+
const mergedIgnorePath = resolve(tempDir, ".ignore");
|
|
400
|
+
writeFileSync(mergedIgnorePath, `${mergedEntries.join("\n")}\n`, "utf8");
|
|
199
401
|
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
402
|
+
return {
|
|
403
|
+
path: mergedIgnorePath,
|
|
404
|
+
cleanup: () => rmSync(tempDir, { recursive: true, force: true }),
|
|
405
|
+
};
|
|
204
406
|
}
|
|
205
407
|
|
|
206
|
-
async function
|
|
207
|
-
|
|
208
|
-
|
|
408
|
+
async function runInit(args) {
|
|
409
|
+
const { targetDir, flags } = parsePathAndFlags(args, ["--force"]);
|
|
410
|
+
const force = flags.has("--force");
|
|
411
|
+
const targetPackageJsonPath = resolve(targetDir, "package.json");
|
|
412
|
+
const oxlintPath = resolve(targetDir, ".oxlintrc.json");
|
|
413
|
+
const oxfmtPath = resolve(targetDir, ".oxfmtrc.json");
|
|
209
414
|
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
415
|
+
if (!existsSync(packageRoot)) {
|
|
416
|
+
console.error("init failed: package root not found");
|
|
417
|
+
process.exit(1);
|
|
418
|
+
}
|
|
213
419
|
|
|
214
|
-
|
|
420
|
+
createExtendsConfig(oxlintPath, "oxlint.config.json", force);
|
|
421
|
+
createExtendsConfig(oxfmtPath, "oxfmt.config.json", force);
|
|
422
|
+
maybeUpdatePackageJson(targetPackageJsonPath, force);
|
|
215
423
|
|
|
216
|
-
|
|
217
|
-
if (formatExitCode !== 0) {
|
|
218
|
-
process.exit(formatExitCode);
|
|
219
|
-
}
|
|
424
|
+
printNextSteps(targetDir);
|
|
220
425
|
}
|
|
221
426
|
|
|
222
|
-
async function
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
printUsage();
|
|
227
|
-
process.exit(0);
|
|
427
|
+
async function runLint(args) {
|
|
428
|
+
if (!hasInstalledPackage("oxlint")) {
|
|
429
|
+
console.error("oxlint is not installed. Run: npm install");
|
|
430
|
+
process.exit(1);
|
|
228
431
|
}
|
|
229
432
|
|
|
230
|
-
const {
|
|
433
|
+
const { targetDir, primaryEnabled, ignorePaths: rawIgnorePaths, ignorePatterns } = parseRunArgs(args, "--fix");
|
|
434
|
+
const ignorePaths = resolveIgnorePaths(targetDir, rawIgnorePaths);
|
|
435
|
+
const mergedIgnoreFile = createMergedIgnoreFile(ignorePaths, ignorePatterns);
|
|
436
|
+
const oxlintBinPath = resolvePackageBin("oxlint", "bin/oxlint");
|
|
437
|
+
const lintArgs = [oxlintBinPath];
|
|
438
|
+
const lintConfigPath = resolveLintConfigPath(targetDir);
|
|
439
|
+
|
|
440
|
+
lintArgs.push("-c", lintConfigPath);
|
|
231
441
|
|
|
232
|
-
if (
|
|
233
|
-
|
|
234
|
-
return;
|
|
442
|
+
if (mergedIgnoreFile) {
|
|
443
|
+
lintArgs.push(`--ignore-path=${mergedIgnoreFile.path}`);
|
|
235
444
|
}
|
|
236
445
|
|
|
237
|
-
if (
|
|
238
|
-
|
|
239
|
-
return;
|
|
446
|
+
if (primaryEnabled) {
|
|
447
|
+
lintArgs.push("--fix");
|
|
240
448
|
}
|
|
241
449
|
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
450
|
+
lintArgs.push(".");
|
|
451
|
+
|
|
452
|
+
try {
|
|
453
|
+
const lintExitCode = await runCommand(process.execPath, lintArgs, targetDir);
|
|
454
|
+
if (lintExitCode !== 0) {
|
|
455
|
+
process.exit(lintExitCode);
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
if (!hasReactProject(targetDir)) {
|
|
459
|
+
console.log("skip react-doctor (no react/next dependency found)");
|
|
460
|
+
return;
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
const reactDoctorEntryPath = resolvePackageEntry("react-doctor");
|
|
464
|
+
const doctorExitCode = await runCommand(process.execPath, [reactDoctorEntryPath, "-y", "."], targetDir);
|
|
465
|
+
if (doctorExitCode !== 0) {
|
|
466
|
+
process.exit(doctorExitCode);
|
|
467
|
+
}
|
|
468
|
+
} finally {
|
|
469
|
+
mergedIgnoreFile?.cleanup();
|
|
245
470
|
}
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
async function runFormat(args) {
|
|
474
|
+
if (!hasInstalledPackage("oxfmt")) {
|
|
475
|
+
console.error("oxfmt is not installed. Run: npm install");
|
|
476
|
+
process.exit(1);
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
const { targetDir, primaryEnabled, ignorePaths: rawIgnorePaths, ignorePatterns } = parseRunArgs(args, "--check");
|
|
480
|
+
const ignorePaths = resolveIgnorePaths(targetDir, rawIgnorePaths);
|
|
481
|
+
const mergedIgnoreFile = createMergedIgnoreFile(ignorePaths, ignorePatterns);
|
|
482
|
+
const oxfmtBinPath = resolvePackageBin("oxfmt", "bin/oxfmt");
|
|
483
|
+
const formatArgs = [oxfmtBinPath];
|
|
484
|
+
const formatConfigPath = resolveFormatConfigPath(targetDir);
|
|
485
|
+
|
|
486
|
+
formatArgs.push("-c", formatConfigPath);
|
|
246
487
|
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
488
|
+
if (mergedIgnoreFile) {
|
|
489
|
+
formatArgs.push(`--ignore-path=${mergedIgnoreFile.path}`);
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
if (primaryEnabled) {
|
|
493
|
+
formatArgs.push("--check");
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
formatArgs.push(".");
|
|
497
|
+
|
|
498
|
+
try {
|
|
499
|
+
const formatExitCode = await runCommand(process.execPath, formatArgs, targetDir);
|
|
500
|
+
if (formatExitCode !== 0) {
|
|
501
|
+
process.exit(formatExitCode);
|
|
502
|
+
}
|
|
503
|
+
} finally {
|
|
504
|
+
mergedIgnoreFile?.cleanup();
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
async function main() {
|
|
509
|
+
try {
|
|
510
|
+
const rawArgs = process.argv.slice(2);
|
|
511
|
+
if (rawArgs.includes("--help") || rawArgs.includes("-h")) {
|
|
512
|
+
printUsage();
|
|
513
|
+
process.exit(0);
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
const { command, args } = parseCommand(rawArgs);
|
|
517
|
+
|
|
518
|
+
if (command === "init") {
|
|
519
|
+
await runInit(args);
|
|
520
|
+
return;
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
if (command === "lint") {
|
|
524
|
+
await runLint(args);
|
|
525
|
+
return;
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
if (command === "format") {
|
|
529
|
+
await runFormat(args);
|
|
530
|
+
return;
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
console.error(`Unknown command: ${command}`);
|
|
534
|
+
printUsage();
|
|
535
|
+
process.exit(1);
|
|
536
|
+
} catch (error) {
|
|
537
|
+
console.error(error instanceof Error ? error.message : String(error));
|
|
538
|
+
process.exit(1);
|
|
539
|
+
}
|
|
254
540
|
}
|
|
255
541
|
|
|
256
542
|
main();
|
package/oxfmt.config.json
CHANGED
|
@@ -1,19 +1,19 @@
|
|
|
1
1
|
{
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
2
|
+
"$schema": "./node_modules/oxfmt/configuration_schema.json",
|
|
3
|
+
"printWidth": 125,
|
|
4
|
+
"tabWidth": 4,
|
|
5
|
+
"semi": true,
|
|
6
|
+
"singleQuote": false,
|
|
7
|
+
"trailingComma": "es5",
|
|
8
|
+
"sortImports": {
|
|
9
|
+
"order": "asc",
|
|
10
|
+
"ignoreCase": true,
|
|
11
|
+
"newlinesBetween": true,
|
|
12
|
+
"groups": ["builtin", "external", "internal", ["parent", "sibling", "index"]],
|
|
13
|
+
"internalPattern": ["@/", "~/"]
|
|
14
|
+
},
|
|
15
|
+
"sortTailwindcss": {
|
|
16
|
+
"functions": ["cn", "clsx", "cva", "tw"]
|
|
17
|
+
},
|
|
18
|
+
"ignorePatterns": ["node_modules/**", ".next/**", "dist/**", "build/**", "coverage/**", "out/**"]
|
|
19
19
|
}
|
package/oxlint.config.json
CHANGED
|
@@ -1,38 +1,38 @@
|
|
|
1
1
|
{
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
2
|
+
"$schema": "./node_modules/oxlint/configuration_schema.json",
|
|
3
|
+
"plugins": ["eslint", "typescript", "unicorn", "oxc", "react", "nextjs", "import", "vitest"],
|
|
4
|
+
"categories": {
|
|
5
|
+
"correctness": "error",
|
|
6
|
+
"suspicious": "warn"
|
|
7
|
+
},
|
|
8
|
+
"env": {
|
|
9
|
+
"browser": true,
|
|
10
|
+
"node": true,
|
|
11
|
+
"es6": true
|
|
12
|
+
},
|
|
13
|
+
"rules": {
|
|
14
|
+
"eslint/no-unused-vars": [
|
|
15
|
+
"warn",
|
|
16
|
+
{
|
|
17
|
+
"varsIgnorePattern": "_"
|
|
18
|
+
}
|
|
19
|
+
],
|
|
20
|
+
"typescript/no-explicit-any": "off",
|
|
21
|
+
"react/prop-types": "off",
|
|
22
|
+
"react/react-in-jsx-scope": "off",
|
|
23
|
+
"react-hooks/refs": "off",
|
|
24
|
+
"react-hooks/incompatible-library": "off",
|
|
25
|
+
"import/no-duplicates": "error",
|
|
26
|
+
"import/no-self-import": "error",
|
|
27
|
+
"import/no-cycle": "warn"
|
|
28
|
+
},
|
|
29
|
+
"ignorePatterns": ["node_modules/**", ".next/**", "dist/**", "build/**", "coverage/**", "out/**"],
|
|
30
|
+
"overrides": [
|
|
31
|
+
{
|
|
32
|
+
"files": ["**/*.{test,spec}.{js,jsx,ts,tsx}", "**/__tests__/**/*.{js,jsx,ts,tsx}"],
|
|
33
|
+
"rules": {
|
|
34
|
+
"typescript/no-explicit-any": "off"
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
]
|
|
38
38
|
}
|
package/package.json
CHANGED
|
@@ -1,60 +1,59 @@
|
|
|
1
1
|
{
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
}
|
|
2
|
+
"name": "@eimerreis/linting",
|
|
3
|
+
"version": "0.4.0",
|
|
4
|
+
"description": "Personal OXC linting and formatting defaults",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"format",
|
|
7
|
+
"lint",
|
|
8
|
+
"nextjs",
|
|
9
|
+
"oxc",
|
|
10
|
+
"oxfmt",
|
|
11
|
+
"oxlint",
|
|
12
|
+
"react",
|
|
13
|
+
"tailwind",
|
|
14
|
+
"typescript"
|
|
15
|
+
],
|
|
16
|
+
"license": "MIT",
|
|
17
|
+
"repository": {
|
|
18
|
+
"url": "https://github.com/eimerreis/linting"
|
|
19
|
+
},
|
|
20
|
+
"bin": {
|
|
21
|
+
"eimerreis-linting": "bin/init.mjs"
|
|
22
|
+
},
|
|
23
|
+
"files": [
|
|
24
|
+
"bin",
|
|
25
|
+
"oxlint.config.json",
|
|
26
|
+
"oxfmt.config.json",
|
|
27
|
+
"README.md",
|
|
28
|
+
"LICENSE"
|
|
29
|
+
],
|
|
30
|
+
"type": "module",
|
|
31
|
+
"exports": {
|
|
32
|
+
"./oxlint": "./oxlint.config.json",
|
|
33
|
+
"./oxfmt": "./oxfmt.config.json",
|
|
34
|
+
"./package.json": "./package.json"
|
|
35
|
+
},
|
|
36
|
+
"publishConfig": {
|
|
37
|
+
"access": "public"
|
|
38
|
+
},
|
|
39
|
+
"scripts": {
|
|
40
|
+
"changeset": "changeset",
|
|
41
|
+
"lint": "node ./bin/init.mjs lint",
|
|
42
|
+
"lint:fix": "node ./bin/init.mjs lint --fix",
|
|
43
|
+
"format": "node ./bin/init.mjs format",
|
|
44
|
+
"format:check": "node ./bin/init.mjs format --check",
|
|
45
|
+
"version-packages": "changeset version",
|
|
46
|
+
"release": "changeset publish"
|
|
47
|
+
},
|
|
48
|
+
"dependencies": {
|
|
49
|
+
"oxfmt": "^0.41.0",
|
|
50
|
+
"oxlint": "^1.56.0",
|
|
51
|
+
"react-doctor": "^0.0.30"
|
|
52
|
+
},
|
|
53
|
+
"devDependencies": {
|
|
54
|
+
"@changesets/cli": "^2.29.7"
|
|
55
|
+
},
|
|
56
|
+
"engines": {
|
|
57
|
+
"node": "^20.19.0 || >=22.12.0"
|
|
58
|
+
}
|
|
60
59
|
}
|