react-doctor 0.2.4 → 0.2.6

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
@@ -1,17 +1,17 @@
1
1
  import { createRequire } from "node:module";
2
2
  import * as Schema from "effect/Schema";
3
3
  import * as fs$1 from "node:fs";
4
- import fs, { existsSync, readdirSync } from "node:fs";
4
+ import fs, { existsSync, readFileSync, readdirSync } from "node:fs";
5
5
  import * as Path from "node:path";
6
6
  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";
@@ -2985,6 +2987,7 @@ const isTailwindAtLeast = (detected, required) => {
2985
2987
  const JSX_FILE_PATTERN = /\.(tsx|jsx)$/;
2986
2988
  const SCORE_API_URL = "https://www.react.doctor/api/score";
2987
2989
  const FETCH_TIMEOUT_MS = 1e4;
2990
+ const GITHUB_VIEWER_PERMISSION_TIMEOUT_MS = 2e3;
2988
2991
  const DEFAULT_BRANCH_CANDIDATES = ["main", "master"];
2989
2992
  const ADOPTABLE_LINT_CONFIG_FILENAMES = [".oxlintrc.json", ".eslintrc.json"];
2990
2993
  const GIT_SHOW_MAX_BUFFER_BYTES = 10 * 1024 * 1024;
@@ -3005,6 +3008,7 @@ const STAGED_FILES_PROJECT_CONFIG_FILENAMES = [
3005
3008
  ];
3006
3009
  const OXLINT_OUTPUT_MAX_BYTES = 50 * 1024 * 1024;
3007
3010
  const OXLINT_SPAWN_TIMEOUT_MS = 6e4;
3011
+ const RECOMMENDED_PNPM_MINIMUM_RELEASE_AGE_MINUTES = 10080;
3008
3012
  const MAX_GLOB_PATTERN_LENGTH_CHARS = 1024;
3009
3013
  var InvalidGlobPatternError = class extends Error {
3010
3014
  pattern;
@@ -3716,7 +3720,7 @@ const layerOtlp = Layer.unwrap(Effect.gen(function* () {
3716
3720
  baseUrl: endpoint.value,
3717
3721
  resource: { serviceName: TRACER_PROJECT_NAME },
3718
3722
  headers
3719
- }).pipe(Layer.provide(NodeHttpClient.layerUndici));
3723
+ }).pipe(Layer.provide(FetchHttpClient.layer));
3720
3724
  }).pipe(Effect.orDie));
3721
3725
  Schema.String.pipe(Schema.brand("OxlintBinaryPath"));
3722
3726
  Schema.String.pipe(Schema.brand("NodeBinaryPath"));
@@ -3729,6 +3733,120 @@ Context.Reference("react-doctor/OxlintSpawnTimeoutMs", { defaultValue: () => {
3729
3733
  } });
3730
3734
  Context.Reference("react-doctor/OxlintOutputMaxBytes", { defaultValue: () => OXLINT_OUTPUT_MAX_BYTES });
3731
3735
  Context.Reference("react-doctor/StagedFilesTempDirPrefix", { defaultValue: () => "react-doctor-staged-" });
