oxlint-plugin-react-doctor 0.5.1-dev.1ca6f0e → 0.5.1-dev.22268f7
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 +93 -90
- package/package.json +2 -2
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) => {
|
|
@@ -24344,6 +24357,21 @@ const noZIndex9999 = defineRule({
|
|
|
24344
24357
|
})
|
|
24345
24358
|
});
|
|
24346
24359
|
//#endregion
|
|
24360
|
+
//#region src/plugin/utils/is-framework-route-or-special-filename.ts
|
|
24361
|
+
const sourceFileExtensionGroup = NEXTJS_SOURCE_FILE_EXTENSION_GROUP;
|
|
24362
|
+
const FRAMEWORK_ROUTE_FILE_PATTERNS = [
|
|
24363
|
+
new RegExp(`^(page|layout|loading|error|not-found|template|default|global-error|route|_app|_document|_error)\\.${sourceFileExtensionGroup}$`),
|
|
24364
|
+
new RegExp(`^(_layout|\\+html|\\+not-found|\\+native-intent)\\.${sourceFileExtensionGroup}$`),
|
|
24365
|
+
new RegExp(`(?:^__root|\\.lazy)\\.${sourceFileExtensionGroup}$`),
|
|
24366
|
+
new RegExp(`^(root|entry\\.client|entry\\.server)\\.${sourceFileExtensionGroup}$`)
|
|
24367
|
+
];
|
|
24368
|
+
const isFrameworkRouteOrSpecialFilename = (rawFilename) => {
|
|
24369
|
+
if (!rawFilename) return false;
|
|
24370
|
+
if (isNextjsMetadataImageRouteFilename(rawFilename)) return true;
|
|
24371
|
+
const basename = path.basename(rawFilename);
|
|
24372
|
+
return FRAMEWORK_ROUTE_FILE_PATTERNS.some((pattern) => pattern.test(basename));
|
|
24373
|
+
};
|
|
24374
|
+
//#endregion
|
|
24347
24375
|
//#region src/plugin/rules/react-builtins/only-export-components-tables.ts
|
|
24348
24376
|
const NOT_REACT_COMPONENT_EXPRESSION_TYPES = new Set([
|
|
24349
24377
|
"ArrayExpression",
|
|
@@ -24395,32 +24423,6 @@ const ENTRY_POINT_BASENAMES = new Set([
|
|
|
24395
24423
|
"client.jsx",
|
|
24396
24424
|
"server.tsx",
|
|
24397
24425
|
"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
24426
|
"app.tsx",
|
|
24425
24427
|
"app.jsx",
|
|
24426
24428
|
"App.tsx",
|
|
@@ -24713,6 +24715,7 @@ const isFileNameAllowed = (filename, checkJS) => {
|
|
|
24713
24715
|
if (filename.includes(".test.") || filename.includes(".spec.") || filename.includes(".cy.") || filename.includes(".stories.")) return false;
|
|
24714
24716
|
for (const segment of NON_FAST_REFRESH_PATH_SEGMENTS) if (filename.includes(segment)) return false;
|
|
24715
24717
|
if (isEntryPointFile(filename)) return false;
|
|
24718
|
+
if (isFrameworkRouteOrSpecialFilename(filename)) return false;
|
|
24716
24719
|
if (isAssetOrUtilityFile(filename)) return false;
|
|
24717
24720
|
if (filename.endsWith(".tsx") || filename.endsWith(".jsx")) return true;
|
|
24718
24721
|
if (checkJS && filename.endsWith(".js")) return true;
|
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.22268f7",
|
|
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",
|
|
@@ -50,7 +50,7 @@
|
|
|
50
50
|
"@types/node": "^25.6.0"
|
|
51
51
|
},
|
|
52
52
|
"engines": {
|
|
53
|
-
"node": "^20.19.0 || >=22.
|
|
53
|
+
"node": "^20.19.0 || >=22.13.0"
|
|
54
54
|
},
|
|
55
55
|
"scripts": {
|
|
56
56
|
"dev": "vp pack --watch",
|