react-doctor 0.2.4 → 0.2.5

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/index.js CHANGED
@@ -7,11 +7,11 @@ import path from "node:path";
7
7
  import { spawn, spawnSync } from "node:child_process";
8
8
  import reactDoctorPlugin, { ALL_REACT_DOCTOR_RULE_KEYS, FRAMEWORK_SPECIFIC_RULE_KEYS, MOTION_LIBRARY_PACKAGES, REACT_COMPILER_RULES, REACT_DOCTOR_RULES } from "oxlint-plugin-react-doctor";
9
9
  import * as Cause from "effect/Cause";
10
- import * as NodeHttpClient from "@effect/platform-node/NodeHttpClient";
11
10
  import * as Config$1 from "effect/Config";
12
11
  import * as Effect from "effect/Effect";
13
12
  import * as Layer from "effect/Layer";
14
13
  import * as Redacted from "effect/Redacted";
14
+ import * as FetchHttpClient from "effect/unstable/http/FetchHttpClient";
15
15
  import * as Otlp from "effect/unstable/observability/Otlp";
16
16
  import * as Context from "effect/Context";
17
17
  import * as Filter from "effect/Filter";
@@ -20,7 +20,9 @@ import * as Ref from "effect/Ref";
20
20
  import * as Stream from "effect/Stream";
21
21
  import * as Cache from "effect/Cache";
22
22
  import * as Console from "effect/Console";
23
- import * as NodeServices from "@effect/platform-node/NodeServices";
23
+ import * as NodeChildProcessSpawner from "@effect/platform-node-shared/NodeChildProcessSpawner";
24
+ import * as NodeFileSystem from "@effect/platform-node-shared/NodeFileSystem";
25
+ import * as NodePath from "@effect/platform-node-shared/NodePath";
24
26
  import * as ChildProcess from "effect/unstable/process/ChildProcess";
25
27
  import { ChildProcessSpawner } from "effect/unstable/process/ChildProcessSpawner";
26
28
  import os from "node:os";
@@ -3005,6 +3007,7 @@ const STAGED_FILES_PROJECT_CONFIG_FILENAMES = [
3005
3007
  ];
3006
3008
  const OXLINT_OUTPUT_MAX_BYTES = 50 * 1024 * 1024;
3007
3009
  const OXLINT_SPAWN_TIMEOUT_MS = 6e4;
3010
+ const RECOMMENDED_PNPM_MINIMUM_RELEASE_AGE_MINUTES = 10080;
3008
3011
  const MAX_GLOB_PATTERN_LENGTH_CHARS = 1024;
3009
3012
  var InvalidGlobPatternError = class extends Error {
3010
3013
  pattern;
@@ -3716,7 +3719,7 @@ const layerOtlp = Layer.unwrap(Effect.gen(function* () {
3716
3719
  baseUrl: endpoint.value,
3717
3720
  resource: { serviceName: TRACER_PROJECT_NAME },
3718
3721
  headers
3719
- }).pipe(Layer.provide(NodeHttpClient.layerUndici));
3722
+ }).pipe(Layer.provide(FetchHttpClient.layer));
3720
3723
  }).pipe(Effect.orDie));
3721
3724
  Schema.String.pipe(Schema.brand("OxlintBinaryPath"));
3722
3725
  Schema.String.pipe(Schema.brand("NodeBinaryPath"));
@@ -3729,6 +3732,120 @@ Context.Reference("react-doctor/OxlintSpawnTimeoutMs", { defaultValue: () => {
3729
3732
  } });
3730
3733
  Context.Reference("react-doctor/OxlintOutputMaxBytes", { defaultValue: () => OXLINT_OUTPUT_MAX_BYTES });
3731
3734
  Context.Reference("react-doctor/StagedFilesTempDirPrefix", { defaultValue: () => "react-doctor-staged-" });
