react-doctor 0.7.7-dev.fbc804e → 0.7.8-dev.4829841

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 +451 -78
  2. package/dist/index.js +436 -75
  3. package/dist/lsp.js +437 -76
  4. package/package.json +6 -6
package/dist/index.js CHANGED
@@ -1,8 +1,10 @@
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]="1872748b-acb2-5e9a-a748-43bfd023eb85")}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]="d7ac2526-002a-57de-91f1-7c670298fa89")}catch(e){}}();
3
3
  import { r as __toESM$1, t as __commonJSMin$1 } from "./chunk-N93fKeF6.js";
4
4
  import { createRequire } from "node:module";
5
5
  import reactDoctorPlugin, { ALL_REACT_DOCTOR_RULE_KEYS, CROSS_FILE_DEPENDENCY_COLLECTORS, CROSS_FILE_RULE_IDS, FRAMEWORK_SPECIFIC_RULE_KEYS, FRAMEWORK_TOKENS, MOTION_LIBRARY_PACKAGES, REACT_COMPILER_RULES, REACT_DOCTOR_RULES, classifySecurityScanFile, collectCrossFileDependencyProbes, resetManifestCaches, shouldReadSecurityScanContent } from "oxlint-plugin-react-doctor";
6
+ import * as Crypto from "node:crypto";
7
+ import crypto, { createHash } from "node:crypto";
6
8
  import * as NFS from "node:fs";
7
9
  import fs from "node:fs";
8
10
  import * as Path from "node:path";
@@ -16,8 +18,6 @@ import { parseJSON5 } from "confbox";
16
18
  import * as NodeUrl from "node:url";
17
19
  import { fileURLToPath } from "node:url";
18
20
  import { createJiti } from "jiti";
19
- import * as Crypto from "node:crypto";
20
- import crypto, { createHash } from "node:crypto";
21
21
  import { ANALYZED_MANIFEST_FILENAMES, DEFAULT_EXTENSIONS } from "deslop-js/analyzed-inputs";
22
22
  import { gzipSync } from "node:zlib";
23
23
  //#region ../../node_modules/.pnpm/effect@4.0.0-beta.70/node_modules/effect/dist/Pipeable.js
@@ -19336,12 +19336,13 @@ var JsonReportDiagnosticV3 = class extends Class("JsonReportDiagnosticV3")({
19336
19336
  fixGroupId: optional(String$1)
19337
19337
  }) {};
19338
19338
  /**
19339
- * Deterministic identity string for a diagnostic. Same diagnostic
19340
- * across two scans yields the same identity; lets baselines,
19341
- * suppression files, content-hash caches, and IDE "ignore this"
19342
- * actions all key off one shared shape.
19339
+ * Deterministic identity string for a retained diagnostic occurrence.
19340
+ * Severity and message distinguish content variants at one normalized site.
19343
19341
  */
