oxlint-plugin-react-doctor 0.3.0 → 0.4.0-dev.36ecd05

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 (3) hide show
  1. package/dist/index.d.ts +342 -0
  2. package/dist/index.js +496 -124
  3. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -829,6 +829,9 @@ const isNextjsMetadataImageRouteFilename = (rawFilename) => {
829
829
  return /^(opengraph-image|twitter-image|icon|apple-icon)\d*\.(jsx?|tsx?)$/.test(path.basename(rawFilename));
830
830
  };
831
831
  //#endregion
832
+ //#region src/plugin/utils/normalize-filename.ts
833
+ const normalizeFilename$1 = (filename) => filename.replaceAll("\\", "/");
834
+ //#endregion
832
835
  //#region src/plugin/utils/is-hidden-from-screen-reader.ts
833
836
  const isHiddenFromScreenReader = (openingElement, settings) => {
834
837
  if (getElementType(openingElement, settings).toLowerCase() === "input") {
@@ -1000,7 +1003,7 @@ const altText = defineRule({
1000
1003
  recommendation: "Give every meaningful image an `alt`, `aria-label`, or `aria-labelledby`.",
1001
1004
  category: "Accessibility",
1002
1005
  create: (context) => {
1003
- if (isNextjsMetadataImageRouteFilename(context.filename)) return {};
1006
+ if (isNextjsMetadataImageRouteFilename(normalizeFilename$1(context.filename ?? ""))) return {};
1004
1007
  const settings = resolveSettings$53(context.settings);
1005
1008
  const checkImg = !settings.elements || settings.elements.includes("img");
1006
1009
  const checkObject = !settings.elements || settings.elements.includes("object");
@@ -1102,7 +1105,7 @@ const anchorAmbiguousText = defineRule({
1102
1105
  });
1103
1106
  //#endregion
1104
1107
  //#region src/plugin/rules/a11y/anchor-has-content.ts
1105
- const MESSAGE$50 = "Blind users can't follow this link because screen readers announce nothing, so add visible text, `aria-label`, or `aria-labelledby`.";
1108
+ const MESSAGE$51 = "Blind users can't follow this link because screen readers announce nothing, so add visible text, `aria-label`, or `aria-labelledby`.";
1106
1109
  const anchorHasContent = defineRule({
1107
1110
  id: "anchor-has-content",
1108
1111
  title: "Anchor has no content",
@@ -1118,7 +1121,7 @@ const anchorHasContent = defineRule({
1118
1121
  for (const attribute of ["title", "aria-label"]) if (hasJsxPropIgnoreCase(opening.attributes, attribute)) return;
1119
1122
  context.report({
1120
1123
  node: opening.name,
1121
- message: MESSAGE$50
1124
+ message: MESSAGE$51
1122
1125
  });
1123
1126
  } })
1124
1127
  });
@@ -1512,7 +1515,7 @@ const parseJsxValue = (value) => {
1512
1515
  };
1513
1516
  //#endregion
1514
1517
  //#region src/plugin/rules/a11y/aria-activedescendant-has-tabindex.ts
1515
- const MESSAGE$49 = "Keyboard users can't focus this element with `aria-activedescendant` because it isn't tabbable, so add `tabIndex={0}`.";
1518
+ const MESSAGE$50 = "Keyboard users can't focus this element with `aria-activedescendant` because it isn't tabbable, so add `tabIndex={0}`.";
1516
1519
  const ariaActivedescendantHasTabindex = defineRule({
1517
1520
  id: "aria-activedescendant-has-tabindex",
1518
1521
  title: "aria-activedescendant missing tabindex",
@@ -1530,14 +1533,14 @@ const ariaActivedescendantHasTabindex = defineRule({
1530
1533
  if (tabIndexValue === null || tabIndexValue >= -1) return;
1531
1534
  context.report({
1532
1535
  node: node.name,
1533
- message: MESSAGE$49
1536
+ message: MESSAGE$50
1534
1537
  });
1535
1538
  return;
1536
1539
  }
1537
1540
  if (isInteractiveElement(tag, node)) return;
1538
1541
  context.report({
1539
1542
  node: node.name,
1540
- message: MESSAGE$49
1543
+ message: MESSAGE$50
1541
1544
  });
1542
1545
  } })
1543
1546
  });
@@ -3073,9 +3076,6 @@ const asyncDeferAwait = defineRule({
3073
3076
  }
3074
3077
  });
3075
3078
  //#endregion
3076
- //#region src/plugin/utils/normalize-filename.ts
3077
- const normalizeFilename$1 = (filename) => filename.replaceAll("\\", "/");
3078
- //#endregion
3079
3079
  //#region src/plugin/utils/get-callee-identifier-trail.ts
3080
3080
  const getCalleeIdentifierTrail = (call) => {
3081
3081
  let entry = call;
@@ -3551,7 +3551,7 @@ const isPureEventBlockerHandler = (attribute) => {
3551
3551
  //#endregion
3552
3552
  //#region src/plugin/rules/a11y/click-events-have-key-events.ts
3553
3553
  const PRESENTATION_ROLES$1 = new Set(["presentation", "none"]);
3554
- const MESSAGE$48 = "Keyboard users can't trigger this click handler because there's no keyboard one, so add `onKeyUp`, `onKeyDown`, or `onKeyPress`.";
3554
+ const MESSAGE$49 = "Keyboard users can't trigger this click handler because there's no keyboard one, so add `onKeyUp`, `onKeyDown`, or `onKeyPress`.";
3555
3555
  const KEY_HANDLERS = [
3556
3556
  "onKeyUp",
3557
3557
  "onKeyDown",
@@ -3583,7 +3583,7 @@ const clickEventsHaveKeyEvents = defineRule({
3583
3583
  if (KEY_HANDLERS.some((handler) => hasJsxPropIgnoreCase(node.attributes, handler))) return;
3584
3584
  context.report({
3585
3585
  node: node.name,
3586
- message: MESSAGE$48
3586
+ message: MESSAGE$49
3587
3587
  });
3588
3588
  } };
3589
3589
  }
@@ -3696,7 +3696,7 @@ const stripParenExpression = (node) => {
3696
3696
  };
3697
3697
  //#endregion
3698
3698
  //#region src/plugin/rules/a11y/control-has-associated-label.ts
3699
- const MESSAGE$47 = "Blind users can't tell what this control does because screen readers find no label, so add visible text, `aria-label`, or `aria-labelledby`.";
3699
+ const MESSAGE$48 = "Blind users can't tell what this control does because screen readers find no label, so add visible text, `aria-label`, or `aria-labelledby`.";
3700
3700
  const DEFAULT_IGNORE_ELEMENTS = ["link", "canvas"];
3701
3701
  const DEFAULT_LABELLING_PROPS = [
3702
3702
  "alt",
@@ -3857,7 +3857,7 @@ const controlHasAssociatedLabel = defineRule({
3857
3857
  for (const child of node.children) if (checkChildForLabel(child, 1, checkContext)) return;
3858
3858
  context.report({
3859
3859
  node: opening,
3860
- message: MESSAGE$47
3860
+ message: MESSAGE$48
3861
3861
  });
3862
3862
  } };
3863
3863
  }
@@ -4193,7 +4193,7 @@ const isEs6Component = (node) => {
4193
4193
  };
4194
4194
  //#endregion
4195
4195
  //#region src/plugin/rules/react-builtins/display-name.ts
4196
- const MESSAGE$46 = "This component shows up as Anonymous in React DevTools because it has no `displayName`.";
4196
+ const MESSAGE$47 = "This component shows up as Anonymous in React DevTools because it has no `displayName`.";
4197
4197
  const DEFAULT_ADDITIONAL_HOCS = [
4198
4198
  "observer",
4199
4199
  "lazy",
@@ -4396,7 +4396,7 @@ const displayName = defineRule({
4396
4396
  const reportAt = (node) => {
4397
4397
  context.report({
4398
4398
  node,
4399
- message: MESSAGE$46
4399
+ message: MESSAGE$47
4400
4400
  });
4401
4401
  };
4402
4402
  return {
@@ -6507,7 +6507,7 @@ const forbidElements = defineRule({
6507
6507
  });
6508
6508
  //#endregion
6509
6509
  //#region src/plugin/rules/react-builtins/forward-ref-uses-ref.ts
6510
- const MESSAGE$45 = "The parent can't reach this component's node because the `forwardRef` wrapper ignores `ref`.";
6510
+ const MESSAGE$46 = "The parent can't reach this component's node because the `forwardRef` wrapper ignores `ref`.";
6511
6511
  const forwardRefUsesRef = defineRule({
6512
6512
  id: "forward-ref-uses-ref",
6513
6513
  title: "forwardRef without ref parameter",
@@ -6527,13 +6527,13 @@ const forwardRefUsesRef = defineRule({
6527
6527
  if (isNodeOfType(onlyParam, "RestElement")) return;
6528
6528
  context.report({
6529
6529
  node: inner,
6530
- message: MESSAGE$45
6530
+ message: MESSAGE$46
6531
6531
  });
6532
6532
  } })
6533
6533
  });
6534
6534
  //#endregion
6535
6535
  //#region src/plugin/rules/a11y/heading-has-content.ts
6536
- const MESSAGE$44 = "Blind users can't use this heading to navigate because screen readers skip it empty, so add text, `aria-label`, or `aria-labelledby`.";
6536
+ const MESSAGE$45 = "Blind users can't use this heading to navigate because screen readers skip it empty, so add text, `aria-label`, or `aria-labelledby`.";
6537
6537
  const DEFAULT_HEADING_TAGS = [
6538
6538
  "h1",
6539
6539
  "h2",
@@ -6566,7 +6566,7 @@ const headingHasContent = defineRule({
6566
6566
  if (isHiddenFromScreenReader(node, context.settings)) return;
6567
6567
  context.report({
6568
6568
  node,
6569
- message: MESSAGE$44
6569
+ message: MESSAGE$45
6570
6570
  });
6571
6571
  } };
6572
6572
  }
@@ -6704,7 +6704,7 @@ const hooksNoNanInDeps = defineRule({
6704
6704
  });
6705
6705
  //#endregion
6706
6706
  //#region src/plugin/rules/a11y/html-has-lang.ts
6707
- const MESSAGE$43 = "Screen readers may mispronounce this page because it doesn't declare a language, so add a `lang` attribute like `en`.";
6707
+ const MESSAGE$44 = "Screen readers may mispronounce this page because it doesn't declare a language, so add a `lang` attribute like `en`.";
6708
6708
  const resolveSettings$39 = (settings) => {
6709
6709
  const reactDoctor = settings?.["react-doctor"];
6710
6710
  return { htmlTags: (typeof reactDoctor === "object" && reactDoctor !== null ? reactDoctor.htmlHasLang ?? {} : {}).htmlTags ?? ["html"] };
@@ -6752,7 +6752,7 @@ const htmlHasLang = defineRule({
6752
6752
  if (!lang) {
6753
6753
  context.report({
6754
6754
  node: node.name,
6755
- message: MESSAGE$43
6755
+ message: MESSAGE$44
6756
6756
  });
6757
6757
  return;
6758
6758
  }
@@ -6760,13 +6760,13 @@ const htmlHasLang = defineRule({
6760
6760
  if (verdict === "missing" || verdict === "empty") {
6761
6761
  context.report({
6762
6762
  node: lang,
6763
- message: MESSAGE$43
6763
+ message: MESSAGE$44
6764
6764
  });
6765
6765
  return;
6766
6766
  }
6767
6767
  if (hasSpread && !lang) context.report({
6768
6768
  node: node.name,
6769
- message: MESSAGE$43
6769
+ message: MESSAGE$44
6770
6770
  });
6771
6771
  } };
6772
6772
  }
@@ -6980,7 +6980,7 @@ const htmlNoNestedInteractive = defineRule({
6980
6980
  });
6981
6981
  //#endregion
6982
6982
  //#region src/plugin/rules/a11y/iframe-has-title.ts
6983
- const MESSAGE$42 = "Blind users can't tell what this `<iframe>` holds because screen readers have no title to read, so add a `title` describing its content.";
6983
+ const MESSAGE$43 = "Blind users can't tell what this `<iframe>` holds because screen readers have no title to read, so add a `title` describing its content.";
6984
6984
  const evaluateTitleValue = (value) => {
6985
6985
  if (!value) return "missing";
6986
6986
  if (isNodeOfType(value, "Literal")) {
@@ -7020,14 +7020,14 @@ const iframeHasTitle = defineRule({
7020
7020
  if (!titleAttr) {
7021
7021
  if (hasSpread || tag === "iframe") context.report({
7022
7022
  node: node.name,
7023
- message: MESSAGE$42
7023
+ message: MESSAGE$43
7024
7024
  });
7025
7025
  return;
7026
7026
  }
7027
7027
  const verdict = evaluateTitleValue(titleAttr.value);
7028
7028
  if (verdict === "missing" || verdict === "empty") context.report({
7029
7029
  node: titleAttr,
7030
- message: MESSAGE$42
7030
+ message: MESSAGE$43
7031
7031
  });
7032
7032
  } })
7033
7033
  });
@@ -7131,7 +7131,7 @@ const iframeMissingSandbox = defineRule({
7131
7131
  });
7132
7132
  //#endregion
7133
7133
  //#region src/plugin/rules/a11y/img-redundant-alt.ts
7134
- const MESSAGE$41 = "Screen reader users hear \"image\" or \"photo\" twice because they already announce it, so describe what the image shows instead.";
7134
+ const MESSAGE$42 = "Screen reader users hear \"image\" or \"photo\" twice because they already announce it, so describe what the image shows instead.";
7135
7135
  const DEFAULT_COMPONENTS = ["img"];
7136
7136
  const DEFAULT_REDUNDANT_WORDS = [
7137
7137
  "image",
@@ -7194,7 +7194,7 @@ const imgRedundantAlt = defineRule({
7194
7194
  if (!altAttribute) return;
7195
7195
  if (altValueRedundant(altAttribute, settings.words)) context.report({
7196
7196
  node: altAttribute,
7197
- message: MESSAGE$41
7197
+ message: MESSAGE$42
7198
7198
  });
7199
7199
  } };
7200
7200
  }
@@ -9595,7 +9595,7 @@ const jsxMaxDepth = defineRule({
9595
9595
  });
9596
9596
  //#endregion
9597
9597
  //#region src/plugin/rules/react-builtins/jsx-no-comment-textnodes.ts
9598
- const MESSAGE$40 = "Your users see this comment as text on the page because `//` & `/*` aren't hidden in JSX.";
9598
+ const MESSAGE$41 = "Your users see this comment as text on the page because `//` & `/*` aren't hidden in JSX.";
9599
9599
  const LITERAL_TEXT_TAGS = new Set([
9600
9600
  "code",
9601
9601
  "pre",
@@ -9631,7 +9631,7 @@ const jsxNoCommentTextnodes = defineRule({
9631
9631
  if (isInsideLiteralTextTag(node)) return;
9632
9632
  context.report({
9633
9633
  node,
9634
- message: MESSAGE$40
9634
+ message: MESSAGE$41
9635
9635
  });
9636
9636
  } })
9637
9637
  });
@@ -9662,7 +9662,7 @@ const isInsideFunctionScope = (node) => {
9662
9662
  };
9663
9663
  //#endregion
9664
9664
  //#region src/plugin/rules/react-builtins/jsx-no-constructed-context-values.ts
9665
- const MESSAGE$39 = "Every reader of this context redraws on each render because you build its `value` inline.";
9665
+ const MESSAGE$40 = "Every reader of this context redraws on each render because you build its `value` inline.";
9666
9666
  const CONTEXT_MODULES$1 = [
9667
9667
  "react",
9668
9668
  "use-context-selector",
@@ -9760,7 +9760,7 @@ const jsxNoConstructedContextValues = defineRule({
9760
9760
  if (!isConstructedValue(innerExpression)) continue;
9761
9761
  context.report({
9762
9762
  node: attribute,
9763
- message: MESSAGE$39
9763
+ message: MESSAGE$40
9764
9764
  });
9765
9765
  }
9766
9766
  }
@@ -9846,7 +9846,7 @@ const isJsxAttributeOnIntrinsicHtmlElement = (attribute) => {
9846
9846
  };
9847
9847
  //#endregion
9848
9848
  //#region src/plugin/rules/react-builtins/jsx-no-jsx-as-prop.ts
9849
- const MESSAGE$38 = "This child redraws every render because the prop gets brand new JSX each time.";
9849
+ const MESSAGE$39 = "This child redraws every render because the prop gets brand new JSX each time.";
9850
9850
  const KNOWN_SLOT_PROP_NAMES = new Set([
9851
9851
  "icon",
9852
9852
  "Icon",
@@ -10115,7 +10115,7 @@ const jsxNoJsxAsProp = defineRule({
10115
10115
  if (!isJsxProducingExpression(expressionNode) && !followsRenderLocalJsxBinding(expressionNode, node)) return;
10116
10116
  context.report({
10117
10117
  node,
10118
- message: MESSAGE$38
10118
+ message: MESSAGE$39
10119
10119
  });
10120
10120
  }
10121
10121
  };
@@ -10403,7 +10403,7 @@ const DATA_ARRAY_PROP_SUFFIXES = [
10403
10403
  ];
10404
10404
  //#endregion
10405
10405
  //#region src/plugin/rules/react-builtins/jsx-no-new-array-as-prop.ts
10406
- const MESSAGE$37 = "This child redraws every render because the prop gets a brand new array each time.";
10406
+ const MESSAGE$38 = "This child redraws every render because the prop gets a brand new array each time.";
10407
10407
  const isDataArrayPropName = (propName) => {
10408
10408
  if (DATA_ARRAY_PROP_NAMES.has(propName)) return true;
10409
10409
  for (const suffix of DATA_ARRAY_PROP_SUFFIXES) if (propName.length > suffix.length && propName.endsWith(suffix)) return true;
@@ -10487,7 +10487,7 @@ const jsxNoNewArrayAsProp = defineRule({
10487
10487
  if (!isArrayProducingExpression(expressionNode) && !followsRenderLocalArrayBinding(expressionNode, node)) return;
10488
10488
  context.report({
10489
10489
  node,
10490
- message: MESSAGE$37
10490
+ message: MESSAGE$38
10491
10491
  });
10492
10492
  }
10493
10493
  };
@@ -10745,7 +10745,7 @@ const SAFE_RECEIVER_NAMES = new Set([
10745
10745
  ]);
10746
10746
  //#endregion
10747
10747
  //#region src/plugin/rules/react-builtins/jsx-no-new-function-as-prop.ts
10748
- const MESSAGE$36 = "This child redraws every render because the prop gets a brand new function each time.";
10748
+ const MESSAGE$37 = "This child redraws every render because the prop gets a brand new function each time.";
10749
10749
  const isAccessorPredicateName = (propName) => {
10750
10750
  for (const prefix of ACCESSOR_PREDICATE_PREFIXES) {
10751
10751
  if (propName.length <= prefix.length) continue;
@@ -10951,7 +10951,7 @@ const jsxNoNewFunctionAsProp = defineRule({
10951
10951
  if (!isFunctionProducingExpression(expressionNode) && !followsRenderLocalFunctionBinding(expressionNode, node)) return;
10952
10952
  context.report({
10953
10953
  node,
10954
- message: MESSAGE$36
10954
+ message: MESSAGE$37
10955
10955
  });
10956
10956
  }
10957
10957
  };
@@ -11171,7 +11171,7 @@ const CONFIG_OBJECT_PROP_SUFFIXES = [
11171
11171
  ];
11172
11172
  //#endregion
11173
11173
  //#region src/plugin/rules/react-builtins/jsx-no-new-object-as-prop.ts
11174
- const MESSAGE$35 = "This child redraws every render because the prop gets a brand new object each time.";
11174
+ const MESSAGE$36 = "This child redraws every render because the prop gets a brand new object each time.";
11175
11175
  const isConfigObjectPropName = (propName) => {
11176
11176
  if (CONFIG_OBJECT_PROP_NAMES.has(propName)) return true;
11177
11177
  for (const suffix of CONFIG_OBJECT_PROP_SUFFIXES) if (propName.length > suffix.length && propName.endsWith(suffix)) return true;
@@ -11259,7 +11259,7 @@ const jsxNoNewObjectAsProp = defineRule({
11259
11259
  if (!isObjectProducingExpression(expressionNode) && !followsRenderLocalObjectBinding(expressionNode, node)) return;
11260
11260
  context.report({
11261
11261
  node,
11262
- message: MESSAGE$35
11262
+ message: MESSAGE$36
11263
11263
  });
11264
11264
  }
11265
11265
  };
@@ -11267,7 +11267,7 @@ const jsxNoNewObjectAsProp = defineRule({
11267
11267
  });
11268
11268
  //#endregion
11269
11269
  //#region src/plugin/rules/react-builtins/jsx-no-script-url.ts
11270
- const MESSAGE$34 = "A `javascript:` URL is an XSS hole that runs injected input as code.";
11270
+ const MESSAGE$35 = "A `javascript:` URL is an XSS hole that runs injected input as code.";
11271
11271
  const JAVASCRIPT_URL_PATTERN = /j[\r\n\t]*a[\r\n\t]*v[\r\n\t]*a[\r\n\t]*s[\r\n\t]*c[\r\n\t]*r[\r\n\t]*i[\r\n\t]*p[\r\n\t]*t[\r\n\t]*:/i;
11272
11272
  const resolveSettings$29 = (settings) => {
11273
11273
  const reactDoctor = settings?.["react-doctor"];
@@ -11308,7 +11308,7 @@ const jsxNoScriptUrl = defineRule({
11308
11308
  if (!value || !isNodeOfType(value, "Literal") || typeof value.value !== "string") continue;
11309
11309
  if (JAVASCRIPT_URL_PATTERN.test(value.value)) context.report({
11310
11310
  node: attribute,
11311
- message: MESSAGE$34
11311
+ message: MESSAGE$35
11312
11312
  });
11313
11313
  }
11314
11314
  } };
@@ -11904,7 +11904,7 @@ const jsxPropsNoSpreadMulti = defineRule({
11904
11904
  });
11905
11905
  //#endregion
11906
11906
  //#region src/plugin/rules/react-builtins/jsx-props-no-spreading.ts
11907
- const MESSAGE$33 = "You can't tell what props reach this element when you spread them.";
11907
+ const MESSAGE$34 = "You can't tell what props reach this element when you spread them.";
11908
11908
  const resolveSettings$25 = (settings) => {
11909
11909
  const reactDoctor = settings?.["react-doctor"];
11910
11910
  const ruleSettings = typeof reactDoctor === "object" && reactDoctor !== null ? reactDoctor.jsxPropsNoSpreading ?? {} : {};
@@ -11945,7 +11945,7 @@ const jsxPropsNoSpreading = defineRule({
11945
11945
  }
11946
11946
  context.report({
11947
11947
  node: attribute,
11948
- message: MESSAGE$33
11948
+ message: MESSAGE$34
11949
11949
  });
11950
11950
  }
11951
11951
  } };
@@ -12101,7 +12101,7 @@ const labelHasAssociatedControl = defineRule({
12101
12101
  });
12102
12102
  //#endregion
12103
12103
  //#region src/plugin/rules/a11y/lang.ts
12104
- const MESSAGE$32 = "Screen readers can't pick the right voice because this `lang` isn't a real language code, so use a valid one like `en` or `en-US`.";
12104
+ const MESSAGE$33 = "Screen readers can't pick the right voice because this `lang` isn't a real language code, so use a valid one like `en` or `en-US`.";
12105
12105
  const COMMON_LANGUAGE_PRIMARY_TAGS = new Set([
12106
12106
  "aa",
12107
12107
  "ab",
@@ -12313,7 +12313,7 @@ const lang = defineRule({
12313
12313
  if (expression.type === "Identifier" && expression.name === "undefined" || expression.type === "Literal" && expression.value === null) {
12314
12314
  context.report({
12315
12315
  node: langAttr,
12316
- message: MESSAGE$32
12316
+ message: MESSAGE$33
12317
12317
  });
12318
12318
  return;
12319
12319
  }
@@ -12322,13 +12322,13 @@ const lang = defineRule({
12322
12322
  if (value === null) return;
12323
12323
  if (!isValidLangTag(value)) context.report({
12324
12324
  node: langAttr,
12325
- message: MESSAGE$32
12325
+ message: MESSAGE$33
12326
12326
  });
12327
12327
  } })
12328
12328
  });
12329
12329
  //#endregion
12330
12330
  //#region src/plugin/rules/a11y/media-has-caption.ts
12331
- const MESSAGE$31 = "Deaf & hard-of-hearing users miss this media without captions, so add a `<track kind=\"captions\">` inside the `<audio>` or `<video>`.";
12331
+ const MESSAGE$32 = "Deaf & hard-of-hearing users miss this media without captions, so add a `<track kind=\"captions\">` inside the `<audio>` or `<video>`.";
12332
12332
  const DEFAULT_AUDIO = ["audio"];
12333
12333
  const DEFAULT_VIDEO = ["video"];
12334
12334
  const DEFAULT_TRACK = ["track"];
@@ -12369,7 +12369,7 @@ const mediaHasCaption = defineRule({
12369
12369
  if (!parent || !isNodeOfType(parent, "JSXElement")) {
12370
12370
  context.report({
12371
12371
  node: node.name,
12372
- message: MESSAGE$31
12372
+ message: MESSAGE$32
12373
12373
  });
12374
12374
  return;
12375
12375
  }
@@ -12386,7 +12386,7 @@ const mediaHasCaption = defineRule({
12386
12386
  return kindValue.value.toLowerCase() === "captions";
12387
12387
  })) context.report({
12388
12388
  node: node.name,
12389
- message: MESSAGE$31
12389
+ message: MESSAGE$32
12390
12390
  });
12391
12391
  } };
12392
12392
  }
@@ -12511,6 +12511,103 @@ const nextjsAsyncClientComponent = defineRule({
12511
12511
  }
12512
12512
  });
12513
12513
  //#endregion
12514
+ //#region src/plugin/constants/nextjs.ts
12515
+ const PAGE_FILE_PATTERN = /\/page\.(tsx?|jsx?)$/;
12516
+ const PAGE_OR_LAYOUT_FILE_PATTERN = /\/(page|layout)\.(tsx?|jsx?)$/;
12517
+ 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;
12518
+ const OG_ROUTE_PATTERN = /\/og\b/i;
12519
+ const PAGES_DIRECTORY_PATTERN = /\/pages\//;
12520
+ const NEXTJS_NAVIGATION_FUNCTIONS = new Set([
12521
+ "redirect",
12522
+ "permanentRedirect",
12523
+ "notFound",
12524
+ "forbidden",
12525
+ "unauthorized"
12526
+ ]);
12527
+ const GOOGLE_FONTS_PATTERN = /fonts\.googleapis\.com/;
12528
+ const POLYFILL_SCRIPT_PATTERN = /polyfill\.io|polyfill\.min\.js|cdn\.polyfill/;
12529
+ const APP_DIRECTORY_PATTERN = /\/app\//;
12530
+ const ROUTE_HANDLER_FILE_PATTERN = /\/route\.(tsx?|jsx?)$/;
12531
+ const CRON_ROUTE_PATTERN = /\/(?:cron|jobs\/cron)(?:\/|$)/i;
12532
+ const MUTATING_ROUTE_SEGMENTS = new Set([
12533
+ "logout",
12534
+ "log-out",
12535
+ "signout",
12536
+ "sign-out",
12537
+ "unsubscribe",
12538
+ "delete",
12539
+ "remove",
12540
+ "revoke",
12541
+ "cancel",
12542
+ "deactivate"
12543
+ ]);
12544
+ const ERROR_BOUNDARY_FILE_PATTERN = /\/(error|global-error)\.(tsx?|jsx?)$/;
12545
+ const GLOBAL_ERROR_FILE_PATTERN = /\/global-error\.(tsx?|jsx?)$/;
12546
+ const ROUTE_HANDLER_HTTP_METHODS = new Set([
12547
+ "GET",
12548
+ "POST",
12549
+ "PUT",
12550
+ "PATCH",
12551
+ "DELETE",
12552
+ "OPTIONS",
12553
+ "HEAD"
12554
+ ]);
12555
+ const GOOGLE_ANALYTICS_SCRIPT_PATTERN = /google-analytics\.com|googletagmanager\.com\/gtag/;
12556
+ const OG_IMAGE_FILE_PATTERN = /\/(opengraph-image|twitter-image)\d*\.(tsx?|jsx?)$/;
12557
+ //#endregion
12558
+ //#region src/plugin/rules/nextjs/nextjs-error-boundary-missing-use-client.ts
12559
+ const nextjsErrorBoundaryMissingUseClient = defineRule({
12560
+ id: "nextjs-error-boundary-missing-use-client",
12561
+ title: "Error boundary missing 'use client'",
12562
+ tags: ["test-noise"],
12563
+ requires: ["nextjs"],
12564
+ severity: "error",
12565
+ recommendation: "Add `'use client'` at the top of this file. Error boundaries must be Client Components to catch and render fallback UI",
12566
+ create: (context) => ({ Program(programNode) {
12567
+ const filename = normalizeFilename$1(context.filename ?? "");
12568
+ if (!APP_DIRECTORY_PATTERN.test(filename)) return;
12569
+ if (!ERROR_BOUNDARY_FILE_PATTERN.test(filename)) return;
12570
+ if (hasDirective(programNode, "use client")) return;
12571
+ context.report({
12572
+ node: programNode,
12573
+ message: "This error boundary silently does nothing without 'use client'. Next.js requires error.tsx to be a Client Component."
12574
+ });
12575
+ } })
12576
+ });
12577
+ //#endregion
12578
+ //#region src/plugin/utils/file-contains-jsx-elements.ts
12579
+ const fileContainsJsxElements = (programNode, tagNames) => {
12580
+ const targetTagNames = new Set(tagNames);
12581
+ const foundTagNames = /* @__PURE__ */ new Set();
12582
+ walkAst(programNode, (child) => {
12583
+ if (foundTagNames.size === targetTagNames.size) return false;
12584
+ if (isNodeOfType(child, "JSXOpeningElement") && isNodeOfType(child.name, "JSXIdentifier") && targetTagNames.has(child.name.name)) foundTagNames.add(child.name.name);
12585
+ });
12586
+ return foundTagNames;
12587
+ };
12588
+ //#endregion
12589
+ //#region src/plugin/rules/nextjs/nextjs-global-error-missing-html-body.ts
12590
+ const REQUIRED_HTML_TAGS = ["html", "body"];
12591
+ const nextjsGlobalErrorMissingHtmlBody = defineRule({
12592
+ id: "nextjs-global-error-missing-html-body",
12593
+ title: "global-error.tsx missing <html>/<body>",
12594
+ tags: ["test-noise"],
12595
+ requires: ["nextjs"],
12596
+ severity: "error",
12597
+ recommendation: "Wrap your error UI in `<html><body>...</body></html>`. The root layout is unmounted when global-error renders",
12598
+ create: (context) => ({ Program(programNode) {
12599
+ const filename = normalizeFilename$1(context.filename ?? "");
12600
+ if (!APP_DIRECTORY_PATTERN.test(filename)) return;
12601
+ if (!GLOBAL_ERROR_FILE_PATTERN.test(filename)) return;
12602
+ const foundTags = fileContainsJsxElements(programNode, REQUIRED_HTML_TAGS);
12603
+ const missingTags = REQUIRED_HTML_TAGS.filter((tagName) => !foundTags.has(tagName)).map((tagName) => `<${tagName}>`);
12604
+ if (missingTags.length > 0) context.report({
12605
+ node: programNode,
12606
+ message: `global-error.tsx is missing ${missingTags.join(" and ")}. The root layout unmounts on error, so this page renders broken HTML.`
12607
+ });
12608
+ } })
12609
+ });
12610
+ //#endregion
12514
12611
  //#region src/plugin/utils/has-jsx-attribute.ts
12515
12612
  const hasJsxAttribute = (attributes, attributeName) => Boolean(findJsxAttribute(attributes, attributeName));
12516
12613
  //#endregion
@@ -12554,37 +12651,6 @@ const nextjsInlineScriptMissingId = defineRule({
12554
12651
  } })
12555
12652
  });
12556
12653
  //#endregion
12557
- //#region src/plugin/constants/nextjs.ts
12558
- const PAGE_FILE_PATTERN = /\/page\.(tsx?|jsx?)$/;
12559
- const PAGE_OR_LAYOUT_FILE_PATTERN = /\/(page|layout)\.(tsx?|jsx?)$/;
12560
- 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;
12561
- const OG_ROUTE_PATTERN = /\/og\b/i;
12562
- const PAGES_DIRECTORY_PATTERN = /\/pages\//;
12563
- const NEXTJS_NAVIGATION_FUNCTIONS = new Set([
12564
- "redirect",
12565
- "permanentRedirect",
12566
- "notFound",
12567
- "forbidden",
12568
- "unauthorized"
12569
- ]);
12570
- const GOOGLE_FONTS_PATTERN = /fonts\.googleapis\.com/;
12571
- const POLYFILL_SCRIPT_PATTERN = /polyfill\.io|polyfill\.min\.js|cdn\.polyfill/;
12572
- const APP_DIRECTORY_PATTERN = /\/app\//;
12573
- const ROUTE_HANDLER_FILE_PATTERN = /\/route\.(tsx?|jsx?)$/;
12574
- const CRON_ROUTE_PATTERN = /\/(?:cron|jobs\/cron)(?:\/|$)/i;
12575
- const MUTATING_ROUTE_SEGMENTS = new Set([
12576
- "logout",
12577
- "log-out",
12578
- "signout",
12579
- "sign-out",
12580
- "unsubscribe",
12581
- "delete",
12582
- "remove",
12583
- "revoke",
12584
- "cancel",
12585
- "deactivate"
12586
- ]);
12587
- //#endregion
12588
12654
  //#region src/plugin/rules/nextjs/nextjs-missing-metadata.ts
12589
12655
  const nextjsMissingMetadata = defineRule({
12590
12656
  id: "nextjs-missing-metadata",
@@ -12734,6 +12800,89 @@ const nextjsNoCssLink = defineRule({
12734
12800
  } })
12735
12801
  });
12736
12802
  //#endregion
12803
+ //#region src/plugin/rules/nextjs/nextjs-no-default-export-in-route-handler.ts
12804
+ const programHasNamedHttpMethodExport = (programNode) => {
12805
+ for (const statement of programNode.body ?? []) {
12806
+ if (!isNodeOfType(statement, "ExportNamedDeclaration")) continue;
12807
+ const declaration = statement.declaration;
12808
+ if (isNodeOfType(declaration, "FunctionDeclaration") && declaration.id?.name && ROUTE_HANDLER_HTTP_METHODS.has(declaration.id.name)) return true;
12809
+ if (isNodeOfType(declaration, "VariableDeclaration")) {
12810
+ for (const declarator of declaration.declarations ?? []) if (isNodeOfType(declarator.id, "Identifier") && ROUTE_HANDLER_HTTP_METHODS.has(declarator.id.name)) return true;
12811
+ }
12812
+ for (const specifier of statement.specifiers ?? []) if (isNodeOfType(specifier, "ExportSpecifier") && isNodeOfType(specifier.exported, "Identifier") && ROUTE_HANDLER_HTTP_METHODS.has(specifier.exported.name)) return true;
12813
+ }
12814
+ return false;
12815
+ };
12816
+ const nextjsNoDefaultExportInRouteHandler = defineRule({
12817
+ id: "nextjs-no-default-export-in-route-handler",
12818
+ title: "Default export in route handler",
12819
+ tags: ["test-noise"],
12820
+ requires: ["nextjs"],
12821
+ severity: "error",
12822
+ recommendation: "Replace `export default` with named HTTP method exports: `export async function GET(request) { … }`",
12823
+ create: (context) => {
12824
+ let isAppRouteHandler = false;
12825
+ let programNode = null;
12826
+ return {
12827
+ Program(node) {
12828
+ const filename = normalizeFilename$1(context.filename ?? "");
12829
+ isAppRouteHandler = APP_DIRECTORY_PATTERN.test(filename) && ROUTE_HANDLER_FILE_PATTERN.test(filename);
12830
+ programNode = node;
12831
+ },
12832
+ ExportDefaultDeclaration(node) {
12833
+ if (!isAppRouteHandler || !programNode) return;
12834
+ if (programHasNamedHttpMethodExport(programNode)) return;
12835
+ context.report({
12836
+ node,
12837
+ message: "Default exports in route.ts are silently ignored. Next.js only recognizes named HTTP method exports (GET, POST, etc.)."
12838
+ });
12839
+ },
12840
+ ExportNamedDeclaration(node) {
12841
+ if (!isAppRouteHandler || !programNode) return;
12842
+ if (!(node.specifiers ?? []).some((specifier) => isNodeOfType(specifier, "ExportSpecifier") && isNodeOfType(specifier.exported, "Identifier") && specifier.exported.name === "default")) return;
12843
+ if (programHasNamedHttpMethodExport(programNode)) return;
12844
+ context.report({
12845
+ node,
12846
+ message: "Default exports in route.ts are silently ignored. Next.js only recognizes named HTTP method exports (GET, POST, etc.)."
12847
+ });
12848
+ }
12849
+ };
12850
+ }
12851
+ });
12852
+ //#endregion
12853
+ //#region src/plugin/rules/nextjs/nextjs-no-edge-og-runtime.ts
12854
+ const nextjsNoEdgeOgRuntime = defineRule({
12855
+ id: "nextjs-no-edge-og-runtime",
12856
+ title: "Edge runtime in OG image route",
12857
+ tags: ["test-noise"],
12858
+ requires: ["nextjs"],
12859
+ severity: "warn",
12860
+ recommendation: "Remove `export const runtime = 'edge'` from OG image files. The default Node.js runtime supports more fonts and APIs",
12861
+ create: (context) => {
12862
+ let isOgImageFile = false;
12863
+ return {
12864
+ Program() {
12865
+ const filename = normalizeFilename$1(context.filename ?? "");
12866
+ isOgImageFile = OG_IMAGE_FILE_PATTERN.test(filename);
12867
+ },
12868
+ ExportNamedDeclaration(node) {
12869
+ if (!isOgImageFile) return;
12870
+ const declaration = node.declaration;
12871
+ if (!isNodeOfType(declaration, "VariableDeclaration")) return;
12872
+ for (const declarator of declaration.declarations ?? []) {
12873
+ if (!isNodeOfType(declarator, "VariableDeclarator")) continue;
12874
+ if (!isNodeOfType(declarator.id, "Identifier")) continue;
12875
+ if (declarator.id.name !== "runtime") continue;
12876
+ if ((isNodeOfType(declarator.init, "Literal") ? declarator.init.value : null) === "edge") context.report({
12877
+ node,
12878
+ message: "Edge runtime limits OG image generation. Node.js runtime supports more fonts, filesystem access, and larger response sizes."
12879
+ });
12880
+ }
12881
+ }
12882
+ };
12883
+ }
12884
+ });
12885
+ //#endregion
12737
12886
  //#region src/plugin/rules/nextjs/nextjs-no-font-link.ts
12738
12887
  const nextjsNoFontLink = defineRule({
12739
12888
  id: "nextjs-no-font-link",
@@ -12754,6 +12903,27 @@ const nextjsNoFontLink = defineRule({
12754
12903
  } })
12755
12904
  });
12756
12905
  //#endregion
12906
+ //#region src/plugin/rules/nextjs/nextjs-no-google-analytics-script.ts
12907
+ const nextjsNoGoogleAnalyticsScript = defineRule({
12908
+ id: "nextjs-no-google-analytics-script",
12909
+ title: "Manual Google Analytics script",
12910
+ tags: ["test-noise"],
12911
+ requires: ["nextjs"],
12912
+ severity: "warn",
12913
+ recommendation: "Use `import { GoogleAnalytics } from '@next/third-parties/google'` for automatic optimization & smaller bundles",
12914
+ create: (context) => ({ JSXOpeningElement(node) {
12915
+ if (!isNodeOfType(node.name, "JSXIdentifier")) return;
12916
+ if (node.name.name !== "script" && node.name.name !== "Script") return;
12917
+ const srcAttribute = findJsxAttribute(node.attributes ?? [], "src");
12918
+ if (!srcAttribute?.value) return;
12919
+ const srcValue = isNodeOfType(srcAttribute.value, "Literal") ? srcAttribute.value.value : null;
12920
+ if (typeof srcValue === "string" && GOOGLE_ANALYTICS_SCRIPT_PATTERN.test(srcValue)) context.report({
12921
+ node,
12922
+ message: "Manual Google Analytics script blocks rendering. Use @next/third-parties for optimal loading strategy."
12923
+ });
12924
+ } })
12925
+ });
12926
+ //#endregion
12757
12927
  //#region src/plugin/rules/nextjs/nextjs-no-head-import.ts
12758
12928
  const nextjsNoHeadImport = defineRule({
12759
12929
  id: "nextjs-no-head-import",
@@ -12784,8 +12954,9 @@ const nextjsNoImgElement = defineRule({
12784
12954
  create: (context) => {
12785
12955
  const filename = normalizeFilename$1(context.filename ?? "");
12786
12956
  const isOgRoute = OG_ROUTE_PATTERN.test(filename);
12957
+ const isMetadataImageRoute = isNextjsMetadataImageRouteFilename(filename);
12787
12958
  return { JSXOpeningElement(node) {
12788
- if (isOgRoute) return;
12959
+ if (isOgRoute || isMetadataImageRoute) return;
12789
12960
  if (isNodeOfType(node.name, "JSXIdentifier") && node.name.name === "img") context.report({
12790
12961
  node,
12791
12962
  message: "Plain <img> ships unoptimized, oversized images to your users."
@@ -12865,6 +13036,32 @@ const nextjsNoRedirectInTryCatch = defineRule({
12865
13036
  }
12866
13037
  });
12867
13038
  //#endregion
13039
+ //#region src/plugin/rules/nextjs/nextjs-no-script-in-head.ts
13040
+ const nextjsNoScriptInHead = defineRule({
13041
+ id: "nextjs-no-script-in-head",
13042
+ title: "next/script inside next/head",
13043
+ tags: ["test-noise"],
13044
+ requires: ["nextjs"],
13045
+ severity: "error",
13046
+ recommendation: "Move `<Script>` outside of `<Head>`. next/script manages its own placement and ignores head context",
13047
+ create: (context) => {
13048
+ let insideHeadDepth = 0;
13049
+ return {
13050
+ JSXOpeningElement(node) {
13051
+ if (!isNodeOfType(node.name, "JSXIdentifier")) return;
13052
+ if (node.name.name === "Head" && !node.selfClosing) insideHeadDepth++;
13053
+ if (node.name.name === "Script" && insideHeadDepth > 0) context.report({
13054
+ node,
13055
+ message: "next/script inside next/head is silently ignored. Move <Script> outside <Head> so it actually loads."
13056
+ });
13057
+ },
13058
+ JSXClosingElement(node) {
13059
+ if (isNodeOfType(node.name, "JSXIdentifier") && node.name.name === "Head") insideHeadDepth = Math.max(0, insideHeadDepth - 1);
13060
+ }
13061
+ };
13062
+ }
13063
+ });
13064
+ //#endregion
12868
13065
  //#region src/plugin/utils/is-cookies-call.ts
12869
13066
  const isCookiesCall = (node) => isNodeOfType(node, "CallExpression") && isNodeOfType(node.callee, "Identifier") && node.callee.name === "cookies";
12870
13067
  //#endregion
@@ -13213,8 +13410,25 @@ const nextjsNoUseSearchParamsWithoutSuspense = defineRule({
13213
13410
  }
13214
13411
  });
13215
13412
  //#endregion
13413
+ //#region src/plugin/rules/nextjs/nextjs-no-vercel-og-import.ts
13414
+ const nextjsNoVercelOgImport = defineRule({
13415
+ id: "nextjs-no-vercel-og-import",
13416
+ title: "@vercel/og import instead of next/og",
13417
+ tags: ["test-noise"],
13418
+ requires: ["nextjs"],
13419
+ severity: "warn",
13420
+ recommendation: "Use `import { ImageResponse } from \"next/og\"`. The `@vercel/og` package is built into Next.js and should not be imported directly",
13421
+ create: (context) => ({ ImportDeclaration(node) {
13422
+ if (node.source?.value !== "@vercel/og") return;
13423
+ context.report({
13424
+ node,
13425
+ message: "@vercel/og is bundled into Next.js. Import from \"next/og\" instead to avoid duplicate code and version mismatch."
13426
+ });
13427
+ } })
13428
+ });
13429
+ //#endregion
13216
13430
  //#region src/plugin/rules/a11y/no-access-key.ts
13217
- const MESSAGE$30 = "Screen reader users can lose their shortcuts because `accessKey` clashes with them, so remove it.";
13431
+ const MESSAGE$31 = "Screen reader users can lose their shortcuts because `accessKey` clashes with them, so remove it.";
13218
13432
  const isUndefinedIdentifier = (expression) => isNodeOfType(expression, "Identifier") && expression.name === "undefined";
13219
13433
  const noAccessKey = defineRule({
13220
13434
  id: "no-access-key",
@@ -13231,7 +13445,7 @@ const noAccessKey = defineRule({
13231
13445
  if (isNodeOfType(attributeValue, "Literal") && typeof attributeValue.value === "string") {
13232
13446
  context.report({
13233
13447
  node: accessKey,
13234
- message: MESSAGE$30
13448
+ message: MESSAGE$31
13235
13449
  });
13236
13450
  return;
13237
13451
  }
@@ -13241,7 +13455,7 @@ const noAccessKey = defineRule({
13241
13455
  if (isUndefinedIdentifier(expression)) return;
13242
13456
  context.report({
13243
13457
  node: accessKey,
13244
- message: MESSAGE$30
13458
+ message: MESSAGE$31
13245
13459
  });
13246
13460
  }
13247
13461
  } })
@@ -13724,7 +13938,7 @@ const noAdjustStateOnPropChange = defineRule({
13724
13938
  });
13725
13939
  //#endregion
13726
13940
  //#region src/plugin/rules/a11y/no-aria-hidden-on-focusable.ts
13727
- const MESSAGE$29 = "Screen reader users tab to this focusable element but hear nothing because `aria-hidden` skips it, so remove `aria-hidden` or stop it being focusable.";
13941
+ const MESSAGE$30 = "Screen reader users tab to this focusable element but hear nothing because `aria-hidden` skips it, so remove `aria-hidden` or stop it being focusable.";
13728
13942
  const noAriaHiddenOnFocusable = defineRule({
13729
13943
  id: "no-aria-hidden-on-focusable",
13730
13944
  title: "aria-hidden on focusable element",
@@ -13751,7 +13965,7 @@ const noAriaHiddenOnFocusable = defineRule({
13751
13965
  const isImplicitlyFocusable = isInteractiveElement(tag, node);
13752
13966
  if (isExplicitlyFocusable || isImplicitlyFocusable) context.report({
13753
13967
  node: ariaHidden,
13754
- message: MESSAGE$29
13968
+ message: MESSAGE$30
13755
13969
  });
13756
13970
  } })
13757
13971
  });
@@ -14119,7 +14333,7 @@ const noArrayIndexAsKey = defineRule({
14119
14333
  });
14120
14334
  //#endregion
14121
14335
  //#region src/plugin/rules/react-builtins/no-array-index-key.ts
14122
- const MESSAGE$28 = "Your users can see & submit the wrong data when this list reorders.";
14336
+ const MESSAGE$29 = "Your users can see & submit the wrong data when this list reorders.";
14123
14337
  const SECOND_INDEX_METHODS = new Set([
14124
14338
  "every",
14125
14339
  "filter",
@@ -14323,7 +14537,7 @@ const noArrayIndexKey = defineRule({
14323
14537
  }
14324
14538
  context.report({
14325
14539
  node: keyAttribute,
14326
- message: MESSAGE$28
14540
+ message: MESSAGE$29
14327
14541
  });
14328
14542
  },
14329
14543
  CallExpression(node) {
@@ -14343,7 +14557,7 @@ const noArrayIndexKey = defineRule({
14343
14557
  if (propName !== "key") continue;
14344
14558
  if (expressionUsesIndex(property.value, indexBinding.name)) context.report({
14345
14559
  node: property,
14346
- message: MESSAGE$28
14560
+ message: MESSAGE$29
14347
14561
  });
14348
14562
  }
14349
14563
  }
@@ -14351,7 +14565,7 @@ const noArrayIndexKey = defineRule({
14351
14565
  });
14352
14566
  //#endregion
14353
14567
  //#region src/plugin/rules/a11y/no-autofocus.ts
14354
- const MESSAGE$27 = "Screen reader & keyboard users get disoriented because `autoFocus` jumps focus on load, so remove it and let people choose where to focus.";
14568
+ const MESSAGE$28 = "Screen reader & keyboard users get disoriented because `autoFocus` jumps focus on load, so remove it and let people choose where to focus.";
14355
14569
  const resolveSettings$21 = (settings) => {
14356
14570
  const reactDoctor = settings?.["react-doctor"];
14357
14571
  return { ignoreNonDOM: (typeof reactDoctor === "object" && reactDoctor !== null ? reactDoctor.noAutofocus ?? {} : {}).ignoreNonDOM ?? true };
@@ -14407,7 +14621,7 @@ const noAutofocus = defineRule({
14407
14621
  }
14408
14622
  context.report({
14409
14623
  node: autoFocusAttribute,
14410
- message: MESSAGE$27
14624
+ message: MESSAGE$28
14411
14625
  });
14412
14626
  } };
14413
14627
  }
@@ -14932,7 +15146,7 @@ const noChainStateUpdates = defineRule({
14932
15146
  });
14933
15147
  //#endregion
14934
15148
  //#region src/plugin/rules/react-builtins/no-children-prop.ts
14935
- const MESSAGE$26 = "Your component can render the wrong children when you pass them through a `children` prop.";
15149
+ const MESSAGE$27 = "Your component can render the wrong children when you pass them through a `children` prop.";
14936
15150
  const noChildrenProp = defineRule({
14937
15151
  id: "no-children-prop",
14938
15152
  title: "Children passed as a prop",
@@ -14944,7 +15158,7 @@ const noChildrenProp = defineRule({
14944
15158
  if (node.name.name !== "children") return;
14945
15159
  context.report({
14946
15160
  node: node.name,
14947
- message: MESSAGE$26
15161
+ message: MESSAGE$27
14948
15162
  });
14949
15163
  },
14950
15164
  CallExpression(node) {
@@ -14957,7 +15171,7 @@ const noChildrenProp = defineRule({
14957
15171
  const propertyKey = property.key;
14958
15172
  if (isNodeOfType(propertyKey, "Identifier") && propertyKey.name === "children" || isNodeOfType(propertyKey, "Literal") && propertyKey.value === "children") context.report({
14959
15173
  node: propertyKey,
14960
- message: MESSAGE$26
15174
+ message: MESSAGE$27
14961
15175
  });
14962
15176
  }
14963
15177
  }
@@ -14965,7 +15179,7 @@ const noChildrenProp = defineRule({
14965
15179
  });
14966
15180
  //#endregion
14967
15181
  //#region src/plugin/rules/react-builtins/no-clone-element.ts
14968
- const MESSAGE$25 = "`React.cloneElement` breaks easily when the cloned element's props change.";
15182
+ const MESSAGE$26 = "`React.cloneElement` breaks easily when the cloned element's props change.";
14969
15183
  const noCloneElement = defineRule({
14970
15184
  id: "no-clone-element",
14971
15185
  title: "Use of cloneElement",
@@ -14978,7 +15192,7 @@ const noCloneElement = defineRule({
14978
15192
  if (isNodeOfType(callee, "Identifier") && callee.name === "cloneElement") {
14979
15193
  if (isImportedFromModule(node, "cloneElement", "react")) context.report({
14980
15194
  node: callee,
14981
- message: MESSAGE$25
15195
+ message: MESSAGE$26
14982
15196
  });
14983
15197
  return;
14984
15198
  }
@@ -14991,7 +15205,7 @@ const noCloneElement = defineRule({
14991
15205
  if (!isImportedFromModule(node, callee.object.name, "react")) return;
14992
15206
  context.report({
14993
15207
  node: callee,
14994
- message: MESSAGE$25
15208
+ message: MESSAGE$26
14995
15209
  });
14996
15210
  }
14997
15211
  } })
@@ -15040,7 +15254,7 @@ const enclosingComponentOrHookName = (node) => {
15040
15254
  };
15041
15255
  //#endregion
15042
15256
  //#region src/plugin/rules/state-and-effects/no-create-context-in-render.ts
15043
- const MESSAGE$24 = "createContext() builds a new context every render, so every consumer gets cut off & resets.";
15257
+ const MESSAGE$25 = "createContext() builds a new context every render, so every consumer gets cut off & resets.";
15044
15258
  const CONTEXT_MODULES = [
15045
15259
  "react",
15046
15260
  "use-context-selector",
@@ -15076,7 +15290,7 @@ const noCreateContextInRender = defineRule({
15076
15290
  if (!componentOrHookName) return;
15077
15291
  context.report({
15078
15292
  node,
15079
- message: `${MESSAGE$24} (called inside "${componentOrHookName}")`
15293
+ message: `${MESSAGE$25} (called inside "${componentOrHookName}")`
15080
15294
  });
15081
15295
  } })
15082
15296
  });
@@ -15216,7 +15430,7 @@ const noCreateStoreInRender = defineRule({
15216
15430
  });
15217
15431
  //#endregion
15218
15432
  //#region src/plugin/rules/react-builtins/no-danger.ts
15219
- const MESSAGE$23 = "`dangerouslySetInnerHTML` is an XSS hole that runs attacker-controlled HTML in your users' browsers.";
15433
+ const MESSAGE$24 = "`dangerouslySetInnerHTML` is an XSS hole that runs attacker-controlled HTML in your users' browsers.";
15220
15434
  const noDanger = defineRule({
15221
15435
  id: "no-danger",
15222
15436
  title: "Use of dangerouslySetInnerHTML",
@@ -15229,7 +15443,7 @@ const noDanger = defineRule({
15229
15443
  if (!propAttribute) return;
15230
15444
  context.report({
15231
15445
  node: propAttribute.name,
15232
- message: MESSAGE$23
15446
+ message: MESSAGE$24
15233
15447
  });
15234
15448
  },
15235
15449
  CallExpression(node) {
@@ -15241,7 +15455,7 @@ const noDanger = defineRule({
15241
15455
  const propertyKey = property.key;
15242
15456
  if (isNodeOfType(propertyKey, "Identifier") && propertyKey.name === "dangerouslySetInnerHTML" || isNodeOfType(propertyKey, "Literal") && propertyKey.value === "dangerouslySetInnerHTML") context.report({
15243
15457
  node: propertyKey,
15244
- message: MESSAGE$23
15458
+ message: MESSAGE$24
15245
15459
  });
15246
15460
  }
15247
15461
  }
@@ -15249,7 +15463,7 @@ const noDanger = defineRule({
15249
15463
  });
15250
15464
  //#endregion
15251
15465
  //#region src/plugin/rules/react-builtins/no-danger-with-children.ts
15252
- const MESSAGE$22 = "React throws an error when you set both children & `dangerouslySetInnerHTML`.";
15466
+ const MESSAGE$23 = "React throws an error when you set both children & `dangerouslySetInnerHTML`.";
15253
15467
  const isLineBreak = (child) => {
15254
15468
  if (!isNodeOfType(child, "JSXText")) return false;
15255
15469
  return child.value.trim().length === 0 && child.value.includes("\n");
@@ -15319,7 +15533,7 @@ const noDangerWithChildren = defineRule({
15319
15533
  if (!hasChildrenProp && !hasNestedChildren) return;
15320
15534
  if (hasJsxPropIgnoreCase(opening.attributes, "dangerouslySetInnerHTML") || spreadPropsShape.hasDangerously) context.report({
15321
15535
  node: opening,
15322
- message: MESSAGE$22
15536
+ message: MESSAGE$23
15323
15537
  });
15324
15538
  },
15325
15539
  CallExpression(node) {
@@ -15331,7 +15545,7 @@ const noDangerWithChildren = defineRule({
15331
15545
  if (!propsShape.hasDangerously) return;
15332
15546
  if (node.arguments.length >= 3 || propsShape.hasChildren) context.report({
15333
15547
  node,
15334
- message: MESSAGE$22
15548
+ message: MESSAGE$23
15335
15549
  });
15336
15550
  }
15337
15551
  })
@@ -15909,7 +16123,7 @@ const isSetStateCallInLifecycle = (setStateCall, lifecycleNames, options = {}) =
15909
16123
  //#endregion
15910
16124
  //#region src/plugin/rules/react-builtins/no-did-mount-set-state.ts
15911
16125
  const LIFECYCLE_NAMES$2 = new Set(["componentDidMount"]);
15912
- const MESSAGE$21 = "Your users see an extra render right after mount when you call `setState` in `componentDidMount`.";
16126
+ const MESSAGE$22 = "Your users see an extra render right after mount when you call `setState` in `componentDidMount`.";
15913
16127
  const resolveSettings$20 = (settings) => {
15914
16128
  const reactDoctor = settings?.["react-doctor"];
15915
16129
  return { mode: (typeof reactDoctor === "object" && reactDoctor !== null ? reactDoctor.noDidMountSetState ?? {} : {}).mode ?? "allowed" };
@@ -15928,7 +16142,7 @@ const noDidMountSetState = defineRule({
15928
16142
  if (!isSetStateCallInLifecycle(node, LIFECYCLE_NAMES$2, { disallowInNestedFunctions: mode === "disallow-in-func" })) return;
15929
16143
  context.report({
15930
16144
  node: node.callee,
15931
- message: MESSAGE$21
16145
+ message: MESSAGE$22
15932
16146
  });
15933
16147
  } };
15934
16148
  }
@@ -15936,7 +16150,7 @@ const noDidMountSetState = defineRule({
15936
16150
  //#endregion
15937
16151
  //#region src/plugin/rules/react-builtins/no-did-update-set-state.ts
15938
16152
  const LIFECYCLE_NAMES$1 = new Set(["componentDidUpdate"]);
15939
- const MESSAGE$20 = "This can loop forever & freeze the component.";
16153
+ const MESSAGE$21 = "This can loop forever & freeze the component.";
15940
16154
  const resolveSettings$19 = (settings) => {
15941
16155
  const reactDoctor = settings?.["react-doctor"];
15942
16156
  return { mode: (typeof reactDoctor === "object" && reactDoctor !== null ? reactDoctor.noDidUpdateSetState ?? {} : {}).mode ?? "allowed" };
@@ -15955,7 +16169,7 @@ const noDidUpdateSetState = defineRule({
15955
16169
  if (!isSetStateCallInLifecycle(node, LIFECYCLE_NAMES$1, { disallowInNestedFunctions: mode === "disallow-in-func" })) return;
15956
16170
  context.report({
15957
16171
  node: node.callee,
15958
- message: MESSAGE$20
16172
+ message: MESSAGE$21
15959
16173
  });
15960
16174
  } };
15961
16175
  }
@@ -15978,7 +16192,7 @@ const isStateMemberExpression = (node) => {
15978
16192
  };
15979
16193
  //#endregion
15980
16194
  //#region src/plugin/rules/react-builtins/no-direct-mutation-state.ts
15981
- const MESSAGE$19 = "Your users see stale data because mutating `this.state` by hand never redraws & gets overwritten.";
16195
+ const MESSAGE$20 = "Your users see stale data because mutating `this.state` by hand never redraws & gets overwritten.";
15982
16196
  const shouldIgnoreMutation = (node) => {
15983
16197
  let isConstructor = false;
15984
16198
  let isInsideCallExpression = false;
@@ -16000,7 +16214,7 @@ const reportIfStateMutation = (context, reportNode, target) => {
16000
16214
  if (shouldIgnoreMutation(reportNode)) return;
16001
16215
  context.report({
16002
16216
  node: reportNode,
16003
- message: MESSAGE$19
16217
+ message: MESSAGE$20
16004
16218
  });
16005
16219
  };
16006
16220
  const noDirectMutationState = defineRule({
@@ -17588,7 +17802,7 @@ const ALLOWED_NAMESPACES = new Set([
17588
17802
  "ReactDOM",
17589
17803
  "ReactDom"
17590
17804
  ]);
17591
- const MESSAGE$18 = "`findDOMNode` crashes your app in React 19 because it was removed.";
17805
+ const MESSAGE$19 = "`findDOMNode` crashes your app in React 19 because it was removed.";
17592
17806
  const noFindDomNode = defineRule({
17593
17807
  id: "no-find-dom-node",
17594
17808
  title: "Use of findDOMNode",
@@ -17599,7 +17813,7 @@ const noFindDomNode = defineRule({
17599
17813
  if (isNodeOfType(callee, "Identifier") && callee.name === "findDOMNode") {
17600
17814
  context.report({
17601
17815
  node: callee,
17602
- message: MESSAGE$18
17816
+ message: MESSAGE$19
17603
17817
  });
17604
17818
  return;
17605
17819
  }
@@ -17610,7 +17824,7 @@ const noFindDomNode = defineRule({
17610
17824
  if (callee.property.name !== "findDOMNode") return;
17611
17825
  context.report({
17612
17826
  node: callee.property,
17613
- message: MESSAGE$18
17827
+ message: MESSAGE$19
17614
17828
  });
17615
17829
  }
17616
17830
  } })
@@ -18139,6 +18353,51 @@ const noIsMounted = defineRule({
18139
18353
  } })
18140
18354
  });
18141
18355
  //#endregion
18356
+ //#region src/plugin/rules/correctness/no-jsx-element-type.ts
18357
+ const MESSAGE$18 = "`JSX.Element` is too narrow: it excludes `null`, strings, numbers, and fragments that components commonly return. Use `React.ReactNode` instead.";
18358
+ const isJsxElementTypeReference = (node) => {
18359
+ if (!isNodeOfType(node, "TSTypeReference")) return false;
18360
+ const typeName = node.typeName;
18361
+ if (!isNodeOfType(typeName, "TSQualifiedName")) return false;
18362
+ return isNodeOfType(typeName.left, "Identifier") && typeName.left.name === "JSX" && isNodeOfType(typeName.right, "Identifier") && typeName.right.name === "Element";
18363
+ };
18364
+ const extractReturnTypeAnnotation = (returnType) => {
18365
+ if (!returnType) return null;
18366
+ if (!isNodeOfType(returnType, "TSTypeAnnotation")) return null;
18367
+ return returnType.typeAnnotation ?? null;
18368
+ };
18369
+ const checkReturnType = (context, returnType) => {
18370
+ const typeAnnotation = extractReturnTypeAnnotation(returnType);
18371
+ if (!typeAnnotation) return;
18372
+ if (isJsxElementTypeReference(typeAnnotation)) context.report({
18373
+ node: typeAnnotation,
18374
+ message: MESSAGE$18
18375
+ });
18376
+ };
18377
+ const noJsxElementType = defineRule({
18378
+ id: "no-jsx-element-type",
18379
+ title: "No JSX.Element",
18380
+ severity: "error",
18381
+ recommendation: "Replace `JSX.Element` with `React.ReactNode`. `JSX.Element` is too narrow: it excludes `null`, strings, numbers, and fragments that components commonly return.",
18382
+ create: (context) => ({
18383
+ FunctionDeclaration(node) {
18384
+ checkReturnType(context, node.returnType);
18385
+ },
18386
+ ArrowFunctionExpression(node) {
18387
+ checkReturnType(context, node.returnType);
18388
+ },
18389
+ FunctionExpression(node) {
18390
+ checkReturnType(context, node.returnType);
18391
+ },
18392
+ TSDeclareFunction(node) {
18393
+ checkReturnType(context, node.returnType);
18394
+ },
18395
+ TSMethodSignature(node) {
18396
+ checkReturnType(context, node.returnType);
18397
+ }
18398
+ })
18399
+ });
18400
+ //#endregion
18142
18401
  //#region src/plugin/rules/design/no-justified-text.ts
18143
18402
  const noJustifiedText = defineRule({
18144
18403
  id: "no-justified-text",
@@ -23259,6 +23518,7 @@ const noUnknownProperty = defineRule({
23259
23518
  create: (context) => {
23260
23519
  const { ignore = [], requireDataLowercase = false } = resolveSettings$10(context.settings);
23261
23520
  const ignoreSet = new Set(ignore);
23521
+ if (isNextjsMetadataImageRouteFilename(context.filename)) ignoreSet.add("tw");
23262
23522
  let fileIsNonReactJsx = false;
23263
23523
  return {
23264
23524
  Program(node) {
@@ -23953,6 +24213,8 @@ const ENTRY_POINT_BASENAMES = new Set([
23953
24213
  "global-error.jsx",
23954
24214
  "route.tsx",
23955
24215
  "route.jsx",
24216
+ "_layout.tsx",
24217
+ "_layout.jsx",
23956
24218
  "_app.tsx",
23957
24219
  "_app.jsx",
23958
24220
  "_document.tsx",
@@ -25375,6 +25637,26 @@ const preferUseReducer = defineRule({
25375
25637
  }
25376
25638
  });
25377
25639
  //#endregion
25640
+ //#region src/plugin/rules/tanstack-query/query-destructure-result.ts
25641
+ const queryDestructureResult = defineRule({
25642
+ id: "query-destructure-result",
25643
+ title: "Destructure TanStack Query result",
25644
+ tags: ["test-noise"],
25645
+ requires: ["tanstack-query"],
25646
+ severity: "error",
25647
+ recommendation: "Destructure only the fields you need, like `const { data, isLoading } = useQuery(...)`. Assigning the whole object bypasses TanStack Query's tracked-property optimization and subscribes to every field.",
25648
+ create: (context) => ({ VariableDeclarator(node) {
25649
+ if (!isNodeOfType(node.id, "Identifier")) return;
25650
+ if (!node.init || !isNodeOfType(node.init, "CallExpression")) return;
25651
+ const calleeName = isNodeOfType(node.init.callee, "Identifier") ? node.init.callee.name : null;
25652
+ if (!calleeName || !TANSTACK_QUERY_HOOKS.has(calleeName)) return;
25653
+ context.report({
25654
+ node: node.id,
25655
+ message: `Destructure ${calleeName}() results instead of assigning the whole query object, so TanStack Query only subscribes to the fields you use.`
25656
+ });
25657
+ } })
25658
+ });
25659
+ //#endregion
25378
25660
  //#region src/plugin/rules/tanstack-query/query-mutation-missing-invalidation.ts
25379
25661
  const queryMutationMissingInvalidation = defineRule({
25380
25662
  id: "query-mutation-missing-invalidation",
@@ -32917,15 +33199,6 @@ const serverFetchWithoutRevalidate = defineRule({
32917
33199
  });
32918
33200
  //#endregion
32919
33201
  //#region src/plugin/rules/server/server-hoist-static-io.ts
32920
- const ROUTE_HANDLER_HTTP_METHODS = new Set([
32921
- "GET",
32922
- "POST",
32923
- "PUT",
32924
- "PATCH",
32925
- "DELETE",
32926
- "OPTIONS",
32927
- "HEAD"
32928
- ]);
32929
33202
  const STATIC_IO_FUNCTIONS = new Set([
32930
33203
  "readFileSync",
32931
33204
  "readFile",
@@ -35440,6 +35713,28 @@ const reactDoctorRules = [
35440
35713
  category: "Bugs"
35441
35714
  }
35442
35715
  },
35716
+ {
35717
+ key: "react-doctor/nextjs-error-boundary-missing-use-client",
35718
+ id: "nextjs-error-boundary-missing-use-client",
35719
+ source: "react-doctor",
35720
+ originallyExternal: false,
35721
+ rule: {
35722
+ ...nextjsErrorBoundaryMissingUseClient,
35723
+ framework: "nextjs",
35724
+ category: "Bugs"
35725
+ }
35726
+ },
35727
+ {
35728
+ key: "react-doctor/nextjs-global-error-missing-html-body",
35729
+ id: "nextjs-global-error-missing-html-body",
35730
+ source: "react-doctor",
35731
+ originallyExternal: false,
35732
+ rule: {
35733
+ ...nextjsGlobalErrorMissingHtmlBody,
35734
+ framework: "nextjs",
35735
+ category: "Bugs"
35736
+ }
35737
+ },
35443
35738
  {
35444
35739
  key: "react-doctor/nextjs-image-missing-sizes",
35445
35740
  id: "nextjs-image-missing-sizes",
@@ -35517,6 +35812,28 @@ const reactDoctorRules = [
35517
35812
  category: "Bugs"
35518
35813
  }
35519
35814
  },
35815
+ {
35816
+ key: "react-doctor/nextjs-no-default-export-in-route-handler",
35817
+ id: "nextjs-no-default-export-in-route-handler",
35818
+ source: "react-doctor",
35819
+ originallyExternal: false,
35820
+ rule: {
35821
+ ...nextjsNoDefaultExportInRouteHandler,
35822
+ framework: "nextjs",
35823
+ category: "Bugs"
35824
+ }
35825
+ },
35826
+ {
35827
+ key: "react-doctor/nextjs-no-edge-og-runtime",
35828
+ id: "nextjs-no-edge-og-runtime",
35829
+ source: "react-doctor",
35830
+ originallyExternal: false,
35831
+ rule: {
35832
+ ...nextjsNoEdgeOgRuntime,
35833
+ framework: "nextjs",
35834
+ category: "Bugs"
35835
+ }
35836
+ },
35520
35837
  {
35521
35838
  key: "react-doctor/nextjs-no-font-link",
35522
35839
  id: "nextjs-no-font-link",
@@ -35528,6 +35845,17 @@ const reactDoctorRules = [
35528
35845
  category: "Bugs"
35529
35846
  }
35530
35847
  },
35848
+ {
35849
+ key: "react-doctor/nextjs-no-google-analytics-script",
35850
+ id: "nextjs-no-google-analytics-script",
35851
+ source: "react-doctor",
35852
+ originallyExternal: false,
35853
+ rule: {
35854
+ ...nextjsNoGoogleAnalyticsScript,
35855
+ framework: "nextjs",
35856
+ category: "Bugs"
35857
+ }
35858
+ },
35531
35859
  {
35532
35860
  key: "react-doctor/nextjs-no-head-import",
35533
35861
  id: "nextjs-no-head-import",
@@ -35583,6 +35911,17 @@ const reactDoctorRules = [
35583
35911
  category: "Bugs"
35584
35912
  }
35585
35913
  },
35914
+ {
35915
+ key: "react-doctor/nextjs-no-script-in-head",
35916
+ id: "nextjs-no-script-in-head",
35917
+ source: "react-doctor",
35918
+ originallyExternal: false,
35919
+ rule: {
35920
+ ...nextjsNoScriptInHead,
35921
+ framework: "nextjs",
35922
+ category: "Bugs"
35923
+ }
35924
+ },
35586
35925
  {
35587
35926
  key: "react-doctor/nextjs-no-side-effect-in-get-handler",
35588
35927
  id: "nextjs-no-side-effect-in-get-handler",
@@ -35605,6 +35944,17 @@ const reactDoctorRules = [
35605
35944
  category: "Bugs"
35606
35945
  }
35607
35946
  },
35947
+ {
35948
+ key: "react-doctor/nextjs-no-vercel-og-import",
35949
+ id: "nextjs-no-vercel-og-import",
35950
+ source: "react-doctor",
35951
+ originallyExternal: false,
35952
+ rule: {
35953
+ ...nextjsNoVercelOgImport,
35954
+ framework: "nextjs",
35955
+ category: "Bugs"
35956
+ }
35957
+ },
35608
35958
  {
35609
35959
  key: "react-doctor/no-access-key",
35610
35960
  id: "no-access-key",
@@ -36155,6 +36505,17 @@ const reactDoctorRules = [
36155
36505
  category: "Bugs"
36156
36506
  }
36157
36507
  },
36508
+ {
36509
+ key: "react-doctor/no-jsx-element-type",
36510
+ id: "no-jsx-element-type",
36511
+ source: "react-doctor",
36512
+ originallyExternal: false,
36513
+ rule: {
36514
+ ...noJsxElementType,
36515
+ framework: "global",
36516
+ category: "Bugs"
36517
+ }
36518
+ },
36158
36519
  {
36159
36520
  key: "react-doctor/no-justified-text",
36160
36521
  id: "no-justified-text",
@@ -36980,6 +37341,17 @@ const reactDoctorRules = [
36980
37341
  category: "Bugs"
36981
37342
  }
36982
37343
  },
37344
+ {
37345
+ key: "react-doctor/query-destructure-result",
37346
+ id: "query-destructure-result",
37347
+ source: "react-doctor",
37348
+ originallyExternal: false,
37349
+ rule: {
37350
+ ...queryDestructureResult,
37351
+ framework: "tanstack-query",
37352
+ category: "Bugs"
37353
+ }
37354
+ },
36983
37355
  {
36984
37356
  key: "react-doctor/query-mutation-missing-invalidation",
36985
37357
  id: "query-mutation-missing-invalidation",