3735
+ const PNPM_WORKSPACE_FILE = "pnpm-workspace.yaml";
3736
+ const PNPM_LOCKFILE = "pnpm-lock.yaml";
3737
+ const PACKAGE_JSON_FILE = "package.json";
3738
+ const PNPM_HARDENING_RULE_KEY = "require-pnpm-hardening";
3739
+ const UTF8_BOM_CHAR = "";
3740
+ const HARDENING_SETTING_KEYS = new Set([
3741
+ "minimumReleaseAge",
3742
+ "blockExoticSubdeps",
3743
+ "trustPolicy"
3744
+ ]);
3745
+ const stripInlineComment = (rawValue) => {
3746
+ let activeQuote = null;
3747
+ for (let charIndex = 0; charIndex < rawValue.length; charIndex += 1) {
3748
+ const currentChar = rawValue[charIndex];
3749
+ if (activeQuote !== null) {
3750
+ if (currentChar === activeQuote) activeQuote = null;
3751
+ continue;
3752
+ }
3753
+ if (currentChar === "\"" || currentChar === "'") {
3754
+ activeQuote = currentChar;
3755
+ continue;
3756
+ }
3757
+ if (currentChar !== "#") continue;
3758
+ const previousChar = rawValue[charIndex - 1];
3759
+ if (charIndex === 0 || previousChar !== void 0 && /\s/.test(previousChar)) return rawValue.slice(0, charIndex);
3760
+ }
3761
+ return rawValue;
3762
+ };
3763
+ const unquote = (rawValue) => rawValue.replace(/^["']|["']$/g, "");
3764
+ const stripBom = (rawContent) => rawContent.startsWith(UTF8_BOM_CHAR) ? rawContent.slice(1) : rawContent;
3765
+ const parseHardeningSettings = (content) => {
3766
+ let minimumReleaseAge = null;
3767
+ let blockExoticSubdeps = null;
3768
+ let trustPolicy = null;
3769
+ const lines = stripBom(content).split(/\r?\n/);
3770
+ for (let lineIndex = 0; lineIndex < lines.length; lineIndex += 1) {
3771
+ const lineText = lines[lineIndex];
3772
+ if (lineText === void 0) continue;
3773
+ if (lineText.search(/\S/) !== 0) continue;
3774
+ const trimmedLine = lineText.trim();
3775
+ if (trimmedLine.startsWith("#")) continue;
3776
+ const colonIndex = trimmedLine.indexOf(":");
3777
+ if (colonIndex <= 0) continue;
3778
+ const settingKey = unquote(trimmedLine.slice(0, colonIndex).trim());
3779
+ if (!HARDENING_SETTING_KEYS.has(settingKey)) continue;
3780
+ const inlineValue = stripInlineComment(trimmedLine.slice(colonIndex + 1)).trim();
3781
+ if (inlineValue.length === 0) continue;
3782
+ const scalar = {
3783
+ value: unquote(inlineValue),
3784
+ line: lineIndex + 1,
3785
+ column: lineText.search(/\S/) + 1
3786
+ };
3787
+ if (settingKey === "minimumReleaseAge") minimumReleaseAge = scalar;
3788
+ else if (settingKey === "blockExoticSubdeps") blockExoticSubdeps = scalar;
3789
+ else if (settingKey === "trustPolicy") trustPolicy = scalar;
3790
+ }
3791
+ return {
3792
+ minimumReleaseAge,
3793
+ blockExoticSubdeps,
3794
+ trustPolicy
3795
+ };
3796
+ };
3797
+ const isPnpmManagedProject = (rootDirectory) => {
3798
+ if (isFile(path.join(rootDirectory, PNPM_LOCKFILE))) return true;
3799
+ if (isFile(path.join(rootDirectory, PNPM_WORKSPACE_FILE))) return true;
3800
+ const packageJsonPath = path.join(rootDirectory, PACKAGE_JSON_FILE);
3801
+ if (!isFile(packageJsonPath)) return false;
3802
+ try {
3803
+ const packageJsonRaw = fs.readFileSync(packageJsonPath, "utf-8");
3804
+ const packageJson = JSON.parse(packageJsonRaw);
3805
+ if (packageJson !== null && typeof packageJson === "object" && "packageManager" in packageJson && typeof packageJson.packageManager === "string" && packageJson.packageManager.startsWith("pnpm@")) return true;
3806
+ } catch {
3807
+ return false;
3808
+ }
3809
+ return false;
3810
+ };
3811
+ const buildHardeningDiagnostic = (input) => ({
3812
+ filePath: PNPM_WORKSPACE_FILE,
3813
+ plugin: "react-doctor",
3814
+ rule: PNPM_HARDENING_RULE_KEY,
3815
+ severity: "warning",
3816
+ message: input.message,
3817
+ help: input.help,
3818
+ line: input.line ?? 0,
3819
+ column: input.column ?? 0,
3820
+ category: "Security"
3821
+ });
3822
+ const checkPnpmHardening = (rootDirectory) => {
3823
+ if (!isPnpmManagedProject(rootDirectory)) return [];
3824
+ const workspacePath = path.join(rootDirectory, PNPM_WORKSPACE_FILE);
3825
+ const settings = parseHardeningSettings(isFile(workspacePath) ? fs.readFileSync(workspacePath, "utf-8") : "");
3826
+ const diagnostics = [];
3827
+ if (settings.minimumReleaseAge === null) diagnostics.push(buildHardeningDiagnostic({
3828
+ message: "pnpm-workspace.yaml is missing `minimumReleaseAge` — newly published versions can ship malware that gets caught and unpublished within hours",
3829
+ help: `Add \`minimumReleaseAge: ${RECOMMENDED_PNPM_MINIMUM_RELEASE_AGE_MINUTES}\` (7 days) to pnpm-workspace.yaml to delay installs until releases have had time to be vetted`
3830
+ }));
3831
+ if (settings.blockExoticSubdeps !== null && settings.blockExoticSubdeps.value.toLowerCase() === "false") diagnostics.push(buildHardeningDiagnostic({
3832
+ line: settings.blockExoticSubdeps.line,
3833
+ column: settings.blockExoticSubdeps.column,
3834
+ message: "`blockExoticSubdeps: false` allows transitive deps from `git:`, `file:`, or tarball URLs — a known supply-chain bypass of the npm registry",
3835
+ help: "Set `blockExoticSubdeps: true` (the default in recent pnpm v11) so transitive deps must come from the registry"
3836
+ }));
3837
+ if (settings.trustPolicy === null) diagnostics.push(buildHardeningDiagnostic({
3838
+ message: "pnpm-workspace.yaml is missing `trustPolicy` — without `no-downgrade`, pnpm silently accepts packages whose trust signals (provenance, signatures) weaken between updates",
3839
+ help: "Add `trustPolicy: no-downgrade` to pnpm-workspace.yaml"
3840
+ }));
3841
+ else if (settings.trustPolicy.value !== "no-downgrade") diagnostics.push(buildHardeningDiagnostic({
3842
+ line: settings.trustPolicy.line,
3843
+ column: settings.trustPolicy.column,
3844
+ message: `\`trustPolicy: ${settings.trustPolicy.value}\` is weaker than \`no-downgrade\` — packages may lose trust signals between updates without you noticing`,
3845
+ help: "Set `trustPolicy: no-downgrade` so pnpm refuses to downgrade trust between resolutions"
3846
+ }));
3847
+ return diagnostics;
3848
+ };
3732
3849
  const REDUCED_MOTION_GREP_PATTERN = "prefers-reduced-motion|useReducedMotion|MotionConfig|reducedMotion";
3733
3850
  const REDUCED_MOTION_FILE_GLOBS = [
3734
3851
  "*.ts",
@@ -4489,7 +4606,7 @@ var Git = class Git extends Context.Service()("react-doctor/Git") {
4489
4606
  };
4490
4607
  })
4491
4608
  });
