react-doctor 0.9.2-dev.3728102 → 0.9.2-dev.86add14

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.js CHANGED
@@ -1,5 +1,5 @@
1
1
 
2
- !function(){try{var e="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof globalThis?globalThis:"undefined"!=typeof self?self:{},n=(new e.Error).stack;n&&(e._sentryDebugIds=e._sentryDebugIds||{},e._sentryDebugIds[n]="3ced4c14-18cb-5b0c-811c-3448b5c2f139")}catch(e){}}();
2
+ !function(){try{var e="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof globalThis?globalThis:"undefined"!=typeof self?self:{},n=(new e.Error).stack;n&&(e._sentryDebugIds=e._sentryDebugIds||{},e._sentryDebugIds[n]="2a10a6ef-ec19-5a23-88d5-c2e4d5e30eec")}catch(e){}}();
3
3
  import { createRequire } from "node:module";
4
4
  import * as NodeChildProcess from "node:child_process";
5
5
  import { execFile, execFileSync, spawn, spawnSync } from "node:child_process";
@@ -13,11 +13,11 @@ import * as Crypto from "node:crypto";
13
13
  import crypto, { createHash, randomUUID } from "node:crypto";
14
14
  import { ResolverFactory } from "oxc-resolver";
15
15
  import ts from "typescript";
16
+ import * as NodeUrl from "node:url";
17
+ import { fileURLToPath, pathToFileURL } from "node:url";
16
18
  import * as OS from "node:os";
17
19
  import os, { tmpdir } from "node:os";
18
20
  import { parseJSON5 } from "confbox";
19
- import * as NodeUrl from "node:url";
20
- import { fileURLToPath, pathToFileURL } from "node:url";
21
21
  import { createJiti } from "jiti";
22
22
  import { ANALYZED_MANIFEST_FILENAMES, DEFAULT_EXTENSIONS } from "deslop-js/analyzed-inputs";
23
23
  import { gzipSync } from "node:zlib";
@@ -65596,10 +65596,16 @@ const getTopLevelBinding = (sourceFile, bindingName) => {
65596
65596
  }
65597
65597
  return null;
65598
65598
  };
65599
- const getFunctionLocalBindings = (functionNode, argumentsList) => {
65599
+ const getFunctionLocalBindings = (functionNode, argumentsList, argumentAnalysis, functionAnalysis) => {
65600
65600
  const localBindings = /* @__PURE__ */ new Map();
65601
65601
  functionNode.parameters.forEach((parameter, parameterIndex) => {
65602
- if (ts.isIdentifier(parameter.name)) localBindings.set(parameter.name.text, argumentsList[parameterIndex] ?? parameter.initializer ?? null);
65602
+ if (ts.isIdentifier(parameter.name)) {
65603
+ const argument = argumentsList[parameterIndex];
65604
+ localBindings.set(parameter.name.text, {
65605
+ expression: argument ?? parameter.initializer ?? null,
65606
+ analysis: argument ? argumentAnalysis : functionAnalysis
65607
+ });
65608
+ }
65603
65609
  });
65604
65610
  return localBindings;
65605
65611
  };
@@ -65735,8 +65741,8 @@ const getStaticConfigValueState = (expression, analysis, visitedExpressions = /*
65735
65741
  };
65736
65742
  if (ts.isIdentifier(unwrappedExpression)) {
65737
65743
  if (analysis.localBindings.has(unwrappedExpression.text)) {
65738
- const localInitializer = analysis.localBindings.get(unwrappedExpression.text);
65739
- return localInitializer ? getStaticConfigValueState(localInitializer, analysis, nextVisitedExpressions) : null;
65744
+ const localReference = analysis.localBindings.get(unwrappedExpression.text);
65745
+ return localReference?.expression ? getStaticConfigValueState(localReference.expression, localReference.analysis, nextVisitedExpressions) : null;
65740
65746
  }
65741
65747
  const topLevelBinding = getTopLevelBinding(analysis.sourceFile, unwrappedExpression.text);
65742
65748
  return topLevelBinding && ts.isExpression(topLevelBinding) ? getStaticConfigValueState(topLevelBinding, analysis, nextVisitedExpressions) : null;
@@ -65778,8 +65784,8 @@ const getReactCompilerFlagState = (expression, analysis, visitedExpressions = /*
65778
65784
  const assignedBinding = getAssignedPropertyInitializer(unwrappedExpression, "reactCompiler");
65779
65785
  if (assignedBinding.wasFound && assignedBinding.initializer) return { isEnabled: !isStaticallyDisabledConfigExpression(assignedBinding.initializer, analysis) };
65780
65786
  if (analysis.localBindings.has(unwrappedExpression.text)) {
65781
- const localInitializer = analysis.localBindings.get(unwrappedExpression.text);
65782
- return localInitializer ? getReactCompilerFlagState(localInitializer, analysis, nextVisitedExpressions) : null;
65787
+ const localReference = analysis.localBindings.get(unwrappedExpression.text);
65788
+ return localReference?.expression ? getReactCompilerFlagState(localReference.expression, localReference.analysis, nextVisitedExpressions) : null;
65783
65789
  }
65784
65790
  const topLevelBinding = getTopLevelBinding(analysis.sourceFile, unwrappedExpression.text);
65785
65791
  return topLevelBinding && ts.isExpression(topLevelBinding) ? getReactCompilerFlagState(topLevelBinding, analysis, nextVisitedExpressions) : null;
@@ -65802,8 +65808,8 @@ const getSelectedObjectProperty = (expression, propertyName, analysis, visitedEx
65802
65808
  visitedExpressions.add(resolvedExpression);
65803
65809
  if (ts.isIdentifier(resolvedExpression)) {
65804
65810
  if (analysis.localBindings.has(resolvedExpression.text)) {
65805
- const localInitializer = analysis.localBindings.get(resolvedExpression.text);
65806
- return localInitializer ? getSelectedObjectProperty(localInitializer, propertyName, analysis, visitedExpressions) : null;
65811
+ const localReference = analysis.localBindings.get(resolvedExpression.text);
65812
+ return localReference?.expression ? getSelectedObjectProperty(localReference.expression, propertyName, localReference.analysis, visitedExpressions) : null;
65807
65813
  }
65808
65814
  const scopedBinding = getScopedConfigBinding(resolvedExpression);
65809
65815
  if (scopedBinding.wasFound) return scopedBinding.initializer ? getSelectedObjectProperty(scopedBinding.initializer, propertyName, analysis, visitedExpressions) : null;
@@ -65812,9 +65818,18 @@ const getSelectedObjectProperty = (expression, propertyName, analysis, visitedEx
65812
65818
  }
65813
65819
  if (!ts.isObjectLiteralExpression(resolvedExpression)) return null;
65814
65820
  for (const property of [...resolvedExpression.properties].reverse()) {
65815
- if (ts.isPropertyAssignment(property) && getStaticPropertyName$2(property.name) === propertyName) return property.initializer;
65816
- if (ts.isMethodDeclaration(property) && getStaticPropertyName$2(property.name) === propertyName) return property;
65817
- if (ts.isShorthandPropertyAssignment(property) && property.name.text === propertyName) return property.name;
65821
+ if (ts.isPropertyAssignment(property) && getStaticPropertyName$2(property.name) === propertyName) return {
65822
+ node: property.initializer,
65823
+ analysis
65824
+ };
65825
+ if (ts.isMethodDeclaration(property) && getStaticPropertyName$2(property.name) === propertyName) return {
65826
+ node: property,
65827
+ analysis
65828
+ };
65829
+ if (ts.isShorthandPropertyAssignment(property) && property.name.text === propertyName) return {
65830
+ node: property.name,
65831
+ analysis
65832
+ };
65818
65833
  if (ts.isSpreadAssignment(property)) {
65819
65834
  const spreadProperty = getSelectedObjectProperty(property.expression, propertyName, analysis, visitedExpressions);
65820
65835
  if (spreadProperty) return spreadProperty;
@@ -65827,8 +65842,8 @@ const configExpressionMayDefineProperty = (expression, propertyName, analysis) =
65827
65842
  while (ts.isParenthesizedExpression(resolvedExpression) || ts.isAsExpression(resolvedExpression) || ts.isTypeAssertionExpression(resolvedExpression) || ts.isSatisfiesExpression(resolvedExpression) || ts.isNonNullExpression(resolvedExpression)) resolvedExpression = resolvedExpression.expression;
65828
65843
  if (ts.isIdentifier(resolvedExpression)) {
65829
65844
  if (analysis.localBindings.has(resolvedExpression.text)) {
65830
- const localInitializer = analysis.localBindings.get(resolvedExpression.text);
65831
- return localInitializer ? configExpressionMayDefineProperty(localInitializer, propertyName, analysis) : true;
65845
+ const localReference = analysis.localBindings.get(resolvedExpression.text);
65846
+ return localReference?.expression ? configExpressionMayDefineProperty(localReference.expression, propertyName, localReference.analysis) : true;
65832
65847
  }
65833
65848
  const scopedBinding = getScopedConfigBinding(resolvedExpression);
65834
65849
  if (scopedBinding.wasFound) return scopedBinding.initializer ? configExpressionMayDefineProperty(scopedBinding.initializer, propertyName, analysis) : true;
@@ -65912,15 +65927,15 @@ const getReExportedConfigModules = (sourceFile, exportName) => {
65912
65927
  }
65913
65928
  return bindings;
65914
65929
  };
65915
- const analyzeConfigModuleExport = (filePath, exportName, allowCompilerTransform, importDepth, visitedModules, argumentsList = []) => {
65930
+ const analyzeConfigModuleExport = (filePath, exportName, allowCompilerTransform, importDepth, visitedModules, argumentsList = [], argumentAnalysis) => {
65916
65931
  if (!isFile(filePath) || importDepth > 4) return false;
65917
65932
  const moduleVisitKey = `${filePath}:${exportName}:${allowCompilerTransform}`;
65918
65933
  if (visitedModules.has(moduleVisitKey)) return false;
65919
65934
  const nextVisitedModules = new Set(visitedModules);
65920
65935
  nextVisitedModules.add(moduleVisitKey);
65921
- return analyzeConfigSourceFileExport(parseConfigSourceFile(filePath, NFS.readFileSync(filePath, "utf-8")), filePath, exportName, allowCompilerTransform, importDepth, nextVisitedModules, argumentsList);
65936
+ return analyzeConfigSourceFileExport(parseConfigSourceFile(filePath, NFS.readFileSync(filePath, "utf-8")), filePath, exportName, allowCompilerTransform, importDepth, nextVisitedModules, argumentsList, argumentAnalysis);
65922
65937
  };
65923
- const analyzeConfigSourceFileExport = (sourceFile, filePath, exportName, allowCompilerTransform, importDepth, visitedModules, argumentsList = []) => {
65938
+ const analyzeConfigSourceFileExport = (sourceFile, filePath, exportName, allowCompilerTransform, importDepth, visitedModules, argumentsList = [], argumentAnalysis) => {
65924
65939
  const analysis = {
65925
65940
  filePath,
65926
65941
  sourceFile,
@@ -65930,14 +65945,27 @@ const analyzeConfigSourceFileExport = (sourceFile, filePath, exportName, allowCo
65930
65945
  localBindings: /* @__PURE__ */ new Map(),
65931
65946
  activeFunctions: /* @__PURE__ */ new Set()
65932
65947
  };
65948
+ const resolvedArgumentAnalysis = argumentAnalysis ?? analysis;
65933
65949
  const exportedConfigNodes = getExportedConfigNodes(sourceFile, exportName);
65934
- return exportedConfigNodes.some((node) => ts.isArrowFunction(node) || ts.isFunctionExpression(node) || ts.isFunctionDeclaration(node) ? analyzeConfigFunction(node, analysis, allowCompilerTransform, argumentsList) : analyzeConfigNode(node, analysis, allowCompilerTransform)) || getReExportedConfigModules(sourceFile, exportName).some((binding) => {
65950
+ return exportedConfigNodes.some((node) => {
65951
+ if (ts.isArrowFunction(node) || ts.isFunctionExpression(node) || ts.isFunctionDeclaration(node)) return analyzeConfigFunction(node, analysis, allowCompilerTransform, argumentsList, resolvedArgumentAnalysis);
65952
+ if (argumentsList.length > 0 && ts.isIdentifier(node)) {
65953
+ const topLevelBinding = getTopLevelBinding(sourceFile, node.text);
65954
+ if (topLevelBinding && ts.isFunctionLike(topLevelBinding)) return analyzeConfigFunction(topLevelBinding, analysis, allowCompilerTransform, argumentsList, resolvedArgumentAnalysis);
65955
+ const importBinding = getImportBinding(sourceFile, node.text);
65956
+ if (importBinding) {
65957
+ const importedFilePath = resolveImportedConfigFile(filePath, importBinding.moduleSpecifier);
65958
+ return Boolean(importedFilePath && analyzeConfigModuleExport(importedFilePath, importBinding.exportName, allowCompilerTransform, importDepth + 1, visitedModules, argumentsList, resolvedArgumentAnalysis));
65959
+ }
65960
+ }
65961
+ return analyzeConfigNode(node, analysis, allowCompilerTransform);
65962
+ }) || getReExportedConfigModules(sourceFile, exportName).some((binding) => {
65935
65963
  if (exportedConfigNodes.length > 0 && binding.isNamespace) return false;
65936
65964
  const importedFilePath = resolveImportedConfigFile(filePath, binding.moduleSpecifier);
65937
- return Boolean(importedFilePath && analyzeConfigModuleExport(importedFilePath, binding.exportName, allowCompilerTransform, importDepth + 1, visitedModules, argumentsList));
65965
+ return Boolean(importedFilePath && analyzeConfigModuleExport(importedFilePath, binding.exportName, allowCompilerTransform, importDepth + 1, visitedModules, argumentsList, resolvedArgumentAnalysis));
65938
65966
  });
65939
65967
  };
65940
- const analyzeConfigFunction = (functionNode, analysis, allowCompilerTransform, argumentsList = []) => {
65968
+ const analyzeConfigFunction = (functionNode, analysis, allowCompilerTransform, argumentsList = [], argumentAnalysis = analysis) => {
65941
65969
  if (!functionNode.body) return false;
65942
65970
  if (analysis.activeFunctions.has(functionNode.pos)) return false;
65943
65971
  const activeFunctions = new Set(analysis.activeFunctions);
@@ -65945,7 +65973,7 @@ const analyzeConfigFunction = (functionNode, analysis, allowCompilerTransform, a
65945
65973
  const functionAnalysis = {
65946
65974
  ...analysis,
65947
65975
  visitedNodes: /* @__PURE__ */ new Set(),
65948
- localBindings: getFunctionLocalBindings(functionNode, argumentsList),
65976
+ localBindings: getFunctionLocalBindings(functionNode, argumentsList, argumentAnalysis, analysis),
65949
65977
  activeFunctions
65950
65978
  };
65951
65979
  if (!ts.isBlock(functionNode.body)) return analyzeConfigNode(functionNode.body, functionAnalysis, allowCompilerTransform);
@@ -65964,7 +65992,7 @@ const analyzeConfigFunction = (functionNode, analysis, allowCompilerTransform, a
65964
65992
  const analyzeImportedConfig = ({ analysis, moduleSpecifier, exportName, allowCompilerTransform, argumentsList }) => {
65965
65993
  const importedFilePath = resolveImportedConfigFile(analysis.filePath, moduleSpecifier);
65966
65994
  if (!importedFilePath) return null;
65967
- return analyzeConfigModuleExport(importedFilePath, exportName, allowCompilerTransform, analysis.importDepth + 1, analysis.visitedModules, argumentsList);
65995
+ return analyzeConfigModuleExport(importedFilePath, exportName, allowCompilerTransform, analysis.importDepth + 1, analysis.visitedModules, argumentsList, analysis);
65968
65996
  };
65969
65997
  const analyzeConfigIdentifier = (identifier, analysis, allowCompilerTransform, isCompilerTransformCollection, excludedPropertyNames) => {
65970
65998
  const assignedReactCompiler = getAssignedPropertyInitializer(identifier, "reactCompiler");
@@ -65978,8 +66006,8 @@ const analyzeConfigIdentifier = (identifier, analysis, allowCompilerTransform, i
65978
66006
  if (assignedPlugins.wasFound) overriddenPropertyNames.add("plugins");
65979
66007
  if (assignedPresets.wasFound) overriddenPropertyNames.add("presets");
65980
66008
  if (analysis.localBindings.has(identifier.text)) {
65981
- const localInitializer = analysis.localBindings.get(identifier.text);
65982
- return Boolean(localInitializer && analyzeConfigNode(localInitializer, analysis, allowCompilerTransform, isCompilerTransformCollection, overriddenPropertyNames));
66009
+ const localReference = analysis.localBindings.get(identifier.text);
66010
+ return Boolean(localReference?.expression && analyzeConfigNode(localReference.expression, localReference.analysis, allowCompilerTransform, isCompilerTransformCollection, overriddenPropertyNames));
65983
66011
  }
65984
66012
  const scopedBinding = getScopedConfigBinding(identifier);
65985
66013
  if (scopedBinding.wasFound) return Boolean(scopedBinding.initializer && analyzeConfigNode(scopedBinding.initializer, analysis, allowCompilerTransform, isCompilerTransformCollection, overriddenPropertyNames));
@@ -66045,7 +66073,7 @@ const analyzeConfigCallTarget = (callExpression, analysis, allowCompilerTransfor
66045
66073
  }
66046
66074
  const selectedProperty = getSelectedObjectProperty(target.expression, propertyName, analysis);
66047
66075
  if (selectedProperty === null) return null;
66048
- return ts.isFunctionLike(selectedProperty) ? analyzeConfigFunction(selectedProperty, analysis, allowCompilerTransform, callExpression.arguments) : analyzeConfigNode(selectedProperty, analysis, allowCompilerTransform);
66076
+ return ts.isFunctionLike(selectedProperty.node) ? analyzeConfigFunction(selectedProperty.node, selectedProperty.analysis, allowCompilerTransform, callExpression.arguments, analysis) : analyzeConfigNode(selectedProperty.node, selectedProperty.analysis, allowCompilerTransform);
66049
66077
  };
66050
66078
  const analyzeConfigNode = (node, analysis, allowCompilerTransform, isCompilerTransformCollection = false, excludedPropertyNames) => {
66051
66079
  const excludedPropertiesVisitKey = excludedPropertyNames ? [...excludedPropertyNames].sort().join(",") : "";
@@ -66145,7 +66173,7 @@ const analyzeConfigNode = (node, analysis, allowCompilerTransform, isCompilerTra
66145
66173
  if (ts.isIdentifier(node.expression)) {
66146
66174
  if (analysis.localBindings.has(node.expression.text) || getScopedConfigBinding(node.expression).wasFound) {
66147
66175
  const selectedProperty = getSelectedObjectProperty(node.expression, node.name.text, analysis);
66148
- return Boolean(selectedProperty && analyzeConfigNode(selectedProperty, analysis, allowCompilerTransform));
66176
+ return Boolean(selectedProperty && analyzeConfigNode(selectedProperty.node, selectedProperty.analysis, allowCompilerTransform));
66149
66177
  }
66150
66178
  const importBinding = getImportBinding(analysis.sourceFile, node.expression.text);
66151
66179
  if (importBinding?.isNamespace) {
@@ -66159,7 +66187,7 @@ const analyzeConfigNode = (node, analysis, allowCompilerTransform, isCompilerTra
66159
66187
  return false;
66160
66188
  }
66161
66189
  const selectedProperty = getSelectedObjectProperty(node.expression, node.name.text, analysis);
66162
- if (selectedProperty) return analyzeConfigNode(selectedProperty, analysis, allowCompilerTransform);
66190
+ if (selectedProperty) return analyzeConfigNode(selectedProperty.node, selectedProperty.analysis, allowCompilerTransform);
66163
66191
  return false;
66164
66192
  }
66165
66193
  return analyzeConfigNode(node.expression, analysis, allowCompilerTransform);
@@ -66168,7 +66196,7 @@ const analyzeConfigNode = (node, analysis, allowCompilerTransform, isCompilerTra
66168
66196
  if (ts.isIdentifier(node.expression)) {
66169
66197
  if (analysis.localBindings.has(node.expression.text) || getScopedConfigBinding(node.expression).wasFound) {
66170
66198
  const selectedProperty = getSelectedObjectProperty(node.expression, node.argumentExpression.text, analysis);
66171
- return Boolean(selectedProperty && analyzeConfigNode(selectedProperty, analysis, allowCompilerTransform));
66199
+ return Boolean(selectedProperty && analyzeConfigNode(selectedProperty.node, selectedProperty.analysis, allowCompilerTransform));
66172
66200
  }
66173
66201
  const importBinding = getImportBinding(analysis.sourceFile, node.expression.text);
66174
66202
  if (importBinding?.isNamespace) {
@@ -66181,7 +66209,7 @@ const analyzeConfigNode = (node, analysis, allowCompilerTransform, isCompilerTra
66181
66209
  }));
66182
66210
  }
66183
66211
  const selectedProperty = getSelectedObjectProperty(node.expression, node.argumentExpression.text, analysis);
66184
- if (selectedProperty) return analyzeConfigNode(selectedProperty, analysis, allowCompilerTransform);
66212
+ if (selectedProperty) return analyzeConfigNode(selectedProperty.node, selectedProperty.analysis, allowCompilerTransform);
66185
66213
  return false;
66186
66214
  }
66187
66215
  return analyzeConfigNode(node.expression, analysis, allowCompilerTransform);
@@ -68558,6 +68586,7 @@ const classifyPackageRole = (filename) => {
68558
68586
  * Resolves the absolute path to read for a diagnostic's `filePath`,
68559
68587
  * accounting for the various shapes oxlint emits:
68560
68588
  *
68589
+ * - `file://` URL — decode to an absolute filesystem path.
68561
68590
  * - Absolute POSIX (`/abs/path/file.tsx`) — pass through.
68562
68591
  * - Absolute Windows (`C:/...` or `C:\...`) — pass through.
68563
68592
  * - `./relative` or bare relative — join against `rootDirectory`.
@@ -68567,6 +68596,7 @@ const classifyPackageRole = (filename) => {
68567
68596
  * use one canonical resolution path.
68568
68597
  */
68569
68598
  const resolveCandidateReadPath = (rootDirectory, filePath) => {
68599
+ if (filePath.startsWith("file://")) return fileURLToPath(filePath);
68570
68600
  const normalizedFile = filePath.replace(/\\/g, "/");
68571
68601
  if (normalizedFile.startsWith("/") || /^[a-zA-Z]:\//.test(normalizedFile) || /^[a-zA-Z]:\\/.test(filePath)) return filePath;
68572
68602
  return `${rootDirectory.replace(/\\/g, "/").replace(/\/$/, "")}/${normalizedFile.replace(/^\.\//, "")}`;
@@ -72875,7 +72905,10 @@ var Git = class Git extends Service()("react-doctor/Git") {
72875
72905
  if (argvLengthChars > spawnArgsLengthCap) return yield* foldSpawnFailure(`spawn ENAMETOOLONG (${argvLengthChars} argv chars exceed ${spawnArgsLengthCap})`);
72876
72906
  const handle = yield* spawner.spawn(make$1(input.command, [...input.args], {
72877
72907
  cwd: input.directory,
72878
- env: input.env,
72908
+ env: input.command === "git" ? {
72909
+ ...input.env,
72910
+ GIT_DIR: void 0
72911
+ } : input.env,
72879
72912
  extendEnv: true
72880
72913
  }));
72881
72914
  const maxStdoutBytes = input.maxStdoutBytes;
@@ -73148,7 +73181,7 @@ var Git = class Git extends Service()("react-doctor/Git") {
73148
73181
  "--diff-filter=ACMR",
73149
73182
  "--relative"
73150
73183
  ]).pipe(map$3((result) => {
73151
- if (result.status !== 0) return [];
73184
+ if (result.status !== 0) return null;
73152
73185
  return splitNullSeparated(result.stdout);
73153
73186
  })),
73154
73187
  showStagedContent: (directory, relativePath, options) => runCommand({
@@ -77571,10 +77604,14 @@ const materializeSourceTree = (input) => gen(function* () {
77571
77604
  materializedFiles.push(relativePath);
77572
77605
  }
77573
77606
  yield* sync(() => {
77574
- for (const configFilename of STAGED_FILES_PROJECT_CONFIG_FILENAMES) {
77575
- const sourcePath = path.join(input.directory, configFilename);
77576
- const targetPath = path.join(resolvedTempDirectory, configFilename);
77577
- if (fs.existsSync(sourcePath) && !fs.existsSync(targetPath)) fs.cpSync(sourcePath, targetPath);
77607
+ for (const subdirectory of ["", ...input.configSubdirectories ?? []]) for (const configFilename of STAGED_FILES_PROJECT_CONFIG_FILENAMES) {
77608
+ const sourcePath = path.join(input.directory, subdirectory, configFilename);
77609
+ const targetPath = path.resolve(resolvedTempDirectory, subdirectory, configFilename);
77610
+ if (!isPathInsideDirectory(targetPath, resolvedTempDirectory)) continue;
77611
+ if (fs.existsSync(sourcePath) && !fs.existsSync(targetPath)) {
77612
+ fs.mkdirSync(path.dirname(targetPath), { recursive: true });
77613
+ fs.cpSync(sourcePath, targetPath);
77614
+ }
77578
77615
  }
77579
77616
  });
77580
77617
  return {
@@ -77607,15 +77644,17 @@ var StagedFiles = class StagedFiles extends Service()("react-doctor/StagedFiles"
77607
77644
  static layerNode = effect(StagedFiles, gen(function* () {
77608
77645
  const git = yield* Git;
77609
77646
  return StagedFiles.of({
77610
- discoverSourceFiles: (directory) => git.stagedFilePaths(directory).pipe(map$3((entries) => entries.filter(isLintableSourceFile)), withSpan("StagedFiles.discoverSourceFiles")),
77611
- materialize: ({ directory, stagedFiles, tempDirectory }) => materializeSourceTree({
77647
+ discoverSourceFiles: (directory) => git.stagedFilePaths(directory).pipe(map$3((entries) => entries === null ? null : entries.filter(isLintableSourceFile)), withSpan("StagedFiles.discoverSourceFiles")),
77648
+ materialize: ({ directory, stagedFiles, tempDirectory, configSubdirectories }) => materializeSourceTree({
77612
77649
  directory,
77613
77650
  files: stagedFiles,
77614
77651
  tempDirectory,
77652
+ configSubdirectories,
77615
77653
  readContent: (relativePath) => git.showStagedContent(directory, relativePath, { maxBufferBytes: GIT_SHOW_MAX_BUFFER_BYTES })
77616
77654
  }).pipe(map$3((tree) => ({
77617
77655
  tempDirectory: tree.tempDirectory,
77618
77656
  stagedFiles: tree.materializedFiles,
77657
+ unmaterializedFiles: tree.unmaterializedFiles,
77619
77658
  cleanup: tree.cleanup
77620
77659
  })), withSpan("StagedFiles.materialize"))
77621
77660
  });
@@ -77630,6 +77669,7 @@ var StagedFiles = class StagedFiles extends Service()("react-doctor/StagedFiles"
77630
77669
  materialize: ({ tempDirectory }) => succeed$2({
77631
77670
  tempDirectory,
77632
77671
  stagedFiles: snapshot.materializedFiles ?? snapshot.sourceFiles ?? [],
77672
+ unmaterializedFiles: [],
77633
77673
  cleanup: () => {}
77634
77674
  })
77635
77675
  }));
@@ -78339,6 +78379,7 @@ const METRIC = {
78339
78379
  cliError: "cli.error",
78340
78380
  cliEnvironmentError: "cli.env_error",
78341
78381
  stagedSnapshotDivergence: "staged.snapshot_divergence",
78382
+ stagedPerProject: "staged.per_project",
78342
78383
  projectDetected: "project.detected",
78343
78384
  projectPathSelected: "project.path_selected",
78344
78385
  projectConfigSelected: "project.config_selected",
@@ -78424,7 +78465,7 @@ const makeNoopConsole = () => ({
78424
78465
  });
78425
78466
  //#endregion
78426
78467
  //#region src/cli/utils/version.ts
78427
- const VERSION = "0.9.2-dev.3728102";
78468
+ const VERSION = "0.9.2-dev.86add14";
78428
78469
  //#endregion
78429
78470
  //#region src/cli/utils/json-mode.ts
78430
78471
  let context = null;
@@ -78782,13 +78823,13 @@ const isDevVersion = (version) => version === "0.0.0" || version.includes("-");
78782
78823
  * uploads source-map artifacts under, so stack frames symbolicate. Honors the
78783
78824
  * standard `SENTRY_RELEASE` override.
78784
78825
  */
78785
- const resolveSentryRelease = () => process.env.SENTRY_RELEASE || `react-doctor@0.9.2-dev.3728102`;
78826
+ const resolveSentryRelease = () => process.env.SENTRY_RELEASE || `react-doctor@0.9.2-dev.86add14`;
78786
78827
  /**
78787
78828
  * Deployment environment shown in Sentry's environment filter. Defaults to
78788
78829
  * `production` for tagged releases and `development` for dev/unbuilt versions,
78789
78830
  * overridable via the standard `SENTRY_ENVIRONMENT` env var.
78790
78831
  */
78791
- const resolveSentryEnvironment = () => process.env.SENTRY_ENVIRONMENT || (isDevVersion("0.9.2-dev.3728102") ? "development" : "production");
78832
+ const resolveSentryEnvironment = () => process.env.SENTRY_ENVIRONMENT || (isDevVersion("0.9.2-dev.86add14") ? "development" : "production");
78792
78833
  /**
78793
78834
  * Performance-tracing sample rate in `[0, 1]`. Reads `SENTRY_TRACES_SAMPLE_RATE`
78794
78835
  * (set to `0` to disable tracing) and falls back to
@@ -92245,16 +92286,19 @@ const formatClusterLocation = (cluster, resolveSourceRoot, hyperlinks) => {
92245
92286
  if (!hyperlinks) return `${location}${contextTag}`;
92246
92287
  return `${formatHyperlink(location, pathToFileURL(resolveAbsolutePath(lead.filePath, resolveSourceRoot(lead))).href)}${contextTag}`;
92247
92288
  };
92248
- const buildDiagnosticClusterLines = (cluster, resolveSourceRoot, renderCodeFrame, hyperlinks) => {
92289
+ const buildDiagnosticClusterLines = (cluster, resolveSourceRoot, resolveFrameSourceRoot, renderCodeFrame, hyperlinks) => {
92249
92290
  const lead = cluster.diagnostics[0];
92250
92291
  const isMultiSite = cluster.diagnostics.length > 1;
92292
+ const sourceRoot = resolveSourceRoot(lead);
92293
+ const frameSourceRoot = resolveFrameSourceRoot(lead);
92294
+ const frameFilePath = Path.isAbsolute(lead.filePath) && sourceRoot !== frameSourceRoot ? Path.relative(sourceRoot, lead.filePath) : lead.filePath;
92251
92295
  const lines = ["", highlighter.gray(`${TOP_ERROR_DETAIL_INDENT}${formatClusterLocation(cluster, resolveSourceRoot, hyperlinks)}`)];
92252
92296
  const codeFrame = renderCodeFrame ? buildCodeFrame({
92253
- filePath: lead.filePath,
92297
+ filePath: frameFilePath,
92254
92298
  line: cluster.startLine,
92255
92299
  column: isMultiSite ? 0 : lead.column,
92256
92300
  endLine: isMultiSite ? cluster.endLine : void 0,
92257
- rootDirectory: resolveSourceRoot(lead)
92301
+ rootDirectory: frameSourceRoot
92258
92302
  }) : null;
92259
92303
  if (codeFrame) {
92260
92304
  const boxInnerWidth = resolveMeasureWidth(8);
@@ -92267,7 +92311,7 @@ const buildDiagnosticClusterLines = (cluster, resolveSourceRoot, renderCodeFrame
92267
92311
  }
92268
92312
  return lines;
92269
92313
  };
92270
- const buildRuleDetailBlock = (ruleKey, ruleDiagnostics, resolveSourceRoot, renderEverySite, isAgentEnvironment, hyperlinks) => {
92314
+ const buildRuleDetailBlock = (ruleKey, ruleDiagnostics, resolveSourceRoot, resolveFrameSourceRoot, renderEverySite, isAgentEnvironment, hyperlinks) => {
92271
92315
  const representative = pickRepresentativeDiagnostic(ruleDiagnostics);
92272
92316
  const { severity } = representative;
92273
92317
  const trailingBadge = formatTrailingSiteBadge(ruleDiagnostics.length);
@@ -92289,7 +92333,7 @@ const buildRuleDetailBlock = (ruleKey, ruleDiagnostics, resolveSourceRoot, rende
92289
92333
  }
92290
92334
  const renderCodeFrame = severity === "error";
92291
92335
  const sites = renderEverySite ? ruleDiagnostics : [representative];
92292
- if (!(isCollapsedWarningGroup && representative.help.includes(representative.filePath))) for (const cluster of clusterNearbyDiagnostics(sites)) lines.push(...buildDiagnosticClusterLines(cluster, resolveSourceRoot, renderCodeFrame, hyperlinks));
92336
+ if (!(isCollapsedWarningGroup && representative.help.includes(representative.filePath))) for (const cluster of clusterNearbyDiagnostics(sites)) lines.push(...buildDiagnosticClusterLines(cluster, resolveSourceRoot, resolveFrameSourceRoot, renderCodeFrame, hyperlinks));
92293
92337
  return lines;
92294
92338
  };
92295
92339
  const selectErrorRuleGroups = (diagnostics, rulePriority) => buildSortedRuleGroups(diagnostics.filter((diagnostic) => diagnostic.severity === "error"), rulePriority);
@@ -92315,7 +92359,7 @@ const buildMigrationScaleAdvisoryLines = (diagnostics) => {
92315
92359
  return lines;
92316
92360
  };
92317
92361
  const getTopErrorRuleKeys = (diagnostics, limit, rulePriority) => new Set(selectTopErrorRuleGroups(diagnostics, limit, rulePriority).map(([ruleKey]) => ruleKey));
92318
- const buildTopErrorsSection = (diagnostics, resolveSourceRoot, hyperlinks, rulePriority) => {
92362
+ const buildTopErrorsSection = (diagnostics, resolveSourceRoot, resolveFrameSourceRoot, hyperlinks, rulePriority) => {
92319
92363
  const topRuleGroups = selectErrorRuleGroups(diagnostics, rulePriority).slice(0, 3);
92320
92364
  if (topRuleGroups.length === 0) return {
92321
92365
  lines: [],
@@ -92325,7 +92369,7 @@ const buildTopErrorsSection = (diagnostics, resolveSourceRoot, hyperlinks, ruleP
92325
92369
  const blockOffsets = [];
92326
92370
  for (const [ruleKey, ruleDiagnostics] of topRuleGroups) {
92327
92371
  blockOffsets.push(lines.length);
92328
- lines.push(...buildRuleDetailBlock(ruleKey, ruleDiagnostics, resolveSourceRoot, false, false, hyperlinks));
92372
+ lines.push(...buildRuleDetailBlock(ruleKey, ruleDiagnostics, resolveSourceRoot, resolveFrameSourceRoot, false, false, hyperlinks));
92329
92373
  lines.push("");
92330
92374
  }
92331
92375
  return {
@@ -92363,18 +92407,19 @@ const buildOverviewHeaderLines = (diagnostics) => {
92363
92407
  * single Effect.forEach over Console.log so failures or fiber
92364
92408
  * interruption produce predictable partial output.
92365
92409
  */
92366
- const printDiagnostics = (diagnostics, isVerbose, sourceRoot, rulePriority, isAgentEnvironment = false, onboarding = {}, hyperlinks = false) => gen(function* () {
92410
+ const printDiagnostics = (diagnostics, isVerbose, sourceRoot, rulePriority, isAgentEnvironment = false, onboarding = {}, hyperlinks = false, frameSourceRoot = sourceRoot) => gen(function* () {
92367
92411
  const sectionPause = onboarding.sectionPause ?? void_;
92368
92412
  const animateCountUp = onboarding.animateCountUp ?? false;
92369
92413
  const resolveSourceRoot = typeof sourceRoot === "function" ? sourceRoot : () => sourceRoot;
92414
+ const resolveFrameSourceRoot = typeof frameSourceRoot === "function" ? frameSourceRoot : () => frameSourceRoot;
92370
92415
  let detailLines;
92371
92416
  let topErrorBlockOffsets = [];
92372
92417
  if (!isVerbose) {
92373
- const topErrors = buildTopErrorsSection(diagnostics, resolveSourceRoot, hyperlinks, rulePriority);
92418
+ const topErrors = buildTopErrorsSection(diagnostics, resolveSourceRoot, resolveFrameSourceRoot, hyperlinks, rulePriority);
92374
92419
  detailLines = topErrors.lines;
92375
92420
  topErrorBlockOffsets = topErrors.blockOffsets;
92376
92421
  } else detailLines = buildSortedRuleGroups(diagnostics, rulePriority).flatMap(([ruleKey, ruleDiagnostics]) => {
92377
- return [...buildRuleDetailBlock(ruleKey, ruleDiagnostics, resolveSourceRoot, true, isAgentEnvironment, hyperlinks), ""];
92422
+ return [...buildRuleDetailBlock(ruleKey, ruleDiagnostics, resolveSourceRoot, resolveFrameSourceRoot, true, isAgentEnvironment, hyperlinks), ""];
92378
92423
  });
92379
92424
  const overflowLine = isVerbose ? void 0 : buildOverflowSummaryLine(diagnostics, rulePriority);
92380
92425
  const categoryTallies = buildCategoryDiagnosticGroups(diagnostics, rulePriority).map(buildCategoryTally);
@@ -93638,26 +93683,26 @@ const finalizeAndRender = (input) => gen(function* () {
93638
93683
  //#region src/cli/utils/get-staged-files.ts
93639
93684
  const stagedFilesLayer = StagedFiles.layerNode.pipe(provide$2(Git.layerNode));
93640
93685
  const getStagedSourceFiles = async (directory) => {
93686
+ let stagedSourceFiles;
93641
93687
  try {
93642
- return [...await runPromise(gen(function* () {
93688
+ stagedSourceFiles = await runPromise(gen(function* () {
93643
93689
  return yield* (yield* StagedFiles).discoverSourceFiles(directory);
93644
- }).pipe(provide(stagedFilesLayer)))];
93690
+ }).pipe(provide(stagedFilesLayer)));
93645
93691
  } catch (error) {
93646
93692
  cliLogger.warn(`Failed to discover staged files: ${messageFromUnknown(error)}`);
93647
93693
  return [];
93648
93694
  }
93695
+ if (stagedSourceFiles === null) throw new CliInputError("Could not read the git index to list staged files. Check that git can read the index in this directory.");
93696
+ return [...stagedSourceFiles];
93649
93697
  };
93650
- const materializeStagedFiles = async (directory, stagedFiles, tempDirectory) => {
93698
+ const materializeStagedFiles = async (input) => {
93651
93699
  const snapshot = await runPromise(gen(function* () {
93652
- return yield* (yield* StagedFiles).materialize({
93653
- directory,
93654
- stagedFiles,
93655
- tempDirectory
93656
- });
93700
+ return yield* (yield* StagedFiles).materialize(input);
93657
93701
  }).pipe(provide(stagedFilesLayer)));
93658
93702
  return {
93659
93703
  tempDirectory: snapshot.tempDirectory,
93660
93704
  stagedFiles: [...snapshot.stagedFiles],
93705
+ unmaterializedFiles: [...snapshot.unmaterializedFiles],
93661
93706
  cleanup: snapshot.cleanup
93662
93707
  };
93663
93708
  };
@@ -95531,13 +95576,18 @@ const printMultiProjectSummary = (input) => gen(function* () {
95531
95576
  const scoreDiagnostics = new Set(filterScansForSurface(completedScans, "score"));
95532
95577
  const displayedScoreDiagnostics = displayDiagnostics.filter((diagnostic) => scoreDiagnostics.has(diagnostic));
95533
95578
  const projectRootByDiagnostic = /* @__PURE__ */ new WeakMap();
95534
- for (const scan of completedScans) for (const diagnostic of scan.result.diagnostics) projectRootByDiagnostic.set(diagnostic, scan.result.project.rootDirectory);
95579
+ const frameSourceRootByDiagnostic = /* @__PURE__ */ new WeakMap();
95580
+ for (const scan of completedScans) for (const diagnostic of scan.result.diagnostics) {
95581
+ projectRootByDiagnostic.set(diagnostic, scan.result.project.rootDirectory);
95582
+ frameSourceRootByDiagnostic.set(diagnostic, scan.frameSourceRoot ?? scan.result.project.rootDirectory);
95583
+ }
95535
95584
  const resolveDiagnosticSourceRoot = (diagnostic) => projectRootByDiagnostic.get(diagnostic) ?? "";
95585
+ const resolveDiagnosticFrameSourceRoot = (diagnostic) => frameSourceRootByDiagnostic.get(diagnostic) ?? resolveDiagnosticSourceRoot(diagnostic);
95536
95586
  const totalScannedFileCount = countUniqueScannedFiles(completedScans.map((scan) => scan.result));
95537
95587
  yield* log(`${highlighter.success("✔")} Scanned ${totalScannedFileCount} ${totalScannedFileCount === 1 ? "file" : "files"} in ${formatElapsedTime(totalElapsedMilliseconds)}`);
95538
95588
  if (displayDiagnostics.length > 0) {
95539
95589
  yield* log("");
95540
- yield* printDiagnostics(displayDiagnostics, verbose, resolveDiagnosticSourceRoot, buildRulePriorityMap(completedScans.map((scan) => scan.result.score)), isCodingAgentEnvironment(), { animateCountUp: animateRender }, shouldRenderHyperlinks(process.stdout));
95590
+ yield* printDiagnostics(displayDiagnostics, verbose, resolveDiagnosticSourceRoot, buildRulePriorityMap(completedScans.map((scan) => scan.result.score)), isCodingAgentEnvironment(), { animateCountUp: animateRender }, shouldRenderHyperlinks(process.stdout), resolveDiagnosticFrameSourceRoot);
95541
95591
  }
95542
95592
  const lowestScoredScan = findLowestScoredScan(completedScans);
95543
95593
  const aggregateScore = lowestScoredScan?.result.score ?? null;
@@ -95925,6 +95975,45 @@ const runExplain = async (fileLineArgument, context) => {
95925
95975
  }
95926
95976
  };
95927
95977
  //#endregion
95978
+ //#region src/cli/utils/run-project-scan-batch.ts
95979
+ /**
95980
+ * Run one scan per project through the same bounded pool as
95981
+ * `diagnose({ projects })`, with the batch spinner and its progress counter.
95982
+ *
95983
+ * Pool members must not toggle the module-level spinner-silent flag themselves —
95984
+ * overlapping save/restore pairs would race — so the batch owns that toggle once
95985
+ * around the whole run.
95986
+ *
95987
+ * A `scanProject` that returns `null` drops the project from the results: diff
95988
+ * mode skips projects with no changed source.
95989
+ */
95990
+ const runProjectScanBatch = async (input) => {
95991
+ const startTime = performance$1.now();
95992
+ const projectCount = input.projects.length;
95993
+ const isMultiProject = projectCount > 1;
95994
+ const batchSpinner = isMultiProject && !input.isQuiet ? spinner(`Scanning ${projectCount} projects…`).start() : null;
95995
+ const ownsBatchSpinnerSilence = isMultiProject && input.isSilent;
95996
+ const wasSpinnerSilent = isSpinnerSilent();
95997
+ if (ownsBatchSpinnerSilence) setSpinnerSilent(true);
95998
+ let finishedProjectCount = 0;
95999
+ let scanOutcomes;
96000
+ try {
96001
+ scanOutcomes = await mapWithConcurrency(input.projects, isMultiProject ? 4 : 1, async (project) => {
96002
+ const scanOutcome = await input.scanProject(project);
96003
+ finishedProjectCount += 1;
96004
+ batchSpinner?.update(`Scanning ${projectCount} projects… (${finishedProjectCount}/${projectCount})`);
96005
+ return scanOutcome;
96006
+ });
96007
+ } finally {
96008
+ if (ownsBatchSpinnerSilence) setSpinnerSilent(wasSpinnerSilent);
96009
+ batchSpinner?.stop();
96010
+ }
96011
+ return {
96012
+ completedScans: scanOutcomes.filter((scanOutcome) => scanOutcome !== null),
96013
+ elapsedMilliseconds: performance$1.now() - startTime
96014
+ };
96015
+ };
96016
+ //#endregion
95928
96017
  //#region src/cli/utils/project-manifest-changed.ts
95929
96018
  const PACKAGE_JSON = "package.json";
95930
96019
  /**
@@ -95941,6 +96030,29 @@ const projectManifestChanged = (rootDirectory, projectDirectory, diffInfo) => {
95941
96030
  return diffInfo.changedFiles.some((filePath) => toForwardSlashes(filePath) === manifestPath);
95942
96031
  };
95943
96032
  //#endregion
96033
+ //#region src/cli/utils/select-staged-projects.ts
96034
+ /**
96035
+ * Appended to the reason a `--staged` run could not scan per package. Shared so
96036
+ * both fallback sites word it identically.
96037
+ */
96038
+ const STAGED_PROJECT_FALLBACK_HINT = "Scanning the staged files at the scan root instead — fix the `projects` entries in your config to scan per package.";
96039
+ /**
96040
+ * Resolve the project directories a `--staged` scan owns. The interactive picker
96041
+ * is always skipped: a commit hook has nobody to answer it.
96042
+ */
96043
+ const selectStagedProjects = async (input) => {
96044
+ if (input.projectFlag) return selectProjects(input.rootDirectory, input.projectFlag, true, void 0);
96045
+ if (input.configProjects === void 0) return [input.rootDirectory];
96046
+ try {
96047
+ return await selectProjects(input.rootDirectory, void 0, true, input.configProjects);
96048
+ } catch (error) {
96049
+ if (!(error instanceof CliInputError)) throw error;
96050
+ cliLogger.warn(`${error.message} ${STAGED_PROJECT_FALLBACK_HINT}`);
96051
+ cliLogger.break();
96052
+ return [input.rootDirectory];
96053
+ }
96054
+ };
96055
+ //#endregion
95944
96056
  //#region src/cli/utils/should-fail-scan-gate.ts
95945
96057
  const shouldFailScanGate = (input) => {
95946
96058
  if (input.blockingLevel === "none") return false;
@@ -96162,8 +96274,13 @@ const inspectAction = async (directory, flags, invocationCommand = "inspect") =>
96162
96274
  json: flags.json
96163
96275
  });
96164
96276
  if (flags.staged) {
96165
- const stagedFiles = await getStagedSourceFiles(resolvedDirectory);
96166
- if (stagedFiles.length === 0) {
96277
+ const configProjectsApply = (userConfig?.projects ?? []).some((projectName) => projectName.trim().length > 0) && scanTarget.requestedDirectory === scanTarget.configSourceDirectory;
96278
+ const projectDirectories = await selectStagedProjects({
96279
+ rootDirectory: resolvedDirectory,
96280
+ projectFlag: flags.project,
96281
+ configProjects: configProjectsApply ? userConfig?.projects : void 0
96282
+ });
96283
+ const reportNothingToScan = (input) => {
96167
96284
  if (isJsonMode) writeJsonReport(buildJsonReport({
96168
96285
  version: VERSION,
96169
96286
  directory: resolvedDirectory,
@@ -96172,71 +96289,197 @@ const inspectAction = async (directory, flags, invocationCommand = "inspect") =>
96172
96289
  scans: [],
96173
96290
  totalElapsedMilliseconds: performance$1.now() - startTime
96174
96291
  }));
96175
- else if (!isScoreOnly) cliLogger.dim("No staged source files found.");
96292
+ else if (!isScoreOnly) if (input.severity === "warn") {
96293
+ cliLogger.warn(input.reason);
96294
+ cliLogger.break();
96295
+ } else cliLogger.dim(input.reason);
96296
+ };
96297
+ const rootStagedFiles = await getStagedSourceFiles(resolvedDirectory);
96298
+ if (rootStagedFiles.length === 0) {
96299
+ reportNothingToScan({ reason: "No staged source files found." });
96176
96300
  return;
96177
96301
  }
96178
- if (!isQuiet) {
96179
- cliLogger.log(`Scanning ${highlighter.info(`${stagedFiles.length}`)} staged files...`);
96302
+ const buildProjectScanContext = async (projectDirectory) => {
96303
+ const projectScanTarget = projectDirectory === resolvedDirectory ? scanTarget : await resolveScanTarget(projectDirectory, { allowAmbiguous: true });
96304
+ const scanDirectory = projectScanTarget.resolvedDirectory;
96305
+ const treeRelativeDirectory = resolveProjectRelativeDirectory(resolvedDirectory, scanDirectory);
96306
+ if (treeRelativeDirectory === null) return null;
96307
+ return {
96308
+ projectDirectory,
96309
+ scanDirectory,
96310
+ treeRelativeDirectory,
96311
+ projectConfig: projectDirectory === resolvedDirectory ? userConfig : mergeReactDoctorConfigs(userConfig, projectScanTarget.userConfig ?? void 0),
96312
+ projectConfigSourceDirectory: projectScanTarget.userConfig?.plugins === void 0 ? scanTarget.configSourceDirectory : projectScanTarget.configSourceDirectory
96313
+ };
96314
+ };
96315
+ const projectScanContexts = [];
96316
+ const seenScanDirectories = /* @__PURE__ */ new Set();
96317
+ for (const projectDirectory of projectDirectories) {
96318
+ const projectScanContext = await buildProjectScanContext(projectDirectory);
96319
+ if (projectScanContext === null) {
96320
+ if (flags.project) throw new CliInputError(`Project "${toRelativePath(projectDirectory, resolvedDirectory)}" is outside ${resolvedDirectory}, so it holds none of the staged files. Run --staged from a directory that contains the project.`);
96321
+ continue;
96322
+ }
96323
+ if (seenScanDirectories.has(projectScanContext.scanDirectory)) continue;
96324
+ seenScanDirectories.add(projectScanContext.scanDirectory);
96325
+ projectScanContexts.push(projectScanContext);
96326
+ }
96327
+ if (projectScanContexts.length === 0) {
96328
+ cliLogger.warn(`No configured project is inside ${resolvedDirectory}. ${STAGED_PROJECT_FALLBACK_HINT}`);
96180
96329
  cliLogger.break();
96330
+ const rootScanContext = await buildProjectScanContext(resolvedDirectory);
96331
+ if (rootScanContext !== null) projectScanContexts.push(rootScanContext);
96181
96332
  }
96182
- const tempDirectory = NFS.mkdtempSync(Path.join(tmpdir(), STAGED_FILES_TEMP_DIR_PREFIX));
96183
- const snapshot = await materializeStagedFiles(resolvedDirectory, stagedFiles, tempDirectory).catch((error) => {
96184
- NFS.rmSync(tempDirectory, {
96185
- recursive: true,
96186
- force: true
96333
+ const contextsByLongestPathFirst = [...projectScanContexts].sort((left, right) => right.treeRelativeDirectory.length - left.treeRelativeDirectory.length);
96334
+ const ownedStagedFiles = /* @__PURE__ */ new Map();
96335
+ for (const stagedFile of rootStagedFiles) {
96336
+ const owner = contextsByLongestPathFirst.find((context) => context.treeRelativeDirectory.length === 0 || stagedFile.startsWith(`${context.treeRelativeDirectory}/`));
96337
+ if (owner === void 0) continue;
96338
+ const ownedFiles = ownedStagedFiles.get(owner.scanDirectory);
96339
+ if (ownedFiles === void 0) ownedStagedFiles.set(owner.scanDirectory, [stagedFile]);
96340
+ else ownedFiles.push(stagedFile);
96341
+ }
96342
+ const stagedProjectScans = [];
96343
+ for (const projectScanContext of projectScanContexts) {
96344
+ const projectStagedFiles = ownedStagedFiles.get(projectScanContext.scanDirectory);
96345
+ if (projectStagedFiles === void 0) continue;
96346
+ stagedProjectScans.push({
96347
+ ...projectScanContext,
96348
+ stagedFiles: projectStagedFiles
96187
96349
  });
96188
- throw error;
96189
- });
96350
+ }
96351
+ if (stagedProjectScans.length === 0) {
96352
+ reportNothingToScan({ reason: "No staged source files in the selected projects." });
96353
+ return;
96354
+ }
96355
+ const selectedStagedFiles = stagedProjectScans.flatMap((projectScan) => projectScan.stagedFiles);
96356
+ const stagedFileCount = selectedStagedFiles.length;
96357
+ const unselectedStagedFileCount = rootStagedFiles.length - stagedFileCount;
96358
+ if (!isQuiet) {
96359
+ cliLogger.log(`Scanning ${highlighter.info(`${stagedFileCount}`)} staged files...`);
96360
+ if (unselectedStagedFileCount > 0) cliLogger.dim(`${unselectedStagedFileCount} more staged file${unselectedStagedFileCount === 1 ? "" : "s"} outside the selected projects.`);
96361
+ cliLogger.break();
96362
+ }
96190
96363
  const stagedWantsLines = resolveScope(flags, userConfig).scope === "lines";
96191
96364
  const stagedLineRanges = stagedWantsLines ? await getChangedLineRanges({
96192
96365
  directory: resolvedDirectory,
96193
96366
  cached: true,
96194
- files: snapshot.stagedFiles
96367
+ files: selectedStagedFiles
96195
96368
  }) : null;
96196
96369
  if (stagedWantsLines && stagedLineRanges === null && !isQuiet) {
96197
96370
  cliLogger.warn("Could not determine staged changed lines; reporting all issues in staged files.");
96198
96371
  cliLogger.break();
96199
96372
  }
96200
- try {
96201
- const scanResult = await inspect(snapshot.tempDirectory, {
96373
+ if (stagedProjectScans.some((projectScan) => projectScan.treeRelativeDirectory.length > 0)) recordCount(METRIC.stagedPerProject, 1, { projectCount: stagedProjectScans.length });
96374
+ const tempDirectory = NFS.mkdtempSync(Path.join(tmpdir(), STAGED_FILES_TEMP_DIR_PREFIX));
96375
+ const configSubdirectories = /* @__PURE__ */ new Set();
96376
+ for (const projectScan of stagedProjectScans) {
96377
+ configSubdirectories.add(projectScan.treeRelativeDirectory);
96378
+ const projectRelativeDirectory = resolveProjectRelativeDirectory(resolvedDirectory, projectScan.projectDirectory);
96379
+ if (projectRelativeDirectory !== null) configSubdirectories.add(projectRelativeDirectory);
96380
+ }
96381
+ const snapshot = await materializeStagedFiles({
96382
+ directory: resolvedDirectory,
96383
+ stagedFiles: selectedStagedFiles,
96384
+ tempDirectory,
96385
+ configSubdirectories: [...configSubdirectories]
96386
+ }).catch((error) => {
96387
+ NFS.rmSync(tempDirectory, {
96388
+ recursive: true,
96389
+ force: true
96390
+ });
96391
+ throw error;
96392
+ });
96393
+ const materializedStagedFiles = new Set(snapshot.stagedFiles);
96394
+ const stagedProjectRuns = stagedProjectScans.map((projectScan) => ({
96395
+ projectScan,
96396
+ includePaths: resolveProjectSourceFilePaths(resolvedDirectory, projectScan.scanDirectory, projectScan.stagedFiles.filter((stagedFile) => materializedStagedFiles.has(stagedFile)))
96397
+ })).filter((projectRun) => projectRun.includePaths.length > 0);
96398
+ const isMultiProject = stagedProjectRuns.length > 1;
96399
+ if (stagedProjectRuns.length === 0) {
96400
+ snapshot.cleanup();
96401
+ reportNothingToScan({
96402
+ reason: `Could not read any of the ${stagedFileCount} staged file${stagedFileCount === 1 ? "" : "s"} out of the index, so nothing was scanned. An unusually large staged file is the usual cause.`,
96403
+ severity: "warn"
96404
+ });
96405
+ return;
96406
+ }
96407
+ if (snapshot.unmaterializedFiles.length > 0 && !isQuiet) {
96408
+ const stagedFileLabel = `staged file${stagedFileCount === 1 ? "" : "s"}`;
96409
+ cliLogger.warn(`Skipped ${snapshot.unmaterializedFiles.length} of ${stagedFileCount} ${stagedFileLabel}; they could not be snapshotted from the index.`);
96410
+ cliLogger.break();
96411
+ }
96412
+ const scanStagedProject = async (projectRun) => {
96413
+ const { projectScan, includePaths } = projectRun;
96414
+ const projectTempDirectory = Path.join(snapshot.tempDirectory, projectScan.treeRelativeDirectory);
96415
+ const scanResult = await inspect(projectTempDirectory, {
96202
96416
  ...scanOptions,
96203
96417
  deadlineEpochMs: scanDeadlineEpochMs,
96204
- includePaths: snapshot.stagedFiles,
96205
- configOverride: userConfig,
96206
- configSourceDirectory: scanTarget.configSourceDirectory ?? void 0,
96207
- changedLineRanges: stagedLineRanges ?? void 0
96418
+ includePaths: [...includePaths],
96419
+ configOverride: projectScan.projectConfig,
96420
+ configSourceDirectory: projectScan.projectConfigSourceDirectory ?? void 0,
96421
+ changedLineRanges: stagedLineRanges === null ? void 0 : resolveProjectChangedLineRanges(resolvedDirectory, projectScan.scanDirectory, stagedLineRanges),
96422
+ suppressRendering: isMultiProject,
96423
+ concurrentScan: isMultiProject
96208
96424
  });
96209
96425
  const remappedDiagnostics = scanResult.diagnostics.map((diagnostic) => ({
96210
96426
  ...diagnostic,
96211
- filePath: Path.isAbsolute(diagnostic.filePath) ? diagnostic.filePath.replaceAll(snapshot.tempDirectory, () => resolvedDirectory) : diagnostic.filePath
96427
+ filePath: Path.isAbsolute(diagnostic.filePath) ? diagnostic.filePath.replaceAll(projectTempDirectory, () => projectScan.scanDirectory) : diagnostic.filePath
96212
96428
  }));
96213
- finalizeScans({
96214
- completedScans: [{
96215
- directory: resolvedDirectory,
96216
- result: {
96217
- ...scanResult,
96218
- diagnostics: remappedDiagnostics,
96219
- project: {
96220
- ...scanResult.project,
96221
- rootDirectory: resolvedDirectory
96222
- }
96223
- },
96224
- config: userConfig
96225
- }],
96226
- mode: "staged",
96227
- diff: null,
96228
- baselineIntended: false,
96229
- isJsonMode,
96230
- isScoreOnly,
96231
- flags,
96232
- categoryFilters,
96233
- userConfig,
96234
- resolvedDirectory,
96235
- startTime
96236
- });
96429
+ return {
96430
+ directory: projectScan.scanDirectory,
96431
+ result: {
96432
+ ...scanResult,
96433
+ diagnostics: remappedDiagnostics,
96434
+ project: {
96435
+ ...scanResult.project,
96436
+ rootDirectory: projectScan.scanDirectory
96437
+ }
96438
+ },
96439
+ config: projectScan.projectConfig,
96440
+ frameSourceRoot: projectTempDirectory
96441
+ };
96442
+ };
96443
+ const stagedBatch = await runProjectScanBatch({
96444
+ projects: stagedProjectRuns,
96445
+ isQuiet,
96446
+ isSilent: scanOptions.silent === true,
96447
+ scanProject: scanStagedProject
96448
+ }).catch((error) => {
96449
+ snapshot.cleanup();
96450
+ throw error;
96451
+ });
96452
+ const completedScans = stagedBatch.completedScans;
96453
+ try {
96454
+ if (!isQuiet && isMultiProject) {
96455
+ const showShareLink = !isShareOptedOut(completedScans, scanOptions.noScore) && !scanOptions.isCi;
96456
+ await runPromise(printMultiProjectSummary({
96457
+ completedScans,
96458
+ categoryFilters,
96459
+ verbose: Boolean(flags.verbose),
96460
+ outputDirectory: flags.outputDir,
96461
+ isOffline: !showShareLink,
96462
+ projectName: Path.basename(resolvedDirectory),
96463
+ totalElapsedMilliseconds: stagedBatch.elapsedMilliseconds
96464
+ }));
96465
+ }
96466
+ if (flags.outputDir && isMultiProject && isQuiet) await runPromise(printDiagnosticsDump(filterDiagnosticsByCategories(filterScansForSurface(completedScans, scanOptions.outputSurface ?? "cli"), categoryFilters), flags.outputDir, false, "stderr"));
96237
96467
  } finally {
96238
96468
  snapshot.cleanup();
96239
96469
  }
96470
+ finalizeScans({
96471
+ completedScans,
96472
+ mode: "staged",
96473
+ diff: null,
96474
+ baselineIntended: false,
96475
+ isJsonMode,
96476
+ isScoreOnly,
96477
+ flags,
96478
+ categoryFilters,
96479
+ userConfig,
96480
+ resolvedDirectory,
96481
+ startTime
96482
+ });
96240
96483
  return;
96241
96484
  }
96242
96485
  const projectDirectories = await selectProjects(resolvedDirectory, flags.project, skipPrompts, userConfig?.projects);
@@ -96301,7 +96544,6 @@ const inspectAction = async (directory, flags, invocationCommand = "inspect") =>
96301
96544
  }
96302
96545
  cliLogger.break();
96303
96546
  }
96304
- const completedScans = [];
96305
96547
  const isMultiProject = projectDirectories.length > 1;
96306
96548
  const scanProject = async (projectDirectory) => {
96307
96549
  const projectScanTarget = projectDirectory === resolvedDirectory ? scanTarget : await resolveScanTarget(projectDirectory, { allowAmbiguous: true });
@@ -96352,29 +96594,13 @@ const inspectAction = async (directory, flags, invocationCommand = "inspect") =>
96352
96594
  config: projectConfig
96353
96595
  };
96354
96596
  };
96355
- const scanLoopStartTime = performance$1.now();
96356
- const projectCount = projectDirectories.length;
96357
- const batchSpinner = isMultiProject && !isQuiet ? spinner(`Scanning ${projectCount} projects…`).start() : null;
96358
- const ownsBatchSpinnerSilence = isMultiProject && scanOptions.silent === true;
96359
- const wasSpinnerSilent = isSpinnerSilent();
96360
- if (ownsBatchSpinnerSilence) setSpinnerSilent(true);
96361
- let finishedProjectCount = 0;
96362
- let scanOutcomes;
96363
- try {
96364
- scanOutcomes = await mapWithConcurrency(projectDirectories, isMultiProject ? 4 : 1, async (projectDirectory) => {
96365
- const scanOutcome = await scanProject(projectDirectory);
96366
- finishedProjectCount += 1;
96367
- batchSpinner?.update(`Scanning ${projectCount} projects… (${finishedProjectCount}/${projectCount})`);
96368
- return scanOutcome;
96369
- });
96370
- } finally {
96371
- if (ownsBatchSpinnerSilence) setSpinnerSilent(wasSpinnerSilent);
96372
- batchSpinner?.stop();
96373
- }
96374
- for (const scanOutcome of scanOutcomes) {
96375
- if (scanOutcome === null) continue;
96376
- completedScans.push(scanOutcome);
96377
- }
96597
+ const projectBatch = await runProjectScanBatch({
96598
+ projects: projectDirectories,
96599
+ isQuiet,
96600
+ isSilent: scanOptions.silent === true,
96601
+ scanProject
96602
+ });
96603
+ const completedScans = projectBatch.completedScans;
96378
96604
  if (!isQuiet && isMultiProject && completedScans.length > 0) {
96379
96605
  const showShareLink = !isShareOptedOut(completedScans, scanOptions.noScore) && !scanOptions.isCi;
96380
96606
  await runPromise(printMultiProjectSummary({
@@ -96384,7 +96610,7 @@ const inspectAction = async (directory, flags, invocationCommand = "inspect") =>
96384
96610
  outputDirectory: flags.outputDir,
96385
96611
  isOffline: !showShareLink,
96386
96612
  projectName: Path.basename(resolvedDirectory),
96387
- totalElapsedMilliseconds: performance$1.now() - scanLoopStartTime
96613
+ totalElapsedMilliseconds: projectBatch.elapsedMilliseconds
96388
96614
  }));
96389
96615
  }
96390
96616
  const selectedSurfaceDiagnostics = filterDiagnosticsByCategories(filterScansForSurface(completedScans, scanOptions.outputSurface ?? "cli"), categoryFilters);
@@ -97530,7 +97756,7 @@ ${highlighter.dim("Learn more:")}
97530
97756
  ${highlighter.info(CI_URL)}
97531
97757
  `;
97532
97758
  const collectCategoryOption = (value, previousValues) => [...previousValues ?? [], value];
97533
- const program = new Command().name("react-doctor").description("Diagnose React codebase health").version(VERSION, "-v, --version", "display the version number").argument("[directory]", "project directory to scan", ".").option("--lint", "enable linting").option("--no-lint", "skip linting").option("--dead-code", "enable dead-code analysis (default)").option("--no-dead-code", "skip dead-code analysis (unused files / exports / dependencies, circular imports)").option("--supply-chain", "enable the dependency supply-chain scan (default)").option("--no-supply-chain", "skip the dependency supply-chain scan (Socket.dev dependency health checks)").option("--verbose", "show every rule and per-file details (default shows top 3 rules)").option("--debug", "force a Sentry trace and print its id at the end (paste it into a bug report)").option("--output-dir <dir>", "directory for the full diagnostics dump (default: a temp folder)").option("--score", "output only the score").option("--json", "output a single structured JSON report (suppresses other output)").option("--json-compact", "with --json, emit compact JSON (no indentation)").option("--json-out <path>", "with --json, write the report to a file instead of stdout").option("-y, --yes", "skip prompts, scan all workspace projects").option("--no-parallel", "lint serially with one worker (default: parallel across CPU cores; set the worker count with REACT_DOCTOR_PARALLEL)").option("--project <name>", "select projects: workspace names or directory paths (comma-separated for multiple); overrides the `projects` config field").option("--scope <value>", "how much supported JS/TS and inline HTML script source to scan/report: full (default), files, changed (only new issues vs base), or lines (issues whose source spans touch changed lines)").option("--base <ref>", "base git ref for files/changed/lines scope (auto-detected when omitted)").option("--include-untracked", "with --scope files/changed/lines, also scan ordinary untracked files (respects .gitignore)").addOption(new Option("--diff [base]", "[deprecated] alias for --scope changed (pass `false` to force a full scan)").hideHelp()).addOption(new Option("--changed-files-from <file>", "scan source files listed in a newline-delimited changed-files file").hideHelp()).option("--no-score", "skip the score API, the share URL, and crash reporting").addOption(new Option("--category <category>", "only show diagnostics in a category (repeatable; e.g. Security)").argParser(collectCategoryOption)).option("--no-telemetry", "alias for --no-score (skip the score API, share URL, and crash reporting)").option("--staged", "scan only staged (git index) files for pre-commit hooks").option("--max-duration <seconds>", "scan time budget for the whole run, shared across workspace projects: past it, remaining lint batches and dead-code are skipped and partial results are reported (skipped files are listed in the JSON report)").option("--blocking <level>", "severity that fails CI: error (default), warning, or none (advisory)").addOption(new Option("--fail-on <level>", "[deprecated] alias for --blocking <level>").hideHelp()).option("--no-respect-inline-disables", "audit mode: neutralize inline lint suppressions before scanning").option("--warnings", "show warning-severity diagnostics (default)").option("--no-warnings", "hide warning-severity diagnostics (errors only)").option("--color", "force colored output").option("--no-color", "disable colored output (also honors NO_COLOR)").addHelpText("after", renderRootHelpEpilog);
97759
+ const program = new Command().name("react-doctor").description("Diagnose React codebase health").version(VERSION, "-v, --version", "display the version number").argument("[directory]", "project directory to scan", ".").option("--lint", "enable linting").option("--no-lint", "skip linting").option("--dead-code", "enable dead-code analysis (default)").option("--no-dead-code", "skip dead-code analysis (unused files / exports / dependencies, circular imports)").option("--supply-chain", "enable the dependency supply-chain scan (default)").option("--no-supply-chain", "skip the dependency supply-chain scan (Socket.dev dependency health checks)").option("--verbose", "show every rule and per-file details (default shows top 3 rules)").option("--debug", "force a Sentry trace and print its id at the end (paste it into a bug report)").option("--output-dir <dir>", "directory for the full diagnostics dump (default: a temp folder)").option("--score", "output only the score").option("--json", "output a single structured JSON report (suppresses other output)").option("--json-compact", "with --json, emit compact JSON (no indentation)").option("--json-out <path>", "with --json, write the report to a file instead of stdout").option("-y, --yes", "skip prompts, scan all workspace projects").option("--no-parallel", "lint serially with one worker (default: parallel across CPU cores; set the worker count with REACT_DOCTOR_PARALLEL)").option("--project <name>", "select projects: workspace names or directory paths (comma-separated for multiple); overrides the `projects` config field").option("--scope <value>", "how much supported JS/TS and inline HTML script source to scan/report: full (default), files, changed (only new issues vs base), or lines (issues whose source spans touch changed lines)").option("--base <ref>", "base git ref for files/changed/lines scope (auto-detected when omitted)").option("--include-untracked", "with --scope files/changed/lines, also scan ordinary untracked files (respects .gitignore)").addOption(new Option("--diff [base]", "[deprecated] alias for --scope changed (pass `false` to force a full scan)").hideHelp()).addOption(new Option("--changed-files-from <file>", "scan source files listed in a newline-delimited changed-files file").hideHelp()).option("--no-score", "skip the score API, the share URL, and crash reporting").addOption(new Option("--category <category>", "only show diagnostics in a category (repeatable; e.g. Security)").argParser(collectCategoryOption)).option("--no-telemetry", "alias for --no-score (skip the score API, share URL, and crash reporting)").option("--staged", "scan only staged (git index) files for pre-commit hooks (honors --project and config `projects`; exits 0 when nothing is staged)").option("--max-duration <seconds>", "scan time budget for the whole run, shared across workspace projects: past it, remaining lint batches and dead-code are skipped and partial results are reported (skipped files are listed in the JSON report)").option("--blocking <level>", "severity that fails CI: error (default), warning, or none (advisory)").addOption(new Option("--fail-on <level>", "[deprecated] alias for --blocking <level>").hideHelp()).option("--no-respect-inline-disables", "audit mode: neutralize inline lint suppressions before scanning").option("--warnings", "show warning-severity diagnostics (default)").option("--no-warnings", "hide warning-severity diagnostics (errors only)").option("--color", "force colored output").option("--no-color", "disable colored output (also honors NO_COLOR)").addHelpText("after", renderRootHelpEpilog);
97534
97760
  program.action(inspectAction);
97535
97761
  program.command("design [directory]").description("Run only the focused UI design diagnostics").addHelpText("after", renderDesignHelpEpilog).action((directory, _options, command) => designAction(directory ?? ".", command.optsWithGlobals()));
97536
97762
  program.command("why <location>").description("Explain why a rule fired (or why a suppression didn't apply) at a file:line").option("--project <name>", "select projects: workspace names or directory paths (comma-separated for multiple)").option("-c, --cwd <cwd>", "working directory", process.cwd()).option("--color", "force colored output").option("--no-color", "disable colored output (also honors NO_COLOR)").action((location, options) => whyAction(location, options));
@@ -97613,4 +97839,4 @@ Promise.resolve().then(() => assertNoRemovedFlags(process.argv)).then(() => prog
97613
97839
  export { toForwardSlashes as A, getCategoryImpact as B, buildRulePriorityMap as C, resolveBlockingLevel as D, formatLearnMoreLine as E, TUI_REPORT_DETAIL_WIDTH_FRACTION as F, runPromise as G, mapWithConcurrency as H, isCiEnvironment as I, succeed$3 as J, sync as K, DIAGNOSTIC_CATEGORY_BUCKETS as L, isUnicodeSupported as M, METRIC as N, isReactDoctorWorkflowInstalled as O, TUI_REPORT_COMPACT_MAX_ROWS as P, Progress as R, hasLintHardFailure as S, formatFixRecipeLine as T, mergeReactDoctorConfigs as U, highlighter as V, resolveScanTarget as W, canAnimateOnboarding as _, resolveCliInspectOptions as a, buildCodeFrame as b, detectLaunchableAgents as c, launchCliAgent as d, isShareOptedOut as f, doctorFace as g, easeOutCubic as h, selectProjects as i, findNearestPackageDirectory as j, recordCount as k, CLI_AGENT_BINARIES as l, printFooter as m, shouldFailScanGate as n, countUniqueScannedFiles as o, inspect as p, void_ as q, discoverWorkspacePackages as r, setUpGitHubActions as s, exitGracefully as t, copyToClipboard as u, computeProjectedScore as v, buildSortedRuleGroups as w, buildNoScoreMessage as x, formatElapsedTime as y, Reporter as z };
97614
97840
 
97615
97841
  //# sourceMappingURL=cli.js.map
97616
- //# debugId=3ced4c14-18cb-5b0c-811c-3448b5c2f139
97842
+ //# debugId=2a10a6ef-ec19-5a23-88d5-c2e4d5e30eec