oxlint-plugin-react-doctor 0.6.0-dev.5f60bef → 0.6.0-dev.6885698

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