4492
- })).pipe(Layer.provide(NodeServices.layer));
4609
+ })).pipe(Layer.provide(NodeChildProcessSpawner.layer.pipe(Layer.provide(Layer.mergeAll(NodeFileSystem.layer, NodePath.layer)))));
4493
4610
  /**
4494
4611
  * Test layer driven by a deterministic snapshot. Each key is a
4495
4612
  * convenience pre-canned response so tests don't have to enumerate
@@ -5912,7 +6029,7 @@ const fileReader = (filesService, rootDirectory) => (filePath) => {
5912
6029
  * Config.resolve(directory)
5913
6030
  * -> Project.discover(resolvedDirectory)
5914
6031
  * -> Git metadata for score attribution
5915
- * -> Stream.fromIterable(checkReducedMotion env diagnostics)
6032
+ * -> Stream.fromIterable(env diagnostics: reduced-motion + pnpm hardening)
5916
6033
  * -> Stream.concat(Linter.run(...)) [folds ReactDoctorError into Ref]
5917
6034
  * -> Stream.concat(DeadCode.run(...)) [folds Error into Ref]
5918
6035
  * -> Stream.filterMap(perElementPipeline.apply) [auto-suppress / severity / ignore / inline]
@@ -5972,7 +6089,7 @@ const runInspect = (input, hooks = {}) => Effect.gen(function* () {
5972
6089
  readFileLinesSync: fileReader(filesService, scanDirectory),
5973
6090
  respectInlineDisables: input.respectInlineDisables
5974
6091
  });
5975
- const environmentDiagnostics = isDiffMode ? [] : checkReducedMotion(scanDirectory);
6092
+ const environmentDiagnostics = isDiffMode ? [] : [...checkReducedMotion(scanDirectory), ...checkPnpmHardening(scanDirectory)];
5976
6093
  const emptyDiagnosticStream = Stream.empty;
5977
6094
  const rawLintStream = linterService.run({
5978
6095
  rootDirectory: scanDirectory,
@@ -0,0 +1,35 @@
1
+ import { createRequire } from "node:module";
2
+ //#region \0rolldown/runtime.js
3
+ var __create = Object.create;
4
+ var __defProp = Object.defineProperty;
5
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
6
+ var __getOwnPropNames = Object.getOwnPropertyNames;
7
+ var __getProtoOf = Object.getPrototypeOf;
8
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
9
+ var __commonJSMin = (cb, mod) => () => (mod || (cb((mod = { exports: {} }).exports, mod), cb = null), mod.exports);
10
+ var __exportAll = (all, no_symbols) => {
11
+ let target = {};
12
+ for (var name in all) __defProp(target, name, {
13
+ get: all[name],
14
+ enumerable: true
15
+ });
16
+ if (!no_symbols) __defProp(target, Symbol.toStringTag, { value: "Module" });
17
+ return target;
18
+ };
19
+ var __copyProps = (to, from, except, desc) => {
20
+ if (from && typeof from === "object" || typeof from === "function") for (var keys = __getOwnPropNames(from), i = 0, n = keys.length, key; i < n; i++) {
21
+ key = keys[i];
22
+ if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, {
23
+ get: ((k) => from[k]).bind(null, key),
24
+ enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable
25
+ });
26
+ }
27
+ return to;
28
+ };
29
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", {
30
+ value: mod,
31
+ enumerable: true
32
+ }) : target, mod));
33
+ var __require = /* @__PURE__ */ createRequire(import.meta.url);
34
+ //#endregion
35
+ export { __toESM as i, __exportAll as n, __require as r, __commonJSMin as t };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "react-doctor",
3
- "version": "0.2.4",
3
+ "version": "0.2.5",
4
4
  "description": "Diagnose and fix React codebases for security, performance, correctness, accessibility, bundle-size, and architecture issues",
