@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 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 oxlint oxfmt
24
+ npm add -D @eimerreis/linting
23
25
  ```
24
26
 
25
- `lint` also executes `react-doctor` via:
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 `npx react-doctor -y .`
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
- "extends": ["./node_modules/@eimerreis/linting/oxlint.config.json"]
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
- "extends": ["./node_modules/@eimerreis/linting/oxfmt.config.json"]
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 { fileURLToPath } from "node:url";
6
- import { dirname, resolve } from "node:path";
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
- const dir = dirname(filePath);
14
- if (!existsSync(dir)) {
15
- mkdirSync(dir, { recursive: true });
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
- ensureDirectory(filePath);
21
- writeFileSync(filePath, `${JSON.stringify(data, null, 2)}\n`, "utf8");
23
+ ensureDirectory(filePath);
24
+ writeFileSync(filePath, `${JSON.stringify(data, null, 2)}\n`, "utf8");
22
25
  }
23
26
 
24
27
  function readJson(filePath) {
25
- return JSON.parse(readFileSync(filePath, "utf8"));
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
- console.log("Usage:");
30
- console.log(" eimerreis-linting init [targetDir] [--force]");
31
- console.log(" eimerreis-linting lint [targetDir] [--fix]");
32
- console.log(" eimerreis-linting format [targetDir] [--check]");
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
- const firstArg = rawArgs[0];
37
- if (!firstArg || firstArg === "init" || firstArg.startsWith("-")) {
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
- command: "init",
40
- args: firstArg === "init" ? rawArgs.slice(1) : rawArgs,
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
- let targetDirArg;
52
- const flags = new Set();
53
-
54
- for (const arg of args) {
55
- if (arg.startsWith("-")) {
56
- if (!allowedFlags.includes(arg)) {
57
- throw new Error(`Unknown flag: ${arg}`);
58
- }
59
- flags.add(arg);
60
- continue;
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
- if (targetDirArg) {
64
- throw new Error(`Unexpected argument: ${arg}`);
65
- }
183
+ return {
184
+ targetDir: resolve(process.cwd(), targetDirArg ?? "."),
185
+ flags,
186
+ };
187
+ }
66
188
 
67
- targetDirArg = arg;
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
- return {
71
- targetDir: resolve(process.cwd(), targetDirArg ?? "."),
72
- flags,
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
- return new Promise((resolvePromise) => {
78
- const child = spawn(command, commandArgs, {
79
- cwd,
80
- stdio: "inherit",
81
- shell: process.platform === "win32",
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
- const packageJsonPath = resolve(cwd, "package.json");
280
+ const packageJsonPath = resolve(cwd, "package.json");
97
281
 
98
- if (!existsSync(packageJsonPath)) {
99
- return false;
100
- }
282
+ if (!existsSync(packageJsonPath)) {
283
+ return false;
284
+ }
101
285
 
102
- try {
103
- const packageJson = readJson(packageJsonPath);
104
- const dependencyFields = ["dependencies", "devDependencies", "peerDependencies"];
286
+ try {
287
+ const packageJson = readJson(packageJsonPath);
288
+ const dependencyFields = ["dependencies", "devDependencies", "peerDependencies"];
105
289
 
106
- return dependencyFields.some((field) => {
107
- const deps = packageJson[field];
108
- return Boolean(deps?.react || deps?.["react-dom"] || deps?.next);
109
- });
110
- } catch {
111
- return false;
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
- if (!packageJson.scripts || typeof packageJson.scripts !== "object") {
117
- packageJson.scripts = {};
118
- }
119
- if (!packageJson.scripts[name] || force) {
120
- packageJson.scripts[name] = command;
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
- if (existsSync(targetPath) && !force) {
126
- console.log(`skip ${targetPath} (already exists)`);
127
- return;
128
- }
129
-
130
- writeJson(targetPath, {
131
- extends: [`./node_modules/${packageName}/${exportPath}`],
132
- });
133
- console.log(`write ${targetPath}`);
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
- if (!existsSync(targetPackageJsonPath)) {
138
- console.log("skip package.json (not found)");
139
- return;
140
- }
321
+ if (!existsSync(targetPackageJsonPath)) {
322
+ console.log("skip package.json (not found)");
323
+ return;
324
+ }
141
325
 
142
- const packageJson = readJson(targetPackageJsonPath);
326
+ const packageJson = readJson(targetPackageJsonPath);
143
327
 
144
- upsertScript(packageJson, "lint", "eimerreis-linting lint", force);
145
- upsertScript(packageJson, "lint:fix", "eimerreis-linting lint --fix", force);
146
- upsertScript(packageJson, "format", "eimerreis-linting format", force);
147
- upsertScript(packageJson, "format:check", "eimerreis-linting format --check", force);
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
- writeJson(targetPackageJsonPath, packageJson);
150
- console.log(`update ${targetPackageJsonPath}`);
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
- const relativePath = targetDir === process.cwd() ? "." : targetDir;
155
- console.log("done");
156
- console.log("next steps:");
157
- console.log(`1) cd ${relativePath}`);
158
- console.log("2) npm add -D @eimerreis/linting oxlint oxfmt");
159
- console.log("3) npm run lint && npm run format:check");
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 runInit(args) {
163
- const { targetDir, flags } = parsePathAndFlags(args, ["--force"]);
164
- const force = flags.has("--force");
165
- const targetPackageJsonPath = resolve(targetDir, "package.json");
166
- const oxlintPath = resolve(targetDir, ".oxlintrc.json");
167
- const oxfmtPath = resolve(targetDir, ".oxfmtrc.json");
168
-
169
- if (!existsSync(packageRoot)) {
170
- console.error("init failed: package root not found");
171
- process.exit(1);
172
- }
173
-
174
- createExtendsConfig(oxlintPath, "oxlint.config.json", force);
175
- createExtendsConfig(oxfmtPath, "oxfmt.config.json", force);
176
- maybeUpdatePackageJson(targetPackageJsonPath, force);
177
- printNextSteps(targetDir);
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
- async function runLint(args) {
181
- const { targetDir, flags } = parsePathAndFlags(args, ["--fix"]);
182
- const lintArgs = ["--no-install", "oxlint"];
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
- if (flags.has("--fix")) {
185
- lintArgs.push("--fix");
186
- }
390
+ const patternEntries = ignorePatterns.map((pattern) => pattern.trim()).filter((pattern) => pattern.length > 0);
187
391
 
188
- lintArgs.push(".");
392
+ const mergedEntries = [...fileEntries, ...patternEntries];
189
393
 
190
- const lintExitCode = await runCommand("npx", lintArgs, targetDir);
191
- if (lintExitCode !== 0) {
192
- process.exit(lintExitCode);
193
- }
394
+ if (mergedEntries.length === 0) {
395
+ return null;
396
+ }
194
397
 
195
- if (!hasReactProject(targetDir)) {
196
- console.log("skip react-doctor (no react/next dependency found)");
197
- return;
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
- const doctorExitCode = await runCommand("npx", ["react-doctor", "-y", "."], targetDir);
201
- if (doctorExitCode !== 0) {
202
- process.exit(doctorExitCode);
203
- }
402
+ return {
403
+ path: mergedIgnorePath,
404
+ cleanup: () => rmSync(tempDir, { recursive: true, force: true }),
405
+ };
204
406
  }
205
407
 
206
- async function runFormat(args) {
207
- const { targetDir, flags } = parsePathAndFlags(args, ["--check"]);
208
- const formatArgs = ["--no-install", "oxfmt"];
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
- if (flags.has("--check")) {
211
- formatArgs.push("--check");
212
- }
415
+ if (!existsSync(packageRoot)) {
416
+ console.error("init failed: package root not found");
417
+ process.exit(1);
418
+ }
213
419
 
214
- formatArgs.push(".");
420
+ createExtendsConfig(oxlintPath, "oxlint.config.json", force);
421
+ createExtendsConfig(oxfmtPath, "oxfmt.config.json", force);
422
+ maybeUpdatePackageJson(targetPackageJsonPath, force);
215
423
 
216
- const formatExitCode = await runCommand("npx", formatArgs, targetDir);
217
- if (formatExitCode !== 0) {
218
- process.exit(formatExitCode);
219
- }
424
+ printNextSteps(targetDir);
220
425
  }
221
426
 
222
- async function main() {
223
- try {
224
- const rawArgs = process.argv.slice(2);
225
- if (rawArgs.includes("--help") || rawArgs.includes("-h")) {
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 { command, args } = parseCommand(rawArgs);
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 (command === "init") {
233
- runInit(args);
234
- return;
442
+ if (mergedIgnoreFile) {
443
+ lintArgs.push(`--ignore-path=${mergedIgnoreFile.path}`);
235
444
  }
236
445
 
237
- if (command === "lint") {
238
- await runLint(args);
239
- return;
446
+ if (primaryEnabled) {
447
+ lintArgs.push("--fix");
240
448
  }
241
449
 
242
- if (command === "format") {
243
- await runFormat(args);
244
- return;
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
- console.error(`Unknown command: ${command}`);
248
- printUsage();
249
- process.exit(1);
250
- } catch (error) {
251
- console.error(error instanceof Error ? error.message : String(error));
252
- process.exit(1);
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
- "$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/**"]
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
  }
@@ -1,38 +1,38 @@
1
1
  {
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
- ]
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
- "name": "@eimerreis/linting",
3
- "version": "0.2.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
- "devDependencies": {
49
- "@changesets/cli": "^2.29.7",
50
- "oxfmt": "^0.41.0",
51
- "oxlint": "^1.56.0"
52
- },
53
- "peerDependencies": {
54
- "oxfmt": "^0.41.0",
55
- "oxlint": "^1.56.0"
56
- },
57
- "engines": {
58
- "node": "^20.19.0 || >=22.12.0"
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
  }