19344
- const buildDiagnosticIdentity = (input) => `${input.filePath}::${input.line}:${input.column}::${input.plugin}/${input.rule}`;
19342
+ const buildDiagnosticIdentity = (input) => {
19343
+ const occurrenceDigest = createHash("sha256").update(JSON.stringify([input.severity, input.message])).digest("hex");
19344
+ return `${input.filePath}::${input.line}:${input.column}::${input.plugin}/${input.rule}::${occurrenceDigest}`;
19345
+ };
19345
19346
  const JsonReportMode = Literals([
19346
19347
  "full",
19347
19348
  "diff",
@@ -33039,7 +33040,7 @@ const SOCKET_FREE_USER_AGENT = "react-doctor-supply-chain";
33039
33040
  const FILE_LINT_CACHE_FILENAME = "file-lint-cache.json";
33040
33041
  const FILE_LINT_CACHE_MAX_FILE_COUNT = 5e4;
33041
33042
  const SIDECAR_LINT_CACHE_FILENAME = "sidecar-lint-cache.json";
33042
- const CORE_PACKAGE_VERSION = "0.7.7";
33043
+ const CORE_PACKAGE_VERSION = "0.7.8";
33043
33044
  const DEAD_CODE_CACHE_FILENAME = "dead-code-cache.json";
33044
33045
  const DEAD_CODE_SUMMARY_CACHE_FILENAME = "dead-code-summaries.json";
33045
33046
  const SUPPLY_CHAIN_PLUGIN = "socket";
@@ -37182,18 +37183,45 @@ const checkReactServerComponentsAdvisory = (scanDirectory, project) => {
37182
37183
  }
37183
37184
  return diagnostics;
37184
37185
  };
37185
- const REDUCED_MOTION_GREP_PATTERN = "prefers-reduced-motion|useReducedMotion|MotionConfig|reducedMotion";
37186
- const REDUCED_MOTION_PATTERN = new RegExp(REDUCED_MOTION_GREP_PATTERN);
37187
- const REDUCED_MOTION_FILE_EXTENSIONS = new Set([
37186
+ const getTypescriptScriptKind = (fileName) => {
37187
+ if (fileName.endsWith(".tsx")) return ts.ScriptKind.TSX;
37188
+ if (fileName.endsWith(".jsx")) return ts.ScriptKind.JSX;
37189
+ if (fileName.endsWith(".ts")) return ts.ScriptKind.TS;
37190
+ return ts.ScriptKind.JS;
37191
+ };
37192
+ const unwrapTypescriptExpression = (expression) => {
37193
+ let currentExpression = expression;
37194
+ while (ts.isParenthesizedExpression(currentExpression) || ts.isAsExpression(currentExpression) || ts.isSatisfiesExpression(currentExpression) || ts.isNonNullExpression(currentExpression) || ts.isTypeAssertionExpression(currentExpression)) currentExpression = currentExpression.expression;
37195
+ return currentExpression;
37196
+ };
37197
+ const EMPTY_MOTION_EXPRESSION_EVIDENCE = {
37198
+ isAnimationFunction: false,
37199
+ isMotionComponent: false,
37200
+ isMotionComponentFactory: false,
37201
+ isMotionComponentNamespace: false,
37202
+ isMotionConfig: false,
37203
+ isMotionNamespace: false,
37204
+ isReducedMotionHook: false
37205
+ };
37206
+ const MOTION_COMPONENT_FACTORY_EXPORT_NAMES = new Set(["m", "motion"]);
37207
+ const MOTION_COMPONENT_NAMESPACE_EXPORT_NAMES = new Set(["Reorder"]);
37208
+ const MOTION_COMPONENT_NAMESPACE_MEMBER_NAMES = new Set(["Item"]);
37209
+ const MOTION_ANIMATION_FUNCTION_EXPORT_NAMES = new Set(["animate"]);
37210
+ const REDUCED_MOTION_HOOK_EXPORT_NAME = "useReducedMotion";
37211
+ const MOTION_CONFIG_EXPORT_NAME = "MotionConfig";
37212
+ const REDUCED_MOTION_PROP_NAME = "reducedMotion";
37213
+ const REDUCED_MOTION_CONFIG_VALUES = new Set(["always", "user"]);
37214
+ const SCRIPT_MODULE_EXTENSIONS = [
37188
37215
  ".ts",
37189
37216
  ".tsx",
37190
37217
  ".js",
37191
- ".jsx",
37192
- ".css",
37193
- ".scss"
37194
- ]);
37195
- const REDUCED_MOTION_FILE_GLOBS = [...REDUCED_MOTION_FILE_EXTENSIONS].map((extension) => `*${extension}`);
37196
- const GIT_GREP_NO_MATCH_STATUS = 1;
37218
+ ".jsx"
37219
+ ];
37220
+ const SCRIPT_FILE_EXTENSIONS = new Set(SCRIPT_MODULE_EXTENSIONS);
37221
+ const STYLE_FILE_EXTENSIONS = new Set([".css", ".scss"]);
37222
+ const MOTION_SOURCE_PREFILTER = /framer-motion|["']motion(?:\/[A-Za-z0-9_./-]+)?["']|MotionConfig|useReducedMotion/;
37223
+ const REDUCED_MOTION_MEDIA_QUERY_PATTERN = /@media\s+([^{}]*\(\s*prefers-reduced-motion\s*:\s*reduce\s*\)[^{}]*)\{/gi;
37224
+ const CSS_DECLARATION_PATTERN = /(?:^|[;{])\s*[-_A-Za-z][-_A-Za-z0-9]*\s*:/;
37197
37225
  const MISSING_REDUCED_MOTION_DIAGNOSTIC = {
37198
37226
  filePath: "package.json",
37199
37227
  plugin: "react-doctor",
@@ -37205,52 +37233,406 @@ const MISSING_REDUCED_MOTION_DIAGNOSTIC = {
37205
37233
  column: 0,
37206
37234
  category: "Accessibility"
37207
37235
  };
37208
- const hasReducedMotionHandlingViaFilesystem = (rootDirectory) => {
37236
+ const isMotionModuleSource = (moduleSource) => {
37237
+ for (const packageName of MOTION_LIBRARY_PACKAGES) if (moduleSource === packageName || moduleSource.startsWith(`${packageName}/`)) return true;
37238
+ return false;
37239
+ };
37240
+ const classifyMotionExport = (exportName) => ({
37241
+ isAnimationFunction: MOTION_ANIMATION_FUNCTION_EXPORT_NAMES.has(exportName),
37242
+ isMotionComponent: false,
37243
+ isMotionComponentFactory: MOTION_COMPONENT_FACTORY_EXPORT_NAMES.has(exportName),
37244
+ isMotionComponentNamespace: MOTION_COMPONENT_NAMESPACE_EXPORT_NAMES.has(exportName),
37245
+ isMotionConfig: exportName === MOTION_CONFIG_EXPORT_NAME,
37246
+ isMotionNamespace: false,
37247
+ isReducedMotionHook: exportName === REDUCED_MOTION_HOOK_EXPORT_NAME
37248
+ });
37249
+ const getImportModuleSource = (node) => {
37250
+ let currentNode = node;
37251
+ while (currentNode) {
37252
+ if (ts.isImportDeclaration(currentNode) && ts.isStringLiteral(currentNode.moduleSpecifier)) return currentNode.moduleSpecifier.text;
37253
+ currentNode = currentNode.parent;
37254
+ }
37255
+ return null;
37256
+ };
37257
+ const getImportedBindingEvidence = (declaration, typeChecker, program, visitedSymbols) => {
37258
+ const moduleSource = getImportModuleSource(declaration);
37259
+ if (!moduleSource) return null;
37260
+ if (isMotionModuleSource(moduleSource) && ts.isNamespaceImport(declaration)) return {
37261
+ ...EMPTY_MOTION_EXPRESSION_EVIDENCE,
37262
+ isMotionNamespace: true
37263
+ };
37264
+ if (isMotionModuleSource(moduleSource) && ts.isImportSpecifier(declaration)) {
37265
+ if (declaration.isTypeOnly) return null;
37266
+ return classifyMotionExport(declaration.propertyName?.text ?? declaration.name.text);
37267
+ }
37268
+ if (isMotionModuleSource(moduleSource) && ts.isImportClause(declaration)) {
37269
+ if (declaration.isTypeOnly) return null;
37270
+ return EMPTY_MOTION_EXPRESSION_EVIDENCE;
37271
+ }
37272
+ if (ts.isImportSpecifier(declaration) && !declaration.isTypeOnly || ts.isImportClause(declaration) && !declaration.isTypeOnly && declaration.name) {
37273
+ const importedName = ts.isImportSpecifier(declaration) ? declaration.propertyName?.text ?? declaration.name.text : "default";
37274
+ const moduleSourceFile = getLocalModuleSourceFile(moduleSource, declaration.getSourceFile().fileName, program);
37275
+ if (!moduleSourceFile) return null;
37276
+ return resolveModuleExportEvidence(moduleSourceFile, importedName, typeChecker, program, visitedSymbols);
37277
+ }
37278
+ return null;
37279
+ };
37280
+ const getLocalModuleSourceFile = (moduleSource, importingFileName, program) => {
37281
+ if (!moduleSource.startsWith(".")) return null;
37282
+ const moduleBasePath = Path.resolve(Path.dirname(importingFileName), moduleSource);
37283
+ const moduleBaseExtension = Path.extname(moduleBasePath);
37284
+ const typescriptModuleBasePath = moduleBaseExtension === ".js" || moduleBaseExtension === ".jsx" ? moduleBasePath.slice(0, -moduleBaseExtension.length) : null;
37285
+ const candidates = [
37286
+ moduleBasePath,
37287
+ ...typescriptModuleBasePath ? [
37288
+ `${typescriptModuleBasePath}.ts`,
37289
+ `${typescriptModuleBasePath}.tsx`,
37290
+ Path.join(typescriptModuleBasePath, "index.ts"),
37291
+ Path.join(typescriptModuleBasePath, "index.tsx")
37292
+ ] : [],
37293
+ ...SCRIPT_MODULE_EXTENSIONS.map((extension) => `${moduleBasePath}${extension}`),
37294
+ ...SCRIPT_MODULE_EXTENSIONS.map((extension) => Path.join(moduleBasePath, `index${extension}`))
37295
+ ];
37296
+ for (const candidate of candidates) {
37297
+ const sourceFile = program.getSourceFile(candidate);
37298
+ if (sourceFile) return sourceFile;
37299
+ }
37300
+ return null;
37301
+ };
37302
+ const resolveModuleExportEvidence = (sourceFile, exportName, typeChecker, program, visitedSymbols) => {
37303
+ const moduleSymbol = typeChecker.getSymbolAtLocation(sourceFile);
37304
+ if (moduleSymbol && visitedSymbols.has(moduleSymbol)) return EMPTY_MOTION_EXPRESSION_EVIDENCE;
37305
+ const nextVisitedSymbols = new Set(visitedSymbols);
37306
+ if (moduleSymbol) nextVisitedSymbols.add(moduleSymbol);
37307
+ for (const statement of sourceFile.statements) {
37308
+ if (exportName === "default" && ts.isExportAssignment(statement) && !statement.isExportEquals) {
37309
+ const evidence = resolveMotionExpressionEvidence(statement.expression, typeChecker, program, nextVisitedSymbols);
37310
+ if (hasMotionExpressionEvidence(evidence)) return evidence;
37311
+ continue;
37312
+ }
37313
+ if (!ts.isExportDeclaration(statement) || !statement.moduleSpecifier) continue;
37314
+ if (statement.isTypeOnly) continue;
37315
+ if (!ts.isStringLiteral(statement.moduleSpecifier)) continue;
37316
+ const moduleSource = statement.moduleSpecifier.text;
37317
+ if (!statement.exportClause) {
37318
+ if (isMotionModuleSource(moduleSource)) {
37319
+ const evidence = classifyMotionExport(exportName);
37320
+ if (hasMotionExpressionEvidence(evidence)) return evidence;
37321
+ continue;
37322
+ }
37323
+ const moduleSourceFile = getLocalModuleSourceFile(moduleSource, sourceFile.fileName, program);
37324
+ if (moduleSourceFile) {
37325
+ const evidence = resolveModuleExportEvidence(moduleSourceFile, exportName, typeChecker, program, nextVisitedSymbols);
37326
+ if (hasMotionExpressionEvidence(evidence)) return evidence;
37327
+ }
37328
+ continue;
37329
+ }
37330
+ if (!ts.isNamedExports(statement.exportClause)) continue;
37331
+ for (const exportSpecifier of statement.exportClause.elements) {
37332
+ if (exportSpecifier.name.text !== exportName || exportSpecifier.isTypeOnly) continue;
37333
+ const sourceExportName = exportSpecifier.propertyName?.text ?? exportSpecifier.name.text;
37334
+ if (isMotionModuleSource(moduleSource)) return classifyMotionExport(sourceExportName);
37335
+ const moduleSourceFile = getLocalModuleSourceFile(moduleSource, sourceFile.fileName, program);
37336
+ if (!moduleSourceFile) continue;
37337
+ return resolveModuleExportEvidence(moduleSourceFile, sourceExportName, typeChecker, program, nextVisitedSymbols);
37338
+ }
37339
+ }
37340
+ if (!moduleSymbol) return EMPTY_MOTION_EXPRESSION_EVIDENCE;
37341
+ const exportedSymbol = typeChecker.getExportsOfModule(moduleSymbol).find((candidateSymbol) => candidateSymbol.name === exportName);
37342
+ if (!exportedSymbol || nextVisitedSymbols.has(exportedSymbol)) return EMPTY_MOTION_EXPRESSION_EVIDENCE;
37343
+ nextVisitedSymbols.add(exportedSymbol);
37344
+ let resolvedEvidence = EMPTY_MOTION_EXPRESSION_EVIDENCE;
37345
+ for (const declaration of exportedSymbol.declarations ?? []) {
37346
+ if (ts.isExportSpecifier(declaration)) {
37347
+ const targetSymbol = typeChecker.getExportSpecifierLocalTargetSymbol(declaration);
37348
+ for (const targetDeclaration of targetSymbol?.declarations ?? []) {
37349
+ const importedEvidence = getImportedBindingEvidence(targetDeclaration, typeChecker, program, nextVisitedSymbols);
37350
+ if (importedEvidence) resolvedEvidence = mergeMotionExpressionEvidence(resolvedEvidence, importedEvidence);
37351
+ if (ts.isVariableDeclaration(targetDeclaration) && targetDeclaration.initializer) resolvedEvidence = mergeMotionExpressionEvidence(resolvedEvidence, resolveMotionExpressionEvidence(targetDeclaration.initializer, typeChecker, program, nextVisitedSymbols));
37352
+ }
37353
+ }
37354
+ if (ts.isVariableDeclaration(declaration) && declaration.initializer) resolvedEvidence = mergeMotionExpressionEvidence(resolvedEvidence, resolveMotionExpressionEvidence(declaration.initializer, typeChecker, program, nextVisitedSymbols));
37355
+ }
37356
+ return resolvedEvidence;
37357
+ };
37358
+ const mergeMotionExpressionEvidence = (leftEvidence, rightEvidence) => ({
37359
+ isAnimationFunction: leftEvidence.isAnimationFunction || rightEvidence.isAnimationFunction,
37360
+ isMotionComponent: leftEvidence.isMotionComponent || rightEvidence.isMotionComponent,
37361
+ isMotionComponentFactory: leftEvidence.isMotionComponentFactory || rightEvidence.isMotionComponentFactory,
37362
+ isMotionComponentNamespace: leftEvidence.isMotionComponentNamespace || rightEvidence.isMotionComponentNamespace,
37363
+ isMotionConfig: leftEvidence.isMotionConfig || rightEvidence.isMotionConfig,
37364
+ isMotionNamespace: leftEvidence.isMotionNamespace || rightEvidence.isMotionNamespace,
37365
+ isReducedMotionHook: leftEvidence.isReducedMotionHook || rightEvidence.isReducedMotionHook
37366
+ });
37367
+ const hasMotionExpressionEvidence = (evidence) => evidence.isAnimationFunction || evidence.isMotionComponent || evidence.isMotionComponentFactory || evidence.isMotionComponentNamespace || evidence.isMotionConfig || evidence.isMotionNamespace || evidence.isReducedMotionHook;
37368
+ const classifyMotionMember = (receiverEvidence, memberName) => {
37369
+ if (receiverEvidence.isMotionNamespace) return classifyMotionExport(memberName);
37370
+ if (receiverEvidence.isMotionComponentFactory) return {
37371
+ ...EMPTY_MOTION_EXPRESSION_EVIDENCE,
37372
+ isMotionComponent: true
37373
+ };
37374
+ if (receiverEvidence.isMotionComponentNamespace && MOTION_COMPONENT_NAMESPACE_MEMBER_NAMES.has(memberName)) return {
37375
+ ...EMPTY_MOTION_EXPRESSION_EVIDENCE,
37376
+ isMotionComponent: true
37377
+ };
37378
+ return EMPTY_MOTION_EXPRESSION_EVIDENCE;
37379
+ };
37380
+ const resolveMotionExpressionEvidence = (expression, typeChecker, program, visitedSymbols = /* @__PURE__ */ new Set()) => {
37381
+ const unwrappedExpression = unwrapTypescriptExpression(expression);
37382
+ if (ts.isIdentifier(unwrappedExpression)) {
37383
+ const symbol = typeChecker.getSymbolAtLocation(unwrappedExpression);
37384
+ if (!symbol || visitedSymbols.has(symbol)) return EMPTY_MOTION_EXPRESSION_EVIDENCE;
37385
+ const nextVisitedSymbols = new Set(visitedSymbols);
37386
+ nextVisitedSymbols.add(symbol);
37387
+ let resolvedEvidence = EMPTY_MOTION_EXPRESSION_EVIDENCE;
37388
+ for (const declaration of symbol.declarations ?? []) {
37389
+ const importedEvidence = getImportedBindingEvidence(declaration, typeChecker, program, nextVisitedSymbols);
37390
+ if (importedEvidence) {
37391
+ resolvedEvidence = mergeMotionExpressionEvidence(resolvedEvidence, importedEvidence);
37392
+ continue;
37393
+ }
37394
+ if (ts.isVariableDeclaration(declaration) && declaration.initializer) resolvedEvidence = mergeMotionExpressionEvidence(resolvedEvidence, resolveMotionExpressionEvidence(declaration.initializer, typeChecker, program, nextVisitedSymbols));
37395
+ if (ts.isBindingElement(declaration) && ts.isObjectBindingPattern(declaration.parent) && ts.isVariableDeclaration(declaration.parent.parent) && declaration.parent.parent.initializer) {
37396
+ const propertyName = declaration.propertyName ?? declaration.name;
37397
+ if (ts.isIdentifier(propertyName) || ts.isStringLiteral(propertyName)) {
37398
+ const receiverEvidence = resolveMotionExpressionEvidence(declaration.parent.parent.initializer, typeChecker, program, nextVisitedSymbols);
37399
+ resolvedEvidence = mergeMotionExpressionEvidence(resolvedEvidence, classifyMotionMember(receiverEvidence, propertyName.text));
37400
+ }
37401
+ }
37402
+ }
37403
+ return resolvedEvidence;
37404
+ }
37405
+ if (ts.isPropertyAccessExpression(unwrappedExpression)) return classifyMotionMember(resolveMotionExpressionEvidence(unwrappedExpression.expression, typeChecker, program, visitedSymbols), unwrappedExpression.name.text);
37406
+ 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);
37407
+ if (ts.isCallExpression(unwrappedExpression)) {
37408
+ 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 {
37409
+ ...EMPTY_MOTION_EXPRESSION_EVIDENCE,
37410
+ isMotionNamespace: true
37411
+ };
37412
+ if (ts.isPropertyAccessExpression(unwrappedExpression.expression) && unwrappedExpression.expression.name.text === "bind") {
37413
+ const boundReceiverEvidence = resolveMotionExpressionEvidence(unwrappedExpression.expression.expression, typeChecker, program, visitedSymbols);
37414
+ if (boundReceiverEvidence.isAnimationFunction) return boundReceiverEvidence;
37415
+ }
37416
+ const calleeEvidence = resolveMotionExpressionEvidence(unwrappedExpression.expression, typeChecker, program, visitedSymbols);
37417
+ if (calleeEvidence.isMotionComponentFactory) return {
37418
+ ...EMPTY_MOTION_EXPRESSION_EVIDENCE,
37419
+ isMotionComponent: true
37420
+ };
37421
+ return calleeEvidence;
37422
+ }
37423
+ return EMPTY_MOTION_EXPRESSION_EVIDENCE;
37424
+ };
37425
+ const getStaticStringExpressionValue = (expression, typeChecker, visitedSymbols = /* @__PURE__ */ new Set()) => {
37426
+ const unwrappedExpression = unwrapTypescriptExpression(expression);
37427
+ if (ts.isStringLiteral(unwrappedExpression) || ts.isNoSubstitutionTemplateLiteral(unwrappedExpression)) return unwrappedExpression.text;
37428
+ if (!ts.isIdentifier(unwrappedExpression)) return null;
37429
+ const symbol = typeChecker.getSymbolAtLocation(unwrappedExpression);
37430
+ if (!symbol || visitedSymbols.has(symbol)) return null;
37431
+ const nextVisitedSymbols = new Set(visitedSymbols);
37432
+ nextVisitedSymbols.add(symbol);
37433
+ for (const declaration of symbol.declarations ?? []) {
37434
+ if (!ts.isVariableDeclaration(declaration) || !declaration.initializer) continue;
37435
+ if (!ts.isVariableDeclarationList(declaration.parent)) continue;
37436
+ if (!(declaration.parent.flags & ts.NodeFlags.Const)) continue;
37437
+ const value = getStaticStringExpressionValue(declaration.initializer, typeChecker, nextVisitedSymbols);
37438
+ if (value !== null) return value;
37439
+ }
37440
+ return null;
37441
+ };
37442
+ const getStaticJsxAttributeValue = (attribute, typeChecker) => {
37443
+ if (!attribute.initializer) return null;
37444
+ if (ts.isStringLiteral(attribute.initializer)) return attribute.initializer.text;
37445
+ if (!ts.isJsxExpression(attribute.initializer) || !attribute.initializer.expression) return null;
37446
+ return getStaticStringExpressionValue(attribute.initializer.expression, typeChecker);
37447
+ };
37448
+ const isExpressionValueConsumed = (expression) => {
37449
+ let currentExpression = expression;
37450
+ while (ts.isParenthesizedExpression(currentExpression.parent) || ts.isAsExpression(currentExpression.parent) || ts.isSatisfiesExpression(currentExpression.parent) || ts.isNonNullExpression(currentExpression.parent) || ts.isTypeAssertionExpression(currentExpression.parent)) currentExpression = currentExpression.parent;
37451
+ return !ts.isExpressionStatement(currentExpression.parent) && !ts.isVoidExpression(currentExpression.parent);
37452
+ };
37453
+ 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) ?? ""));
37454
+ const collectScriptMotionEvidence = (sourceFile, typeChecker, program) => {
37455
+ const evidence = {
37456
+ hasMotionUse: false,
37457
+ hasReducedMotionHandling: false
37458
+ };
37459
+ const visitNode = (node) => {
37460
+ if (ts.isCallExpression(node)) {
37461
+ const calleeEvidence = resolveMotionExpressionEvidence(node.expression, typeChecker, program);
37462
+ 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;
37463
+ if (calleeEvidence.isAnimationFunction || isAnimationFunctionCallViaCallOrApply) evidence.hasMotionUse = true;
37464
+ if (calleeEvidence.isReducedMotionHook && isExpressionValueConsumed(node)) evidence.hasReducedMotionHandling = true;
37465
+ }
37466
+ if (ts.isJsxOpeningElement(node) || ts.isJsxSelfClosingElement(node)) {
37467
+ const tagEvidence = ts.isJsxNamespacedName(node.tagName) ? EMPTY_MOTION_EXPRESSION_EVIDENCE : resolveMotionExpressionEvidence(node.tagName, typeChecker, program);
37468
+ if (tagEvidence.isMotionComponent || tagEvidence.isMotionComponentFactory) evidence.hasMotionUse = true;
37469
+ if (tagEvidence.isMotionConfig && jsxElementHasReducedMotionConfiguration(node.attributes, typeChecker)) evidence.hasReducedMotionHandling = true;
37470
+ }
37471
+ ts.forEachChild(node, visitNode);
37472
+ };
37473
+ visitNode(sourceFile);
37474
+ return evidence;
37475
+ };
37476
+ const analyzeReducedMotionSources = (sources) => {
37477
+ if (!sources.some(({ sourceText }) => MOTION_SOURCE_PREFILTER.test(sourceText))) return {
37478
+ hasMotionUse: false,
37479
+ hasReducedMotionHandling: false
37480
+ };
37481
+ const compilerOptions = {
37482
+ allowJs: true,
37483
+ checkJs: false,
37484
+ jsx: ts.JsxEmit.Preserve,
37485
+ module: ts.ModuleKind.ESNext,
37486
+ moduleResolution: ts.ModuleResolutionKind.Bundler,
37487
+ noEmit: true,
37488
+ noLib: true,
37489
+ noResolve: true,
37490
+ skipLibCheck: true,
37491
+ target: ts.ScriptTarget.Latest
37492
+ };
37493
+ const sourceTextByFileName = new Map(sources.map(({ fileName: sourceFileName, sourceText: content }) => [Path.resolve(sourceFileName), content]));
37494
+ const compilerHost = ts.createCompilerHost(compilerOptions);
37495
+ const getDefaultSourceFile = compilerHost.getSourceFile.bind(compilerHost);
37496
+ 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);
37497
+ const program = ts.createProgram([...sourceTextByFileName.keys()], compilerOptions, compilerHost);
37498
+ const typeChecker = program.getTypeChecker();
37499
+ const evidence = {
37500
+ hasMotionUse: false,
37501
+ hasReducedMotionHandling: false
37502
+ };
37503
+ for (const sourceFileName of sourceTextByFileName.keys()) {
37504
+ const sourceFile = program.getSourceFile(sourceFileName);
37505
+ if (!sourceFile) continue;
37506
+ const fileEvidence = collectScriptMotionEvidence(sourceFile, typeChecker, program);
37507
+ evidence.hasMotionUse ||= fileEvidence.hasMotionUse;
37508
+ evidence.hasReducedMotionHandling ||= fileEvidence.hasReducedMotionHandling;
37509
+ }
37510
+ return evidence;
37511
+ };
37512
+ const removeCssCommentsAndStrings = (content) => {
37513
+ let sanitizedContent = "";
37514
+ let quote = null;
37515
+ let isInsideComment = false;
37516
+ let isInsideLineComment = false;
37517
+ for (let characterIndex = 0; characterIndex < content.length; characterIndex += 1) {
37518
+ const character = content[characterIndex];
37519
+ const nextCharacter = content[characterIndex + 1];
37520
+ if (isInsideLineComment) {
37521
+ sanitizedContent += character === "\n" ? "\n" : " ";
37522
+ if (character === "\n") isInsideLineComment = false;
37523
+ continue;
37524
+ }
37525
+ if (isInsideComment) {
37526
+ if (character === "*" && nextCharacter === "/") {
37527
+ sanitizedContent += " ";
37528
+ characterIndex += 1;
37529
+ isInsideComment = false;
37530
+ } else sanitizedContent += character === "\n" ? "\n" : " ";
37531
+ continue;
37532
+ }
37533
+ if (quote) {
37534
+ if (character === "\\") {
37535
+ sanitizedContent += " ";
37536
+ characterIndex += 1;
37537
+ continue;
37538
+ }
37539
+ if (character === quote) quote = null;
37540
+ sanitizedContent += character === "\n" ? "\n" : " ";
37541
+ continue;
37542
+ }
37543
+ if (character === "/" && nextCharacter === "*") {
37544
+ sanitizedContent += " ";
37545
+ characterIndex += 1;
37546
+ isInsideComment = true;
37547
+ continue;
37548
+ }
37549
+ if (character === "/" && nextCharacter === "/") {
37550
+ sanitizedContent += " ";
37551
+ characterIndex += 1;
37552
+ isInsideLineComment = true;
37553
+ continue;
37554
+ }
37555
+ if (character === "\"" || character === "'") {
37556
+ sanitizedContent += " ";
37557
+ quote = character;
37558
+ continue;
37559
+ }
37560
+ sanitizedContent += character;
37561
+ }
37562
+ return sanitizedContent;
37563
+ };
37564
+ const findMatchingClosingBrace = (content, openingBraceIndex) => {
37565
+ let depth = 0;
37566
+ for (let characterIndex = openingBraceIndex; characterIndex < content.length; characterIndex += 1) {
37567
+ if (content[characterIndex] === "{") depth += 1;
37568
+ if (content[characterIndex] !== "}") continue;
37569
+ depth -= 1;
37570
+ if (depth === 0) return characterIndex;
37571
+ }
37572
+ return -1;
37573
+ };
37574
+ const hasReducedMotionMediaQuery = (content) => {
37575
+ const sanitizedContent = removeCssCommentsAndStrings(content);
37576
+ REDUCED_MOTION_MEDIA_QUERY_PATTERN.lastIndex = 0;
37577
+ for (const match of sanitizedContent.matchAll(REDUCED_MOTION_MEDIA_QUERY_PATTERN)) {
37578
+ const mediaPrelude = match[1];
37579
+ if (/\bnot\b/i.test(mediaPrelude)) continue;
37580
+ const openingBraceIndex = (match.index ?? 0) + match[0].lastIndexOf("{");
37581
+ const closingBraceIndex = findMatchingClosingBrace(sanitizedContent, openingBraceIndex);
37582
+ if (closingBraceIndex === -1) continue;
37583
+ const body = sanitizedContent.slice(openingBraceIndex + 1, closingBraceIndex);
37584
+ if (CSS_DECLARATION_PATTERN.test(body)) return true;
37585
+ }
37586
+ return false;
37587
+ };
37588
+ const collectProjectMotionEvidence = (rootDirectory) => {
37589
+ const scriptSources = [];
37590
+ let hasReducedMotionHandling = false;
37209
37591
  for (const { absolutePath, name } of walkSourceTreeFiles(rootDirectory)) {
37210
- if (!REDUCED_MOTION_FILE_EXTENSIONS.has(Path.extname(name))) continue;
37592
+ const extension = Path.extname(name);
37593
+ if (!SCRIPT_FILE_EXTENSIONS.has(extension) && !STYLE_FILE_EXTENSIONS.has(extension)) continue;
37211
37594
  let content;
37212
37595
  try {
37213
37596
  content = NFS.readFileSync(absolutePath, "utf-8");
37214
37597
  } catch {
37215
37598
  continue;
37216
37599
  }
37217
- if (REDUCED_MOTION_PATTERN.test(content)) return true;
37600
+ if (STYLE_FILE_EXTENSIONS.has(extension)) {
37601
+ if (hasReducedMotionMediaQuery(content)) hasReducedMotionHandling = true;
37602
+ continue;
37603
+ }
37604
+ scriptSources.push({
37605
+ fileName: absolutePath,
37606
+ sourceText: content
37607
+ });
37218
37608
  }
37219
- return false;
37609
+ if (scriptSources.length === 0) return {
37610
+ hasMotionUse: false,
37611
+ hasReducedMotionHandling
37612
+ };
37613
+ const scriptEvidence = analyzeReducedMotionSources(scriptSources);
37614
+ const hasMotionUse = scriptEvidence.hasMotionUse;
37615
+ hasReducedMotionHandling ||= scriptEvidence.hasReducedMotionHandling;
37616
+ return {
37617
+ hasMotionUse,
37618
+ hasReducedMotionHandling
37619
+ };
37220
37620
  };
37221
37621
  const checkReducedMotion = (rootDirectory) => {
37222
37622
  const packageJsonPath = Path.join(rootDirectory, "package.json");
37223
37623
  if (!isFile(packageJsonPath)) return [];
37224
- let hasMotionLibrary = false;
37225
37624
  try {
37226
37625
  const packageJson = readPackageJson(packageJsonPath);
37227
37626
  const allDependencies = {
37228
37627
  ...packageJson.dependencies,
37229
37628
  ...packageJson.devDependencies
37230
37629
  };
37231
- hasMotionLibrary = Object.keys(allDependencies).some((packageName) => MOTION_LIBRARY_PACKAGES.has(packageName));
37630
+ if (!Object.keys(allDependencies).some((packageName) => MOTION_LIBRARY_PACKAGES.has(packageName))) return [];
37232
37631
  } catch {
37233
37632
  return [];
37234
37633
  }
37235
- if (!hasMotionLibrary) return [];
37236
- const result = spawnSync("git", [
37237
- "grep",
37238
- "--untracked",
37239
- "-lE",
37240
- REDUCED_MOTION_GREP_PATTERN,
37241
- "--",
37242
- ...REDUCED_MOTION_FILE_GLOBS
37243
- ], {
37244
- cwd: rootDirectory,
37245
- encoding: "utf-8",
37246
- stdio: [
37247
- "ignore",
37248
- "pipe",
37249
- "pipe"
37250
- ]
37251
- });
37252
- if (!(!result.error && (result.status === 0 || result.status === GIT_GREP_NO_MATCH_STATUS))) return hasReducedMotionHandlingViaFilesystem(rootDirectory) ? [] : [MISSING_REDUCED_MOTION_DIAGNOSTIC];
37253
- return result.status === 0 && result.stdout.split("\n").some((filePath) => filePath.length > 0 && !hasIgnoredPathSegment(filePath)) ? [] : [MISSING_REDUCED_MOTION_DIAGNOSTIC];
37634
+ const evidence = collectProjectMotionEvidence(rootDirectory);
37635
+ return evidence.hasMotionUse && !evidence.hasReducedMotionHandling ? [MISSING_REDUCED_MOTION_DIAGNOSTIC] : [];
37254
37636
  };
37255
37637
  const buildSecurityScanDiagnostic = (finding, entry, relativePath) => ({
37256
37638
  filePath: relativePath,
@@ -39853,23 +40235,12 @@ const REACT_USE_BINDING_RESOLUTION = {
39853
40235
  isReactUseBinding: true,
39854
40236
  isReactNamespaceBinding: false
39855
40237
  };
39856
- const getScriptKind$2 = (filename) => {
39857
- if (filename.endsWith(".tsx")) return ts.ScriptKind.TSX;
39858
- if (filename.endsWith(".jsx")) return ts.ScriptKind.JSX;
39859
- if (filename.endsWith(".ts")) return ts.ScriptKind.TS;
39860
- return ts.ScriptKind.JS;
39861
- };
39862
40238
  const getUtf16Offset$1 = (sourceText, utf8Offset) => Buffer.from(sourceText).subarray(0, utf8Offset).toString("utf8").length;
39863
- const unwrapExpression = (expression) => {
39864
- let currentExpression = expression;
39865
- while (ts.isParenthesizedExpression(currentExpression) || ts.isAsExpression(currentExpression) || ts.isSatisfiesExpression(currentExpression) || ts.isNonNullExpression(currentExpression) || ts.isTypeAssertionExpression(currentExpression)) currentExpression = currentExpression.expression;
39866
- return currentExpression;
39867
- };
39868
40239
  const getStaticPropertyName = (node) => {
39869
40240
  if (!node) return null;
39870
40241
  if (ts.isIdentifier(node) || ts.isStringLiteral(node) || ts.isNumericLiteral(node)) return node.text;
39871
40242
  if (ts.isComputedPropertyName(node)) {
39872
- const expression = unwrapExpression(node.expression);
40243
+ const expression = unwrapTypescriptExpression(node.expression);
39873
40244
  if (ts.isStringLiteral(expression) || ts.isNoSubstitutionTemplateLiteral(expression)) return expression.text;
39874
40245
  }
39875
40246
  return null;
@@ -39898,7 +40269,7 @@ const isReactUseObjectBindingElement = (bindingElement) => {
39898
40269
  return getStaticPropertyName(bindingElement.propertyName) === USE_IDENTIFIER;
39899
40270
  };
39900
40271
  const isReactRequireCall = (expression) => {
39901
- const unwrappedExpression = unwrapExpression(expression);
40272
+ const unwrappedExpression = unwrapTypescriptExpression(expression);
39902
40273
  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;
39903
40274
  };
39904
40275
  const getModuleSource = (node) => {
@@ -40000,7 +40371,7 @@ const isIdentifierShadowedByLocalBinding = (identifier, sourceFile) => {
40000
40371
  return false;
40001
40372
  };
40002
40373
  const isReactNamespaceExpression = (expression, reactImportBindings, sourceFile, visitedDeclarations) => {
40003
- const unwrappedExpression = unwrapExpression(expression);
40374
+ const unwrappedExpression = unwrapTypescriptExpression(expression);
40004
40375
  if (isReactRequireCall(unwrappedExpression)) return true;
40005
40376
  if (!ts.isIdentifier(unwrappedExpression)) return false;
40006
40377
  if (reactImportBindings.namespaceNames.has(unwrappedExpression.text) && !isIdentifierShadowedByLocalBinding(unwrappedExpression, sourceFile)) return true;
@@ -40008,7 +40379,7 @@ const isReactNamespaceExpression = (expression, reactImportBindings, sourceFile,
40008
40379
  };
40009
40380
  const isReactUseExpression = (expression, reactImportBindings, sourceFile, visitedDeclarations) => {
40010
40381
  if (!expression) return false;
40011
- const unwrappedExpression = unwrapExpression(expression);
40382
+ const unwrappedExpression = unwrapTypescriptExpression(expression);
40012
40383
  if (ts.isIdentifier(unwrappedExpression)) {
40013
40384
  if (reactImportBindings.useImportNames.has(unwrappedExpression.text) && !isIdentifierShadowedByLocalBinding(unwrappedExpression, sourceFile)) return true;
40014
40385
  if (unwrappedExpression.text === USE_IDENTIFIER) return false;
@@ -40108,7 +40479,7 @@ const findUseCallIdentifier = (sourceFile, useOffset) => {
40108
40479
  return matchedIdentifier;
40109
40480
  };
40110
40481
  const resolveUseCallBinding = (sourceText, filename, utf8Offset) => {
40111
- const sourceFile = ts.createSourceFile(filename, sourceText, ts.ScriptTarget.Latest, true, getScriptKind$2(filename));
40482
+ const sourceFile = ts.createSourceFile(filename, sourceText, ts.ScriptTarget.Latest, true, getTypescriptScriptKind(filename));
40112
40483
  const useIdentifier = findUseCallIdentifier(sourceFile, getUtf16Offset$1(sourceText, utf8Offset));
40113
40484
  if (!useIdentifier) return null;
40114
40485
  return resolveIdentifierBinding(useIdentifier, collectReactImportBindings(sourceFile), sourceFile);
@@ -40144,12 +40515,6 @@ const WORKLET_ACCEPTING_CALLEES = new Set([
40144
40515
  "executeOnUIRuntimeSync"
40145
40516
  ]);
40146
40517
  const GESTURE_ROOT_IDENTIFIER = "Gesture";
40147
- const getScriptKind$1 = (filename) => {
40148
- if (filename.endsWith(".tsx")) return ts.ScriptKind.TSX;
40149
- if (filename.endsWith(".jsx")) return ts.ScriptKind.JSX;
40150
- if (filename.endsWith(".ts")) return ts.ScriptKind.TS;
40151
- return ts.ScriptKind.JS;
40152
- };
40153
40518
  const getUtf16Offset = (sourceText, utf8Offset) => Buffer.from(sourceText).subarray(0, utf8Offset).toString("utf8").length;
40154
40519
  const hasWorkletDirective = (node) => {
40155
40520
  if (!ts.isFunctionDeclaration(node) && !ts.isFunctionExpression(node) && !ts.isArrowFunction(node) && !ts.isMethodDeclaration(node)) return false;
@@ -40221,17 +40586,11 @@ const shouldSuppressCompilerFindingInWorklet = (diagnostic, project, rootDirecto
40221
40586
  } catch {
40222
40587
  return false;
40223
40588
  }
40224
- return isOffsetInsideWorklet(ts.createSourceFile(absolutePath, sourceText, ts.ScriptTarget.Latest, true, getScriptKind$1(absolutePath)), getUtf16Offset(sourceText, primaryLabel.span.offset));
40589
+ return isOffsetInsideWorklet(ts.createSourceFile(absolutePath, sourceText, ts.ScriptTarget.Latest, true, getTypescriptScriptKind(absolutePath)), getUtf16Offset(sourceText, primaryLabel.span.offset));
40225
40590
  };
40226
40591
  const MANUAL_MEMOIZATION_PLUGIN = "react-doctor";
40227
40592
  const MANUAL_MEMOIZATION_RULE = "react-compiler-no-manual-memoization";
40228
40593
  const REACT_COMPILER_PLUGIN = "react-hooks-js";
40229
- const getScriptKind = (filename) => {
40230
- if (filename.endsWith(".tsx")) return ts.ScriptKind.TSX;
40231
- if (filename.endsWith(".jsx")) return ts.ScriptKind.JSX;
40232
- if (filename.endsWith(".ts")) return ts.ScriptKind.TS;
40233
- return ts.ScriptKind.JS;
40234
- };
40235
40594
  const isManualMemoizationDiagnostic = (diagnostic) => diagnostic.plugin === MANUAL_MEMOIZATION_PLUGIN && diagnostic.rule === MANUAL_MEMOIZATION_RULE;
40236
40595
  const nodeLineRange = (sourceFile, node) => ({
40237
40596
  startLine: sourceFile.getLineAndCharacterOfPosition(node.getStart(sourceFile)).line + 1,
@@ -40293,7 +40652,7 @@ const suppressMemoizationInBailedOutFunctions = (diagnostics, rootDirectory) =>
40293
40652
  try {
40294
40653
  const buffer = NFS.readFileSync(absolutePath);
40295
40654
  parsed = {
40296
- sourceFile: ts.createSourceFile(absolutePath, buffer.toString("utf8"), ts.ScriptTarget.Latest, true, getScriptKind(absolutePath)),
40655
+ sourceFile: ts.createSourceFile(absolutePath, buffer.toString("utf8"), ts.ScriptTarget.Latest, true, getTypescriptScriptKind(absolutePath)),
40297
40656
  buffer
40298
40657
  };
40299
40658
  } catch {
@@ -42884,7 +43243,9 @@ const toJsonReportDiagnostic = (diagnostic, projectRoot, reportRoot) => {
42884
43243
  line: diagnostic.line,
42885
43244
  column: diagnostic.column,
42886
43245
  plugin: diagnostic.plugin,
42887
- rule: diagnostic.rule
43246
+ rule: diagnostic.rule,
43247
+ severity: diagnostic.severity,
43248
+ message: diagnostic.message
42888
43249
  }),
42889
43250
  normalizedFilePath,
42890
43251
  category: ruleIdentity.category,
@@ -43278,4 +43639,4 @@ const toJsonReport = (result, options) => buildJsonReport({
43278
43639
  export { AmbiguousProjectError, NoReactDependencyError, NotADirectoryError, PackageJsonNotFoundError, ProjectNotFoundError, ReactDoctorError, buildJsonReport, buildJsonReportError, clearCaches, defineConfig, diagnose, filterSourceFiles, getDiffInfo, hasReactRuntime, isProjectDiscoveryError, isReactDoctorError, summarizeDiagnostics, toJsonReport };
43279
43640
 
43280
43641
  //# sourceMappingURL=index.js.map
43281
- //# debugId=1872748b-acb2-5e9a-a748-43bfd023eb85
43642
+ //# debugId=d7ac2526-002a-57de-91f1-7c670298fa89