5
5
  "keywords": [
6
6
  "accessibility",
@@ -49,22 +49,23 @@
49
49
  }
50
50
  },
51
51
  "dependencies": {
52
- "@effect/platform-node": "4.0.0-beta.70",
52
+ "@effect/platform-node-shared": "4.0.0-beta.70",
53
53
  "agent-install": "0.0.5",
54
+ "conf": "^15.1.0",
54
55
  "deslop-js": "^0.0.12",
55
56
  "effect": "4.0.0-beta.70",
56
57
  "oxlint": "^1.66.0",
57
58
  "prompts": "^2.4.2",
58
59
  "typescript": ">=5.0.4 <7",
59
- "oxlint-plugin-react-doctor": "0.2.4"
60
+ "oxlint-plugin-react-doctor": "0.2.5"
60
61
  },
61
62
  "devDependencies": {
62
63
  "@types/prompts": "^2.4.9",
63
64
  "commander": "^14.0.3",
64
65
  "eslint-plugin-react-hooks": "^7.1.1",
65
66
  "ora": "^9.4.0",
66
- "@react-doctor/api": "0.2.4",
67
- "@react-doctor/core": "0.2.4"
67
+ "@react-doctor/api": "0.2.5",
68
+ "@react-doctor/core": "0.2.5"
68
69
  },
69
70
  "peerDependencies": {
70
71
  "eslint-plugin-react-hooks": "^6 || ^7"
@@ -75,7 +76,7 @@
75
76
  }
76
77
  },
77
78
  "engines": {
78
- "node": ">=22.13.0"
79
+ "node": "^20.19.0 || >=22.12.0"
79
80
  },
80
81
  "scripts": {
81
82
  "dev": "vp pack --watch",