react-doctor 0.0.1 → 0.0.2

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/dist/cli.cjs ADDED
@@ -0,0 +1,423 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+
4
+ var path = require('path');
5
+ var commander = require('commander');
6
+ var pc = require('picocolors');
7
+ var fs = require('fs');
8
+ var child_process = require('child_process');
9
+ var os = require('os');
10
+
11
+ function _interopDefault (e) { return e && e.__esModule ? e : { default: e }; }
12
+
13
+ var path__default = /*#__PURE__*/_interopDefault(path);
14
+ var pc__default = /*#__PURE__*/_interopDefault(pc);
15
+ var fs__default = /*#__PURE__*/_interopDefault(fs);
16
+ var os__default = /*#__PURE__*/_interopDefault(os);
17
+
18
+ var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require : typeof Proxy !== "undefined" ? new Proxy(x, {
19
+ get: (a, b) => (typeof require !== "undefined" ? require : a)[b]
20
+ }) : x)(function(x) {
21
+ if (typeof require !== "undefined") return require.apply(this, arguments);
22
+ throw Error('Dynamic require of "' + x + '" is not supported');
23
+ });
24
+ var highlighter = {
25
+ error: pc__default.default.red,
26
+ warn: pc__default.default.yellow,
27
+ info: pc__default.default.cyan,
28
+ success: pc__default.default.green,
29
+ dim: pc__default.default.dim
30
+ };
31
+
32
+ // src/utils/logger.ts
33
+ var logger = {
34
+ error(...args) {
35
+ console.log(highlighter.error(args.join(" ")));
36
+ },
37
+ warn(...args) {
38
+ console.log(highlighter.warn(args.join(" ")));
39
+ },
40
+ info(...args) {
41
+ console.log(highlighter.info(args.join(" ")));
42
+ },
43
+ success(...args) {
44
+ console.log(highlighter.success(args.join(" ")));
45
+ },
46
+ dim(...args) {
47
+ console.log(highlighter.dim(args.join(" ")));
48
+ },
49
+ log(...args) {
50
+ console.log(args.join(" "));
51
+ },
52
+ break() {
53
+ console.log("");
54
+ }
55
+ };
56
+
57
+ // src/utils/handle-error.ts
58
+ var handleError = (error) => {
59
+ logger.break();
60
+ logger.error("Something went wrong. Please check the error below for more details.");
61
+ logger.error("If the problem persists, please open an issue on GitHub.");
62
+ logger.error("");
63
+ if (error instanceof Error) {
64
+ logger.error(error.message);
65
+ }
66
+ logger.break();
67
+ process.exit(1);
68
+ };
69
+
70
+ // src/constants.ts
71
+ var SOURCE_FILE_PATTERN = /\.(tsx?|jsx?)$/;
72
+ var SEPARATOR_LENGTH = 62;
73
+ var SPAWN_MAX_BUFFER_BYTES = 100 * 1024 * 1024;
74
+ var FRAMEWORK_PACKAGES = {
75
+ next: "nextjs",
76
+ vite: "vite",
77
+ "react-scripts": "cra",
78
+ "@remix-run/react": "remix",
79
+ gatsby: "gatsby"
80
+ };
81
+ var FRAMEWORK_DISPLAY_NAMES = {
82
+ nextjs: "Next.js",
83
+ vite: "Vite",
84
+ cra: "Create React App",
85
+ remix: "Remix",
86
+ gatsby: "Gatsby",
87
+ unknown: "React"
88
+ };
89
+ var formatFrameworkName = (framework) => FRAMEWORK_DISPLAY_NAMES[framework];
90
+ var countSourceFiles = (rootDirectory) => {
91
+ const result = child_process.spawnSync("git", ["ls-files", "--cached", "--others", "--exclude-standard"], {
92
+ cwd: rootDirectory,
93
+ encoding: "utf-8"
94
+ });
95
+ if (result.error || result.status !== 0) {
96
+ return 0;
97
+ }
98
+ return result.stdout.split("\n").filter((filePath) => filePath.length > 0 && SOURCE_FILE_PATTERN.test(filePath)).length;
99
+ };
100
+ var detectFramework = (dependencies) => {
101
+ for (const [packageName, frameworkName] of Object.entries(FRAMEWORK_PACKAGES)) {
102
+ if (dependencies[packageName]) {
103
+ return frameworkName;
104
+ }
105
+ }
106
+ return "unknown";
107
+ };
108
+ var extractDependencyInfo = (packageJson) => {
109
+ const allDependencies = {
110
+ ...packageJson.dependencies,
111
+ ...packageJson.devDependencies
112
+ };
113
+ return {
114
+ reactVersion: allDependencies.react ?? null,
115
+ framework: detectFramework(allDependencies)
116
+ };
117
+ };
118
+ var parsePnpmWorkspacePatterns = (rootDirectory) => {
119
+ const workspacePath = path__default.default.join(rootDirectory, "pnpm-workspace.yaml");
120
+ if (!fs__default.default.existsSync(workspacePath)) return [];
121
+ const content = fs__default.default.readFileSync(workspacePath, "utf-8");
122
+ const patterns = [];
123
+ let insidePackagesBlock = false;
124
+ for (const line of content.split("\n")) {
125
+ const trimmed = line.trim();
126
+ if (trimmed === "packages:") {
127
+ insidePackagesBlock = true;
128
+ continue;
129
+ }
130
+ if (insidePackagesBlock && trimmed.startsWith("-")) {
131
+ patterns.push(trimmed.replace(/^-\s*/, "").replace(/["']/g, ""));
132
+ } else if (insidePackagesBlock && trimmed.length > 0 && !trimmed.startsWith("#")) {
133
+ insidePackagesBlock = false;
134
+ }
135
+ }
136
+ return patterns;
137
+ };
138
+ var getWorkspacePatterns = (rootDirectory, packageJson) => {
139
+ const pnpmPatterns = parsePnpmWorkspacePatterns(rootDirectory);
140
+ if (pnpmPatterns.length > 0) return pnpmPatterns;
141
+ if (Array.isArray(packageJson.workspaces)) {
142
+ return packageJson.workspaces;
143
+ }
144
+ if (packageJson.workspaces?.packages) {
145
+ return packageJson.workspaces.packages;
146
+ }
147
+ return [];
148
+ };
149
+ var resolveWorkspaceDirectories = (rootDirectory, pattern) => {
150
+ const cleanPattern = pattern.replace(/["']/g, "").replace(/\/\*\*$/, "/*");
151
+ if (!cleanPattern.includes("*")) {
152
+ const directoryPath = path__default.default.join(rootDirectory, cleanPattern);
153
+ if (fs__default.default.existsSync(directoryPath) && fs__default.default.existsSync(path__default.default.join(directoryPath, "package.json"))) {
154
+ return [directoryPath];
155
+ }
156
+ return [];
157
+ }
158
+ const baseDirectory = path__default.default.join(
159
+ rootDirectory,
160
+ cleanPattern.slice(0, cleanPattern.indexOf("*"))
161
+ );
162
+ if (!fs__default.default.existsSync(baseDirectory) || !fs__default.default.statSync(baseDirectory).isDirectory()) {
163
+ return [];
164
+ }
165
+ return fs__default.default.readdirSync(baseDirectory).map((entry) => path__default.default.join(baseDirectory, entry)).filter(
166
+ (entryPath) => fs__default.default.statSync(entryPath).isDirectory() && fs__default.default.existsSync(path__default.default.join(entryPath, "package.json"))
167
+ );
168
+ };
169
+ var findReactInWorkspaces = (rootDirectory, packageJson) => {
170
+ const patterns = getWorkspacePatterns(rootDirectory, packageJson);
171
+ const result = { reactVersion: null, framework: "unknown" };
172
+ for (const pattern of patterns) {
173
+ const directories = resolveWorkspaceDirectories(rootDirectory, pattern);
174
+ for (const workspaceDirectory of directories) {
175
+ const workspacePackageJson = JSON.parse(
176
+ fs__default.default.readFileSync(path__default.default.join(workspaceDirectory, "package.json"), "utf-8")
177
+ );
178
+ const info = extractDependencyInfo(workspacePackageJson);
179
+ if (info.reactVersion && !result.reactVersion) {
180
+ result.reactVersion = info.reactVersion;
181
+ }
182
+ if (info.framework !== "unknown" && result.framework === "unknown") {
183
+ result.framework = info.framework;
184
+ }
185
+ if (result.reactVersion && result.framework !== "unknown") {
186
+ return result;
187
+ }
188
+ }
189
+ }
190
+ return result;
191
+ };
192
+ var discoverProject = (directory) => {
193
+ const packageJsonPath = path__default.default.join(directory, "package.json");
194
+ if (!fs__default.default.existsSync(packageJsonPath)) {
195
+ throw new Error(`No package.json found in ${directory}`);
196
+ }
197
+ const packageJson = JSON.parse(fs__default.default.readFileSync(packageJsonPath, "utf-8"));
198
+ let { reactVersion, framework } = extractDependencyInfo(packageJson);
199
+ if (!reactVersion) {
200
+ const workspaceInfo = findReactInWorkspaces(directory, packageJson);
201
+ reactVersion = workspaceInfo.reactVersion;
202
+ framework = workspaceInfo.framework;
203
+ } else if (framework === "unknown") {
204
+ const workspaceInfo = findReactInWorkspaces(directory, packageJson);
205
+ if (workspaceInfo.framework !== "unknown") {
206
+ framework = workspaceInfo.framework;
207
+ }
208
+ }
209
+ const hasTypeScript = fs__default.default.existsSync(path__default.default.join(directory, "tsconfig.json"));
210
+ const sourceFileCount = countSourceFiles(directory);
211
+ return {
212
+ rootDirectory: directory,
213
+ reactVersion,
214
+ framework,
215
+ hasTypeScript,
216
+ sourceFileCount
217
+ };
218
+ };
219
+
220
+ // src/oxlint-config.ts
221
+ var OXLINT_CONFIG = {
222
+ plugins: ["react", "jsx-a11y", "react-perf", "import", "typescript"],
223
+ rules: {
224
+ "react/rules-of-hooks": "error",
225
+ "react/no-direct-mutation-state": "error",
226
+ "react/jsx-no-duplicate-props": "error",
227
+ "react/jsx-key": "error",
228
+ "react/no-children-prop": "warn",
229
+ "react/no-danger": "warn",
230
+ "react/jsx-no-script-url": "error",
231
+ "react/no-render-return-value": "warn",
232
+ "react/no-string-refs": "warn",
233
+ "react/no-unescaped-entities": "warn",
234
+ "react/no-is-mounted": "warn",
235
+ "react/require-render-return": "error",
236
+ "react/no-unknown-property": "warn",
237
+ "jsx-a11y/alt-text": "error",
238
+ "jsx-a11y/anchor-is-valid": "warn",
239
+ "jsx-a11y/click-events-have-key-events": "warn",
240
+ "jsx-a11y/no-static-element-interactions": "warn",
241
+ "jsx-a11y/no-noninteractive-element-interactions": "warn",
242
+ "jsx-a11y/role-has-required-aria-props": "error",
243
+ "jsx-a11y/no-autofocus": "warn",
244
+ "jsx-a11y/heading-has-content": "warn",
245
+ "jsx-a11y/html-has-lang": "warn",
246
+ "jsx-a11y/no-redundant-roles": "warn",
247
+ "jsx-a11y/scope": "warn",
248
+ "jsx-a11y/tabindex-no-positive": "warn",
249
+ "jsx-a11y/label-has-associated-control": "warn",
250
+ "jsx-a11y/no-distracting-elements": "error",
251
+ "jsx-a11y/iframe-has-title": "warn",
252
+ "react-perf/jsx-no-new-object-as-prop": "warn",
253
+ "react-perf/jsx-no-new-array-as-prop": "warn",
254
+ "react-perf/jsx-no-new-function-as-prop": "warn",
255
+ "react-perf/jsx-no-jsx-as-prop": "warn",
256
+ "import/no-cycle": "error",
257
+ "import/no-self-import": "error",
258
+ "import/no-duplicates": "warn",
259
+ "import/no-named-default": "warn",
260
+ "typescript/consistent-type-imports": "warn",
261
+ "typescript/no-explicit-any": "warn",
262
+ "typescript/no-non-null-assertion": "warn",
263
+ "typescript/prefer-ts-expect-error": "warn",
264
+ "typescript/no-unnecessary-type-assertion": "warn"
265
+ }
266
+ };
267
+
268
+ // src/utils/run-oxlint.ts
269
+ var PLUGIN_CATEGORY_MAP = {
270
+ react: "Correctness",
271
+ "react-hooks": "Correctness",
272
+ "jsx-a11y": "Accessibility",
273
+ "react-perf": "Performance",
274
+ import: "Bundle Size",
275
+ typescript: "TypeScript",
276
+ eslint: "Code Quality",
277
+ oxc: "Code Quality",
278
+ unicorn: "Code Quality"
279
+ };
280
+ var normalizePluginName = (rawPlugin) => rawPlugin.replace(/^eslint-plugin-/, "").replace(/^typescript-eslint$/, "typescript");
281
+ var parseRuleCode = (code) => {
282
+ const match = code.match(/^(.+)\((.+)\)$/);
283
+ if (!match) return { plugin: "unknown", rule: code };
284
+ return { plugin: normalizePluginName(match[1]), rule: match[2] };
285
+ };
286
+ var resolveOxlintBinary = () => {
287
+ const oxlintMainPath = __require.resolve("oxlint");
288
+ const oxlintPackageDirectory = path__default.default.resolve(path__default.default.dirname(oxlintMainPath), "..");
289
+ return path__default.default.join(oxlintPackageDirectory, "bin", "oxlint");
290
+ };
291
+ var runOxlint = (rootDirectory, hasTypeScript) => {
292
+ const configPath = path__default.default.join(os__default.default.tmpdir(), `react-doctor-oxlintrc-${process.pid}.json`);
293
+ try {
294
+ fs__default.default.writeFileSync(configPath, JSON.stringify(OXLINT_CONFIG, null, 2));
295
+ const oxlintBinary = resolveOxlintBinary();
296
+ const args = [oxlintBinary, "-c", configPath, "--format", "json"];
297
+ if (hasTypeScript) {
298
+ args.push("--tsconfig", "./tsconfig.json");
299
+ }
300
+ args.push(".");
301
+ const result = child_process.spawnSync(process.execPath, args, {
302
+ cwd: rootDirectory,
303
+ encoding: "utf-8",
304
+ maxBuffer: SPAWN_MAX_BUFFER_BYTES
305
+ });
306
+ if (result.error) {
307
+ throw new Error(`Failed to run oxlint: ${result.error.message}`);
308
+ }
309
+ const stdout = result.stdout.trim();
310
+ if (!stdout) {
311
+ return [];
312
+ }
313
+ const output = JSON.parse(stdout);
314
+ return output.diagnostics.map((diagnostic) => {
315
+ const { plugin, rule } = parseRuleCode(diagnostic.code);
316
+ const primaryLabel = diagnostic.labels[0];
317
+ return {
318
+ filePath: diagnostic.filename,
319
+ plugin,
320
+ rule,
321
+ severity: diagnostic.severity,
322
+ message: diagnostic.message,
323
+ help: diagnostic.help,
324
+ line: primaryLabel?.span.line ?? 0,
325
+ column: primaryLabel?.span.column ?? 0,
326
+ category: PLUGIN_CATEGORY_MAP[plugin] ?? "Other"
327
+ };
328
+ });
329
+ } finally {
330
+ if (fs__default.default.existsSync(configPath)) {
331
+ fs__default.default.unlinkSync(configPath);
332
+ }
333
+ }
334
+ };
335
+
336
+ // src/scan.ts
337
+ var groupDiagnosticsByCategory = (diagnostics) => {
338
+ const groups = /* @__PURE__ */ new Map();
339
+ for (const diagnostic of diagnostics) {
340
+ const existing = groups.get(diagnostic.category) ?? [];
341
+ existing.push(diagnostic);
342
+ groups.set(diagnostic.category, existing);
343
+ }
344
+ return groups;
345
+ };
346
+ var printCategorySection = (category, diagnostics) => {
347
+ const separatorFill = "\u2500".repeat(SEPARATOR_LENGTH - category.length - 6);
348
+ logger.log(`\u2500\u2500\u2500\u2500 ${category} ${separatorFill}`);
349
+ logger.break();
350
+ for (const diagnostic of diagnostics) {
351
+ const icon = diagnostic.severity === "error" ? highlighter.error("\u2717") : highlighter.warn("\u26A0");
352
+ logger.log(` ${icon} ${diagnostic.message}`);
353
+ if (diagnostic.help) {
354
+ logger.dim(` ${diagnostic.help}`);
355
+ }
356
+ logger.dim(` ${diagnostic.filePath}:${diagnostic.line}:${diagnostic.column}`);
357
+ logger.dim(` Rule: ${diagnostic.plugin}/${diagnostic.rule}`);
358
+ logger.break();
359
+ }
360
+ };
361
+ var printSummary = (diagnostics) => {
362
+ const errorCount = diagnostics.filter((diagnostic) => diagnostic.severity === "error").length;
363
+ const warningCount = diagnostics.filter((diagnostic) => diagnostic.severity === "warning").length;
364
+ logger.log("\u2500".repeat(SEPARATOR_LENGTH));
365
+ logger.break();
366
+ const parts = [];
367
+ if (errorCount > 0) {
368
+ parts.push(highlighter.error(`${errorCount} error${errorCount === 1 ? "" : "s"}`));
369
+ }
370
+ if (warningCount > 0) {
371
+ parts.push(highlighter.warn(`${warningCount} warning${warningCount === 1 ? "" : "s"}`));
372
+ }
373
+ logger.log(parts.join(" "));
374
+ };
375
+ var scan = (directory) => {
376
+ const projectInfo = discoverProject(directory);
377
+ if (!projectInfo.reactVersion) {
378
+ throw new Error("No React dependency found in package.json");
379
+ }
380
+ const frameworkLabel = formatFrameworkName(projectInfo.framework);
381
+ const languageLabel = projectInfo.hasTypeScript ? "TypeScript" : "JavaScript";
382
+ logger.log(
383
+ `Found: ${frameworkLabel} \xB7 React ${projectInfo.reactVersion} \xB7 ${languageLabel} \xB7 ${projectInfo.sourceFileCount} source files`
384
+ );
385
+ logger.break();
386
+ const diagnostics = runOxlint(directory, projectInfo.hasTypeScript);
387
+ if (diagnostics.length === 0) {
388
+ logger.success("No issues found!");
389
+ return;
390
+ }
391
+ const groupedDiagnostics = groupDiagnosticsByCategory(diagnostics);
392
+ for (const [category, categoryDiagnostics] of groupedDiagnostics) {
393
+ printCategorySection(category, categoryDiagnostics);
394
+ }
395
+ printSummary(diagnostics);
396
+ };
397
+
398
+ // src/cli.ts
399
+ var VERSION = "0.0.2";
400
+ process.on("SIGINT", () => process.exit(0));
401
+ process.on("SIGTERM", () => process.exit(0));
402
+ var program = new commander.Command().name("react-doctor").description("Diagnose React codebase health").version(VERSION, "-v, --version", "display the version number").argument("[directory]", "project directory to scan", ".").action((directory) => {
403
+ try {
404
+ const resolvedDirectory = path__default.default.resolve(directory);
405
+ logger.log(`react-doctor v${VERSION}`);
406
+ logger.break();
407
+ logger.dim(`Scanning ${resolvedDirectory}...`);
408
+ logger.break();
409
+ scan(resolvedDirectory);
410
+ } catch (error) {
411
+ handleError(error);
412
+ }
413
+ }).addHelpText(
414
+ "after",
415
+ `
416
+ ${highlighter.dim("Learn more:")}
417
+ ${highlighter.info("https://github.com/aidenybai/react-doctor")}
418
+ `
419
+ );
420
+ var main = async () => {
421
+ await program.parseAsync();
422
+ };
423
+ main();
package/dist/cli.d.cts ADDED
@@ -0,0 +1,2 @@
1
+
2
+ export { }
package/package.json CHANGED
@@ -1,11 +1,31 @@
1
1
  {
2
2
  "name": "react-doctor",
3
- "version": "0.0.1",
4
- "main": "dist/index.js",
5
- "module": "dist/index.mjs",
6
- "types": "dist/index.d.ts",
3
+ "version": "0.0.2",
4
+ "bin": {
5
+ "react-doctor": "./dist/cli.cjs"
6
+ },
7
+ "files": [
8
+ "dist"
9
+ ],
10
+ "type": "module",
11
+ "exports": {
12
+ ".": {
13
+ "types": "./dist/cli.d.ts",
14
+ "import": "./dist/cli.js",
15
+ "require": "./dist/cli.cjs"
16
+ }
17
+ },
18
+ "dependencies": {
19
+ "commander": "^14.0.3",
20
+ "oxlint": "^1.47.0",
21
+ "picocolors": "^1.1.1"
22
+ },
23
+ "devDependencies": {
24
+ "tsup": "^8.5.1"
25
+ },
7
26
  "scripts": {
8
- "build": "tsc",
9
- "dev": "tsc --watch"
27
+ "dev": "tsup --watch",
28
+ "build": "rm -rf dist && NODE_ENV=production tsup",
29
+ "typecheck": "tsc --noEmit"
10
30
  }
11
31
  }
package/CHANGELOG.md DELETED
@@ -1,7 +0,0 @@
1
- # react-doctor
2
-
3
- ## 0.0.1
4
-
5
- ### Patch Changes
6
-
7
- - init
package/dist/index.d.ts DELETED
@@ -1 +0,0 @@
1
- export {};
package/dist/index.js DELETED
@@ -1 +0,0 @@
1
- export {};
package/src/index.ts DELETED
@@ -1 +0,0 @@
1
- export {};
package/tsconfig.json DELETED
@@ -1,8 +0,0 @@
1
- {
2
- "extends": "../../tsconfig.json",
3
- "compilerOptions": {
4
- "outDir": "dist",
5
- "rootDir": "src"
6
- },
7
- "include": ["src"]
8
- }