oxlint-plugin-react-doctor 0.6.0-dev.4d7c9dc → 0.6.0-dev.5f60bef
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 +89 -96
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -1573,7 +1573,6 @@ const LAYOUT_FILE_NAMES = [
|
|
|
1573
1573
|
];
|
|
1574
1574
|
const METADATA_EXPORT_NAMES = ["metadata", "generateMetadata"];
|
|
1575
1575
|
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;
|
|
1576
|
-
const PAGES_DIRECTORY_PATTERN = /\/pages\//;
|
|
1577
1576
|
const NEXTJS_NAVIGATION_FUNCTIONS = new Set([
|
|
1578
1577
|
"redirect",
|
|
1579
1578
|
"permanentRedirect",
|
|
@@ -1591,7 +1590,6 @@ const CACHE_REVALIDATION_FUNCTION_NAMES = new Set([
|
|
|
1591
1590
|
]);
|
|
1592
1591
|
const GOOGLE_FONTS_PATTERN = /fonts\.googleapis\.com/;
|
|
1593
1592
|
const POLYFILL_SCRIPT_PATTERN = /polyfill\.io|polyfill\.min\.js|cdn\.polyfill/;
|
|
1594
|
-
const APP_DIRECTORY_PATTERN = /\/app\//;
|
|
1595
1593
|
const ROUTE_HANDLER_FILE_PATTERN = new RegExp(`/route\\.${NEXTJS_SOURCE_FILE_EXTENSION_GROUP}$`);
|
|
1596
1594
|
const CRON_ROUTE_PATTERN = /\/(?:cron|jobs\/cron)(?:\/|$)/i;
|
|
1597
1595
|
const MUTATING_ROUTE_SEGMENTS = new Set([
|
|
@@ -1628,7 +1626,7 @@ const isNextjsMetadataImageRouteFilename = (rawFilename) => {
|
|
|
1628
1626
|
};
|
|
1629
1627
|
//#endregion
|
|
1630
1628
|
//#region src/plugin/utils/normalize-filename.ts
|
|
1631
|
-
const normalizeFilename
|
|
1629
|
+
const normalizeFilename = (filename) => filename.replaceAll("\\", "/");
|
|
1632
1630
|
//#endregion
|
|
1633
1631
|
//#region src/plugin/utils/is-generated-image-render-context.ts
|
|
1634
1632
|
const IMAGE_RESPONSE_MODULES = ["next/og", "@vercel/og"];
|
|
@@ -1636,7 +1634,7 @@ const SATORI_MODULE = "satori";
|
|
|
1636
1634
|
const generatedImageJsxCache = /* @__PURE__ */ new WeakMap();
|
|
1637
1635
|
const isGeneratedImageRenderFilename = (rawFilename) => {
|
|
1638
1636
|
if (!rawFilename) return false;
|
|
1639
|
-
return isNextjsMetadataImageRouteFilename(normalizeFilename
|
|
1637
|
+
return isNextjsMetadataImageRouteFilename(normalizeFilename(rawFilename));
|
|
1640
1638
|
};
|
|
1641
1639
|
const isImageResponseCallee = (contextNode, callee) => {
|
|
1642
1640
|
if (isNodeOfType(callee, "Identifier")) return IMAGE_RESPONSE_MODULES.some((moduleSource) => getImportedNameFromModule(contextNode, callee.name, moduleSource) === "ImageResponse");
|
|
@@ -4598,7 +4596,7 @@ const asyncParallel = defineRule({
|
|
|
4598
4596
|
severity: "warn",
|
|
4599
4597
|
recommendation: "Use `const [a, b] = await Promise.all([fetchA(), fetchB()])` so independent calls run at the same time",
|
|
4600
4598
|
create: (context) => {
|
|
4601
|
-
const filename = normalizeFilename
|
|
4599
|
+
const filename = normalizeFilename(context.filename ?? "");
|
|
4602
4600
|
const isBrowserTestFile = BROWSER_TEST_FILE_PATTERN.test(filename);
|
|
4603
4601
|
let hasTestLibraryImport = false;
|
|
4604
4602
|
const shouldSkipFile = () => isBrowserTestFile || hasTestLibraryImport;
|
|
@@ -8182,7 +8180,7 @@ const expoNoNonInlinedEnv = defineRule({
|
|
|
8182
8180
|
severity: "warn",
|
|
8183
8181
|
recommendation: "Read env vars with static dotted access (`process.env.EXPO_PUBLIC_NAME`). Computed access and destructuring aren't inlined by babel-preset-expo and resolve to `undefined` at runtime.",
|
|
8184
8182
|
create: (context) => {
|
|
8185
|
-
const filename = normalizeFilename
|
|
8183
|
+
const filename = normalizeFilename(context.filename ?? "");
|
|
8186
8184
|
if (filename && NODE_OR_BUILD_FILE.test(filename)) return EMPTY_VISITORS$3;
|
|
8187
8185
|
return {
|
|
8188
8186
|
MemberExpression(node) {
|
|
@@ -11617,7 +11615,7 @@ const jsxFilenameExtension = defineRule({
|
|
|
11617
11615
|
create: (context) => {
|
|
11618
11616
|
const settings = resolveSettings$33(context.settings);
|
|
11619
11617
|
const allowedExtensions = normalizeExtensions(settings.extensions);
|
|
11620
|
-
const filename = normalizeFilename
|
|
11618
|
+
const filename = normalizeFilename(context.filename ?? "fixture.tsx");
|
|
11621
11619
|
const extensionOnly = path.extname(filename).slice(1);
|
|
11622
11620
|
const fileHasAllowedExtension = allowedExtensions.has(extensionOnly);
|
|
11623
11621
|
let didReportMismatch = false;
|
|
@@ -15257,6 +15255,52 @@ const nextjsAsyncClientComponent = defineRule({
|
|
|
15257
15255
|
}
|
|
15258
15256
|
});
|
|
15259
15257
|
//#endregion
|
|
15258
|
+
//#region src/plugin/utils/get-project-relative-filename.ts
|
|
15259
|
+
const getProjectRelativeFilename = (filename, rootDirectory) => {
|
|
15260
|
+
const normalizedFilename = normalizeFilename(filename);
|
|
15261
|
+
if (!rootDirectory) return normalizedFilename;
|
|
15262
|
+
const rootDirectoryPrefix = `${normalizeFilename(rootDirectory).replace(/\/+$/, "")}/`;
|
|
15263
|
+
if (!normalizedFilename.startsWith(rootDirectoryPrefix)) return normalizedFilename;
|
|
15264
|
+
return normalizedFilename.slice(rootDirectoryPrefix.length);
|
|
15265
|
+
};
|
|
15266
|
+
//#endregion
|
|
15267
|
+
//#region src/plugin/utils/get-react-doctor-setting.ts
|
|
15268
|
+
const readReactDoctorSettingsBag = (settings) => {
|
|
15269
|
+
const reactDoctorSettings = settings?.["react-doctor"];
|
|
15270
|
+
if (typeof reactDoctorSettings !== "object" || reactDoctorSettings === null || Array.isArray(reactDoctorSettings)) return null;
|
|
15271
|
+
return reactDoctorSettings;
|
|
15272
|
+
};
|
|
15273
|
+
const readOwnPropertyValue = (bag, settingName) => Object.getOwnPropertyDescriptor(bag, settingName)?.value;
|
|
15274
|
+
const getReactDoctorStringSetting = (settings, settingName) => {
|
|
15275
|
+
const bag = readReactDoctorSettingsBag(settings);
|
|
15276
|
+
if (!bag) return void 0;
|
|
15277
|
+
const settingValue = readOwnPropertyValue(bag, settingName);
|
|
15278
|
+
return typeof settingValue === "string" ? settingValue : void 0;
|
|
15279
|
+
};
|
|
15280
|
+
const getReactDoctorNumberSetting = (settings, settingName) => {
|
|
15281
|
+
const bag = readReactDoctorSettingsBag(settings);
|
|
15282
|
+
if (!bag) return void 0;
|
|
15283
|
+
const settingValue = readOwnPropertyValue(bag, settingName);
|
|
15284
|
+
return typeof settingValue === "number" && Number.isFinite(settingValue) ? settingValue : void 0;
|
|
15285
|
+
};
|
|
15286
|
+
const getReactDoctorStringArraySetting = (settings, settingName) => {
|
|
15287
|
+
const bag = readReactDoctorSettingsBag(settings);
|
|
15288
|
+
if (!bag) return [];
|
|
15289
|
+
const settingValue = readOwnPropertyValue(bag, settingName);
|
|
15290
|
+
if (!Array.isArray(settingValue)) return [];
|
|
15291
|
+
return settingValue.filter((entry) => typeof entry === "string" && entry.length > 0);
|
|
15292
|
+
};
|
|
15293
|
+
//#endregion
|
|
15294
|
+
//#region src/plugin/utils/is-in-project-directory.ts
|
|
15295
|
+
const isInProjectDirectory = (context, directoryPath) => {
|
|
15296
|
+
const filename = normalizeFilename(context.filename ?? "");
|
|
15297
|
+
if (filename.length === 0) return false;
|
|
15298
|
+
const directorySegment = `/${directoryPath}/`;
|
|
15299
|
+
const relativeFilename = getProjectRelativeFilename(filename, getReactDoctorStringSetting(context.settings, "rootDirectory"));
|
|
15300
|
+
if (relativeFilename !== filename) return relativeFilename.startsWith(`${directoryPath}/`) || relativeFilename.includes(directorySegment);
|
|
15301
|
+
return filename.indexOf(directorySegment, 1) !== -1;
|
|
15302
|
+
};
|
|
15303
|
+
//#endregion
|
|
15260
15304
|
//#region src/plugin/rules/nextjs/nextjs-error-boundary-missing-use-client.ts
|
|
15261
15305
|
const nextjsErrorBoundaryMissingUseClient = defineRule({
|
|
15262
15306
|
id: "nextjs-error-boundary-missing-use-client",
|
|
@@ -15266,8 +15310,8 @@ const nextjsErrorBoundaryMissingUseClient = defineRule({
|
|
|
15266
15310
|
severity: "error",
|
|
15267
15311
|
recommendation: "Add `'use client'` at the top of this file. Error boundaries must be Client Components to catch and render fallback UI",
|
|
15268
15312
|
create: (context) => ({ Program(programNode) {
|
|
15269
|
-
const filename = normalizeFilename
|
|
15270
|
-
if (!
|
|
15313
|
+
const filename = normalizeFilename(context.filename ?? "");
|
|
15314
|
+
if (!isInProjectDirectory(context, "app")) return;
|
|
15271
15315
|
if (!ERROR_BOUNDARY_FILE_PATTERN.test(filename)) return;
|
|
15272
15316
|
if (hasDirective(programNode, "use client")) return;
|
|
15273
15317
|
context.report({
|
|
@@ -15298,8 +15342,8 @@ const nextjsGlobalErrorMissingHtmlBody = defineRule({
|
|
|
15298
15342
|
severity: "error",
|
|
15299
15343
|
recommendation: "Wrap your error UI in `<html><body>...</body></html>`. The root layout is unmounted when global-error renders",
|
|
15300
15344
|
create: (context) => ({ Program(programNode) {
|
|
15301
|
-
const filename = normalizeFilename
|
|
15302
|
-
if (!
|
|
15345
|
+
const filename = normalizeFilename(context.filename ?? "");
|
|
15346
|
+
if (!isInProjectDirectory(context, "app")) return;
|
|
15303
15347
|
if (!GLOBAL_ERROR_FILE_PATTERN.test(filename)) return;
|
|
15304
15348
|
const foundTags = fileContainsJsxElements(programNode, REQUIRED_HTML_TAGS);
|
|
15305
15349
|
const missingTags = REQUIRED_HTML_TAGS.filter((tagName) => !foundTags.has(tagName)).map((tagName) => `<${tagName}>`);
|
|
@@ -15430,7 +15474,7 @@ const nextjsMissingMetadata = defineRule({
|
|
|
15430
15474
|
severity: "warn",
|
|
15431
15475
|
recommendation: "Add metadata or `generateMetadata()` so search engines and social previews get a title and description.",
|
|
15432
15476
|
create: (context) => ({ Program(programNode) {
|
|
15433
|
-
const filename = normalizeFilename
|
|
15477
|
+
const filename = normalizeFilename(context.filename ?? "");
|
|
15434
15478
|
if (!PAGE_FILE_PATTERN.test(filename)) return;
|
|
15435
15479
|
if (INTERNAL_PAGE_PATH_PATTERN.test(filename)) return;
|
|
15436
15480
|
if (programNode.body?.some((statement) => {
|
|
@@ -15563,8 +15607,8 @@ const nextjsNoClientFetchForServerData = defineRule({
|
|
|
15563
15607
|
if (!fileHasUseClient || !isHookCall$1(node, EFFECT_HOOK_NAMES$1)) return;
|
|
15564
15608
|
const callback = getEffectCallback(node);
|
|
15565
15609
|
if (!callback || !containsFetchCall(callback, { stopAtFunctionBoundary: true })) return;
|
|
15566
|
-
const filename = normalizeFilename
|
|
15567
|
-
if (PAGE_OR_LAYOUT_FILE_PATTERN.test(filename) ||
|
|
15610
|
+
const filename = normalizeFilename(context.filename ?? "");
|
|
15611
|
+
if (PAGE_OR_LAYOUT_FILE_PATTERN.test(filename) || isInProjectDirectory(context, "pages")) context.report({
|
|
15568
15612
|
node,
|
|
15569
15613
|
message: "useEffect + fetch in a page/layout makes your users wait through an extra round trip & loading spinner."
|
|
15570
15614
|
});
|
|
@@ -15663,8 +15707,8 @@ const nextjsNoDefaultExportInRouteHandler = defineRule({
|
|
|
15663
15707
|
let programNode = null;
|
|
15664
15708
|
return {
|
|
15665
15709
|
Program(node) {
|
|
15666
|
-
const filename = normalizeFilename
|
|
15667
|
-
isAppRouteHandler =
|
|
15710
|
+
const filename = normalizeFilename(context.filename ?? "");
|
|
15711
|
+
isAppRouteHandler = isInProjectDirectory(context, "app") && ROUTE_HANDLER_FILE_PATTERN.test(filename);
|
|
15668
15712
|
programNode = node;
|
|
15669
15713
|
},
|
|
15670
15714
|
ExportDefaultDeclaration(node) {
|
|
@@ -15700,7 +15744,7 @@ const nextjsNoEdgeOgRuntime = defineRule({
|
|
|
15700
15744
|
let isOgImageFile = false;
|
|
15701
15745
|
return {
|
|
15702
15746
|
Program() {
|
|
15703
|
-
const filename = normalizeFilename
|
|
15747
|
+
const filename = normalizeFilename(context.filename ?? "");
|
|
15704
15748
|
isOgImageFile = OG_IMAGE_FILE_PATTERN.test(filename);
|
|
15705
15749
|
},
|
|
15706
15750
|
ExportNamedDeclaration(node) {
|
|
@@ -15776,8 +15820,7 @@ const nextjsNoHeadImport = defineRule({
|
|
|
15776
15820
|
recommendation: "Use the Metadata API because `next/head` is ignored in the App Router and meta tags will not render.",
|
|
15777
15821
|
create: (context) => ({ ImportDeclaration(node) {
|
|
15778
15822
|
if (node.source?.value !== "next/head") return;
|
|
15779
|
-
|
|
15780
|
-
if (!APP_DIRECTORY_PATTERN.test(filename)) return;
|
|
15823
|
+
if (!isInProjectDirectory(context, "app")) return;
|
|
15781
15824
|
context.report({
|
|
15782
15825
|
node,
|
|
15783
15826
|
message: "next/head silently does nothing in the App Router, so your meta tags never render."
|
|
@@ -16235,7 +16278,7 @@ const nextjsNoSideEffectInGetHandler = defineRule({
|
|
|
16235
16278
|
resolveBinding = buildProgramBindingLookup(node);
|
|
16236
16279
|
},
|
|
16237
16280
|
ExportNamedDeclaration(node) {
|
|
16238
|
-
const filename = normalizeFilename
|
|
16281
|
+
const filename = normalizeFilename(context.filename ?? "");
|
|
16239
16282
|
if (!ROUTE_HANDLER_FILE_PATTERN.test(filename)) return;
|
|
16240
16283
|
if (CRON_ROUTE_PATTERN.test(filename)) return;
|
|
16241
16284
|
if (!isExportedGetHandler(node)) return;
|
|
@@ -17014,7 +17057,7 @@ const nextjsNoUseSearchParamsWithoutSuspense = defineRule({
|
|
|
17014
17057
|
let suspenseLocalNames = /* @__PURE__ */ new Set();
|
|
17015
17058
|
return {
|
|
17016
17059
|
Program(programNode) {
|
|
17017
|
-
const filename = normalizeFilename
|
|
17060
|
+
const filename = normalizeFilename(context.filename ?? "");
|
|
17018
17061
|
isPageOrLayoutFile = PAGE_OR_LAYOUT_FILE_PATTERN.test(filename);
|
|
17019
17062
|
if (!isPageOrLayoutFile) return;
|
|
17020
17063
|
hasAncestorLayoutSuspense = hasAncestorSuspenseLayout(context.filename ?? "");
|
|
@@ -18666,40 +18709,13 @@ const classifyPackagePlatform = (filename) => {
|
|
|
18666
18709
|
return result;
|
|
18667
18710
|
};
|
|
18668
18711
|
//#endregion
|
|
18669
|
-
//#region src/plugin/utils/get-react-doctor-setting.ts
|
|
18670
|
-
const readReactDoctorSettingsBag = (settings) => {
|
|
18671
|
-
const reactDoctorSettings = settings?.["react-doctor"];
|
|
18672
|
-
if (typeof reactDoctorSettings !== "object" || reactDoctorSettings === null || Array.isArray(reactDoctorSettings)) return null;
|
|
18673
|
-
return reactDoctorSettings;
|
|
18674
|
-
};
|
|
18675
|
-
const readOwnPropertyValue = (bag, settingName) => Object.getOwnPropertyDescriptor(bag, settingName)?.value;
|
|
18676
|
-
const getReactDoctorStringSetting = (settings, settingName) => {
|
|
18677
|
-
const bag = readReactDoctorSettingsBag(settings);
|
|
18678
|
-
if (!bag) return void 0;
|
|
18679
|
-
const settingValue = readOwnPropertyValue(bag, settingName);
|
|
18680
|
-
return typeof settingValue === "string" ? settingValue : void 0;
|
|
18681
|
-
};
|
|
18682
|
-
const getReactDoctorNumberSetting = (settings, settingName) => {
|
|
18683
|
-
const bag = readReactDoctorSettingsBag(settings);
|
|
18684
|
-
if (!bag) return void 0;
|
|
18685
|
-
const settingValue = readOwnPropertyValue(bag, settingName);
|
|
18686
|
-
return typeof settingValue === "number" && Number.isFinite(settingValue) ? settingValue : void 0;
|
|
18687
|
-
};
|
|
18688
|
-
const getReactDoctorStringArraySetting = (settings, settingName) => {
|
|
18689
|
-
const bag = readReactDoctorSettingsBag(settings);
|
|
18690
|
-
if (!bag) return [];
|
|
18691
|
-
const settingValue = readOwnPropertyValue(bag, settingName);
|
|
18692
|
-
if (!Array.isArray(settingValue)) return [];
|
|
18693
|
-
return settingValue.filter((entry) => typeof entry === "string" && entry.length > 0);
|
|
18694
|
-
};
|
|
18695
|
-
//#endregion
|
|
18696
18712
|
//#region src/plugin/utils/is-react-native-file.ts
|
|
18697
18713
|
const WEB_FILE_EXTENSION_PATTERN = /\.web\.[cm]?[jt]sx?$/;
|
|
18698
18714
|
const NATIVE_FILE_EXTENSION_PATTERN = /\.(?:ios|android|native)\.[cm]?[jt]sx?$/;
|
|
18699
18715
|
const classifyReactNativeFileTarget = (context) => {
|
|
18700
18716
|
const rawFilename = context.filename;
|
|
18701
18717
|
if (!rawFilename) return "unknown";
|
|
18702
|
-
const filename = normalizeFilename
|
|
18718
|
+
const filename = normalizeFilename(rawFilename);
|
|
18703
18719
|
if (NATIVE_FILE_EXTENSION_PATTERN.test(filename)) return "react-native";
|
|
18704
18720
|
if (WEB_FILE_EXTENSION_PATTERN.test(filename)) return "web";
|
|
18705
18721
|
const packagePlatform = classifyPackagePlatform(filename);
|
|
@@ -18756,7 +18772,7 @@ const noBarrelImport = defineRule({
|
|
|
18756
18772
|
if (didReportForFile) return;
|
|
18757
18773
|
const source = node.source?.value;
|
|
18758
18774
|
if (typeof source !== "string" || !source.startsWith(".")) return;
|
|
18759
|
-
const filename = normalizeFilename
|
|
18775
|
+
const filename = normalizeFilename(context.filename ?? "");
|
|
18760
18776
|
if (!filename) return;
|
|
18761
18777
|
const importRequests = getRuntimeImportRequests(node);
|
|
18762
18778
|
if (importRequests.length === 0) return;
|
|
@@ -26227,15 +26243,6 @@ const CLIENT_APP_DIRECTORY_FRAMEWORKS = new Set([
|
|
|
26227
26243
|
"vite"
|
|
26228
26244
|
]);
|
|
26229
26245
|
const isInsideDirectory = (pathSegments, directoryNames) => pathSegments.some((pathSegment) => directoryNames.has(pathSegment));
|
|
26230
|
-
const normalizeFilename = (filename) => filename.replaceAll("\\", "/");
|
|
26231
|
-
const normalizeDirectory = (directory) => normalizeFilename(directory).replace(/\/+$/, "");
|
|
26232
|
-
const getProjectRelativeFilename = (filename, rootDirectory) => {
|
|
26233
|
-
const normalizedFilename = normalizeFilename(filename);
|
|
26234
|
-
if (!rootDirectory) return normalizedFilename;
|
|
26235
|
-
const rootDirectoryPrefix = `${normalizeDirectory(rootDirectory)}/`;
|
|
26236
|
-
if (!normalizedFilename.startsWith(rootDirectoryPrefix)) return normalizedFilename;
|
|
26237
|
-
return normalizedFilename.slice(rootDirectoryPrefix.length);
|
|
26238
|
-
};
|
|
26239
26246
|
const getClassifiablePathSegments = (pathSegments) => {
|
|
26240
26247
|
const srcIndex = pathSegments.lastIndexOf("src");
|
|
26241
26248
|
if (srcIndex === -1) return pathSegments;
|
|
@@ -26297,7 +26304,6 @@ const tokenizeIdentifierWords = (identifierName) => {
|
|
|
26297
26304
|
const getIdentifierTrailingWord = (identifierName) => tokenizeIdentifierWords(identifierName).at(-1) ?? identifierName.toLowerCase();
|
|
26298
26305
|
//#endregion
|
|
26299
26306
|
//#region src/plugin/constants/tanstack.ts
|
|
26300
|
-
const TANSTACK_ROUTE_FILE_PATTERN = /\/routes\//;
|
|
26301
26307
|
const TANSTACK_ROOT_ROUTE_FILE_PATTERN = /__root\.(tsx?|jsx?)$/;
|
|
26302
26308
|
const TANSTACK_ROUTE_PROPERTY_ORDER = [
|
|
26303
26309
|
"params",
|
|
@@ -26409,7 +26415,7 @@ const noSecretsInClientCode = defineRule({
|
|
|
26409
26415
|
severity: "warn",
|
|
26410
26416
|
recommendation: "Move secrets to server-only code. Anything in client env variables gets shipped to the browser, so it can't hold secrets.",
|
|
26411
26417
|
create: (context) => {
|
|
26412
|
-
const filename = normalizeFilename
|
|
26418
|
+
const filename = normalizeFilename(context.filename ?? "");
|
|
26413
26419
|
const framework = getReactDoctorStringSetting(context.settings, "framework");
|
|
26414
26420
|
const rootDirectory = getReactDoctorStringSetting(context.settings, "rootDirectory");
|
|
26415
26421
|
let shouldUseVariableNameHeuristic = classifySecretFileExposure(filename, {
|
|
@@ -29575,7 +29581,7 @@ const onlyExportComponents = defineRule({
|
|
|
29575
29581
|
allowConstantExport: settings.allowConstantExport
|
|
29576
29582
|
};
|
|
29577
29583
|
return { Program(node) {
|
|
29578
|
-
if (!isFileNameAllowed(normalizeFilename
|
|
29584
|
+
if (!isFileNameAllowed(normalizeFilename(context.filename ?? ""), settings.checkJS)) return;
|
|
29579
29585
|
const allNodes = collectAllNodes(node);
|
|
29580
29586
|
const exports = [];
|
|
29581
29587
|
let hasReactExport = false;
|
|
@@ -32090,7 +32096,7 @@ const renderingSvgPrecision = defineRule({
|
|
|
32090
32096
|
recommendation: "Round path, points, and transform decimals to 1 or 2 digits. The extra precision adds bytes with no visible difference.",
|
|
32091
32097
|
create: (context) => {
|
|
32092
32098
|
const filename = context.filename;
|
|
32093
|
-
const isAutoGenerated = isAutoGeneratedSvgFile(filename ? normalizeFilename
|
|
32099
|
+
const isAutoGenerated = isAutoGeneratedSvgFile(filename ? normalizeFilename(filename) : void 0);
|
|
32094
32100
|
return { JSXAttribute(node) {
|
|
32095
32101
|
if (isAutoGenerated) return;
|
|
32096
32102
|
if (!isNodeOfType(node.name, "JSXIdentifier")) return;
|
|
@@ -33240,7 +33246,7 @@ const rnDetoxMissingAwait = defineRule({
|
|
|
33240
33246
|
severity: "warn",
|
|
33241
33247
|
recommendation: "Prepend `await` to Detox actions, `waitFor(...)` chains, and `expect(element(...))` assertions. They return promises tied to Detox's synchronization, so a missing await runs steps out of order.",
|
|
33242
33248
|
create: (context) => {
|
|
33243
|
-
const filename = normalizeFilename
|
|
33249
|
+
const filename = normalizeFilename(context.filename ?? "");
|
|
33244
33250
|
if (!filename || !DETOX_TEST_FILE.test(filename)) return EMPTY_VISITORS$2;
|
|
33245
33251
|
return { ExpressionStatement(node) {
|
|
33246
33252
|
const expression = node.expression;
|
|
@@ -34970,7 +34976,7 @@ const rnPreferContentInsetAdjustment = defineRetiredRule({
|
|
|
34970
34976
|
//#region src/plugin/utils/is-expo-managed-file.ts
|
|
34971
34977
|
const isExpoManagedFileActive = (context) => {
|
|
34972
34978
|
const rawFilename = context.filename;
|
|
34973
|
-
const filename = rawFilename ? normalizeFilename
|
|
34979
|
+
const filename = rawFilename ? normalizeFilename(rawFilename) : void 0;
|
|
34974
34980
|
if (filename) {
|
|
34975
34981
|
const packagePlatform = classifyPackagePlatform(filename);
|
|
34976
34982
|
if (packagePlatform === "expo") return true;
|
|
@@ -39584,8 +39590,8 @@ const serverFetchWithoutRevalidate = defineRule({
|
|
|
39584
39590
|
let isServerSideFile = false;
|
|
39585
39591
|
return {
|
|
39586
39592
|
Program(node) {
|
|
39587
|
-
const filename = normalizeFilename
|
|
39588
|
-
if (!APP_ROUTER_FILE_PATTERN.test(filename)) {
|
|
39593
|
+
const filename = normalizeFilename(context.filename ?? "");
|
|
39594
|
+
if (!isInProjectDirectory(context, "app") || !APP_ROUTER_FILE_PATTERN.test(filename)) {
|
|
39589
39595
|
isServerSideFile = false;
|
|
39590
39596
|
return;
|
|
39591
39597
|
}
|
|
@@ -39670,7 +39676,6 @@ const collectRequestTaintedNames = (handlerBody, handlerParamNames) => {
|
|
|
39670
39676
|
});
|
|
39671
39677
|
return taintedNames;
|
|
39672
39678
|
};
|
|
39673
|
-
const PAGES_ROUTER_API_PATH_PATTERN = /\/pages\/api\//;
|
|
39674
39679
|
const inspectHandlerBody = (context, handlerBody, handlerLabel, handlerParamNames) => {
|
|
39675
39680
|
const requestTaintedNames = collectRequestTaintedNames(handlerBody, handlerParamNames);
|
|
39676
39681
|
walkAst(handlerBody, (child) => {
|
|
@@ -39711,8 +39716,7 @@ const serverHoistStaticIo = defineRule({
|
|
|
39711
39716
|
inspectHandlerBody(context, declaration.body, `${handlerName} route handler`, collectHandlerParamNames(declaration.params ?? []));
|
|
39712
39717
|
},
|
|
39713
39718
|
ExportDefaultDeclaration(node) {
|
|
39714
|
-
|
|
39715
|
-
if (!PAGES_ROUTER_API_PATH_PATTERN.test(filename)) return;
|
|
39719
|
+
if (!isInProjectDirectory(context, "pages/api")) return;
|
|
39716
39720
|
const declaration = node.declaration;
|
|
39717
39721
|
if (!isFunctionLike$1(declaration)) return;
|
|
39718
39722
|
if (!declaration.async) return;
|
|
@@ -40766,7 +40770,7 @@ const tanstackStartMissingHeadContent = defineRule({
|
|
|
40766
40770
|
collectVariableAlias(node);
|
|
40767
40771
|
},
|
|
40768
40772
|
JSXOpeningElement(node) {
|
|
40769
|
-
const filename = normalizeFilename
|
|
40773
|
+
const filename = normalizeFilename(context.filename ?? "");
|
|
40770
40774
|
if (!TANSTACK_ROOT_ROUTE_FILE_PATTERN.test(filename)) return;
|
|
40771
40775
|
if (isNodeOfType(node.name, "JSXIdentifier")) {
|
|
40772
40776
|
if (node.name.name === DOCUMENT_HEAD_ELEMENT_NAME) hasDocumentHeadElement = true;
|
|
@@ -40781,7 +40785,7 @@ const tanstackStartMissingHeadContent = defineRule({
|
|
|
40781
40785
|
if (isInsideDocumentHeadElement(node) && isCustomJsxElementName(node.name)) hasCustomHeadChildElement = true;
|
|
40782
40786
|
},
|
|
40783
40787
|
"Program:exit"(programNode) {
|
|
40784
|
-
const filename = normalizeFilename
|
|
40788
|
+
const filename = normalizeFilename(context.filename ?? "");
|
|
40785
40789
|
if (!TANSTACK_ROOT_ROUTE_FILE_PATTERN.test(filename)) return;
|
|
40786
40790
|
if (hasDocumentHeadElement && !hasHeadContentElement && !hasCustomHeadChildElement) context.report({
|
|
40787
40791
|
node: programNode,
|
|
@@ -40807,8 +40811,7 @@ const tanstackStartNoAnchorElement = defineRule({
|
|
|
40807
40811
|
severity: "warn",
|
|
40808
40812
|
recommendation: "Use `Link` from `@tanstack/react-router` so internal navigation keeps client state, preloading, and typed routes.",
|
|
40809
40813
|
create: (context) => ({ JSXOpeningElement(node) {
|
|
40810
|
-
|
|
40811
|
-
if (!TANSTACK_ROUTE_FILE_PATTERN.test(filename)) return;
|
|
40814
|
+
if (!isInProjectDirectory(context, "routes")) return;
|
|
40812
40815
|
if (!isNodeOfType(node.name, "JSXIdentifier") || node.name.name !== "a") return;
|
|
40813
40816
|
const attributes = node.attributes ?? [];
|
|
40814
40817
|
const hrefAttribute = findJsxAttribute(attributes, "href");
|
|
@@ -40891,6 +40894,7 @@ const tanstackStartNoNavigateInRender = defineRule({
|
|
|
40891
40894
|
severity: "warn",
|
|
40892
40895
|
recommendation: "Use `throw redirect({ to: '/path' })` in `beforeLoad` or `loader`. navigate() during render causes hydration issues.",
|
|
40893
40896
|
create: (context) => {
|
|
40897
|
+
const isRouteFile = isInProjectDirectory(context, "routes");
|
|
40894
40898
|
let deferredCallbackDepth = 0;
|
|
40895
40899
|
let eventHandlerDepth = 0;
|
|
40896
40900
|
const isDeferredHookCall = (node) => isHookCall$1(node, EFFECT_HOOK_NAMES$1) || isHookCall$1(node, "useCallback") || isHookCall$1(node, "useMemo");
|
|
@@ -40954,8 +40958,7 @@ const tanstackStartNoNavigateInRender = defineRule({
|
|
|
40954
40958
|
};
|
|
40955
40959
|
return {
|
|
40956
40960
|
CallExpression(node) {
|
|
40957
|
-
|
|
40958
|
-
if (!TANSTACK_ROUTE_FILE_PATTERN.test(filename)) return;
|
|
40961
|
+
if (!isRouteFile) return;
|
|
40959
40962
|
if (isDeferredHookCall(node)) deferredCallbackDepth++;
|
|
40960
40963
|
if (deferredCallbackDepth > 0 || eventHandlerDepth > 0) return;
|
|
40961
40964
|
if (isNodeOfType(node.callee, "Identifier") && node.callee.name === "navigate" && (node.arguments?.length ?? 0) > 0) {
|
|
@@ -40967,48 +40970,39 @@ const tanstackStartNoNavigateInRender = defineRule({
|
|
|
40967
40970
|
}
|
|
40968
40971
|
},
|
|
40969
40972
|
"CallExpression:exit"(node) {
|
|
40970
|
-
|
|
40971
|
-
if (!TANSTACK_ROUTE_FILE_PATTERN.test(filename)) return;
|
|
40973
|
+
if (!isRouteFile) return;
|
|
40972
40974
|
if (isDeferredHookCall(node)) deferredCallbackDepth = Math.max(0, deferredCallbackDepth - 1);
|
|
40973
40975
|
},
|
|
40974
40976
|
JSXAttribute(node) {
|
|
40975
|
-
|
|
40976
|
-
if (!TANSTACK_ROUTE_FILE_PATTERN.test(filename)) return;
|
|
40977
|
+
if (!isRouteFile) return;
|
|
40977
40978
|
if (isEventHandlerAttribute(node)) eventHandlerDepth++;
|
|
40978
40979
|
},
|
|
40979
40980
|
"JSXAttribute:exit"(node) {
|
|
40980
|
-
|
|
40981
|
-
if (!TANSTACK_ROUTE_FILE_PATTERN.test(filename)) return;
|
|
40981
|
+
if (!isRouteFile) return;
|
|
40982
40982
|
if (isEventHandlerAttribute(node)) eventHandlerDepth = Math.max(0, eventHandlerDepth - 1);
|
|
40983
40983
|
},
|
|
40984
40984
|
Property(node) {
|
|
40985
|
-
|
|
40986
|
-
if (!TANSTACK_ROUTE_FILE_PATTERN.test(filename)) return;
|
|
40985
|
+
if (!isRouteFile) return;
|
|
40987
40986
|
if (isEventHandlerProperty(node)) eventHandlerDepth++;
|
|
40988
40987
|
},
|
|
40989
40988
|
"Property:exit"(node) {
|
|
40990
|
-
|
|
40991
|
-
if (!TANSTACK_ROUTE_FILE_PATTERN.test(filename)) return;
|
|
40989
|
+
if (!isRouteFile) return;
|
|
40992
40990
|
if (isEventHandlerProperty(node)) eventHandlerDepth = Math.max(0, eventHandlerDepth - 1);
|
|
40993
40991
|
},
|
|
40994
40992
|
VariableDeclarator(node) {
|
|
40995
|
-
|
|
40996
|
-
if (!TANSTACK_ROUTE_FILE_PATTERN.test(filename)) return;
|
|
40993
|
+
if (!isRouteFile) return;
|
|
40997
40994
|
if (isHandlerNamedVariableDeclarator(node)) eventHandlerDepth++;
|
|
40998
40995
|
},
|
|
40999
40996
|
"VariableDeclarator:exit"(node) {
|
|
41000
|
-
|
|
41001
|
-
if (!TANSTACK_ROUTE_FILE_PATTERN.test(filename)) return;
|
|
40997
|
+
if (!isRouteFile) return;
|
|
41002
40998
|
if (isHandlerNamedVariableDeclarator(node)) eventHandlerDepth = Math.max(0, eventHandlerDepth - 1);
|
|
41003
40999
|
},
|
|
41004
41000
|
FunctionDeclaration(node) {
|
|
41005
|
-
|
|
41006
|
-
if (!TANSTACK_ROUTE_FILE_PATTERN.test(filename)) return;
|
|
41001
|
+
if (!isRouteFile) return;
|
|
41007
41002
|
if (isHandlerNamedFunctionDeclaration(node)) eventHandlerDepth++;
|
|
41008
41003
|
},
|
|
41009
41004
|
"FunctionDeclaration:exit"(node) {
|
|
41010
|
-
|
|
41011
|
-
if (!TANSTACK_ROUTE_FILE_PATTERN.test(filename)) return;
|
|
41005
|
+
if (!isRouteFile) return;
|
|
41012
41006
|
if (isHandlerNamedFunctionDeclaration(node)) eventHandlerDepth = Math.max(0, eventHandlerDepth - 1);
|
|
41013
41007
|
}
|
|
41014
41008
|
};
|
|
@@ -41093,8 +41087,7 @@ const tanstackStartNoUseEffectFetch = defineRule({
|
|
|
41093
41087
|
severity: "warn",
|
|
41094
41088
|
recommendation: "Fetch data in the route `loader` instead. The router loads it before rendering and avoids waterfalls.",
|
|
41095
41089
|
create: (context) => ({ CallExpression(node) {
|
|
41096
|
-
|
|
41097
|
-
if (!TANSTACK_ROUTE_FILE_PATTERN.test(filename)) return;
|
|
41090
|
+
if (!isInProjectDirectory(context, "routes")) return;
|
|
41098
41091
|
if (!isHookCall$1(node, EFFECT_HOOK_NAMES$1)) return;
|
|
41099
41092
|
const callback = node.arguments?.[0];
|
|
41100
41093
|
if (!callback) return;
|