@eimerreis/linting 0.1.0 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/README.md +17 -4
  2. package/bin/init.mjs +183 -29
  3. package/package.json +8 -5
package/README.md CHANGED
@@ -2,6 +2,8 @@
2
2
 
3
3
  Personal linting and formatting defaults for new JavaScript/TypeScript projects.
4
4
 
5
+ Requires Node `^20.19.0 || >=22.12.0`.
6
+
5
7
  Built on:
6
8
 
7
9
  - `oxlint` for linting
@@ -20,6 +22,12 @@ Default focus:
20
22
  npm add -D @eimerreis/linting oxlint oxfmt
21
23
  ```
22
24
 
25
+ `lint` also executes `react-doctor` via:
26
+
27
+ ```bash
28
+ npx react-doctor -y .
29
+ ```
30
+
23
31
  ## Quick Start
24
32
 
25
33
  From your project root:
@@ -39,7 +47,7 @@ This creates:
39
47
  - `.oxlintrc.json`
40
48
  - `.oxfmtrc.json`
41
49
 
42
- And updates `package.json` scripts (if present):
50
+ And updates `package.json` scripts (if present) so linting is executed through this package:
43
51
 
44
52
  - `lint`
45
53
  - `lint:fix`
@@ -49,11 +57,16 @@ And updates `package.json` scripts (if present):
49
57
  ### CLI options
50
58
 
51
59
  ```bash
