react-doctor 0.7.7-dev.71e6da6 → 0.7.7-dev.79abd71

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (4) hide show
  1. package/dist/cli.js +427 -69
  2. package/dist/index.js +424 -66
  3. package/dist/lsp.js +424 -66
  4. package/package.json +4 -4
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]="50d648e9-77ea-5f0f-87a2-6d2a2461613e")}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]="3de13f8e-2b7e-5141-a62e-0cce9208b352")}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";
@@ -40410,18 +40410,45 @@ const checkReactServerComponentsAdvisory = (scanDirectory, project) => {
40410
40410
  }
40411
40411
  return diagnostics;
40412
40412
  };
40413
- const REDUCED_MOTION_GREP_PATTERN = "prefers-reduced-motion|useReducedMotion|MotionConfig|reducedMotion";
40414
- const REDUCED_MOTION_PATTERN = new RegExp(REDUCED_MOTION_GREP_PATTERN);
40415
- const REDUCED_MOTION_FILE_EXTENSIONS = new Set([
40413
+ const getTypescriptScriptKind = (fileName) => {
40414
+ if (fileName.endsWith(".tsx")) return ts.ScriptKind.TSX;
40415
+ if (fileName.endsWith(".jsx")) return ts.ScriptKind.JSX;
40416
+ if (fileName.endsWith(".ts")) return ts.ScriptKind.TS;
40417
+ return ts.ScriptKind.JS;
40418
+ };
40419
+ const unwrapTypescriptExpression = (expression) => {
40420
+ let currentExpression = expression;
40421
+ while (ts.isParenthesizedExpression(currentExpression) || ts.isAsExpression(currentExpression) || ts.isSatisfiesExpression(currentExpression) || ts.isNonNullExpression(currentExpression) || ts.isTypeAssertionExpression(currentExpression)) currentExpression = currentExpression.expression;
40422
+ return currentExpression;
40423
+ };
40424
+ const EMPTY_MOTION_EXPRESSION_EVIDENCE = {
40425
+ isAnimationFunction: false,
40426
+ isMotionComponent: false,
40427
+ isMotionComponentFactory: false,
40428
+ isMotionComponentNamespace: false,
40429
+ isMotionConfig: false,
40430
+ isMotionNamespace: false,
40431
+ isReducedMotionHook: false
40432
+ };
40433
+ const MOTION_COMPONENT_FACTORY_EXPORT_NAMES = new Set(["m", "motion"]);
40434
+ const MOTION_COMPONENT_NAMESPACE_EXPORT_NAMES = new Set(["Reorder"]);
40435
+ const MOTION_COMPONENT_NAMESPACE_MEMBER_NAMES = new Set(["Item"]);
40436
+ const MOTION_ANIMATION_FUNCTION_EXPORT_NAMES = new Set(["animate"]);
40437
+ const REDUCED_MOTION_HOOK_EXPORT_NAME = "useReducedMotion";
40438
+ const MOTION_CONFIG_EXPORT_NAME = "MotionConfig";
40439
+ const REDUCED_MOTION_PROP_NAME = "reducedMotion";
40440
+ const REDUCED_MOTION_CONFIG_VALUES = new Set(["always", "user"]);
40441
+ const SCRIPT_MODULE_EXTENSIONS = [
40416
40442
  ".ts",
40417
40443
  ".tsx",
40418
40444
  ".js",
40419
- ".jsx",
40420
- ".css",
40421
- ".scss"
40422
- ]);
40423
- const REDUCED_MOTION_FILE_GLOBS = [...REDUCED_MOTION_FILE_EXTENSIONS].map((extension) => `*${extension}`);
40424
- const GIT_GREP_NO_MATCH_STATUS = 1;
40445
+ ".jsx"
40446
+ ];
40447
+ const SCRIPT_FILE_EXTENSIONS = new Set(SCRIPT_MODULE_EXTENSIONS);
40448
+ const STYLE_FILE_EXTENSIONS = new Set([".css", ".scss"]);
40449
+ const MOTION_SOURCE_PREFILTER = /framer-motion|["']motion(?:\/[A-Za-z0-9_./-]+)?["']|MotionConfig|useReducedMotion/;
40450
+ const REDUCED_MOTION_MEDIA_QUERY_PATTERN = /@media\s+([^{}]*\(\s*prefers-reduced-motion\s*:\s*reduce\s*\)[^{}]*)\{/gi;
40451
+ const CSS_DECLARATION_PATTERN = /(?:^|[;{])\s*[-_A-Za-z][-_A-Za-z0-9]*\s*:/;
40425
40452
  const MISSING_REDUCED_MOTION_DIAGNOSTIC = {
40426
40453
  filePath: "package.json",
40427
40454
  plugin: "react-doctor",
@@ -40433,52 +40460,406 @@ const MISSING_REDUCED_MOTION_DIAGNOSTIC = {
40433
40460
  column: 0,
40434
40461
  category: "Accessibility"
40435
40462
  };
40436
- const hasReducedMotionHandlingViaFilesystem = (rootDirectory) => {
40463
+ const isMotionModuleSource = (moduleSource) => {
40464
+ for (const packageName of MOTION_LIBRARY_PACKAGES) if (moduleSource === packageName || moduleSource.startsWith(`${packageName}/`)) return true;
40465
+ return false;
40466
+ };
40467
+ const classifyMotionExport = (exportName) => ({
40468
+ isAnimationFunction: MOTION_ANIMATION_FUNCTION_EXPORT_NAMES.has(exportName),
40469
+ isMotionComponent: false,
40470
+ isMotionComponentFactory: MOTION_COMPONENT_FACTORY_EXPORT_NAMES.has(exportName),
40471
+ isMotionComponentNamespace: MOTION_COMPONENT_NAMESPACE_EXPORT_NAMES.has(exportName),
40472
+ isMotionConfig: exportName === MOTION_CONFIG_EXPORT_NAME,
40473
+ isMotionNamespace: false,
40474
+ isReducedMotionHook: exportName === REDUCED_MOTION_HOOK_EXPORT_NAME
40475
+ });
40476
+ const getImportModuleSource = (node) => {
40477
+ let currentNode = node;
40478
+ while (currentNode) {
40479
+ if (ts.isImportDeclaration(currentNode) && ts.isStringLiteral(currentNode.moduleSpecifier)) return currentNode.moduleSpecifier.text;
40480
+ currentNode = currentNode.parent;
40481
+ }
40482
+ return null;
40483
+ };
40484
+ const getImportedBindingEvidence = (declaration, typeChecker, program, visitedSymbols) => {
40485
+ const moduleSource = getImportModuleSource(declaration);
40486
+ if (!moduleSource) return null;
40487
+ if (isMotionModuleSource(moduleSource) && ts.isNamespaceImport(declaration)) return {
40488
+ ...EMPTY_MOTION_EXPRESSION_EVIDENCE,
40489
+ isMotionNamespace: true
40490
+ };
40491
+ if (isMotionModuleSource(moduleSource) && ts.isImportSpecifier(declaration)) {
40492
+ if (declaration.isTypeOnly) return null;
40493
+ return classifyMotionExport(declaration.propertyName?.text ?? declaration.name.text);
40494
+ }
40495
+ if (isMotionModuleSource(moduleSource) && ts.isImportClause(declaration)) {
40496
+ if (declaration.isTypeOnly) return null;
40497
+ return EMPTY_MOTION_EXPRESSION_EVIDENCE;
40498
+ }
40499
+ if (ts.isImportSpecifier(declaration) && !declaration.isTypeOnly || ts.isImportClause(declaration) && !declaration.isTypeOnly && declaration.name) {
40500
+ const importedName = ts.isImportSpecifier(declaration) ? declaration.propertyName?.text ?? declaration.name.text : "default";
40501
+ const moduleSourceFile = getLocalModuleSourceFile(moduleSource, declaration.getSourceFile().fileName, program);
40502
+ if (!moduleSourceFile) return null;
40503
+ return resolveModuleExportEvidence(moduleSourceFile, importedName, typeChecker, program, visitedSymbols);
40504
+ }
40505
+ return null;
40506
+ };
40507
+ const getLocalModuleSourceFile = (moduleSource, importingFileName, program) => {
40508
+ if (!moduleSource.startsWith(".")) return null;
40509
+ const moduleBasePath = Path.resolve(Path.dirname(importingFileName), moduleSource);
40510
+ const moduleBaseExtension = Path.extname(moduleBasePath);
40511
+ const typescriptModuleBasePath = moduleBaseExtension === ".js" || moduleBaseExtension === ".jsx" ? moduleBasePath.slice(0, -moduleBaseExtension.length) : null;
40512
+ const candidates = [
40513
+ moduleBasePath,
40514
+ ...typescriptModuleBasePath ? [
40515
+ `${typescriptModuleBasePath}.ts`,
40516
+ `${typescriptModuleBasePath}.tsx`,
40517
+ Path.join(typescriptModuleBasePath, "index.ts"),
40518
+ Path.join(typescriptModuleBasePath, "index.tsx")
40519
+ ] : [],
40520
+ ...SCRIPT_MODULE_EXTENSIONS.map((extension) => `${moduleBasePath}${extension}`),
40521
+ ...SCRIPT_MODULE_EXTENSIONS.map((extension) => Path.join(moduleBasePath, `index${extension}`))
40522
+ ];
40523
+ for (const candidate of candidates) {
40524
+ const sourceFile = program.getSourceFile(candidate);
40525
+ if (sourceFile) return sourceFile;
40526
+ }
40527
+ return null;
40528
+ };
40529
+ const resolveModuleExportEvidence = (sourceFile, exportName, typeChecker, program, visitedSymbols) => {
40530
+ const moduleSymbol = typeChecker.getSymbolAtLocation(sourceFile);
40531
+ if (moduleSymbol && visitedSymbols.has(moduleSymbol)) return EMPTY_MOTION_EXPRESSION_EVIDENCE;
40532
+ const nextVisitedSymbols = new Set(visitedSymbols);
40533
+ if (moduleSymbol) nextVisitedSymbols.add(moduleSymbol);
40534
+ for (const statement of sourceFile.statements) {
40535
+ if (exportName === "default" && ts.isExportAssignment(statement) && !statement.isExportEquals) {
40536
+ const evidence = resolveMotionExpressionEvidence(statement.expression, typeChecker, program, nextVisitedSymbols);
40537
+ if (hasMotionExpressionEvidence(evidence)) return evidence;
40538
+ continue;
40539
+ }
40540
+ if (!ts.isExportDeclaration(statement) || !statement.moduleSpecifier) continue;
40541
+ if (statement.isTypeOnly) continue;
40542
+ if (!ts.isStringLiteral(statement.moduleSpecifier)) continue;
40543
+ const moduleSource = statement.moduleSpecifier.text;
40544
+ if (!statement.exportClause) {
40545
+ if (isMotionModuleSource(moduleSource)) {
40546
+ const evidence = classifyMotionExport(exportName);
40547
+ if (hasMotionExpressionEvidence(evidence)) return evidence;
40548
+ continue;
40549
+ }
40550
+ const moduleSourceFile = getLocalModuleSourceFile(moduleSource, sourceFile.fileName, program);
40551
+ if (moduleSourceFile) {
40552
+ const evidence = resolveModuleExportEvidence(moduleSourceFile, exportName, typeChecker, program, nextVisitedSymbols);
40553
+ if (hasMotionExpressionEvidence(evidence)) return evidence;
40554
+ }
40555
+ continue;
40556
+ }
40557
+ if (!ts.isNamedExports(statement.exportClause)) continue;
40558
+ for (const exportSpecifier of statement.exportClause.elements) {
40559
+ if (exportSpecifier.name.text !== exportName || exportSpecifier.isTypeOnly) continue;
40560
+ const sourceExportName = exportSpecifier.propertyName?.text ?? exportSpecifier.name.text;
40561
+ if (isMotionModuleSource(moduleSource)) return classifyMotionExport(sourceExportName);
40562
+ const moduleSourceFile = getLocalModuleSourceFile(moduleSource, sourceFile.fileName, program);
40563
+ if (!moduleSourceFile) continue;
40564
+ return resolveModuleExportEvidence(moduleSourceFile, sourceExportName, typeChecker, program, nextVisitedSymbols);
40565
+ }
40566
+ }
40567
+ if (!moduleSymbol) return EMPTY_MOTION_EXPRESSION_EVIDENCE;
40568
+ const exportedSymbol = typeChecker.getExportsOfModule(moduleSymbol).find((candidateSymbol) => candidateSymbol.name === exportName);
40569
+ if (!exportedSymbol || nextVisitedSymbols.has(exportedSymbol)) return EMPTY_MOTION_EXPRESSION_EVIDENCE;
40570
+ nextVisitedSymbols.add(exportedSymbol);
40571
+ let resolvedEvidence = EMPTY_MOTION_EXPRESSION_EVIDENCE;
40572
+ for (const declaration of exportedSymbol.declarations ?? []) {
40573
+ if (ts.isExportSpecifier(declaration)) {
40574
+ const targetSymbol = typeChecker.getExportSpecifierLocalTargetSymbol(declaration);
40575
+ for (const targetDeclaration of targetSymbol?.declarations ?? []) {
40576
+ const importedEvidence = getImportedBindingEvidence(targetDeclaration, typeChecker, program, nextVisitedSymbols);
40577
+ if (importedEvidence) resolvedEvidence = mergeMotionExpressionEvidence(resolvedEvidence, importedEvidence);
40578
+ if (ts.isVariableDeclaration(targetDeclaration) && targetDeclaration.initializer) resolvedEvidence = mergeMotionExpressionEvidence(resolvedEvidence, resolveMotionExpressionEvidence(targetDeclaration.initializer, typeChecker, program, nextVisitedSymbols));
40579
+ }
40580
+ }
40581
+ if (ts.isVariableDeclaration(declaration) && declaration.initializer) resolvedEvidence = mergeMotionExpressionEvidence(resolvedEvidence, resolveMotionExpressionEvidence(declaration.initializer, typeChecker, program, nextVisitedSymbols));
40582
+ }
40583
+ return resolvedEvidence;
40584
+ };
40585
+ const mergeMotionExpressionEvidence = (leftEvidence, rightEvidence) => ({
40586
+ isAnimationFunction: leftEvidence.isAnimationFunction || rightEvidence.isAnimationFunction,
40587
+ isMotionComponent: leftEvidence.isMotionComponent || rightEvidence.isMotionComponent,
40588
+ isMotionComponentFactory: leftEvidence.isMotionComponentFactory || rightEvidence.isMotionComponentFactory,
40589
+ isMotionComponentNamespace: leftEvidence.isMotionComponentNamespace || rightEvidence.isMotionComponentNamespace,
40590
+ isMotionConfig: leftEvidence.isMotionConfig || rightEvidence.isMotionConfig,
40591
+ isMotionNamespace: leftEvidence.isMotionNamespace || rightEvidence.isMotionNamespace,
40592
+ isReducedMotionHook: leftEvidence.isReducedMotionHook || rightEvidence.isReducedMotionHook
40593
+ });
40594
+ const hasMotionExpressionEvidence = (evidence) => evidence.isAnimationFunction || evidence.isMotionComponent || evidence.isMotionComponentFactory || evidence.isMotionComponentNamespace || evidence.isMotionConfig || evidence.isMotionNamespace || evidence.isReducedMotionHook;
40595
+ const classifyMotionMember = (receiverEvidence, memberName) => {
40596
+ if (receiverEvidence.isMotionNamespace) return classifyMotionExport(memberName);
40597
+ if (receiverEvidence.isMotionComponentFactory) return {
40598
+ ...EMPTY_MOTION_EXPRESSION_EVIDENCE,
40599
+ isMotionComponent: true
40600
+ };
40601
+ if (receiverEvidence.isMotionComponentNamespace && MOTION_COMPONENT_NAMESPACE_MEMBER_NAMES.has(memberName)) return {
40602
+ ...EMPTY_MOTION_EXPRESSION_EVIDENCE,
40603
+ isMotionComponent: true
40604
+ };
40605
+ return EMPTY_MOTION_EXPRESSION_EVIDENCE;
40606
+ };
40607
+ const resolveMotionExpressionEvidence = (expression, typeChecker, program, visitedSymbols = /* @__PURE__ */ new Set()) => {
40608
+ const unwrappedExpression = unwrapTypescriptExpression(expression);
40609
+ if (ts.isIdentifier(unwrappedExpression)) {
40610
+ const symbol = typeChecker.getSymbolAtLocation(unwrappedExpression);
40611
+ if (!symbol || visitedSymbols.has(symbol)) return EMPTY_MOTION_EXPRESSION_EVIDENCE;
40612
+ const nextVisitedSymbols = new Set(visitedSymbols);
40613
+ nextVisitedSymbols.add(symbol);
40614
+ let resolvedEvidence = EMPTY_MOTION_EXPRESSION_EVIDENCE;
40615
+ for (const declaration of symbol.declarations ?? []) {
40616
+ const importedEvidence = getImportedBindingEvidence(declaration, typeChecker, program, nextVisitedSymbols);
40617
+ if (importedEvidence) {
40618
+ resolvedEvidence = mergeMotionExpressionEvidence(resolvedEvidence, importedEvidence);
40619
+ continue;
40620
+ }
40621
+ if (ts.isVariableDeclaration(declaration) && declaration.initializer) resolvedEvidence = mergeMotionExpressionEvidence(resolvedEvidence, resolveMotionExpressionEvidence(declaration.initializer, typeChecker, program, nextVisitedSymbols));
40622
+ if (ts.isBindingElement(declaration) && ts.isObjectBindingPattern(declaration.parent) && ts.isVariableDeclaration(declaration.parent.parent) && declaration.parent.parent.initializer) {
40623
+ const propertyName = declaration.propertyName ?? declaration.name;
40624
+ if (ts.isIdentifier(propertyName) || ts.isStringLiteral(propertyName)) {
40625
+ const receiverEvidence = resolveMotionExpressionEvidence(declaration.parent.parent.initializer, typeChecker, program, nextVisitedSymbols);
40626
+ resolvedEvidence = mergeMotionExpressionEvidence(resolvedEvidence, classifyMotionMember(receiverEvidence, propertyName.text));
40627
+ }
40628
+ }
40629
+ }
40630
+ return resolvedEvidence;
40631
+ }
40632
+ if (ts.isPropertyAccessExpression(unwrappedExpression)) return classifyMotionMember(resolveMotionExpressionEvidence(unwrappedExpression.expression, typeChecker, program, visitedSymbols), unwrappedExpression.name.text);
40633
+ if (ts.isElementAccessExpression(unwrappedExpression) && unwrappedExpression.argumentExpression && (ts.isStringLiteral(unwrappedExpression.argumentExpression) || ts.isNoSubstitutionTemplateLiteral(unwrappedExpression.argumentExpression))) return classifyMotionMember(resolveMotionExpressionEvidence(unwrappedExpression.expression, typeChecker, program, visitedSymbols), unwrappedExpression.argumentExpression.text);
40634
+ if (ts.isCallExpression(unwrappedExpression)) {
40635
+ if (ts.isIdentifier(unwrappedExpression.expression) && unwrappedExpression.expression.text === "require" && !typeChecker.getSymbolAtLocation(unwrappedExpression.expression) && unwrappedExpression.arguments.length === 1 && ts.isStringLiteral(unwrappedExpression.arguments[0]) && isMotionModuleSource(unwrappedExpression.arguments[0].text)) return {
40636
+ ...EMPTY_MOTION_EXPRESSION_EVIDENCE,
40637
+ isMotionNamespace: true
40638
+ };
40639
+ if (ts.isPropertyAccessExpression(unwrappedExpression.expression) && unwrappedExpression.expression.name.text === "bind") {
40640
+ const boundReceiverEvidence = resolveMotionExpressionEvidence(unwrappedExpression.expression.expression, typeChecker, program, visitedSymbols);
40641
+ if (boundReceiverEvidence.isAnimationFunction) return boundReceiverEvidence;
40642
+ }
40643
+ const calleeEvidence = resolveMotionExpressionEvidence(unwrappedExpression.expression, typeChecker, program, visitedSymbols);
40644
+ if (calleeEvidence.isMotionComponentFactory) return {
40645
+ ...EMPTY_MOTION_EXPRESSION_EVIDENCE,
40646
+ isMotionComponent: true
40647
+ };
40648
+ return calleeEvidence;
40649
+ }
40650
+ return EMPTY_MOTION_EXPRESSION_EVIDENCE;
40651
+ };
40652
+ const getStaticStringExpressionValue = (expression, typeChecker, visitedSymbols = /* @__PURE__ */ new Set()) => {
40653
+ const unwrappedExpression = unwrapTypescriptExpression(expression);
40654
+ if (ts.isStringLiteral(unwrappedExpression) || ts.isNoSubstitutionTemplateLiteral(unwrappedExpression)) return unwrappedExpression.text;
40655
+ if (!ts.isIdentifier(unwrappedExpression)) return null;
40656
+ const symbol = typeChecker.getSymbolAtLocation(unwrappedExpression);
40657
+ if (!symbol || visitedSymbols.has(symbol)) return null;
40658
+ const nextVisitedSymbols = new Set(visitedSymbols);
40659
+ nextVisitedSymbols.add(symbol);
40660
+ for (const declaration of symbol.declarations ?? []) {
40661
+ if (!ts.isVariableDeclaration(declaration) || !declaration.initializer) continue;
40662
+ if (!ts.isVariableDeclarationList(declaration.parent)) continue;
40663
+ if (!(declaration.parent.flags & ts.NodeFlags.Const)) continue;
40664
+ const value = getStaticStringExpressionValue(declaration.initializer, typeChecker, nextVisitedSymbols);
40665
+ if (value !== null) return value;
40666
+ }
40667
+ return null;
40668
+ };
40669
+ const getStaticJsxAttributeValue = (attribute, typeChecker) => {
40670
+ if (!attribute.initializer) return null;
40671
+ if (ts.isStringLiteral(attribute.initializer)) return attribute.initializer.text;
40672
+ if (!ts.isJsxExpression(attribute.initializer) || !attribute.initializer.expression) return null;
40673
+ return getStaticStringExpressionValue(attribute.initializer.expression, typeChecker);
40674
+ };
40675
+ const isExpressionValueConsumed = (expression) => {
40676
+ let currentExpression = expression;
40677
+ while (ts.isParenthesizedExpression(currentExpression.parent) || ts.isAsExpression(currentExpression.parent) || ts.isSatisfiesExpression(currentExpression.parent) || ts.isNonNullExpression(currentExpression.parent) || ts.isTypeAssertionExpression(currentExpression.parent)) currentExpression = currentExpression.parent;
40678
+ return !ts.isExpressionStatement(currentExpression.parent) && !ts.isVoidExpression(currentExpression.parent);
40679
+ };
40680
+ const jsxElementHasReducedMotionConfiguration = (attributes, typeChecker) => attributes.properties.some((attribute) => ts.isJsxAttribute(attribute) && ts.isIdentifier(attribute.name) && attribute.name.text === REDUCED_MOTION_PROP_NAME && REDUCED_MOTION_CONFIG_VALUES.has(getStaticJsxAttributeValue(attribute, typeChecker) ?? ""));
40681
+ const collectScriptMotionEvidence = (sourceFile, typeChecker, program) => {
40682
+ const evidence = {
40683
+ hasMotionUse: false,
40684
+ hasReducedMotionHandling: false
40685
+ };
40686
+ const visitNode = (node) => {
40687
+ if (ts.isCallExpression(node)) {
40688
+ const calleeEvidence = resolveMotionExpressionEvidence(node.expression, typeChecker, program);
40689
+ const isAnimationFunctionCallViaCallOrApply = (ts.isPropertyAccessExpression(node.expression) && (node.expression.name.text === "call" || node.expression.name.text === "apply") || ts.isElementAccessExpression(node.expression) && node.expression.argumentExpression && (ts.isStringLiteral(node.expression.argumentExpression) || ts.isNoSubstitutionTemplateLiteral(node.expression.argumentExpression)) && (node.expression.argumentExpression.text === "call" || node.expression.argumentExpression.text === "apply")) && resolveMotionExpressionEvidence(node.expression.expression, typeChecker, program).isAnimationFunction;
40690
+ if (calleeEvidence.isAnimationFunction || isAnimationFunctionCallViaCallOrApply) evidence.hasMotionUse = true;
40691
+ if (calleeEvidence.isReducedMotionHook && isExpressionValueConsumed(node)) evidence.hasReducedMotionHandling = true;
40692
+ }
40693
+ if (ts.isJsxOpeningElement(node) || ts.isJsxSelfClosingElement(node)) {
40694
+ const tagEvidence = ts.isJsxNamespacedName(node.tagName) ? EMPTY_MOTION_EXPRESSION_EVIDENCE : resolveMotionExpressionEvidence(node.tagName, typeChecker, program);
40695
+ if (tagEvidence.isMotionComponent || tagEvidence.isMotionComponentFactory) evidence.hasMotionUse = true;
40696
+ if (tagEvidence.isMotionConfig && jsxElementHasReducedMotionConfiguration(node.attributes, typeChecker)) evidence.hasReducedMotionHandling = true;
40697
+ }
40698
+ ts.forEachChild(node, visitNode);
40699
+ };
40700
+ visitNode(sourceFile);
40701
+ return evidence;
40702
+ };
40703
+ const analyzeReducedMotionSources = (sources) => {
40704
+ if (!sources.some(({ sourceText }) => MOTION_SOURCE_PREFILTER.test(sourceText))) return {
40705
+ hasMotionUse: false,
40706
+ hasReducedMotionHandling: false
40707
+ };
40708
+ const compilerOptions = {
40709
+ allowJs: true,
40710
+ checkJs: false,
40711
+ jsx: ts.JsxEmit.Preserve,
40712
+ module: ts.ModuleKind.ESNext,
40713
+ moduleResolution: ts.ModuleResolutionKind.Bundler,
40714
+ noEmit: true,
40715
+ noLib: true,
40716
+ noResolve: true,
40717
+ skipLibCheck: true,
40718
+ target: ts.ScriptTarget.Latest
40719
+ };
40720
+ const sourceTextByFileName = new Map(sources.map(({ fileName: sourceFileName, sourceText: content }) => [Path.resolve(sourceFileName), content]));
40721
+ const compilerHost = ts.createCompilerHost(compilerOptions);
40722
+ const getDefaultSourceFile = compilerHost.getSourceFile.bind(compilerHost);
40723
+ compilerHost.getSourceFile = (requestedFileName, languageVersionOrOptions, onError, shouldCreateNewSourceFile) => sourceTextByFileName.has(Path.resolve(requestedFileName)) ? ts.createSourceFile(Path.resolve(requestedFileName), sourceTextByFileName.get(Path.resolve(requestedFileName)) ?? "", languageVersionOrOptions, true, getTypescriptScriptKind(requestedFileName)) : getDefaultSourceFile(requestedFileName, languageVersionOrOptions, onError, shouldCreateNewSourceFile);
40724
+ const program = ts.createProgram([...sourceTextByFileName.keys()], compilerOptions, compilerHost);
40725
+ const typeChecker = program.getTypeChecker();
40726
+ const evidence = {
40727
+ hasMotionUse: false,
40728
+ hasReducedMotionHandling: false
40729
+ };
40730
+ for (const sourceFileName of sourceTextByFileName.keys()) {
40731
+ const sourceFile = program.getSourceFile(sourceFileName);
40732
+ if (!sourceFile) continue;
40733
+ const fileEvidence = collectScriptMotionEvidence(sourceFile, typeChecker, program);
40734
+ evidence.hasMotionUse ||= fileEvidence.hasMotionUse;
40735
+ evidence.hasReducedMotionHandling ||= fileEvidence.hasReducedMotionHandling;
40736
+ }
40737
+ return evidence;
40738
+ };
40739
+ const removeCssCommentsAndStrings = (content) => {
40740
+ let sanitizedContent = "";
40741
+ let quote = null;
40742
+ let isInsideComment = false;
40743
+ let isInsideLineComment = false;
40744
+ for (let characterIndex = 0; characterIndex < content.length; characterIndex += 1) {
40745
+ const character = content[characterIndex];
40746
+ const nextCharacter = content[characterIndex + 1];
40747
+ if (isInsideLineComment) {
40748
+ sanitizedContent += character === "\n" ? "\n" : " ";
40749
+ if (character === "\n") isInsideLineComment = false;
40750
+ continue;
40751
+ }
40752
+ if (isInsideComment) {
40753
+ if (character === "*" && nextCharacter === "/") {
40754
+ sanitizedContent += " ";
40755
+ characterIndex += 1;
40756
+ isInsideComment = false;
40757
+ } else sanitizedContent += character === "\n" ? "\n" : " ";
40758
+ continue;
40759
+ }
40760
+ if (quote) {
40761
+ if (character === "\\") {
40762
+ sanitizedContent += " ";
40763
+ characterIndex += 1;
40764
+ continue;
40765
+ }
40766
+ if (character === quote) quote = null;
40767
+ sanitizedContent += character === "\n" ? "\n" : " ";
40768
+ continue;
40769
+ }
40770
+ if (character === "/" && nextCharacter === "*") {
40771
+ sanitizedContent += " ";
40772
+ characterIndex += 1;
40773
+ isInsideComment = true;
40774
+ continue;
40775
+ }
40776
+ if (character === "/" && nextCharacter === "/") {
40777
+ sanitizedContent += " ";
40778
+ characterIndex += 1;
40779
+ isInsideLineComment = true;
40780
+ continue;
40781
+ }
40782
+ if (character === "\"" || character === "'") {
40783
+ sanitizedContent += " ";
40784
+ quote = character;
40785
+ continue;
40786
+ }
40787
+ sanitizedContent += character;
40788
+ }
40789
+ return sanitizedContent;
40790
+ };
40791
+ const findMatchingClosingBrace = (content, openingBraceIndex) => {
40792
+ let depth = 0;
40793
+ for (let characterIndex = openingBraceIndex; characterIndex < content.length; characterIndex += 1) {
40794
+ if (content[characterIndex] === "{") depth += 1;
40795
+ if (content[characterIndex] !== "}") continue;
40796
+ depth -= 1;
40797
+ if (depth === 0) return characterIndex;
40798
+ }
40799
+ return -1;
40800
+ };
40801
+ const hasReducedMotionMediaQuery = (content) => {
40802
+ const sanitizedContent = removeCssCommentsAndStrings(content);
40803
+ REDUCED_MOTION_MEDIA_QUERY_PATTERN.lastIndex = 0;
40804
+ for (const match of sanitizedContent.matchAll(REDUCED_MOTION_MEDIA_QUERY_PATTERN)) {
40805
+ const mediaPrelude = match[1];
40806
+ if (/\bnot\b/i.test(mediaPrelude)) continue;
40807
+ const openingBraceIndex = (match.index ?? 0) + match[0].lastIndexOf("{");
40808
+ const closingBraceIndex = findMatchingClosingBrace(sanitizedContent, openingBraceIndex);
40809
+ if (closingBraceIndex === -1) continue;
40810
+ const body = sanitizedContent.slice(openingBraceIndex + 1, closingBraceIndex);
40811
+ if (CSS_DECLARATION_PATTERN.test(body)) return true;
40812
+ }
40813
+ return false;
40814
+ };
40815
+ const collectProjectMotionEvidence = (rootDirectory) => {
40816
+ const scriptSources = [];
40817
+ let hasReducedMotionHandling = false;
40437
40818
  for (const { absolutePath, name } of walkSourceTreeFiles(rootDirectory)) {
40438
- if (!REDUCED_MOTION_FILE_EXTENSIONS.has(Path.extname(name))) continue;
40819
+ const extension = Path.extname(name);
40820
+ if (!SCRIPT_FILE_EXTENSIONS.has(extension) && !STYLE_FILE_EXTENSIONS.has(extension)) continue;
40439
40821
  let content;
40440
40822
  try {
40441
40823
  content = NFS.readFileSync(absolutePath, "utf-8");
40442
40824
  } catch {
40443
40825
  continue;
40444
40826
  }
40445
- if (REDUCED_MOTION_PATTERN.test(content)) return true;
40827
+ if (STYLE_FILE_EXTENSIONS.has(extension)) {
40828
+ if (hasReducedMotionMediaQuery(content)) hasReducedMotionHandling = true;
40829
+ continue;
40830
+ }
40831
+ scriptSources.push({
40832
+ fileName: absolutePath,
40833
+ sourceText: content
40834
+ });
40446
40835
  }
40447
- return false;
40836
+ if (scriptSources.length === 0) return {
40837
+ hasMotionUse: false,
40838
+ hasReducedMotionHandling
40839
+ };
40840
+ const scriptEvidence = analyzeReducedMotionSources(scriptSources);
40841
+ const hasMotionUse = scriptEvidence.hasMotionUse;
40842
+ hasReducedMotionHandling ||= scriptEvidence.hasReducedMotionHandling;
40843
+ return {
40844
+ hasMotionUse,
40845
+ hasReducedMotionHandling
40846
+ };
40448
40847
  };
40449
40848
  const checkReducedMotion = (rootDirectory) => {
40450
40849
  const packageJsonPath = Path.join(rootDirectory, "package.json");
40451
40850
  if (!isFile(packageJsonPath)) return [];
40452
- let hasMotionLibrary = false;
40453
40851
  try {
40454
40852
  const packageJson = readPackageJson$1(packageJsonPath);
40455
40853
  const allDependencies = {
40456
40854
  ...packageJson.dependencies,
40457
40855
  ...packageJson.devDependencies
40458
40856
  };
40459
- hasMotionLibrary = Object.keys(allDependencies).some((packageName) => MOTION_LIBRARY_PACKAGES.has(packageName));
40857
+ if (!Object.keys(allDependencies).some((packageName) => MOTION_LIBRARY_PACKAGES.has(packageName))) return [];
40460
40858
  } catch {
40461
40859
  return [];
40462
40860
  }
40463
- if (!hasMotionLibrary) return [];
40464
- const result = spawnSync("git", [
40465
- "grep",
40466
- "--untracked",
40467
- "-lE",
40468
- REDUCED_MOTION_GREP_PATTERN,
40469
- "--",
40470
- ...REDUCED_MOTION_FILE_GLOBS
40471
- ], {
40472
- cwd: rootDirectory,
40473
- encoding: "utf-8",
40474
- stdio: [
40475
- "ignore",
40476
- "pipe",
40477
- "pipe"
40478
- ]
40479
- });
40480
- if (!(!result.error && (result.status === 0 || result.status === GIT_GREP_NO_MATCH_STATUS))) return hasReducedMotionHandlingViaFilesystem(rootDirectory) ? [] : [MISSING_REDUCED_MOTION_DIAGNOSTIC];
40481
- return result.status === 0 && result.stdout.split("\n").some((filePath) => filePath.length > 0 && !hasIgnoredPathSegment(filePath)) ? [] : [MISSING_REDUCED_MOTION_DIAGNOSTIC];
40861
+ const evidence = collectProjectMotionEvidence(rootDirectory);
40862
+ return evidence.hasMotionUse && !evidence.hasReducedMotionHandling ? [MISSING_REDUCED_MOTION_DIAGNOSTIC] : [];
40482
40863
  };
40483
40864
  const buildSecurityScanDiagnostic = (finding, entry, relativePath) => ({
40484
40865
  filePath: relativePath,
@@ -43097,23 +43478,12 @@ const REACT_USE_BINDING_RESOLUTION = {
43097
43478
  isReactUseBinding: true,
43098
43479
  isReactNamespaceBinding: false
43099
43480
  };
43100
- const getScriptKind$2 = (filename) => {
43101
- if (filename.endsWith(".tsx")) return ts.ScriptKind.TSX;
43102
- if (filename.endsWith(".jsx")) return ts.ScriptKind.JSX;
43103
- if (filename.endsWith(".ts")) return ts.ScriptKind.TS;
43104
- return ts.ScriptKind.JS;
43105
- };
43106
43481
  const getUtf16Offset$1 = (sourceText, utf8Offset) => Buffer.from(sourceText).subarray(0, utf8Offset).toString("utf8").length;
43107
- const unwrapExpression = (expression) => {
43108
- let currentExpression = expression;
43109
- while (ts.isParenthesizedExpression(currentExpression) || ts.isAsExpression(currentExpression) || ts.isSatisfiesExpression(currentExpression) || ts.isNonNullExpression(currentExpression) || ts.isTypeAssertionExpression(currentExpression)) currentExpression = currentExpression.expression;
43110
- return currentExpression;
43111
- };
43112
43482
  const getStaticPropertyName = (node) => {
43113
43483
  if (!node) return null;
43114
43484
  if (ts.isIdentifier(node) || ts.isStringLiteral(node) || ts.isNumericLiteral(node)) return node.text;
43115
43485
  if (ts.isComputedPropertyName(node)) {
43116
- const expression = unwrapExpression(node.expression);
43486
+ const expression = unwrapTypescriptExpression(node.expression);
43117
43487
  if (ts.isStringLiteral(expression) || ts.isNoSubstitutionTemplateLiteral(expression)) return expression.text;
43118
43488
  }
43119
43489
  return null;
@@ -43142,7 +43512,7 @@ const isReactUseObjectBindingElement = (bindingElement) => {
43142
43512
  return getStaticPropertyName(bindingElement.propertyName) === USE_IDENTIFIER;
43143
43513
  };
43144
43514
  const isReactRequireCall = (expression) => {
43145
- const unwrappedExpression = unwrapExpression(expression);
43515
+ const unwrappedExpression = unwrapTypescriptExpression(expression);
43146
43516
  return ts.isCallExpression(unwrappedExpression) && ts.isIdentifier(unwrappedExpression.expression) && unwrappedExpression.expression.text === REQUIRE_IDENTIFIER && unwrappedExpression.arguments.length === 1 && ts.isStringLiteral(unwrappedExpression.arguments[0]) && unwrappedExpression.arguments[0].text === REACT_MODULE_SOURCE;
43147
43517
  };
43148
43518
  const getModuleSource = (node) => {
@@ -43244,7 +43614,7 @@ const isIdentifierShadowedByLocalBinding = (identifier, sourceFile) => {
43244
43614
  return false;
43245
43615
  };
43246
43616
  const isReactNamespaceExpression = (expression, reactImportBindings, sourceFile, visitedDeclarations) => {
43247
- const unwrappedExpression = unwrapExpression(expression);
43617
+ const unwrappedExpression = unwrapTypescriptExpression(expression);
43248
43618
  if (isReactRequireCall(unwrappedExpression)) return true;
43249
43619
  if (!ts.isIdentifier(unwrappedExpression)) return false;
43250
43620
  if (reactImportBindings.namespaceNames.has(unwrappedExpression.text) && !isIdentifierShadowedByLocalBinding(unwrappedExpression, sourceFile)) return true;
@@ -43252,7 +43622,7 @@ const isReactNamespaceExpression = (expression, reactImportBindings, sourceFile,
43252
43622
  };
43253
43623
  const isReactUseExpression = (expression, reactImportBindings, sourceFile, visitedDeclarations) => {
43254
43624
  if (!expression) return false;
43255
- const unwrappedExpression = unwrapExpression(expression);
43625
+ const unwrappedExpression = unwrapTypescriptExpression(expression);
43256
43626
  if (ts.isIdentifier(unwrappedExpression)) {
43257
43627
  if (reactImportBindings.useImportNames.has(unwrappedExpression.text) && !isIdentifierShadowedByLocalBinding(unwrappedExpression, sourceFile)) return true;
43258
43628
  if (unwrappedExpression.text === USE_IDENTIFIER) return false;
@@ -43352,7 +43722,7 @@ const findUseCallIdentifier = (sourceFile, useOffset) => {
43352
43722
  return matchedIdentifier;
43353
43723
  };
43354
43724
  const resolveUseCallBinding = (sourceText, filename, utf8Offset) => {
43355
- const sourceFile = ts.createSourceFile(filename, sourceText, ts.ScriptTarget.Latest, true, getScriptKind$2(filename));
43725
+ const sourceFile = ts.createSourceFile(filename, sourceText, ts.ScriptTarget.Latest, true, getTypescriptScriptKind(filename));
43356
43726
  const useIdentifier = findUseCallIdentifier(sourceFile, getUtf16Offset$1(sourceText, utf8Offset));
43357
43727
  if (!useIdentifier) return null;
43358
43728
  return resolveIdentifierBinding(useIdentifier, collectReactImportBindings(sourceFile), sourceFile);
@@ -43388,12 +43758,6 @@ const WORKLET_ACCEPTING_CALLEES = new Set([
43388
43758
  "executeOnUIRuntimeSync"
43389
43759
  ]);
43390
43760
  const GESTURE_ROOT_IDENTIFIER = "Gesture";
43391
- const getScriptKind$1 = (filename) => {
43392
- if (filename.endsWith(".tsx")) return ts.ScriptKind.TSX;
43393
- if (filename.endsWith(".jsx")) return ts.ScriptKind.JSX;
43394
- if (filename.endsWith(".ts")) return ts.ScriptKind.TS;
43395
- return ts.ScriptKind.JS;
43396
- };
43397
43761
  const getUtf16Offset = (sourceText, utf8Offset) => Buffer.from(sourceText).subarray(0, utf8Offset).toString("utf8").length;
43398
43762
  const hasWorkletDirective = (node) => {
43399
43763
  if (!ts.isFunctionDeclaration(node) && !ts.isFunctionExpression(node) && !ts.isArrowFunction(node) && !ts.isMethodDeclaration(node)) return false;
@@ -43465,17 +43829,11 @@ const shouldSuppressCompilerFindingInWorklet = (diagnostic, project, rootDirecto
43465
43829
  } catch {
43466
43830
  return false;
43467
43831
  }
43468
- return isOffsetInsideWorklet(ts.createSourceFile(absolutePath, sourceText, ts.ScriptTarget.Latest, true, getScriptKind$1(absolutePath)), getUtf16Offset(sourceText, primaryLabel.span.offset));
43832
+ return isOffsetInsideWorklet(ts.createSourceFile(absolutePath, sourceText, ts.ScriptTarget.Latest, true, getTypescriptScriptKind(absolutePath)), getUtf16Offset(sourceText, primaryLabel.span.offset));
43469
43833
  };
43470
43834
  const MANUAL_MEMOIZATION_PLUGIN = "react-doctor";
43471
43835
  const MANUAL_MEMOIZATION_RULE = "react-compiler-no-manual-memoization";
43472
43836
  const REACT_COMPILER_PLUGIN = "react-hooks-js";
43473
- const getScriptKind = (filename) => {
43474
- if (filename.endsWith(".tsx")) return ts.ScriptKind.TSX;
43475
- if (filename.endsWith(".jsx")) return ts.ScriptKind.JSX;
43476
- if (filename.endsWith(".ts")) return ts.ScriptKind.TS;
43477
- return ts.ScriptKind.JS;
43478
- };
43479
43837
  const isManualMemoizationDiagnostic = (diagnostic) => diagnostic.plugin === MANUAL_MEMOIZATION_PLUGIN && diagnostic.rule === MANUAL_MEMOIZATION_RULE;
43480
43838
  const nodeLineRange = (sourceFile, node) => ({
43481
43839
  startLine: sourceFile.getLineAndCharacterOfPosition(node.getStart(sourceFile)).line + 1,
@@ -43537,7 +43895,7 @@ const suppressMemoizationInBailedOutFunctions = (diagnostics, rootDirectory) =>
43537
43895
  try {
43538
43896
  const buffer = NFS.readFileSync(absolutePath);
43539
43897
  parsed = {
43540
- sourceFile: ts.createSourceFile(absolutePath, buffer.toString("utf8"), ts.ScriptTarget.Latest, true, getScriptKind(absolutePath)),
43898
+ sourceFile: ts.createSourceFile(absolutePath, buffer.toString("utf8"), ts.ScriptTarget.Latest, true, getTypescriptScriptKind(absolutePath)),
43541
43899
  buffer
43542
43900
  };
43543
43901
  } catch {
@@ -46774,7 +47132,7 @@ const makeNoopConsole = () => ({
46774
47132
  });
46775
47133
  //#endregion
46776
47134
  //#region src/cli/utils/version.ts
46777
- const VERSION = "0.7.7-dev.71e6da6";
47135
+ const VERSION = "0.7.7-dev.79abd71";
46778
47136
  //#endregion
46779
47137
  //#region src/cli/utils/json-mode.ts
46780
47138
  let context = null;
@@ -47143,13 +47501,13 @@ const isDevVersion = (version) => version === "0.0.0" || version.includes("-");
47143
47501
  * uploads source-map artifacts under, so stack frames symbolicate. Honors the
47144
47502
  * standard `SENTRY_RELEASE` override.
47145
47503
  */
47146
- const resolveSentryRelease = () => process.env.SENTRY_RELEASE || `react-doctor@0.7.7-dev.71e6da6`;
47504
+ const resolveSentryRelease = () => process.env.SENTRY_RELEASE || `react-doctor@0.7.7-dev.79abd71`;
47147
47505
  /**
47148
47506
  * Deployment environment shown in Sentry's environment filter. Defaults to
47149
47507
  * `production` for tagged releases and `development` for dev/unbuilt versions,
47150
47508
  * overridable via the standard `SENTRY_ENVIRONMENT` env var.
47151
47509
  */
47152
- const resolveSentryEnvironment = () => process.env.SENTRY_ENVIRONMENT || (isDevVersion("0.7.7-dev.71e6da6") ? "development" : "production");
47510
+ const resolveSentryEnvironment = () => process.env.SENTRY_ENVIRONMENT || (isDevVersion("0.7.7-dev.79abd71") ? "development" : "production");
47153
47511
  /**
47154
47512
  * Performance-tracing sample rate in `[0, 1]`. Reads `SENTRY_TRACES_SAMPLE_RATE`
47155
47513
  * (set to `0` to disable tracing) and falls back to
@@ -65415,4 +65773,4 @@ Promise.resolve().then(() => assertNoRemovedFlags(process.argv)).then(() => prog
65415
65773
  export {};
65416
65774
 
65417
65775
  //# sourceMappingURL=cli.js.map
65418
- //# debugId=50d648e9-77ea-5f0f-87a2-6d2a2461613e
65776
+ //# debugId=3de13f8e-2b7e-5141-a62e-0cce9208b352