3736
+ const PNPM_WORKSPACE_FILE = "pnpm-workspace.yaml";
3737
+ const PNPM_LOCKFILE = "pnpm-lock.yaml";
3738
+ const PACKAGE_JSON_FILE = "package.json";
3739
+ const PNPM_HARDENING_RULE_KEY = "require-pnpm-hardening";
3740
+ const UTF8_BOM_CHAR = "";
3741
+ const HARDENING_SETTING_KEYS = new Set([
3742
+ "minimumReleaseAge",
3743
+ "blockExoticSubdeps",
3744
+ "trustPolicy"
3745
+ ]);
3746
+ const stripInlineComment = (rawValue) => {
3747
+ let activeQuote = null;
3748
+ for (let charIndex = 0; charIndex < rawValue.length; charIndex += 1) {
3749
+ const currentChar = rawValue[charIndex];
3750
+ if (activeQuote !== null) {
3751
+ if (currentChar === activeQuote) activeQuote = null;
3752
+ continue;
3753
+ }
3754
+ if (currentChar === "\"" || currentChar === "'") {
3755
+ activeQuote = currentChar;
3756
+ continue;
3757
+ }
3758
+ if (currentChar !== "#") continue;
3759
+ const previousChar = rawValue[charIndex - 1];
3760
+ if (charIndex === 0 || previousChar !== void 0 && /\s/.test(previousChar)) return rawValue.slice(0, charIndex);
3761
+ }
3762
+ return rawValue;
3763
+ };
3764
+ const unquote = (rawValue) => rawValue.replace(/^["']|["']$/g, "");
3765
+ const stripBom = (rawContent) => rawContent.startsWith(UTF8_BOM_CHAR) ? rawContent.slice(1) : rawContent;
3766
+ const parseHardeningSettings = (content) => {
3767
+ let minimumReleaseAge = null;
3768
+ let blockExoticSubdeps = null;
3769
+ let trustPolicy = null;
3770
+ const lines = stripBom(content).split(/\r?\n/);
3771
+ for (let lineIndex = 0; lineIndex < lines.length; lineIndex += 1) {
3772
+ const lineText = lines[lineIndex];
3773
+ if (lineText === void 0) continue;
3774
+ if (lineText.search(/\S/) !== 0) continue;
3775
+ const trimmedLine = lineText.trim();
3776
+ if (trimmedLine.startsWith("#")) continue;
3777
+ const colonIndex = trimmedLine.indexOf(":");
3778
+ if (colonIndex <= 0) continue;
3779
+ const settingKey = unquote(trimmedLine.slice(0, colonIndex).trim());
3780
+ if (!HARDENING_SETTING_KEYS.has(settingKey)) continue;
3781
+ const inlineValue = stripInlineComment(trimmedLine.slice(colonIndex + 1)).trim();
3782
+ if (inlineValue.length === 0) continue;
3783
+ const scalar = {
3784
+ value: unquote(inlineValue),
3785
+ line: lineIndex + 1,
3786
+ column: lineText.search(/\S/) + 1
3787
+ };
3788
+ if (settingKey === "minimumReleaseAge") minimumReleaseAge = scalar;
3789
+ else if (settingKey === "blockExoticSubdeps") blockExoticSubdeps = scalar;
3790
+ else if (settingKey === "trustPolicy") trustPolicy = scalar;
3791
+ }
3792
+ return {
3793
+ minimumReleaseAge,
3794
+ blockExoticSubdeps,
3795
+ trustPolicy
3796
+ };
3797
+ };
3798
+ const isPnpmManagedProject = (rootDirectory) => {
3799
+ if (isFile(path.join(rootDirectory, PNPM_LOCKFILE))) return true;
3800
+ if (isFile(path.join(rootDirectory, PNPM_WORKSPACE_FILE))) return true;
3801
+ const packageJsonPath = path.join(rootDirectory, PACKAGE_JSON_FILE);
3802
+ if (!isFile(packageJsonPath)) return false;
3803
+ try {
3804
+ const packageJsonRaw = fs.readFileSync(packageJsonPath, "utf-8");
3805
+ const packageJson = JSON.parse(packageJsonRaw);
3806
+ if (packageJson !== null && typeof packageJson === "object" && "packageManager" in packageJson && typeof packageJson.packageManager === "string" && packageJson.packageManager.startsWith("pnpm@")) return true;
3807
+ } catch {
3808
+ return false;
3809
+ }
3810
+ return false;
3811
+ };
3812
+ const buildHardeningDiagnostic = (input) => ({
3813
+ filePath: PNPM_WORKSPACE_FILE,
3814
+ plugin: "react-doctor",
3815
+ rule: PNPM_HARDENING_RULE_KEY,
3816
+ severity: "warning",
3817
+ message: input.message,
3818
+ help: input.help,
3819
+ line: input.line ?? 0,
3820
+ column: input.column ?? 0,
3821
+ category: "Security"
3822
+ });
3823
+ const checkPnpmHardening = (rootDirectory) => {
3824
+ if (!isPnpmManagedProject(rootDirectory)) return [];
3825
+ const workspacePath = path.join(rootDirectory, PNPM_WORKSPACE_FILE);
3826
+ const settings = parseHardeningSettings(isFile(workspacePath) ? fs.readFileSync(workspacePath, "utf-8") : "");
3827
+ const diagnostics = [];
3828
+ if (settings.minimumReleaseAge === null) diagnostics.push(buildHardeningDiagnostic({
3829
+ message: "pnpm-workspace.yaml is missing `minimumReleaseAge` — newly published versions can ship malware that gets caught and unpublished within hours",
3830
+ 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`
3831
+ }));
3832
+ if (settings.blockExoticSubdeps !== null && settings.blockExoticSubdeps.value.toLowerCase() === "false") diagnostics.push(buildHardeningDiagnostic({
3833
+ line: settings.blockExoticSubdeps.line,
3834
+ column: settings.blockExoticSubdeps.column,
3835
+ message: "`blockExoticSubdeps: false` allows transitive deps from `git:`, `file:`, or tarball URLs — a known supply-chain bypass of the npm registry",
3836
+ help: "Set `blockExoticSubdeps: true` (the default in recent pnpm v11) so transitive deps must come from the registry"
3837
+ }));
3838
+ if (settings.trustPolicy === null) diagnostics.push(buildHardeningDiagnostic({
3839
+ message: "pnpm-workspace.yaml is missing `trustPolicy` — without `no-downgrade`, pnpm silently accepts packages whose trust signals (provenance, signatures) weaken between updates",
3840
+ help: "Add `trustPolicy: no-downgrade` to pnpm-workspace.yaml"
3841
+ }));
3842
+ else if (settings.trustPolicy.value !== "no-downgrade") diagnostics.push(buildHardeningDiagnostic({
3843
+ line: settings.trustPolicy.line,
3844
+ column: settings.trustPolicy.column,
3845
+ message: `\`trustPolicy: ${settings.trustPolicy.value}\` is weaker than \`no-downgrade\` — packages may lose trust signals between updates without you noticing`,
3846
+ help: "Set `trustPolicy: no-downgrade` so pnpm refuses to downgrade trust between resolutions"
3847
+ }));
3848
+ return diagnostics;
3849
+ };
3732
3850
  const REDUCED_MOTION_GREP_PATTERN = "prefers-reduced-motion|useReducedMotion|MotionConfig|reducedMotion";
3733
3851
  const REDUCED_MOTION_FILE_GLOBS = [
3734
3852
  "*.ts",
@@ -4324,6 +4442,20 @@ const parseGithubRepoFromRemoteUrl = (remoteUrl) => {
4324
4442
  const urlMatch = /^(?:https?:\/\/github\.com\/|ssh:\/\/git@github\.com\/)([^/\s]+)\/([^/\s]+)$/.exec(withoutGitSuffix);
4325
4443
  return urlMatch ? `${urlMatch[1]}/${urlMatch[2]}` : null;
4326
4444
  };
4445
+ const parseGithubRepo = (repo) => {
4446
+ const [owner, name, ...extraParts] = repo.split("/");
4447
+ if (owner === void 0 || name === void 0 || extraParts.length > 0) return null;
4448
+ if (owner.length === 0 || name.length === 0) return null;
4449
+ return {
4450
+ owner,
4451
+ name
4452
+ };
4453
+ };
4454
+ const parseGithubViewerPermission = (stdout) => {
4455
+ const value = trimOrNull(stdout);
4456
+ if (value === null || value === "null") return null;
4457
+ return /^[A-Z_]+$/.test(value) ? value.toLowerCase() : null;
4458
+ };
4327
4459
  const splitNullSeparated = (value) => value.split("\0").filter((entry) => entry.length > 0);
4328
4460
  /**
4329
4461
  * `Git` wraps every `git`-via-subprocess call react-doctor makes
@@ -4349,9 +4481,10 @@ var Git = class Git extends Context.Service()("react-doctor/Git") {
4349
4481
  * reason: GitInvocationFailed })` so the rest of the codebase
4350
4482
  * sees a single failure channel.
4351
4483
  */
4352
- const runGit = (directory, args) => Effect.scoped(Effect.gen(function* () {
4353
- const handle = yield* spawner.spawn(ChildProcess.make("git", [...args], {
4354
- cwd: directory,
4484
+ const runCommand = (input) => Effect.scoped(Effect.gen(function* () {
4485
+ const handle = yield* spawner.spawn(ChildProcess.make(input.command, [...input.args], {
4486
+ cwd: input.directory,
4487
+ env: input.env,
4355
4488
  extendEnv: true
4356
4489
  }));
4357
4490
  const [stdout, stderr, status] = yield* Effect.all([
@@ -4364,11 +4497,23 @@ var Git = class Git extends Context.Service()("react-doctor/Git") {
4364
4497
  stdout,
4365
4498
  stderr
4366
4499
  };
4367
- })).pipe(Effect.catchTag("PlatformError", (cause) => new ReactDoctorError({ reason: new GitInvocationFailed({
4368
- args: [...args],
4369
- directory,
4370
- cause
4371
- }) })));
4500
+ })).pipe(Effect.catchTag("PlatformError", (cause) => {
4501
+ if (input.command !== "git") return Effect.succeed({
4502
+ status: 127,
4503
+ stdout: "",
4504
+ stderr: String(cause)
4505
+ });
4506
+ return new ReactDoctorError({ reason: new GitInvocationFailed({
4507
+ args: [...input.args],
4508
+ directory: input.directory,
4509
+ cause
4510
+ }) });
4511
+ }));
4512
+ const runGit = (directory, args) => runCommand({
4513
+ command: "git",
4514
+ args,
4515
+ directory
4516
+ });
4372
4517
  const currentBranch = (directory) => runGit(directory, [
4373
4518
  "rev-parse",
4374
4519
  "--abbrev-ref",
@@ -4403,11 +4548,43 @@ var Git = class Git extends Context.Service()("react-doctor/Git") {
4403
4548
  "--get",
4404
4549
  "remote.origin.url"
4405
4550
  ]).pipe(Effect.map((result) => result.status === 0 ? parseGithubRepoFromRemoteUrl(result.stdout) : null));
4551
+ const githubViewerPermission = (input) => Effect.gen(function* () {
4552
+ const parsedRepo = parseGithubRepo(input.repo);
4553
+ if (parsedRepo === null) return null;
4554
+ const resultOption = yield* runCommand({
4555
+ command: "gh",
4556
+ args: [
4557
+ "api",
4558
+ "graphql",
4559
+ "-F",
4560
+ `owner=${parsedRepo.owner}`,
4561
+ "-F",
4562
+ `name=${parsedRepo.name}`,
4563
+ "-f",
4564
+ `query=
4565
+ query(\$owner: String!, \$name: String!) {
4566
+ repository(owner: \$owner, name: \$name) {
4567
+ viewerPermission
4568
+ }
4569
+ }
4570
+ `,
4571
+ "--jq",
4572
+ ".data.repository.viewerPermission"
4573
+ ],
4574
+ directory: input.directory,
4575
+ env: { GH_PROMPT_DISABLED: "1" }
4576
+ }).pipe(Effect.timeoutOption(GITHUB_VIEWER_PERMISSION_TIMEOUT_MS));
4577
+ if (Option.isNone(resultOption)) return null;
4578
+ const result = resultOption.value;
4579
+ if (result.status !== 0) return null;
4580
+ return parseGithubViewerPermission(result.stdout);
4581
+ }).pipe(Effect.catch(() => Effect.succeed(null)));
4406
4582
  return Git.of({
4407
4583
  currentBranch,
4408
4584
  defaultBranch,
4409
4585
  headSha,
4410
4586
  githubRepo,
4587
+ githubViewerPermission,
4411
4588
  branchExists,
4412
4589
  diffSelection: ({ directory, explicitBaseBranch }) => Effect.gen(function* () {
4413
4590
  if (explicitBaseBranch !== void 0 && explicitBaseBranch.trim().length === 0) return yield* Effect.fail(new ReactDoctorError({ reason: new GitBaseBranchInvalid({ detail: "Diff base branch cannot be empty." }) }));
@@ -4489,7 +4666,7 @@ var Git = class Git extends Context.Service()("react-doctor/Git") {
4489
4666
  };
4490
4667
  })
4491
4668
  });
4492
- })).pipe(Layer.provide(NodeServices.layer));
4669
+ })).pipe(Layer.provide(NodeChildProcessSpawner.layer.pipe(Layer.provide(Layer.mergeAll(NodeFileSystem.layer, NodePath.layer)))));
4493
4670
  /**
4494
4671
  * Test layer driven by a deterministic snapshot. Each key is a
4495
4672
  * convenience pre-canned response so tests don't have to enumerate
@@ -4502,6 +4679,7 @@ var Git = class Git extends Context.Service()("react-doctor/Git") {
4502
4679
  defaultBranch: () => Effect.succeed(snapshot.defaultBranch ?? null),
4503
4680
  headSha: () => Effect.succeed(snapshot.headSha ?? null),
4504
4681
  githubRepo: () => Effect.succeed(snapshot.githubRepo ?? null),
4682
+ githubViewerPermission: () => Effect.succeed(snapshot.githubViewerPermission ?? null),
4505
4683
  branchExists: (_directory, branch) => Effect.succeed(snapshot.branchExists?.get(branch) ?? false),
4506
4684
  diffSelection: () => Effect.succeed(snapshot.diffSelection ?? null),
4507
4685
  stagedFilePaths: () => Effect.succeed(snapshot.stagedFiles ?? []),
@@ -5844,7 +6022,11 @@ const calculateScore = async (diagnostics, options = {}) => {
5844
6022
  ...options.metadata?.framework ? { framework: options.metadata.framework } : {},
5845
6023
  ...options.metadata?.reactVersion ? { reactVersion: options.metadata.reactVersion } : {},
5846
6024
  ...typeof options.metadata?.sourceFileCount === "number" ? { sourceFileCount: options.metadata.sourceFileCount } : {},
5847
- ...options.metadata?.defaultBranch ? { defaultBranch: options.metadata.defaultBranch } : {}
6025
+ ...options.metadata?.defaultBranch ? { defaultBranch: options.metadata.defaultBranch } : {},
6026
+ ...options.metadata?.doctorVersion ? { doctorVersion: options.metadata.doctorVersion } : {},
6027
+ ...options.metadata?.githubEventName ? { githubEventName: options.metadata.githubEventName } : {},
6028
+ ...options.metadata?.githubActorAssociation ? { githubActorAssociation: options.metadata.githubActorAssociation } : {},
6029
+ ...options.metadata?.githubViewerPermission ? { githubViewerPermission: options.metadata.githubViewerPermission } : {}
5848
6030
  }));
5849
6031
  const response = await fetch(requestUrl, {
5850
6032
  method: "POST",
@@ -5889,6 +6071,32 @@ var Score = class Score extends Context.Service()("react-doctor/Score") {
5889
6071
  }) }));
5890
6072
  static layerOf = (result) => Layer.succeed(Score, Score.of({ compute: () => Effect.succeed(result) }));
5891
6073
  };
6074
+ const getObjectProperty = (value, propertyName) => {
6075
+ if (typeof value !== "object" || value === null) return void 0;
6076
+ return Reflect.get(value, propertyName);
6077
+ };
6078
+ const getStringProperty = (value, propertyName) => {
6079
+ const propertyValue = getObjectProperty(value, propertyName);
6080
+ return typeof propertyValue === "string" && propertyValue.length > 0 ? propertyValue : void 0;
6081
+ };
6082
+ const readGithubEventPayload = (eventPath) => {
6083
+ if (eventPath === void 0 || eventPath.length === 0) return null;
6084
+ try {
6085
+ return JSON.parse(readFileSync(eventPath, "utf8"));
6086
+ } catch {
6087
+ return null;
6088
+ }
6089
+ };
6090
+ const resolveGithubActionsScoreMetadata = (environment = process.env) => {
6091
+ if (environment.GITHUB_ACTIONS !== "true") return {};
6092
+ const pullRequest = getObjectProperty(readGithubEventPayload(environment.GITHUB_EVENT_PATH), "pull_request");
6093
+ const eventName = environment.GITHUB_EVENT_NAME;
6094
+ const actorAssociation = getStringProperty(pullRequest, "author_association");
6095
+ return {
6096
+ ...eventName !== void 0 && eventName.length > 0 ? { githubEventName: eventName } : {},
6097
+ ...actorAssociation !== void 0 ? { githubActorAssociation: actorAssociation } : {}
6098
+ };
6099
+ };
5892
6100
  const NO_HOOKS = {
5893
6101
  beforeLint: () => Effect.void,
5894
6102
  afterLint: () => Effect.void
@@ -5912,7 +6120,7 @@ const fileReader = (filesService, rootDirectory) => (filePath) => {
5912
6120
  * Config.resolve(directory)
5913
6121
  * -> Project.discover(resolvedDirectory)
5914
6122
  * -> Git metadata for score attribution
5915
- * -> Stream.fromIterable(checkReducedMotion env diagnostics)
6123
+ * -> Stream.fromIterable(env diagnostics: reduced-motion + pnpm hardening)
5916
6124
  * -> Stream.concat(Linter.run(...)) [folds ReactDoctorError into Ref]
5917
6125
  * -> Stream.concat(DeadCode.run(...)) [folds Error into Ref]
5918
6126
  * -> Stream.filterMap(perElementPipeline.apply) [auto-suppress / severity / ignore / inline]
@@ -5944,13 +6152,21 @@ const runInspect = (input, hooks = {}) => Effect.gen(function* () {
5944
6152
  gitService.headSha(scanDirectory).pipe(Effect.orElseSucceed(() => null)),
5945
6153
  gitService.defaultBranch(scanDirectory).pipe(Effect.orElseSucceed(() => null))
5946
6154
  ], { concurrency: 3 });
6155
+ const githubActionsScoreMetadata = input.isCi ? resolveGithubActionsScoreMetadata() : {};
6156
+ const githubViewerPermission = input.resolveLocalGithubViewerPermission === true && !input.isCi && repo !== null ? yield* gitService.githubViewerPermission({
6157
+ directory: scanDirectory,
6158
+ repo
6159
+ }).pipe(Effect.orElseSucceed(() => null)) : null;
5947
6160
  const scoreMetadata = {
5948
6161
  ...repo !== null ? { repo } : {},
5949
6162
  ...sha !== null ? { sha } : {},
5950
6163
  framework: project.framework,
5951
6164
  ...project.reactVersion !== null ? { reactVersion: project.reactVersion } : {},
5952
6165
  sourceFileCount: project.sourceFileCount,
5953
- ...defaultBranch !== null ? { defaultBranch } : {}
6166
+ ...defaultBranch !== null ? { defaultBranch } : {},
6167
+ ...input.doctorVersion !== void 0 ? { doctorVersion: input.doctorVersion } : {},
6168
+ ...githubActionsScoreMetadata,
6169
+ ...githubViewerPermission !== null ? { githubViewerPermission } : {}
5954
6170
  };
5955
6171
  const lintIncludePaths = computeJsxIncludePaths([...input.includePaths]) ?? resolveLintIncludePaths(scanDirectory, resolvedConfig.config);
5956
6172
  const beforeLint = hooks.beforeLint ?? NO_HOOKS.beforeLint;
@@ -5972,7 +6188,7 @@ const runInspect = (input, hooks = {}) => Effect.gen(function* () {
5972
6188
  readFileLinesSync: fileReader(filesService, scanDirectory),
5973
6189
  respectInlineDisables: input.respectInlineDisables
5974
6190
  });
5975
- const environmentDiagnostics = isDiffMode ? [] : checkReducedMotion(scanDirectory);
6191
+ const environmentDiagnostics = isDiffMode ? [] : [...checkReducedMotion(scanDirectory), ...checkPnpmHardening(scanDirectory)];
5976
6192
  const emptyDiagnosticStream = Stream.empty;
5977
6193
  const rawLintStream = linterService.run({
5978
6194
  rootDirectory: scanDirectory,
@@ -6500,7 +6716,8 @@ const diagnose = async (directory, options = {}) => {
6500
6716
  adoptExistingLintConfig: initialLoadedConfig?.config?.adoptExistingLintConfig ?? true,
6501
6717
  ignoredTags: new Set(initialLoadedConfig?.config?.ignore?.tags ?? []),
6502
6718
  runDeadCode: options.deadCode ?? initialLoadedConfig?.config?.deadCode ?? true,
6503
- isCi: false
6719
+ isCi: false,
6720
+ resolveLocalGithubViewerPermission: true
6504
6721
  });
6505
6722
  const output = await Effect.runPromise(program.pipe(Effect.provide(buildLayerStack()), Effect.provide(layerOtlp), Effect.catchReasons("ReactDoctorError", {
6506
6723
  NoReactDependency: (reason) => Effect.die(new NoReactDependencyError(reason.directory)),
@@ -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.6",
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.6"
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.6",
68
+ "@react-doctor/core": "0.2.6"
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",