52
- eimerreis-linting [init] [targetDir] [--force]
60
+ eimerreis-linting init [targetDir] [--force]
61
+ eimerreis-linting lint [targetDir] [--fix]
62
+ eimerreis-linting format [targetDir] [--check]
53
63
  ```
54
64
 
55
- - `targetDir`: scaffold config in another directory
56
- - `--force`: overwrite existing `.oxlintrc.json` / `.oxfmtrc.json` and script values
65
+ - `init --force`: overwrite existing `.oxlintrc.json` / `.oxfmtrc.json` and script values
66
+ - `lint --fix`: run `oxlint --fix .` and then `npx react-doctor -y .`
67
+ - `format --check`: run `oxfmt --check .`
68
+
69
+ `react-doctor` runs only when the target package has `react`, `react-dom`, or `next` in dependencies/devDependencies/peerDependencies.
57
70
 
58
71
  ## Manual Setup
59
72
 
package/bin/init.mjs CHANGED
@@ -1,5 +1,6 @@
1
1
  #!/usr/bin/env node
2
2
 
3
+ import { spawn } from "node:child_process";
3
4
  import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
4
5
  import { fileURLToPath } from "node:url";
5
6
  import { dirname, resolve } from "node:path";
@@ -8,22 +9,6 @@ import process from "node:process";
8
9
  const packageName = "@eimerreis/linting";
9
10
  const packageRoot = resolve(dirname(fileURLToPath(import.meta.url)), "..");
10
11
 
11
- const rawArgs = process.argv.slice(2);
12
-
13
- if (rawArgs.includes("--help") || rawArgs.includes("-h")) {
14
- console.log("Usage: eimerreis-linting [init] [targetDir] [--force]");
15
- process.exit(0);
16
- }
17
-
18
- const args = rawArgs[0] === "init" ? rawArgs.slice(1) : rawArgs;
19
- const force = args.includes("--force");
20
- const targetArg = args.find((arg) => !arg.startsWith("-"));
21
- const targetDir = resolve(process.cwd(), targetArg ?? ".");
22
-
23
- const targetPackageJsonPath = resolve(targetDir, "package.json");
24
- const oxlintPath = resolve(targetDir, ".oxlintrc.json");
25
- const oxfmtPath = resolve(targetDir, ".oxfmtrc.json");
26
-
27
12
  function ensureDirectory(filePath) {
28
13
  const dir = dirname(filePath);
29
14
  if (!existsSync(dir)) {
@@ -40,7 +25,94 @@ function readJson(filePath) {
40
25
  return JSON.parse(readFileSync(filePath, "utf8"));
41
26
  }
42
27
 
43
- function upsertScript(packageJson, name, command) {
28
+ 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]");
33
+ }
34
+
35
+ function parseCommand(rawArgs) {
36
+ const firstArg = rawArgs[0];
37
+ if (!firstArg || firstArg === "init" || firstArg.startsWith("-")) {
38
+ return {
39
+ command: "init",
40
+ args: firstArg === "init" ? rawArgs.slice(1) : rawArgs,
41
+ };
42
+ }
43
+
44
+ return {
45
+ command: firstArg,
46
+ args: rawArgs.slice(1),
47
+ };
48
+ }
49
+
50
+ 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;
61
+ }
62
+
63
+ if (targetDirArg) {
64
+ throw new Error(`Unexpected argument: ${arg}`);
65
+ }
66
+
67
+ targetDirArg = arg;
68
+ }
69
+
70
+ return {
71
+ targetDir: resolve(process.cwd(), targetDirArg ?? "."),
72
+ flags,
73
+ };
74
+ }
75
+
76
+ 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",
82
+ });
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
+ }
94
+
95
+ function hasReactProject(cwd) {
96
+ const packageJsonPath = resolve(cwd, "package.json");
97
+
98
+ if (!existsSync(packageJsonPath)) {
99
+ return false;
100
+ }
101
+
102
+ try {
103
+ const packageJson = readJson(packageJsonPath);
104
+ const dependencyFields = ["dependencies", "devDependencies", "peerDependencies"];
105
+
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
+ }
113
+ }
114
+
115
+ function upsertScript(packageJson, name, command, force) {
44
116
  if (!packageJson.scripts || typeof packageJson.scripts !== "object") {
45
117
  packageJson.scripts = {};
46
118
  }
@@ -49,7 +121,7 @@ function upsertScript(packageJson, name, command) {
49
121
  }
50
122
  }
51
123
 
52
- function createExtendsConfig(targetPath, exportPath) {
124
+ function createExtendsConfig(targetPath, exportPath, force) {
53
125
  if (existsSync(targetPath) && !force) {
54
126
  console.log(`skip ${targetPath} (already exists)`);
55
127
  return;
@@ -61,7 +133,7 @@ function createExtendsConfig(targetPath, exportPath) {
61
133
  console.log(`write ${targetPath}`);
62
134
  }
63
135
 
64
- function maybeUpdatePackageJson() {
136
+ function maybeUpdatePackageJson(targetPackageJsonPath, force) {
65
137
  if (!existsSync(targetPackageJsonPath)) {
66
138
  console.log("skip package.json (not found)");
67
139
  return;
@@ -69,16 +141,16 @@ function maybeUpdatePackageJson() {
69
141
 
70
142
  const packageJson = readJson(targetPackageJsonPath);
71
143
 
72
- upsertScript(packageJson, "lint", "oxlint .");
73
- upsertScript(packageJson, "lint:fix", "oxlint --fix .");
74
- upsertScript(packageJson, "format", "oxfmt .");
75
- upsertScript(packageJson, "format:check", "oxfmt --check .");
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);
76
148
 
77
149
  writeJson(targetPackageJsonPath, packageJson);
78
150
  console.log(`update ${targetPackageJsonPath}`);
79
151
  }
80
152
 
81
- function printNextSteps() {
153
+ function printNextSteps(targetDir) {
82
154
  const relativePath = targetDir === process.cwd() ? "." : targetDir;
83
155
  console.log("done");
84
156
  console.log("next steps:");
@@ -87,16 +159,98 @@ function printNextSteps() {
87
159
  console.log("3) npm run lint && npm run format:check");
88
160
  }
89
161
 
90
- function main() {
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
+
91
169
  if (!existsSync(packageRoot)) {
92
170
  console.error("init failed: package root not found");
93
171
  process.exit(1);
94
172
  }
95
173
 
96
- createExtendsConfig(oxlintPath, "oxlint.config.json");
97
- createExtendsConfig(oxfmtPath, "oxfmt.config.json");
98
- maybeUpdatePackageJson();
99
- printNextSteps();
174
+ createExtendsConfig(oxlintPath, "oxlint.config.json", force);
175
+ createExtendsConfig(oxfmtPath, "oxfmt.config.json", force);
176
+ maybeUpdatePackageJson(targetPackageJsonPath, force);
177
+ printNextSteps(targetDir);
178
+ }
179
+
180
+ async function runLint(args) {
181
+ const { targetDir, flags } = parsePathAndFlags(args, ["--fix"]);
182
+ const lintArgs = ["--no-install", "oxlint"];
183
+
184
+ if (flags.has("--fix")) {
185
+ lintArgs.push("--fix");
186
+ }
187
+
188
+ lintArgs.push(".");
189
+
190
+ const lintExitCode = await runCommand("npx", lintArgs, targetDir);
191
+ if (lintExitCode !== 0) {
192
+ process.exit(lintExitCode);
193
+ }
194
+
195
+ if (!hasReactProject(targetDir)) {
196
+ console.log("skip react-doctor (no react/next dependency found)");
197
+ return;
198
+ }
199
+
200
+ const doctorExitCode = await runCommand("npx", ["react-doctor", "-y", "."], targetDir);
201
+ if (doctorExitCode !== 0) {
202
+ process.exit(doctorExitCode);
203
+ }
204
+ }
205
+
206
+ async function runFormat(args) {
207
+ const { targetDir, flags } = parsePathAndFlags(args, ["--check"]);
208
+ const formatArgs = ["--no-install", "oxfmt"];
209
+
210
+ if (flags.has("--check")) {
211
+ formatArgs.push("--check");
212
+ }
213
+
214
+ formatArgs.push(".");
215
+
216
+ const formatExitCode = await runCommand("npx", formatArgs, targetDir);
217
+ if (formatExitCode !== 0) {
218
+ process.exit(formatExitCode);
219
+ }
220
+ }
221
+
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);
228
+ }
229
+
230
+ const { command, args } = parseCommand(rawArgs);
231
+
232
+ if (command === "init") {
233
+ runInit(args);
234
+ return;
235
+ }
236
+
237
+ if (command === "lint") {
238
+ await runLint(args);
239
+ return;
240
+ }
241
+
242
+ if (command === "format") {
243
+ await runFormat(args);
244
+ return;
245
+ }
246
+
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
+ }
100
254
  }
101
255
 
102
256
  main();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@eimerreis/linting",
3
- "version": "0.1.0",
3
+ "version": "0.2.0",
4
4
  "description": "Personal OXC linting and formatting defaults",
5
5
  "keywords": [
6
6
  "format",
@@ -14,6 +14,9 @@
14
14
  "typescript"
15
15
  ],
16
16
  "license": "MIT",
17
+ "repository": {
18
+ "url": "https://github.com/eimerreis/linting"
19
+ },
17
20
  "bin": {
18
21
  "eimerreis-linting": "bin/init.mjs"
19
22
  },
@@ -35,10 +38,10 @@
35
38
  },
36
39
  "scripts": {
37
40
  "changeset": "changeset",
38
- "lint": "oxlint .",
39
- "lint:fix": "oxlint --fix .",
40
- "format": "oxfmt .",
41
- "format:check": "oxfmt --check .",
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",
42
45
  "version-packages": "changeset version",
43
46
  "release": "changeset publish"
44
47
  },