oxlint-plugin-react-doctor 0.5.1-dev.f5f539a → 0.5.1-dev.fee3fc4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +497 -284
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -1132,6 +1132,15 @@ const isMemberProperty = (node, propertyName) => Boolean(node && isNodeOfType(no
|
|
|
1132
1132
|
const NEXTJS_SOURCE_FILE_EXTENSION_GROUP = "(?:tsx?|jsx?|mts|mjs)";
|
|
1133
1133
|
const PAGE_FILE_PATTERN = new RegExp(`/page\\.${NEXTJS_SOURCE_FILE_EXTENSION_GROUP}$`);
|
|
1134
1134
|
const PAGE_OR_LAYOUT_FILE_PATTERN = new RegExp(`/(page|layout)\\.${NEXTJS_SOURCE_FILE_EXTENSION_GROUP}$`);
|
|
1135
|
+
const LAYOUT_FILE_NAMES = [
|
|
1136
|
+
"layout.tsx",
|
|
1137
|
+
"layout.jsx",
|
|
1138
|
+
"layout.ts",
|
|
1139
|
+
"layout.js",
|
|
1140
|
+
"layout.mts",
|
|
1141
|
+
"layout.mjs"
|
|
1142
|
+
];
|
|
1143
|
+
const METADATA_EXPORT_NAMES = ["metadata", "generateMetadata"];
|
|
1135
1144
|
const INTERNAL_PAGE_PATH_PATTERN = /\/(?:(?:\((?:dashboard|admin|settings|account|internal|manage|console|portal|auth|onboarding|app|ee|protected)\))|(?:dashboard|admin|settings|account|internal|manage|console|portal))\//i;
|
|
1136
1145
|
const PAGES_DIRECTORY_PATTERN = /\/pages\//;
|
|
1137
1146
|
const NEXTJS_NAVIGATION_FUNCTIONS = new Set([
|
|
@@ -12543,6 +12552,64 @@ const nextjsInlineScriptMissingId = defineRule({
|
|
|
12543
12552
|
} })
|
|
12544
12553
|
});
|
|
12545
12554
|
//#endregion
|
|
12555
|
+
//#region src/plugin/utils/parse-export-specifiers.ts
|
|
12556
|
+
const getSpecifierName = (rawName) => rawName.replace(/^type\s+/, "").trim();
|
|
12557
|
+
const parseExportSpecifiers = (specifiersText, declarationIsTypeOnly) => specifiersText.split(",").map((specifierText) => specifierText.trim()).filter(Boolean).map((specifierText) => {
|
|
12558
|
+
const isTypeOnly = declarationIsTypeOnly || specifierText.startsWith("type ");
|
|
12559
|
+
const [rawLocalName, rawExportedName] = specifierText.split(/\s+as\s+/);
|
|
12560
|
+
const localName = getSpecifierName(rawLocalName ?? "");
|
|
12561
|
+
return {
|
|
12562
|
+
localName,
|
|
12563
|
+
exportedName: getSpecifierName(rawExportedName ?? localName),
|
|
12564
|
+
isTypeOnly
|
|
12565
|
+
};
|
|
12566
|
+
});
|
|
12567
|
+
//#endregion
|
|
12568
|
+
//#region src/plugin/utils/strip-js-comments.ts
|
|
12569
|
+
const BLOCK_COMMENT_PATTERN = /\/\*[\s\S]*?\*\//g;
|
|
12570
|
+
const LINE_COMMENT_PATTERN = /^\s*\/\/.*$/gm;
|
|
12571
|
+
const stripJsComments = (sourceText) => sourceText.replace(BLOCK_COMMENT_PATTERN, "").replace(LINE_COMMENT_PATTERN, "");
|
|
12572
|
+
//#endregion
|
|
12573
|
+
//#region src/plugin/utils/does-module-export-name.ts
|
|
12574
|
+
const DEFAULT_EXPORT_DECLARATION_PATTERN = /^\s*export\s+default\b/m;
|
|
12575
|
+
const NAMED_EXPORT_DECLARATION_PATTERN = /^\s*export\s+(?:declare\s+)?(?:(?:async\s+)?function|(?:abstract\s+)?class|const|let|var|enum|interface|type)\s+([\w$]+)/gm;
|
|
12576
|
+
const LOCAL_EXPORT_SPECIFIER_DECLARATION_PATTERN$1 = /^\s*export\s+(?:type\s+)?\{([\s\S]*?)\}(?:\s+from\s+["'][^"']+["'])?\s*;?\s*(?:(?:\/\/[^\n]*)?\s*)/gm;
|
|
12577
|
+
const doesSourceTextExportName = (sourceText, exportedName) => {
|
|
12578
|
+
const strippedSource = stripJsComments(sourceText);
|
|
12579
|
+
if (exportedName === "default" && DEFAULT_EXPORT_DECLARATION_PATTERN.test(strippedSource)) return true;
|
|
12580
|
+
for (const match of strippedSource.matchAll(NAMED_EXPORT_DECLARATION_PATTERN)) if (match[1] === exportedName) return true;
|
|
12581
|
+
for (const match of strippedSource.matchAll(LOCAL_EXPORT_SPECIFIER_DECLARATION_PATTERN$1)) if (parseExportSpecifiers(match[1] ?? "", false).map((specifier) => specifier.exportedName).includes(exportedName)) return true;
|
|
12582
|
+
return false;
|
|
12583
|
+
};
|
|
12584
|
+
const doesModuleExportName = (filePath, exportedName) => {
|
|
12585
|
+
try {
|
|
12586
|
+
return doesSourceTextExportName(fs.readFileSync(filePath, "utf8"), exportedName);
|
|
12587
|
+
} catch {
|
|
12588
|
+
return false;
|
|
12589
|
+
}
|
|
12590
|
+
};
|
|
12591
|
+
//#endregion
|
|
12592
|
+
//#region src/plugin/utils/has-ancestor-layout-matching.ts
|
|
12593
|
+
const hasAncestorLayoutMatching = (pageFilename, matchesLayout) => {
|
|
12594
|
+
const normalizedPage = pageFilename.replaceAll("\\", "/");
|
|
12595
|
+
let currentDirectory = path.dirname(normalizedPage);
|
|
12596
|
+
for (let level = 0; level < 30; level++) {
|
|
12597
|
+
for (const layoutFileName of LAYOUT_FILE_NAMES) {
|
|
12598
|
+
const layoutPath = path.join(currentDirectory, layoutFileName);
|
|
12599
|
+
if (layoutPath.replaceAll("\\", "/") === normalizedPage) continue;
|
|
12600
|
+
if (matchesLayout(layoutPath)) return true;
|
|
12601
|
+
}
|
|
12602
|
+
const parentDirectory = path.dirname(currentDirectory);
|
|
12603
|
+
if (path.basename(currentDirectory) === "app" && path.basename(parentDirectory) !== "app") break;
|
|
12604
|
+
if (parentDirectory === currentDirectory) break;
|
|
12605
|
+
currentDirectory = parentDirectory;
|
|
12606
|
+
}
|
|
12607
|
+
return false;
|
|
12608
|
+
};
|
|
12609
|
+
//#endregion
|
|
12610
|
+
//#region src/plugin/utils/find-ancestor-metadata-layout.ts
|
|
12611
|
+
const hasAncestorMetadataLayout = (pageFilename) => hasAncestorLayoutMatching(pageFilename, (layoutPath) => METADATA_EXPORT_NAMES.some((exportName) => doesModuleExportName(layoutPath, exportName)));
|
|
12612
|
+
//#endregion
|
|
12546
12613
|
//#region src/plugin/rules/nextjs/nextjs-missing-metadata.ts
|
|
12547
12614
|
const nextjsMissingMetadata = defineRule({
|
|
12548
12615
|
id: "nextjs-missing-metadata",
|
|
@@ -12555,13 +12622,15 @@ const nextjsMissingMetadata = defineRule({
|
|
|
12555
12622
|
const filename = normalizeFilename$1(context.filename ?? "");
|
|
12556
12623
|
if (!PAGE_FILE_PATTERN.test(filename)) return;
|
|
12557
12624
|
if (INTERNAL_PAGE_PATH_PATTERN.test(filename)) return;
|
|
12558
|
-
if (
|
|
12625
|
+
if (programNode.body?.some((statement) => {
|
|
12559
12626
|
if (!isNodeOfType(statement, "ExportNamedDeclaration")) return false;
|
|
12560
12627
|
const declaration = statement.declaration;
|
|
12561
|
-
if (isNodeOfType(declaration, "VariableDeclaration")) return declaration.declarations?.some((declarator) => isNodeOfType(declarator.id, "Identifier") && (declarator.id.name
|
|
12628
|
+
if (isNodeOfType(declaration, "VariableDeclaration")) return declaration.declarations?.some((declarator) => isNodeOfType(declarator.id, "Identifier") && METADATA_EXPORT_NAMES.includes(declarator.id.name));
|
|
12562
12629
|
if (isNodeOfType(declaration, "FunctionDeclaration")) return declaration.id?.name === "generateMetadata";
|
|
12563
|
-
return
|
|
12564
|
-
}))
|
|
12630
|
+
return (statement.specifiers ?? []).some((specifier) => isNodeOfType(specifier, "ExportSpecifier") && isNodeOfType(specifier.exported, "Identifier") && METADATA_EXPORT_NAMES.includes(specifier.exported.name));
|
|
12631
|
+
})) return;
|
|
12632
|
+
if (hasAncestorMetadataLayout(context.filename ?? "")) return;
|
|
12633
|
+
context.report({
|
|
12565
12634
|
node: programNode,
|
|
12566
12635
|
message: "This page has no metadata, so search engines and social previews get no title or description."
|
|
12567
12636
|
});
|
|
@@ -13452,43 +13521,6 @@ const parseSourceFile = (absoluteFilePath) => {
|
|
|
13452
13521
|
return parsedProgram;
|
|
13453
13522
|
};
|
|
13454
13523
|
//#endregion
|
|
13455
|
-
//#region src/plugin/utils/parse-export-specifiers.ts
|
|
13456
|
-
const getSpecifierName = (rawName) => rawName.replace(/^type\s+/, "").trim();
|
|
13457
|
-
const parseExportSpecifiers = (specifiersText, declarationIsTypeOnly) => specifiersText.split(",").map((specifierText) => specifierText.trim()).filter(Boolean).map((specifierText) => {
|
|
13458
|
-
const isTypeOnly = declarationIsTypeOnly || specifierText.startsWith("type ");
|
|
13459
|
-
const [rawLocalName, rawExportedName] = specifierText.split(/\s+as\s+/);
|
|
13460
|
-
const localName = getSpecifierName(rawLocalName ?? "");
|
|
13461
|
-
return {
|
|
13462
|
-
localName,
|
|
13463
|
-
exportedName: getSpecifierName(rawExportedName ?? localName),
|
|
13464
|
-
isTypeOnly
|
|
13465
|
-
};
|
|
13466
|
-
});
|
|
13467
|
-
//#endregion
|
|
13468
|
-
//#region src/plugin/utils/strip-js-comments.ts
|
|
13469
|
-
const BLOCK_COMMENT_PATTERN = /\/\*[\s\S]*?\*\//g;
|
|
13470
|
-
const LINE_COMMENT_PATTERN = /^\s*\/\/.*$/gm;
|
|
13471
|
-
const stripJsComments = (sourceText) => sourceText.replace(BLOCK_COMMENT_PATTERN, "").replace(LINE_COMMENT_PATTERN, "");
|
|
13472
|
-
//#endregion
|
|
13473
|
-
//#region src/plugin/utils/does-module-export-name.ts
|
|
13474
|
-
const DEFAULT_EXPORT_DECLARATION_PATTERN = /^\s*export\s+default\b/m;
|
|
13475
|
-
const NAMED_EXPORT_DECLARATION_PATTERN = /^\s*export\s+(?:declare\s+)?(?:(?:async\s+)?function|(?:abstract\s+)?class|const|let|var|enum|interface|type)\s+([\w$]+)/gm;
|
|
13476
|
-
const LOCAL_EXPORT_SPECIFIER_DECLARATION_PATTERN$1 = /^\s*export\s+(?:type\s+)?\{([\s\S]*?)\}(?:\s+from\s+["'][^"']+["'])?\s*;?\s*(?:(?:\/\/[^\n]*)?\s*)/gm;
|
|
13477
|
-
const doesSourceTextExportName = (sourceText, exportedName) => {
|
|
13478
|
-
const strippedSource = stripJsComments(sourceText);
|
|
13479
|
-
if (exportedName === "default" && DEFAULT_EXPORT_DECLARATION_PATTERN.test(strippedSource)) return true;
|
|
13480
|
-
for (const match of strippedSource.matchAll(NAMED_EXPORT_DECLARATION_PATTERN)) if (match[1] === exportedName) return true;
|
|
13481
|
-
for (const match of strippedSource.matchAll(LOCAL_EXPORT_SPECIFIER_DECLARATION_PATTERN$1)) if (parseExportSpecifiers(match[1] ?? "", false).map((specifier) => specifier.exportedName).includes(exportedName)) return true;
|
|
13482
|
-
return false;
|
|
13483
|
-
};
|
|
13484
|
-
const doesModuleExportName = (filePath, exportedName) => {
|
|
13485
|
-
try {
|
|
13486
|
-
return doesSourceTextExportName(fs.readFileSync(filePath, "utf8"), exportedName);
|
|
13487
|
-
} catch {
|
|
13488
|
-
return false;
|
|
13489
|
-
}
|
|
13490
|
-
};
|
|
13491
|
-
//#endregion
|
|
13492
13524
|
//#region src/plugin/utils/is-barrel-index-module.ts
|
|
13493
13525
|
const INDEX_MODULE_FILE_PATTERN = /^index\.(?:[cm]?[jt]sx?|mjs)$/;
|
|
13494
13526
|
const BINDING_IMPORT_DECLARATION_PATTERN = /^\s*import\s+(type\s+)?(?!["'])([^;]*?)\s+from\s+["']([^"']+)["']\s*;?\s*(?:(?:\/\/[^\n]*)?\s*)/gm;
|
|
@@ -13972,29 +14004,10 @@ const astMentionsSuspense = (programNode) => {
|
|
|
13972
14004
|
};
|
|
13973
14005
|
//#endregion
|
|
13974
14006
|
//#region src/plugin/utils/find-ancestor-suspense-layout.ts
|
|
13975
|
-
const
|
|
13976
|
-
|
|
13977
|
-
|
|
13978
|
-
|
|
13979
|
-
"layout.js"
|
|
13980
|
-
];
|
|
13981
|
-
const hasAncestorSuspenseLayout = (pageFilename) => {
|
|
13982
|
-
const normalizedPage = pageFilename.replaceAll("\\", "/");
|
|
13983
|
-
let currentDirectory = path.dirname(normalizedPage);
|
|
13984
|
-
for (let level = 0; level < 30; level++) {
|
|
13985
|
-
for (const layoutFileName of LAYOUT_FILE_NAMES) {
|
|
13986
|
-
const layoutPath = path.join(currentDirectory, layoutFileName);
|
|
13987
|
-
if (layoutPath.replaceAll("\\", "/") === normalizedPage) continue;
|
|
13988
|
-
const programRoot = parseSourceFile(layoutPath);
|
|
13989
|
-
if (programRoot && astMentionsSuspense(programRoot)) return true;
|
|
13990
|
-
}
|
|
13991
|
-
if (path.basename(currentDirectory) === "app") break;
|
|
13992
|
-
const parentDirectory = path.dirname(currentDirectory);
|
|
13993
|
-
if (parentDirectory === currentDirectory) break;
|
|
13994
|
-
currentDirectory = parentDirectory;
|
|
13995
|
-
}
|
|
13996
|
-
return false;
|
|
13997
|
-
};
|
|
14007
|
+
const hasAncestorSuspenseLayout = (pageFilename) => hasAncestorLayoutMatching(pageFilename, (layoutPath) => {
|
|
14008
|
+
const programRoot = parseSourceFile(layoutPath);
|
|
14009
|
+
return Boolean(programRoot && astMentionsSuspense(programRoot));
|
|
14010
|
+
});
|
|
13998
14011
|
//#endregion
|
|
13999
14012
|
//#region src/plugin/rules/nextjs/nextjs-no-use-search-params-without-suspense.ts
|
|
14000
14013
|
const astContainsUseSearchParams = (root) => {
|
|
@@ -15324,6 +15337,182 @@ const createRelativeImportSource = (filename, targetFilePath) => {
|
|
|
15324
15337
|
return relativePath.startsWith(".") ? relativePath : `./${relativePath}`;
|
|
15325
15338
|
};
|
|
15326
15339
|
//#endregion
|
|
15340
|
+
//#region src/react-native-dependency-names.ts
|
|
15341
|
+
const EXPO_MANAGED_DEPENDENCY_NAMES = new Set([
|
|
15342
|
+
"expo",
|
|
15343
|
+
"expo-router",
|
|
15344
|
+
"@expo/cli",
|
|
15345
|
+
"@expo/metro-config",
|
|
15346
|
+
"@expo/metro-runtime"
|
|
15347
|
+
]);
|
|
15348
|
+
const REACT_NATIVE_DEPENDENCY_NAMES = new Set([
|
|
15349
|
+
"react-native",
|
|
15350
|
+
"react-native-tvos",
|
|
15351
|
+
...EXPO_MANAGED_DEPENDENCY_NAMES,
|
|
15352
|
+
"react-native-windows",
|
|
15353
|
+
"react-native-macos"
|
|
15354
|
+
]);
|
|
15355
|
+
const REACT_NATIVE_DEPENDENCY_PREFIXES = ["@react-native/", "@react-native-"];
|
|
15356
|
+
const isExpoManagedDependencyName = (dependencyName) => EXPO_MANAGED_DEPENDENCY_NAMES.has(dependencyName);
|
|
15357
|
+
const isReactNativeDependencyName = (dependencyName) => {
|
|
15358
|
+
if (REACT_NATIVE_DEPENDENCY_NAMES.has(dependencyName)) return true;
|
|
15359
|
+
for (const prefix of REACT_NATIVE_DEPENDENCY_PREFIXES) if (dependencyName.startsWith(prefix)) return true;
|
|
15360
|
+
return false;
|
|
15361
|
+
};
|
|
15362
|
+
//#endregion
|
|
15363
|
+
//#region src/plugin/utils/classify-package-platform.ts
|
|
15364
|
+
const WEB_FRAMEWORK_DEPENDENCY_NAMES = new Set([
|
|
15365
|
+
"next",
|
|
15366
|
+
"vite",
|
|
15367
|
+
"react-scripts",
|
|
15368
|
+
"gatsby",
|
|
15369
|
+
"@remix-run/react",
|
|
15370
|
+
"@remix-run/node",
|
|
15371
|
+
"@docusaurus/core",
|
|
15372
|
+
"@docusaurus/preset-classic",
|
|
15373
|
+
"@storybook/react",
|
|
15374
|
+
"@storybook/react-vite",
|
|
15375
|
+
"@storybook/react-webpack5",
|
|
15376
|
+
"@storybook/nextjs",
|
|
15377
|
+
"@storybook/web-components",
|
|
15378
|
+
"storybook",
|
|
15379
|
+
"react-dom",
|
|
15380
|
+
"@vitejs/plugin-react",
|
|
15381
|
+
"@vitejs/plugin-react-swc"
|
|
15382
|
+
]);
|
|
15383
|
+
const cachedPlatformByPackageDirectory = /* @__PURE__ */ new Map();
|
|
15384
|
+
const cachedPackageDirectoryByFilename = /* @__PURE__ */ new Map();
|
|
15385
|
+
const findNearestPackageDirectory = (filename) => {
|
|
15386
|
+
if (!filename) return null;
|
|
15387
|
+
const fromCache = cachedPackageDirectoryByFilename.get(filename);
|
|
15388
|
+
if (fromCache !== void 0) return fromCache;
|
|
15389
|
+
let currentDirectory = path.dirname(filename);
|
|
15390
|
+
while (true) {
|
|
15391
|
+
const candidatePackageJsonPath = path.join(currentDirectory, "package.json");
|
|
15392
|
+
let hasPackageJson = false;
|
|
15393
|
+
try {
|
|
15394
|
+
hasPackageJson = fs.statSync(candidatePackageJsonPath).isFile();
|
|
15395
|
+
} catch {
|
|
15396
|
+
hasPackageJson = false;
|
|
15397
|
+
}
|
|
15398
|
+
if (hasPackageJson) {
|
|
15399
|
+
cachedPackageDirectoryByFilename.set(filename, currentDirectory);
|
|
15400
|
+
return currentDirectory;
|
|
15401
|
+
}
|
|
15402
|
+
const parentDirectory = path.dirname(currentDirectory);
|
|
15403
|
+
if (parentDirectory === currentDirectory) {
|
|
15404
|
+
cachedPackageDirectoryByFilename.set(filename, null);
|
|
15405
|
+
return null;
|
|
15406
|
+
}
|
|
15407
|
+
currentDirectory = parentDirectory;
|
|
15408
|
+
}
|
|
15409
|
+
};
|
|
15410
|
+
const readPackageJsonSafe = (packageJsonPath) => {
|
|
15411
|
+
let rawContents;
|
|
15412
|
+
try {
|
|
15413
|
+
rawContents = fs.readFileSync(packageJsonPath, "utf-8");
|
|
15414
|
+
} catch {
|
|
15415
|
+
return null;
|
|
15416
|
+
}
|
|
15417
|
+
try {
|
|
15418
|
+
const parsed = JSON.parse(rawContents);
|
|
15419
|
+
if (typeof parsed === "object" && parsed !== null) return parsed;
|
|
15420
|
+
return null;
|
|
15421
|
+
} catch {
|
|
15422
|
+
return null;
|
|
15423
|
+
}
|
|
15424
|
+
};
|
|
15425
|
+
const DEPENDENCY_SECTION_NAMES = [
|
|
15426
|
+
"dependencies",
|
|
15427
|
+
"devDependencies",
|
|
15428
|
+
"peerDependencies",
|
|
15429
|
+
"optionalDependencies"
|
|
15430
|
+
];
|
|
15431
|
+
const iterateDependencyNames = function* (packageJson) {
|
|
15432
|
+
for (const sectionName of DEPENDENCY_SECTION_NAMES) {
|
|
15433
|
+
const section = packageJson[sectionName];
|
|
15434
|
+
if (!section) continue;
|
|
15435
|
+
for (const dependencyName of Object.keys(section)) yield dependencyName;
|
|
15436
|
+
}
|
|
15437
|
+
};
|
|
15438
|
+
const isReactNativeAware = (packageJson) => {
|
|
15439
|
+
if (typeof packageJson["react-native"] === "string") return true;
|
|
15440
|
+
for (const dependencyName of iterateDependencyNames(packageJson)) if (isReactNativeDependencyName(dependencyName)) return true;
|
|
15441
|
+
return false;
|
|
15442
|
+
};
|
|
15443
|
+
const isExpoManaged = (packageJson) => {
|
|
15444
|
+
for (const dependencyName of iterateDependencyNames(packageJson)) if (isExpoManagedDependencyName(dependencyName)) return true;
|
|
15445
|
+
return false;
|
|
15446
|
+
};
|
|
15447
|
+
const isWebFrameworkOnly = (packageJson) => {
|
|
15448
|
+
for (const dependencyName of iterateDependencyNames(packageJson)) if (WEB_FRAMEWORK_DEPENDENCY_NAMES.has(dependencyName)) return true;
|
|
15449
|
+
return false;
|
|
15450
|
+
};
|
|
15451
|
+
const classifyPackagePlatform = (filename) => {
|
|
15452
|
+
const packageDirectory = findNearestPackageDirectory(filename);
|
|
15453
|
+
if (!packageDirectory) return "unknown";
|
|
15454
|
+
const cached = cachedPlatformByPackageDirectory.get(packageDirectory);
|
|
15455
|
+
if (cached !== void 0) return cached;
|
|
15456
|
+
const packageJson = readPackageJsonSafe(path.join(packageDirectory, "package.json"));
|
|
15457
|
+
if (!packageJson) {
|
|
15458
|
+
cachedPlatformByPackageDirectory.set(packageDirectory, "unknown");
|
|
15459
|
+
return "unknown";
|
|
15460
|
+
}
|
|
15461
|
+
let result;
|
|
15462
|
+
if (isExpoManaged(packageJson)) result = "expo";
|
|
15463
|
+
else if (isReactNativeAware(packageJson)) result = "react-native";
|
|
15464
|
+
else if (isWebFrameworkOnly(packageJson)) result = "web";
|
|
15465
|
+
else result = "unknown";
|
|
15466
|
+
cachedPlatformByPackageDirectory.set(packageDirectory, result);
|
|
15467
|
+
return result;
|
|
15468
|
+
};
|
|
15469
|
+
//#endregion
|
|
15470
|
+
//#region src/plugin/utils/get-react-doctor-setting.ts
|
|
15471
|
+
const readReactDoctorSettingsBag = (settings) => {
|
|
15472
|
+
const reactDoctorSettings = settings?.["react-doctor"];
|
|
15473
|
+
if (typeof reactDoctorSettings !== "object" || reactDoctorSettings === null || Array.isArray(reactDoctorSettings)) return null;
|
|
15474
|
+
return reactDoctorSettings;
|
|
15475
|
+
};
|
|
15476
|
+
const readOwnPropertyValue = (bag, settingName) => Object.getOwnPropertyDescriptor(bag, settingName)?.value;
|
|
15477
|
+
const getReactDoctorStringSetting = (settings, settingName) => {
|
|
15478
|
+
const bag = readReactDoctorSettingsBag(settings);
|
|
15479
|
+
if (!bag) return void 0;
|
|
15480
|
+
const settingValue = readOwnPropertyValue(bag, settingName);
|
|
15481
|
+
return typeof settingValue === "string" ? settingValue : void 0;
|
|
15482
|
+
};
|
|
15483
|
+
const getReactDoctorNumberSetting = (settings, settingName) => {
|
|
15484
|
+
const bag = readReactDoctorSettingsBag(settings);
|
|
15485
|
+
if (!bag) return void 0;
|
|
15486
|
+
const settingValue = readOwnPropertyValue(bag, settingName);
|
|
15487
|
+
return typeof settingValue === "number" && Number.isFinite(settingValue) ? settingValue : void 0;
|
|
15488
|
+
};
|
|
15489
|
+
const getReactDoctorStringArraySetting = (settings, settingName) => {
|
|
15490
|
+
const bag = readReactDoctorSettingsBag(settings);
|
|
15491
|
+
if (!bag) return [];
|
|
15492
|
+
const settingValue = readOwnPropertyValue(bag, settingName);
|
|
15493
|
+
if (!Array.isArray(settingValue)) return [];
|
|
15494
|
+
return settingValue.filter((entry) => typeof entry === "string" && entry.length > 0);
|
|
15495
|
+
};
|
|
15496
|
+
//#endregion
|
|
15497
|
+
//#region src/plugin/utils/is-react-native-file.ts
|
|
15498
|
+
const WEB_FILE_EXTENSION_PATTERN = /\.web\.[cm]?[jt]sx?$/;
|
|
15499
|
+
const NATIVE_FILE_EXTENSION_PATTERN = /\.(?:ios|android|native)\.[cm]?[jt]sx?$/;
|
|
15500
|
+
const classifyReactNativeFileTarget = (context) => {
|
|
15501
|
+
const rawFilename = context.filename;
|
|
15502
|
+
if (!rawFilename) return "unknown";
|
|
15503
|
+
const filename = normalizeFilename$1(rawFilename);
|
|
15504
|
+
if (NATIVE_FILE_EXTENSION_PATTERN.test(filename)) return "react-native";
|
|
15505
|
+
if (WEB_FILE_EXTENSION_PATTERN.test(filename)) return "web";
|
|
15506
|
+
const packagePlatform = classifyPackagePlatform(filename);
|
|
15507
|
+
if (packagePlatform === "web") return "web";
|
|
15508
|
+
if (packagePlatform === "expo" || packagePlatform === "react-native") return "react-native";
|
|
15509
|
+
const framework = getReactDoctorStringSetting(context.settings, "framework");
|
|
15510
|
+
if (framework === "react-native" || framework === "expo") return "react-native";
|
|
15511
|
+
if (framework === "nextjs" || framework === "vite" || framework === "cra" || framework === "remix" || framework === "gatsby" || framework === "tanstack-start") return "web";
|
|
15512
|
+
return "unknown";
|
|
15513
|
+
};
|
|
15514
|
+
const isReactNativeFileActive = (context) => classifyReactNativeFileTarget(context) !== "web";
|
|
15515
|
+
//#endregion
|
|
15327
15516
|
//#region src/plugin/rules/bundle-size/no-barrel-import.ts
|
|
15328
15517
|
const getLiteralName = (node) => {
|
|
15329
15518
|
if (node.type === "Identifier" && typeof node.name === "string") return node.name;
|
|
@@ -15341,7 +15530,8 @@ const getRuntimeImportRequests = (node) => {
|
|
|
15341
15530
|
return [{ importedName: null }];
|
|
15342
15531
|
});
|
|
15343
15532
|
};
|
|
15344
|
-
const buildReportMessage = (filename, barrelFilePath, importRequests) => {
|
|
15533
|
+
const buildReportMessage = (filename, barrelFilePath, importRequests, isReactNativeTarget) => {
|
|
15534
|
+
const costSentence = isReactNativeTarget ? "This ships extra code in your app bundle & slows startup." : "This ships extra code to your users & slows page load.";
|
|
15345
15535
|
const directImportSources = /* @__PURE__ */ new Set();
|
|
15346
15536
|
for (const request of importRequests) {
|
|
15347
15537
|
if (!request.importedName) continue;
|
|
@@ -15350,9 +15540,9 @@ const buildReportMessage = (filename, barrelFilePath, importRequests) => {
|
|
|
15350
15540
|
}
|
|
15351
15541
|
if (directImportSources.size === 1) {
|
|
15352
15542
|
const [directImportSource] = directImportSources;
|
|
15353
|
-
return
|
|
15543
|
+
return `${costSentence} Import directly from "${directImportSource}".`;
|
|
15354
15544
|
}
|
|
15355
|
-
if (directImportSources.size > 1) return
|
|
15545
|
+
if (directImportSources.size > 1) return `${costSentence} Import directly from: ${[...directImportSources].map((source) => `"${source}"`).join(", ")}.`;
|
|
15356
15546
|
return "Importing from an index file pulls in extra code. Import directly from the source file instead.";
|
|
15357
15547
|
};
|
|
15358
15548
|
const noBarrelImport = defineRule({
|
|
@@ -15376,7 +15566,7 @@ const noBarrelImport = defineRule({
|
|
|
15376
15566
|
didReportForFile = true;
|
|
15377
15567
|
context.report({
|
|
15378
15568
|
node,
|
|
15379
|
-
message: buildReportMessage(filename, resolvedImportPath, importRequests)
|
|
15569
|
+
message: buildReportMessage(filename, resolvedImportPath, importRequests, classifyReactNativeFileTarget(context) === "react-native")
|
|
15380
15570
|
});
|
|
15381
15571
|
}
|
|
15382
15572
|
} };
|
|
@@ -20669,33 +20859,6 @@ const noPolymorphicChildren = defineRule({
|
|
|
20669
20859
|
} })
|
|
20670
20860
|
});
|
|
20671
20861
|
//#endregion
|
|
20672
|
-
//#region src/plugin/utils/get-react-doctor-setting.ts
|
|
20673
|
-
const readReactDoctorSettingsBag = (settings) => {
|
|
20674
|
-
const reactDoctorSettings = settings?.["react-doctor"];
|
|
20675
|
-
if (typeof reactDoctorSettings !== "object" || reactDoctorSettings === null || Array.isArray(reactDoctorSettings)) return null;
|
|
20676
|
-
return reactDoctorSettings;
|
|
20677
|
-
};
|
|
20678
|
-
const readOwnPropertyValue = (bag, settingName) => Object.getOwnPropertyDescriptor(bag, settingName)?.value;
|
|
20679
|
-
const getReactDoctorStringSetting = (settings, settingName) => {
|
|
20680
|
-
const bag = readReactDoctorSettingsBag(settings);
|
|
20681
|
-
if (!bag) return void 0;
|
|
20682
|
-
const settingValue = readOwnPropertyValue(bag, settingName);
|
|
20683
|
-
return typeof settingValue === "string" ? settingValue : void 0;
|
|
20684
|
-
};
|
|
20685
|
-
const getReactDoctorNumberSetting = (settings, settingName) => {
|
|
20686
|
-
const bag = readReactDoctorSettingsBag(settings);
|
|
20687
|
-
if (!bag) return void 0;
|
|
20688
|
-
const settingValue = readOwnPropertyValue(bag, settingName);
|
|
20689
|
-
return typeof settingValue === "number" && Number.isFinite(settingValue) ? settingValue : void 0;
|
|
20690
|
-
};
|
|
20691
|
-
const getReactDoctorStringArraySetting = (settings, settingName) => {
|
|
20692
|
-
const bag = readReactDoctorSettingsBag(settings);
|
|
20693
|
-
if (!bag) return [];
|
|
20694
|
-
const settingValue = readOwnPropertyValue(bag, settingName);
|
|
20695
|
-
if (!Array.isArray(settingValue)) return [];
|
|
20696
|
-
return settingValue.filter((entry) => typeof entry === "string" && entry.length > 0);
|
|
20697
|
-
};
|
|
20698
|
-
//#endregion
|
|
20699
20862
|
//#region src/plugin/rules/correctness/no-prevent-default.ts
|
|
20700
20863
|
const PREVENT_DEFAULT_ELEMENTS = new Map([["form", ["onSubmit"]], ["a", ["onClick"]]]);
|
|
20701
20864
|
const SERVER_CAPABLE_FRAMEWORKS = new Set([
|
|
@@ -24344,6 +24507,21 @@ const noZIndex9999 = defineRule({
|
|
|
24344
24507
|
})
|
|
24345
24508
|
});
|
|
24346
24509
|
//#endregion
|
|
24510
|
+
//#region src/plugin/utils/is-framework-route-or-special-filename.ts
|
|
24511
|
+
const sourceFileExtensionGroup = NEXTJS_SOURCE_FILE_EXTENSION_GROUP;
|
|
24512
|
+
const FRAMEWORK_ROUTE_FILE_PATTERNS = [
|
|
24513
|
+
new RegExp(`^(page|layout|loading|error|not-found|template|default|global-error|route|_app|_document|_error)\\.${sourceFileExtensionGroup}$`),
|
|
24514
|
+
new RegExp(`^(_layout|\\+html|\\+not-found|\\+native-intent)\\.${sourceFileExtensionGroup}$`),
|
|
24515
|
+
new RegExp(`(?:^__root|\\.lazy)\\.${sourceFileExtensionGroup}$`),
|
|
24516
|
+
new RegExp(`^(root|entry\\.client|entry\\.server)\\.${sourceFileExtensionGroup}$`)
|
|
24517
|
+
];
|
|
24518
|
+
const isFrameworkRouteOrSpecialFilename = (rawFilename) => {
|
|
24519
|
+
if (!rawFilename) return false;
|
|
24520
|
+
if (isNextjsMetadataImageRouteFilename(rawFilename)) return true;
|
|
24521
|
+
const basename = path.basename(rawFilename);
|
|
24522
|
+
return FRAMEWORK_ROUTE_FILE_PATTERNS.some((pattern) => pattern.test(basename));
|
|
24523
|
+
};
|
|
24524
|
+
//#endregion
|
|
24347
24525
|
//#region src/plugin/rules/react-builtins/only-export-components-tables.ts
|
|
24348
24526
|
const NOT_REACT_COMPONENT_EXPRESSION_TYPES = new Set([
|
|
24349
24527
|
"ArrayExpression",
|
|
@@ -24395,32 +24573,6 @@ const ENTRY_POINT_BASENAMES = new Set([
|
|
|
24395
24573
|
"client.jsx",
|
|
24396
24574
|
"server.tsx",
|
|
24397
24575
|
"server.jsx",
|
|
24398
|
-
"page.tsx",
|
|
24399
|
-
"page.jsx",
|
|
24400
|
-
"layout.tsx",
|
|
24401
|
-
"layout.jsx",
|
|
24402
|
-
"loading.tsx",
|
|
24403
|
-
"loading.jsx",
|
|
24404
|
-
"error.tsx",
|
|
24405
|
-
"error.jsx",
|
|
24406
|
-
"not-found.tsx",
|
|
24407
|
-
"not-found.jsx",
|
|
24408
|
-
"template.tsx",
|
|
24409
|
-
"template.jsx",
|
|
24410
|
-
"default.tsx",
|
|
24411
|
-
"default.jsx",
|
|
24412
|
-
"global-error.tsx",
|
|
24413
|
-
"global-error.jsx",
|
|
24414
|
-
"route.tsx",
|
|
24415
|
-
"route.jsx",
|
|
24416
|
-
"_layout.tsx",
|
|
24417
|
-
"_layout.jsx",
|
|
24418
|
-
"_app.tsx",
|
|
24419
|
-
"_app.jsx",
|
|
24420
|
-
"_document.tsx",
|
|
24421
|
-
"_document.jsx",
|
|
24422
|
-
"_error.tsx",
|
|
24423
|
-
"_error.jsx",
|
|
24424
24576
|
"app.tsx",
|
|
24425
24577
|
"app.jsx",
|
|
24426
24578
|
"App.tsx",
|
|
@@ -24713,6 +24865,7 @@ const isFileNameAllowed = (filename, checkJS) => {
|
|
|
24713
24865
|
if (filename.includes(".test.") || filename.includes(".spec.") || filename.includes(".cy.") || filename.includes(".stories.")) return false;
|
|
24714
24866
|
for (const segment of NON_FAST_REFRESH_PATH_SEGMENTS) if (filename.includes(segment)) return false;
|
|
24715
24867
|
if (isEntryPointFile(filename)) return false;
|
|
24868
|
+
if (isFrameworkRouteOrSpecialFilename(filename)) return false;
|
|
24716
24869
|
if (isAssetOrUtilityFile(filename)) return false;
|
|
24717
24870
|
if (filename.endsWith(".tsx") || filename.endsWith(".jsx")) return true;
|
|
24718
24871
|
if (checkJS && filename.endsWith(".js")) return true;
|
|
@@ -28585,29 +28738,236 @@ const isInsidePlatformOsWebBranch = (node) => {
|
|
|
28585
28738
|
//#endregion
|
|
28586
28739
|
//#region src/plugin/rules/react-native/utils/collect-text-wrapper-components.ts
|
|
28587
28740
|
const isFunctionNode = (node) => isNodeOfType(node, "ArrowFunctionExpression") || isNodeOfType(node, "FunctionExpression") || isNodeOfType(node, "FunctionDeclaration");
|
|
28588
|
-
const
|
|
28741
|
+
const COMPONENT_WRAPPER_CALLEE_NAMES = new Set(["memo", "forwardRef"]);
|
|
28742
|
+
const resolveCalleeName = (callee) => {
|
|
28743
|
+
if (isNodeOfType(callee, "Identifier")) return callee.name;
|
|
28744
|
+
if (isNodeOfType(callee, "MemberExpression") && isNodeOfType(callee.property, "Identifier")) return callee.property.name;
|
|
28745
|
+
return null;
|
|
28746
|
+
};
|
|
28747
|
+
const unwrapComponentDefinition = (node) => {
|
|
28748
|
+
let current = stripParenExpression(node);
|
|
28749
|
+
while (isNodeOfType(current, "CallExpression")) {
|
|
28750
|
+
const calleeName = resolveCalleeName(current.callee);
|
|
28751
|
+
const firstArgument = current.arguments?.[0];
|
|
28752
|
+
if (!calleeName || !COMPONENT_WRAPPER_CALLEE_NAMES.has(calleeName) || !firstArgument) break;
|
|
28753
|
+
current = stripParenExpression(firstArgument);
|
|
28754
|
+
}
|
|
28755
|
+
return current;
|
|
28756
|
+
};
|
|
28757
|
+
const resolveChildrenPropertyLocalName = (property) => {
|
|
28758
|
+
if (!isNodeOfType(property, "Property")) return null;
|
|
28759
|
+
if (!isNodeOfType(property.key, "Identifier") || property.key.name !== "children") return null;
|
|
28760
|
+
const value = property.value;
|
|
28761
|
+
if (isNodeOfType(value, "Identifier")) return value.name;
|
|
28762
|
+
if (isNodeOfType(value, "AssignmentPattern") && isNodeOfType(value.left, "Identifier")) return value.left.name;
|
|
28763
|
+
return null;
|
|
28764
|
+
};
|
|
28765
|
+
const resolveParamChildrenBindings = (functionNode) => {
|
|
28766
|
+
const bindings = {
|
|
28767
|
+
childrenNames: /* @__PURE__ */ new Set(),
|
|
28768
|
+
propsObjectNames: /* @__PURE__ */ new Set()
|
|
28769
|
+
};
|
|
28770
|
+
const firstParam = functionNode.params?.[0];
|
|
28771
|
+
if (!firstParam) return bindings;
|
|
28772
|
+
if (isNodeOfType(firstParam, "Identifier")) {
|
|
28773
|
+
bindings.propsObjectNames.add(firstParam.name);
|
|
28774
|
+
return bindings;
|
|
28775
|
+
}
|
|
28776
|
+
if (!isNodeOfType(firstParam, "ObjectPattern")) return bindings;
|
|
28777
|
+
let didDestructureChildren = false;
|
|
28778
|
+
let restName = null;
|
|
28779
|
+
for (const property of firstParam.properties ?? []) {
|
|
28780
|
+
if (isNodeOfType(property, "RestElement") && isNodeOfType(property.argument, "Identifier")) {
|
|
28781
|
+
restName = property.argument.name;
|
|
28782
|
+
continue;
|
|
28783
|
+
}
|
|
28784
|
+
const localName = resolveChildrenPropertyLocalName(property);
|
|
28785
|
+
if (localName) {
|
|
28786
|
+
didDestructureChildren = true;
|
|
28787
|
+
bindings.childrenNames.add(localName);
|
|
28788
|
+
}
|
|
28789
|
+
}
|
|
28790
|
+
if (restName && !didDestructureChildren) bindings.propsObjectNames.add(restName);
|
|
28791
|
+
return bindings;
|
|
28792
|
+
};
|
|
28793
|
+
const MAX_CHILDREN_ALIAS_PASSES = 3;
|
|
28794
|
+
const isPropsObjectExpression = (expression, bindings) => {
|
|
28795
|
+
if (!expression) return false;
|
|
28796
|
+
const value = stripParenExpression(expression);
|
|
28797
|
+
if (isNodeOfType(value, "Identifier")) return bindings.propsObjectNames.has(value.name);
|
|
28798
|
+
return isNodeOfType(value, "MemberExpression") && isNodeOfType(value.object, "ThisExpression") && isNodeOfType(value.property, "Identifier") && value.property.name === "props";
|
|
28799
|
+
};
|
|
28800
|
+
const isChildrenValueExpression = (expression, bindings) => {
|
|
28801
|
+
if (!expression) return false;
|
|
28802
|
+
const value = stripParenExpression(expression);
|
|
28803
|
+
if (isNodeOfType(value, "Identifier")) return bindings.childrenNames.has(value.name);
|
|
28804
|
+
return isNodeOfType(value, "MemberExpression") && isNodeOfType(value.property, "Identifier") && value.property.name === "children" && isPropsObjectExpression(value.object, bindings);
|
|
28805
|
+
};
|
|
28806
|
+
const collectChildrenAliases = (functionNode, bindings) => {
|
|
28589
28807
|
const { body } = functionNode;
|
|
28590
|
-
if (!body) return
|
|
28591
|
-
|
|
28592
|
-
|
|
28593
|
-
|
|
28594
|
-
|
|
28595
|
-
|
|
28808
|
+
if (!body || !isNodeOfType(body, "BlockStatement")) return;
|
|
28809
|
+
for (let pass = 0; pass < MAX_CHILDREN_ALIAS_PASSES; pass += 1) {
|
|
28810
|
+
const sizeBeforePass = bindings.childrenNames.size;
|
|
28811
|
+
walkAst(body, (node) => {
|
|
28812
|
+
if (isFunctionNode(node)) return false;
|
|
28813
|
+
if (!isNodeOfType(node, "VariableDeclarator") || !node.init) return void 0;
|
|
28814
|
+
if (isNodeOfType(node.id, "Identifier")) {
|
|
28815
|
+
if (isChildrenValueExpression(node.init, bindings)) bindings.childrenNames.add(node.id.name);
|
|
28816
|
+
return;
|
|
28817
|
+
}
|
|
28818
|
+
if (isNodeOfType(node.id, "ObjectPattern") && isPropsObjectExpression(node.init, bindings)) for (const property of node.id.properties ?? []) {
|
|
28819
|
+
const localName = resolveChildrenPropertyLocalName(property);
|
|
28820
|
+
if (localName) bindings.childrenNames.add(localName);
|
|
28821
|
+
}
|
|
28822
|
+
});
|
|
28823
|
+
if (bindings.childrenNames.size === sizeBeforePass) break;
|
|
28824
|
+
}
|
|
28825
|
+
};
|
|
28826
|
+
const collectJsxRootsFromExpression = (expression, roots) => {
|
|
28827
|
+
const value = stripParenExpression(expression);
|
|
28828
|
+
if (isNodeOfType(value, "JSXElement") || isNodeOfType(value, "JSXFragment")) {
|
|
28829
|
+
roots.push(value);
|
|
28830
|
+
return;
|
|
28831
|
+
}
|
|
28832
|
+
if (isNodeOfType(value, "ConditionalExpression")) {
|
|
28833
|
+
if (value.consequent) collectJsxRootsFromExpression(value.consequent, roots);
|
|
28834
|
+
if (value.alternate) collectJsxRootsFromExpression(value.alternate, roots);
|
|
28835
|
+
return;
|
|
28836
|
+
}
|
|
28837
|
+
if (isNodeOfType(value, "LogicalExpression")) {
|
|
28838
|
+
if (value.left) collectJsxRootsFromExpression(value.left, roots);
|
|
28839
|
+
if (value.right) collectJsxRootsFromExpression(value.right, roots);
|
|
28840
|
+
}
|
|
28841
|
+
};
|
|
28842
|
+
const collectReturnedJsxRoots = (functionNode) => {
|
|
28843
|
+
const roots = [];
|
|
28844
|
+
const { body } = functionNode;
|
|
28845
|
+
if (!body) return roots;
|
|
28846
|
+
if (!isNodeOfType(body, "BlockStatement")) {
|
|
28847
|
+
collectJsxRootsFromExpression(body, roots);
|
|
28848
|
+
return roots;
|
|
28849
|
+
}
|
|
28850
|
+
walkAst(body, (node) => {
|
|
28851
|
+
if (isFunctionNode(node) && node !== functionNode) return false;
|
|
28852
|
+
if (isNodeOfType(node, "ReturnStatement") && node.argument) {
|
|
28853
|
+
collectJsxRootsFromExpression(node.argument, roots);
|
|
28854
|
+
return false;
|
|
28855
|
+
}
|
|
28856
|
+
});
|
|
28857
|
+
return roots;
|
|
28858
|
+
};
|
|
28859
|
+
const isChildrenForwardingJsxChild = (child, bindings) => isNodeOfType(child, "JSXExpressionContainer") && isChildrenValueExpression(child.expression, bindings);
|
|
28860
|
+
const isChildrenForwardingAttribute = (attribute, bindings) => {
|
|
28861
|
+
if (isNodeOfType(attribute, "JSXSpreadAttribute")) return isPropsObjectExpression(attribute.argument, bindings);
|
|
28862
|
+
return isNodeOfType(attribute, "JSXAttribute") && isNodeOfType(attribute.name, "JSXIdentifier") && attribute.name.name === "children" && isNodeOfType(attribute.value, "JSXExpressionContainer") && isChildrenValueExpression(attribute.value.expression, bindings);
|
|
28863
|
+
};
|
|
28864
|
+
const jsxRootForwardsChildrenIntoText = (jsxRoot, bindings, isTextHandlingElement) => {
|
|
28865
|
+
let didForwardIntoText = false;
|
|
28866
|
+
walkAst(jsxRoot, (node) => {
|
|
28867
|
+
if (didForwardIntoText || isFunctionNode(node)) return false;
|
|
28868
|
+
if (!isNodeOfType(node, "JSXElement")) return void 0;
|
|
28869
|
+
const elementName = resolveJsxElementName(node.openingElement);
|
|
28870
|
+
if (!elementName || !isTextHandlingElement(elementName)) return;
|
|
28871
|
+
didForwardIntoText = (node.children ?? []).some((child) => isChildrenForwardingJsxChild(child, bindings)) || (node.openingElement.attributes ?? []).some((attribute) => isChildrenForwardingAttribute(attribute, bindings));
|
|
28872
|
+
});
|
|
28873
|
+
return didForwardIntoText;
|
|
28874
|
+
};
|
|
28875
|
+
const isMeaningfulJsxChild = (child) => !isNodeOfType(child, "JSXText") || Boolean(child.value?.trim());
|
|
28876
|
+
const jsxRootRendersChildrenOutsideText = (jsxRoot, bindings, isTextHandlingElement) => {
|
|
28877
|
+
let didRenderOutsideText = false;
|
|
28878
|
+
walkAst(jsxRoot, (node) => {
|
|
28879
|
+
if (didRenderOutsideText || isFunctionNode(node)) return false;
|
|
28880
|
+
if (!isNodeOfType(node, "JSXElement") && !isNodeOfType(node, "JSXFragment")) return;
|
|
28881
|
+
if (isNodeOfType(node, "JSXElement")) {
|
|
28882
|
+
const elementName = resolveJsxElementName(node.openingElement);
|
|
28883
|
+
if (elementName && isTextHandlingElement(elementName)) return false;
|
|
28884
|
+
if (!(node.children ?? []).some(isMeaningfulJsxChild) && (node.openingElement.attributes ?? []).some((attribute) => isChildrenForwardingAttribute(attribute, bindings))) {
|
|
28885
|
+
didRenderOutsideText = true;
|
|
28886
|
+
return;
|
|
28887
|
+
}
|
|
28888
|
+
}
|
|
28889
|
+
didRenderOutsideText = (node.children ?? []).some((child) => isChildrenForwardingJsxChild(child, bindings));
|
|
28890
|
+
});
|
|
28891
|
+
return didRenderOutsideText;
|
|
28892
|
+
};
|
|
28893
|
+
const resolveStyledFactoryBaseName = (definitionNode) => {
|
|
28894
|
+
let current = stripParenExpression(definitionNode);
|
|
28895
|
+
while (current) {
|
|
28896
|
+
if (isNodeOfType(current, "TaggedTemplateExpression")) {
|
|
28897
|
+
current = stripParenExpression(current.tag);
|
|
28898
|
+
continue;
|
|
28899
|
+
}
|
|
28900
|
+
if (isNodeOfType(current, "CallExpression")) {
|
|
28901
|
+
const callee = stripParenExpression(current.callee);
|
|
28902
|
+
if (isNodeOfType(callee, "Identifier") && callee.name === "styled") {
|
|
28903
|
+
const baseArgument = current.arguments?.[0];
|
|
28904
|
+
if (!baseArgument) return null;
|
|
28905
|
+
const base = stripParenExpression(baseArgument);
|
|
28906
|
+
return isNodeOfType(base, "Identifier") ? base.name : null;
|
|
28907
|
+
}
|
|
28908
|
+
current = callee;
|
|
28909
|
+
continue;
|
|
28910
|
+
}
|
|
28911
|
+
if (isNodeOfType(current, "MemberExpression")) {
|
|
28912
|
+
if (isNodeOfType(current.object, "Identifier") && current.object.name === "styled" && isNodeOfType(current.property, "Identifier")) return current.property.name;
|
|
28913
|
+
current = stripParenExpression(current.object);
|
|
28914
|
+
continue;
|
|
28915
|
+
}
|
|
28916
|
+
return null;
|
|
28917
|
+
}
|
|
28918
|
+
return null;
|
|
28919
|
+
};
|
|
28920
|
+
const resolveClassRenderFunction = (classNode) => {
|
|
28921
|
+
if (!isNodeOfType(classNode, "ClassDeclaration") && !isNodeOfType(classNode, "ClassExpression")) return null;
|
|
28922
|
+
for (const member of classNode.body?.body ?? []) {
|
|
28923
|
+
if (!isNodeOfType(member, "MethodDefinition")) continue;
|
|
28924
|
+
if (!isNodeOfType(member.key, "Identifier") || member.key.name !== "render") continue;
|
|
28925
|
+
return member.value && isFunctionNode(member.value) ? member.value : null;
|
|
28596
28926
|
}
|
|
28597
28927
|
return null;
|
|
28598
28928
|
};
|
|
28599
|
-
const recordWrapperFromDeclaration = (componentName,
|
|
28929
|
+
const recordWrapperFromDeclaration = (componentName, definitionNode, isTextHandlingElement, wrappers) => {
|
|
28600
28930
|
if (!componentName || !isReactComponentName(componentName)) return;
|
|
28601
|
-
if (
|
|
28602
|
-
|
|
28603
|
-
|
|
28931
|
+
if (wrappers.has(componentName)) return;
|
|
28932
|
+
if (!definitionNode) return;
|
|
28933
|
+
const unwrapped = unwrapComponentDefinition(definitionNode);
|
|
28934
|
+
const styledBaseName = resolveStyledFactoryBaseName(unwrapped);
|
|
28935
|
+
if (styledBaseName && isTextHandlingElement(styledBaseName)) {
|
|
28936
|
+
wrappers.add(componentName);
|
|
28937
|
+
return;
|
|
28938
|
+
}
|
|
28939
|
+
const functionNode = resolveClassRenderFunction(unwrapped) ?? (isFunctionNode(unwrapped) ? unwrapped : null);
|
|
28940
|
+
if (!functionNode) return;
|
|
28941
|
+
const bindings = resolveParamChildrenBindings(functionNode);
|
|
28942
|
+
collectChildrenAliases(functionNode, bindings);
|
|
28943
|
+
const jsxRoots = collectReturnedJsxRoots(functionNode);
|
|
28944
|
+
if (jsxRoots.some((jsxRoot) => jsxRootRendersChildrenOutsideText(jsxRoot, bindings, isTextHandlingElement))) return;
|
|
28945
|
+
for (const jsxRoot of jsxRoots) {
|
|
28946
|
+
if (isNodeOfType(jsxRoot, "JSXElement")) {
|
|
28947
|
+
const rootName = resolveJsxElementName(jsxRoot.openingElement);
|
|
28948
|
+
if (rootName && isTextHandlingElement(rootName)) {
|
|
28949
|
+
wrappers.add(componentName);
|
|
28950
|
+
return;
|
|
28951
|
+
}
|
|
28952
|
+
}
|
|
28953
|
+
if (jsxRootForwardsChildrenIntoText(jsxRoot, bindings, isTextHandlingElement)) {
|
|
28954
|
+
wrappers.add(componentName);
|
|
28955
|
+
return;
|
|
28956
|
+
}
|
|
28957
|
+
}
|
|
28604
28958
|
};
|
|
28959
|
+
const MAX_TRANSITIVE_WRAPPER_PASSES = 3;
|
|
28605
28960
|
const collectTextWrapperComponents = (programNode, isTextHandlingRoot) => {
|
|
28606
28961
|
const wrappers = /* @__PURE__ */ new Set();
|
|
28607
|
-
|
|
28608
|
-
|
|
28609
|
-
|
|
28610
|
-
|
|
28962
|
+
const isTextHandlingElement = (elementName) => isTextHandlingRoot(elementName) || wrappers.has(elementName);
|
|
28963
|
+
for (let pass = 0; pass < MAX_TRANSITIVE_WRAPPER_PASSES; pass += 1) {
|
|
28964
|
+
const sizeBeforePass = wrappers.size;
|
|
28965
|
+
walkAst(programNode, (node) => {
|
|
28966
|
+
if (isNodeOfType(node, "VariableDeclarator")) recordWrapperFromDeclaration(node.id && isNodeOfType(node.id, "Identifier") ? node.id.name : null, node.init, isTextHandlingElement, wrappers);
|
|
28967
|
+
else if (isNodeOfType(node, "FunctionDeclaration") || isNodeOfType(node, "ClassDeclaration")) recordWrapperFromDeclaration(node.id && isNodeOfType(node.id, "Identifier") ? node.id.name : null, node, isTextHandlingElement, wrappers);
|
|
28968
|
+
});
|
|
28969
|
+
if (wrappers.size === sizeBeforePass) break;
|
|
28970
|
+
}
|
|
28611
28971
|
return wrappers;
|
|
28612
28972
|
};
|
|
28613
28973
|
//#endregion
|
|
@@ -28675,6 +29035,7 @@ const rnNoRawText = defineRule({
|
|
|
28675
29035
|
title: "Raw text outside a Text component",
|
|
28676
29036
|
requires: ["react-native"],
|
|
28677
29037
|
severity: "error",
|
|
29038
|
+
tags: ["test-noise"],
|
|
28678
29039
|
recommendation: "Text outside a `<Text>` component crashes on React Native. Wrap it like `<Text>{value}</Text>`.",
|
|
28679
29040
|
create: (context) => {
|
|
28680
29041
|
let isDomComponentFile = false;
|
|
@@ -28920,136 +29281,6 @@ const rnPreferContentInsetAdjustment = defineRetiredRule({
|
|
|
28920
29281
|
recommendation: "Retired: SafeAreaView wrappers are valid; prefer native content inset adjustment only when manual inset plumbing causes scroll jumps or duplicated safe-area offsets."
|
|
28921
29282
|
});
|
|
28922
29283
|
//#endregion
|
|
28923
|
-
//#region src/react-native-dependency-names.ts
|
|
28924
|
-
const EXPO_MANAGED_DEPENDENCY_NAMES = new Set([
|
|
28925
|
-
"expo",
|
|
28926
|
-
"expo-router",
|
|
28927
|
-
"@expo/cli",
|
|
28928
|
-
"@expo/metro-config",
|
|
28929
|
-
"@expo/metro-runtime"
|
|
28930
|
-
]);
|
|
28931
|
-
const REACT_NATIVE_DEPENDENCY_NAMES = new Set([
|
|
28932
|
-
"react-native",
|
|
28933
|
-
"react-native-tvos",
|
|
28934
|
-
...EXPO_MANAGED_DEPENDENCY_NAMES,
|
|
28935
|
-
"react-native-windows",
|
|
28936
|
-
"react-native-macos"
|
|
28937
|
-
]);
|
|
28938
|
-
const REACT_NATIVE_DEPENDENCY_PREFIXES = ["@react-native/", "@react-native-"];
|
|
28939
|
-
const isExpoManagedDependencyName = (dependencyName) => EXPO_MANAGED_DEPENDENCY_NAMES.has(dependencyName);
|
|
28940
|
-
const isReactNativeDependencyName = (dependencyName) => {
|
|
28941
|
-
if (REACT_NATIVE_DEPENDENCY_NAMES.has(dependencyName)) return true;
|
|
28942
|
-
for (const prefix of REACT_NATIVE_DEPENDENCY_PREFIXES) if (dependencyName.startsWith(prefix)) return true;
|
|
28943
|
-
return false;
|
|
28944
|
-
};
|
|
28945
|
-
//#endregion
|
|
28946
|
-
//#region src/plugin/utils/classify-package-platform.ts
|
|
28947
|
-
const WEB_FRAMEWORK_DEPENDENCY_NAMES = new Set([
|
|
28948
|
-
"next",
|
|
28949
|
-
"vite",
|
|
28950
|
-
"react-scripts",
|
|
28951
|
-
"gatsby",
|
|
28952
|
-
"@remix-run/react",
|
|
28953
|
-
"@remix-run/node",
|
|
28954
|
-
"@docusaurus/core",
|
|
28955
|
-
"@docusaurus/preset-classic",
|
|
28956
|
-
"@storybook/react",
|
|
28957
|
-
"@storybook/react-vite",
|
|
28958
|
-
"@storybook/react-webpack5",
|
|
28959
|
-
"@storybook/nextjs",
|
|
28960
|
-
"@storybook/web-components",
|
|
28961
|
-
"storybook",
|
|
28962
|
-
"react-dom",
|
|
28963
|
-
"@vitejs/plugin-react",
|
|
28964
|
-
"@vitejs/plugin-react-swc"
|
|
28965
|
-
]);
|
|
28966
|
-
const cachedPlatformByPackageDirectory = /* @__PURE__ */ new Map();
|
|
28967
|
-
const cachedPackageDirectoryByFilename = /* @__PURE__ */ new Map();
|
|
28968
|
-
const findNearestPackageDirectory = (filename) => {
|
|
28969
|
-
if (!filename) return null;
|
|
28970
|
-
const fromCache = cachedPackageDirectoryByFilename.get(filename);
|
|
28971
|
-
if (fromCache !== void 0) return fromCache;
|
|
28972
|
-
let currentDirectory = path.dirname(filename);
|
|
28973
|
-
while (true) {
|
|
28974
|
-
const candidatePackageJsonPath = path.join(currentDirectory, "package.json");
|
|
28975
|
-
let hasPackageJson = false;
|
|
28976
|
-
try {
|
|
28977
|
-
hasPackageJson = fs.statSync(candidatePackageJsonPath).isFile();
|
|
28978
|
-
} catch {
|
|
28979
|
-
hasPackageJson = false;
|
|
28980
|
-
}
|
|
28981
|
-
if (hasPackageJson) {
|
|
28982
|
-
cachedPackageDirectoryByFilename.set(filename, currentDirectory);
|
|
28983
|
-
return currentDirectory;
|
|
28984
|
-
}
|
|
28985
|
-
const parentDirectory = path.dirname(currentDirectory);
|
|
28986
|
-
if (parentDirectory === currentDirectory) {
|
|
28987
|
-
cachedPackageDirectoryByFilename.set(filename, null);
|
|
28988
|
-
return null;
|
|
28989
|
-
}
|
|
28990
|
-
currentDirectory = parentDirectory;
|
|
28991
|
-
}
|
|
28992
|
-
};
|
|
28993
|
-
const readPackageJsonSafe = (packageJsonPath) => {
|
|
28994
|
-
let rawContents;
|
|
28995
|
-
try {
|
|
28996
|
-
rawContents = fs.readFileSync(packageJsonPath, "utf-8");
|
|
28997
|
-
} catch {
|
|
28998
|
-
return null;
|
|
28999
|
-
}
|
|
29000
|
-
try {
|
|
29001
|
-
const parsed = JSON.parse(rawContents);
|
|
29002
|
-
if (typeof parsed === "object" && parsed !== null) return parsed;
|
|
29003
|
-
return null;
|
|
29004
|
-
} catch {
|
|
29005
|
-
return null;
|
|
29006
|
-
}
|
|
29007
|
-
};
|
|
29008
|
-
const DEPENDENCY_SECTION_NAMES = [
|
|
29009
|
-
"dependencies",
|
|
29010
|
-
"devDependencies",
|
|
29011
|
-
"peerDependencies",
|
|
29012
|
-
"optionalDependencies"
|
|
29013
|
-
];
|
|
29014
|
-
const iterateDependencyNames = function* (packageJson) {
|
|
29015
|
-
for (const sectionName of DEPENDENCY_SECTION_NAMES) {
|
|
29016
|
-
const section = packageJson[sectionName];
|
|
29017
|
-
if (!section) continue;
|
|
29018
|
-
for (const dependencyName of Object.keys(section)) yield dependencyName;
|
|
29019
|
-
}
|
|
29020
|
-
};
|
|
29021
|
-
const isReactNativeAware = (packageJson) => {
|
|
29022
|
-
if (typeof packageJson["react-native"] === "string") return true;
|
|
29023
|
-
for (const dependencyName of iterateDependencyNames(packageJson)) if (isReactNativeDependencyName(dependencyName)) return true;
|
|
29024
|
-
return false;
|
|
29025
|
-
};
|
|
29026
|
-
const isExpoManaged = (packageJson) => {
|
|
29027
|
-
for (const dependencyName of iterateDependencyNames(packageJson)) if (isExpoManagedDependencyName(dependencyName)) return true;
|
|
29028
|
-
return false;
|
|
29029
|
-
};
|
|
29030
|
-
const isWebFrameworkOnly = (packageJson) => {
|
|
29031
|
-
for (const dependencyName of iterateDependencyNames(packageJson)) if (WEB_FRAMEWORK_DEPENDENCY_NAMES.has(dependencyName)) return true;
|
|
29032
|
-
return false;
|
|
29033
|
-
};
|
|
29034
|
-
const classifyPackagePlatform = (filename) => {
|
|
29035
|
-
const packageDirectory = findNearestPackageDirectory(filename);
|
|
29036
|
-
if (!packageDirectory) return "unknown";
|
|
29037
|
-
const cached = cachedPlatformByPackageDirectory.get(packageDirectory);
|
|
29038
|
-
if (cached !== void 0) return cached;
|
|
29039
|
-
const packageJson = readPackageJsonSafe(path.join(packageDirectory, "package.json"));
|
|
29040
|
-
if (!packageJson) {
|
|
29041
|
-
cachedPlatformByPackageDirectory.set(packageDirectory, "unknown");
|
|
29042
|
-
return "unknown";
|
|
29043
|
-
}
|
|
29044
|
-
let result;
|
|
29045
|
-
if (isExpoManaged(packageJson)) result = "expo";
|
|
29046
|
-
else if (isReactNativeAware(packageJson)) result = "react-native";
|
|
29047
|
-
else if (isWebFrameworkOnly(packageJson)) result = "web";
|
|
29048
|
-
else result = "unknown";
|
|
29049
|
-
cachedPlatformByPackageDirectory.set(packageDirectory, result);
|
|
29050
|
-
return result;
|
|
29051
|
-
};
|
|
29052
|
-
//#endregion
|
|
29053
29284
|
//#region src/plugin/utils/is-expo-managed-file.ts
|
|
29054
29285
|
const isExpoManagedFileActive = (context) => {
|
|
29055
29286
|
const rawFilename = context.filename;
|
|
@@ -39085,24 +39316,6 @@ const reactDoctorRules = [
|
|
|
39085
39316
|
];
|
|
39086
39317
|
const ruleRegistry = Object.fromEntries(reactDoctorRules.map((rule) => [rule.id, rule.rule]));
|
|
39087
39318
|
//#endregion
|
|
39088
|
-
//#region src/plugin/utils/is-react-native-file.ts
|
|
39089
|
-
const WEB_FILE_EXTENSION_PATTERN = /\.web\.[cm]?[jt]sx?$/;
|
|
39090
|
-
const NATIVE_FILE_EXTENSION_PATTERN = /\.(?:ios|android|native)\.[cm]?[jt]sx?$/;
|
|
39091
|
-
const isReactNativeFileActive = (context) => {
|
|
39092
|
-
const rawFilename = context.filename;
|
|
39093
|
-
if (!rawFilename) return true;
|
|
39094
|
-
const filename = normalizeFilename$1(rawFilename);
|
|
39095
|
-
if (NATIVE_FILE_EXTENSION_PATTERN.test(filename)) return true;
|
|
39096
|
-
if (WEB_FILE_EXTENSION_PATTERN.test(filename)) return false;
|
|
39097
|
-
const packagePlatform = classifyPackagePlatform(filename);
|
|
39098
|
-
if (packagePlatform === "web") return false;
|
|
39099
|
-
if (packagePlatform === "expo" || packagePlatform === "react-native") return true;
|
|
39100
|
-
const framework = getReactDoctorStringSetting(context.settings, "framework");
|
|
39101
|
-
if (framework === "react-native" || framework === "expo") return true;
|
|
39102
|
-
if (framework === "nextjs" || framework === "vite" || framework === "cra" || framework === "remix" || framework === "gatsby" || framework === "tanstack-start") return false;
|
|
39103
|
-
return true;
|
|
39104
|
-
};
|
|
39105
|
-
//#endregion
|
|
39106
39319
|
//#region src/plugin/utils/wrap-react-native-rule.ts
|
|
39107
39320
|
const EMPTY_VISITORS = {};
|
|
39108
39321
|
const wrapReactNativeRule = (rule) => {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "oxlint-plugin-react-doctor",
|
|
3
|
-
"version": "0.5.1-dev.
|
|
3
|
+
"version": "0.5.1-dev.fee3fc4",
|
|
4
4
|
"description": "oxlint plugin for React Doctor: diagnose React codebases for security, performance, correctness, accessibility, bundle-size, and architecture issues",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"accessibility",
|