oxlint-plugin-react-doctor 0.3.0 → 0.4.0-dev.15bd9d8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.ts +342 -0
- package/dist/index.js +487 -119
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -1102,7 +1102,7 @@ const anchorAmbiguousText = defineRule({
|
|
|
1102
1102
|
});
|
|
1103
1103
|
//#endregion
|
|
1104
1104
|
//#region src/plugin/rules/a11y/anchor-has-content.ts
|
|
1105
|
-
const MESSAGE$
|
|
1105
|
+
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
1106
|
const anchorHasContent = defineRule({
|
|
1107
1107
|
id: "anchor-has-content",
|
|
1108
1108
|
title: "Anchor has no content",
|
|
@@ -1118,7 +1118,7 @@ const anchorHasContent = defineRule({
|
|
|
1118
1118
|
for (const attribute of ["title", "aria-label"]) if (hasJsxPropIgnoreCase(opening.attributes, attribute)) return;
|
|
1119
1119
|
context.report({
|
|
1120
1120
|
node: opening.name,
|
|
1121
|
-
message: MESSAGE$
|
|
1121
|
+
message: MESSAGE$51
|
|
1122
1122
|
});
|
|
1123
1123
|
} })
|
|
1124
1124
|
});
|
|
@@ -1512,7 +1512,7 @@ const parseJsxValue = (value) => {
|
|
|
1512
1512
|
};
|
|
1513
1513
|
//#endregion
|
|
1514
1514
|
//#region src/plugin/rules/a11y/aria-activedescendant-has-tabindex.ts
|
|
1515
|
-
const MESSAGE$
|
|
1515
|
+
const MESSAGE$50 = "Keyboard users can't focus this element with `aria-activedescendant` because it isn't tabbable, so add `tabIndex={0}`.";
|
|
1516
1516
|
const ariaActivedescendantHasTabindex = defineRule({
|
|
1517
1517
|
id: "aria-activedescendant-has-tabindex",
|
|
1518
1518
|
title: "aria-activedescendant missing tabindex",
|
|
@@ -1530,14 +1530,14 @@ const ariaActivedescendantHasTabindex = defineRule({
|
|
|
1530
1530
|
if (tabIndexValue === null || tabIndexValue >= -1) return;
|
|
1531
1531
|
context.report({
|
|
1532
1532
|
node: node.name,
|
|
1533
|
-
message: MESSAGE$
|
|
1533
|
+
message: MESSAGE$50
|
|
1534
1534
|
});
|
|
1535
1535
|
return;
|
|
1536
1536
|
}
|
|
1537
1537
|
if (isInteractiveElement(tag, node)) return;
|
|
1538
1538
|
context.report({
|
|
1539
1539
|
node: node.name,
|
|
1540
|
-
message: MESSAGE$
|
|
1540
|
+
message: MESSAGE$50
|
|
1541
1541
|
});
|
|
1542
1542
|
} })
|
|
1543
1543
|
});
|
|
@@ -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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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",
|
|
@@ -12865,6 +13035,32 @@ const nextjsNoRedirectInTryCatch = defineRule({
|
|
|
12865
13035
|
}
|
|
12866
13036
|
});
|
|
12867
13037
|
//#endregion
|
|
13038
|
+
//#region src/plugin/rules/nextjs/nextjs-no-script-in-head.ts
|
|
13039
|
+
const nextjsNoScriptInHead = defineRule({
|
|
13040
|
+
id: "nextjs-no-script-in-head",
|
|
13041
|
+
title: "next/script inside next/head",
|
|
13042
|
+
tags: ["test-noise"],
|
|
13043
|
+
requires: ["nextjs"],
|
|
13044
|
+
severity: "error",
|
|
13045
|
+
recommendation: "Move `<Script>` outside of `<Head>`. next/script manages its own placement and ignores head context",
|
|
13046
|
+
create: (context) => {
|
|
13047
|
+
let insideHeadDepth = 0;
|
|
13048
|
+
return {
|
|
13049
|
+
JSXOpeningElement(node) {
|
|
13050
|
+
if (!isNodeOfType(node.name, "JSXIdentifier")) return;
|
|
13051
|
+
if (node.name.name === "Head" && !node.selfClosing) insideHeadDepth++;
|
|
13052
|
+
if (node.name.name === "Script" && insideHeadDepth > 0) context.report({
|
|
13053
|
+
node,
|
|
13054
|
+
message: "next/script inside next/head is silently ignored. Move <Script> outside <Head> so it actually loads."
|
|
13055
|
+
});
|
|
13056
|
+
},
|
|
13057
|
+
JSXClosingElement(node) {
|
|
13058
|
+
if (isNodeOfType(node.name, "JSXIdentifier") && node.name.name === "Head") insideHeadDepth = Math.max(0, insideHeadDepth - 1);
|
|
13059
|
+
}
|
|
13060
|
+
};
|
|
13061
|
+
}
|
|
13062
|
+
});
|
|
13063
|
+
//#endregion
|
|
12868
13064
|
//#region src/plugin/utils/is-cookies-call.ts
|
|
12869
13065
|
const isCookiesCall = (node) => isNodeOfType(node, "CallExpression") && isNodeOfType(node.callee, "Identifier") && node.callee.name === "cookies";
|
|
12870
13066
|
//#endregion
|
|
@@ -13213,8 +13409,25 @@ const nextjsNoUseSearchParamsWithoutSuspense = defineRule({
|
|
|
13213
13409
|
}
|
|
13214
13410
|
});
|
|
13215
13411
|
//#endregion
|
|
13412
|
+
//#region src/plugin/rules/nextjs/nextjs-no-vercel-og-import.ts
|
|
13413
|
+
const nextjsNoVercelOgImport = defineRule({
|
|
13414
|
+
id: "nextjs-no-vercel-og-import",
|
|
13415
|
+
title: "@vercel/og import instead of next/og",
|
|
13416
|
+
tags: ["test-noise"],
|
|
13417
|
+
requires: ["nextjs"],
|
|
13418
|
+
severity: "warn",
|
|
13419
|
+
recommendation: "Use `import { ImageResponse } from \"next/og\"`. The `@vercel/og` package is built into Next.js and should not be imported directly",
|
|
13420
|
+
create: (context) => ({ ImportDeclaration(node) {
|
|
13421
|
+
if (node.source?.value !== "@vercel/og") return;
|
|
13422
|
+
context.report({
|
|
13423
|
+
node,
|
|
13424
|
+
message: "@vercel/og is bundled into Next.js. Import from \"next/og\" instead to avoid duplicate code and version mismatch."
|
|
13425
|
+
});
|
|
13426
|
+
} })
|
|
13427
|
+
});
|
|
13428
|
+
//#endregion
|
|
13216
13429
|
//#region src/plugin/rules/a11y/no-access-key.ts
|
|
13217
|
-
const MESSAGE$
|
|
13430
|
+
const MESSAGE$31 = "Screen reader users can lose their shortcuts because `accessKey` clashes with them, so remove it.";
|
|
13218
13431
|
const isUndefinedIdentifier = (expression) => isNodeOfType(expression, "Identifier") && expression.name === "undefined";
|
|
13219
13432
|
const noAccessKey = defineRule({
|
|
13220
13433
|
id: "no-access-key",
|
|
@@ -13231,7 +13444,7 @@ const noAccessKey = defineRule({
|
|
|
13231
13444
|
if (isNodeOfType(attributeValue, "Literal") && typeof attributeValue.value === "string") {
|
|
13232
13445
|
context.report({
|
|
13233
13446
|
node: accessKey,
|
|
13234
|
-
message: MESSAGE$
|
|
13447
|
+
message: MESSAGE$31
|
|
13235
13448
|
});
|
|
13236
13449
|
return;
|
|
13237
13450
|
}
|
|
@@ -13241,7 +13454,7 @@ const noAccessKey = defineRule({
|
|
|
13241
13454
|
if (isUndefinedIdentifier(expression)) return;
|
|
13242
13455
|
context.report({
|
|
13243
13456
|
node: accessKey,
|
|
13244
|
-
message: MESSAGE$
|
|
13457
|
+
message: MESSAGE$31
|
|
13245
13458
|
});
|
|
13246
13459
|
}
|
|
13247
13460
|
} })
|
|
@@ -13724,7 +13937,7 @@ const noAdjustStateOnPropChange = defineRule({
|
|
|
13724
13937
|
});
|
|
13725
13938
|
//#endregion
|
|
13726
13939
|
//#region src/plugin/rules/a11y/no-aria-hidden-on-focusable.ts
|
|
13727
|
-
const MESSAGE$
|
|
13940
|
+
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
13941
|
const noAriaHiddenOnFocusable = defineRule({
|
|
13729
13942
|
id: "no-aria-hidden-on-focusable",
|
|
13730
13943
|
title: "aria-hidden on focusable element",
|
|
@@ -13751,7 +13964,7 @@ const noAriaHiddenOnFocusable = defineRule({
|
|
|
13751
13964
|
const isImplicitlyFocusable = isInteractiveElement(tag, node);
|
|
13752
13965
|
if (isExplicitlyFocusable || isImplicitlyFocusable) context.report({
|
|
13753
13966
|
node: ariaHidden,
|
|
13754
|
-
message: MESSAGE$
|
|
13967
|
+
message: MESSAGE$30
|
|
13755
13968
|
});
|
|
13756
13969
|
} })
|
|
13757
13970
|
});
|
|
@@ -14119,7 +14332,7 @@ const noArrayIndexAsKey = defineRule({
|
|
|
14119
14332
|
});
|
|
14120
14333
|
//#endregion
|
|
14121
14334
|
//#region src/plugin/rules/react-builtins/no-array-index-key.ts
|
|
14122
|
-
const MESSAGE$
|
|
14335
|
+
const MESSAGE$29 = "Your users can see & submit the wrong data when this list reorders.";
|
|
14123
14336
|
const SECOND_INDEX_METHODS = new Set([
|
|
14124
14337
|
"every",
|
|
14125
14338
|
"filter",
|
|
@@ -14323,7 +14536,7 @@ const noArrayIndexKey = defineRule({
|
|
|
14323
14536
|
}
|
|
14324
14537
|
context.report({
|
|
14325
14538
|
node: keyAttribute,
|
|
14326
|
-
message: MESSAGE$
|
|
14539
|
+
message: MESSAGE$29
|
|
14327
14540
|
});
|
|
14328
14541
|
},
|
|
14329
14542
|
CallExpression(node) {
|
|
@@ -14343,7 +14556,7 @@ const noArrayIndexKey = defineRule({
|
|
|
14343
14556
|
if (propName !== "key") continue;
|
|
14344
14557
|
if (expressionUsesIndex(property.value, indexBinding.name)) context.report({
|
|
14345
14558
|
node: property,
|
|
14346
|
-
message: MESSAGE$
|
|
14559
|
+
message: MESSAGE$29
|
|
14347
14560
|
});
|
|
14348
14561
|
}
|
|
14349
14562
|
}
|
|
@@ -14351,7 +14564,7 @@ const noArrayIndexKey = defineRule({
|
|
|
14351
14564
|
});
|
|
14352
14565
|
//#endregion
|
|
14353
14566
|
//#region src/plugin/rules/a11y/no-autofocus.ts
|
|
14354
|
-
const MESSAGE$
|
|
14567
|
+
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
14568
|
const resolveSettings$21 = (settings) => {
|
|
14356
14569
|
const reactDoctor = settings?.["react-doctor"];
|
|
14357
14570
|
return { ignoreNonDOM: (typeof reactDoctor === "object" && reactDoctor !== null ? reactDoctor.noAutofocus ?? {} : {}).ignoreNonDOM ?? true };
|
|
@@ -14407,7 +14620,7 @@ const noAutofocus = defineRule({
|
|
|
14407
14620
|
}
|
|
14408
14621
|
context.report({
|
|
14409
14622
|
node: autoFocusAttribute,
|
|
14410
|
-
message: MESSAGE$
|
|
14623
|
+
message: MESSAGE$28
|
|
14411
14624
|
});
|
|
14412
14625
|
} };
|
|
14413
14626
|
}
|
|
@@ -14932,7 +15145,7 @@ const noChainStateUpdates = defineRule({
|
|
|
14932
15145
|
});
|
|
14933
15146
|
//#endregion
|
|
14934
15147
|
//#region src/plugin/rules/react-builtins/no-children-prop.ts
|
|
14935
|
-
const MESSAGE$
|
|
15148
|
+
const MESSAGE$27 = "Your component can render the wrong children when you pass them through a `children` prop.";
|
|
14936
15149
|
const noChildrenProp = defineRule({
|
|
14937
15150
|
id: "no-children-prop",
|
|
14938
15151
|
title: "Children passed as a prop",
|
|
@@ -14944,7 +15157,7 @@ const noChildrenProp = defineRule({
|
|
|
14944
15157
|
if (node.name.name !== "children") return;
|
|
14945
15158
|
context.report({
|
|
14946
15159
|
node: node.name,
|
|
14947
|
-
message: MESSAGE$
|
|
15160
|
+
message: MESSAGE$27
|
|
14948
15161
|
});
|
|
14949
15162
|
},
|
|
14950
15163
|
CallExpression(node) {
|
|
@@ -14957,7 +15170,7 @@ const noChildrenProp = defineRule({
|
|
|
14957
15170
|
const propertyKey = property.key;
|
|
14958
15171
|
if (isNodeOfType(propertyKey, "Identifier") && propertyKey.name === "children" || isNodeOfType(propertyKey, "Literal") && propertyKey.value === "children") context.report({
|
|
14959
15172
|
node: propertyKey,
|
|
14960
|
-
message: MESSAGE$
|
|
15173
|
+
message: MESSAGE$27
|
|
14961
15174
|
});
|
|
14962
15175
|
}
|
|
14963
15176
|
}
|
|
@@ -14965,7 +15178,7 @@ const noChildrenProp = defineRule({
|
|
|
14965
15178
|
});
|
|
14966
15179
|
//#endregion
|
|
14967
15180
|
//#region src/plugin/rules/react-builtins/no-clone-element.ts
|
|
14968
|
-
const MESSAGE$
|
|
15181
|
+
const MESSAGE$26 = "`React.cloneElement` breaks easily when the cloned element's props change.";
|
|
14969
15182
|
const noCloneElement = defineRule({
|
|
14970
15183
|
id: "no-clone-element",
|
|
14971
15184
|
title: "Use of cloneElement",
|
|
@@ -14978,7 +15191,7 @@ const noCloneElement = defineRule({
|
|
|
14978
15191
|
if (isNodeOfType(callee, "Identifier") && callee.name === "cloneElement") {
|
|
14979
15192
|
if (isImportedFromModule(node, "cloneElement", "react")) context.report({
|
|
14980
15193
|
node: callee,
|
|
14981
|
-
message: MESSAGE$
|
|
15194
|
+
message: MESSAGE$26
|
|
14982
15195
|
});
|
|
14983
15196
|
return;
|
|
14984
15197
|
}
|
|
@@ -14991,7 +15204,7 @@ const noCloneElement = defineRule({
|
|
|
14991
15204
|
if (!isImportedFromModule(node, callee.object.name, "react")) return;
|
|
14992
15205
|
context.report({
|
|
14993
15206
|
node: callee,
|
|
14994
|
-
message: MESSAGE$
|
|
15207
|
+
message: MESSAGE$26
|
|
14995
15208
|
});
|
|
14996
15209
|
}
|
|
14997
15210
|
} })
|
|
@@ -15040,7 +15253,7 @@ const enclosingComponentOrHookName = (node) => {
|
|
|
15040
15253
|
};
|
|
15041
15254
|
//#endregion
|
|
15042
15255
|
//#region src/plugin/rules/state-and-effects/no-create-context-in-render.ts
|
|
15043
|
-
const MESSAGE$
|
|
15256
|
+
const MESSAGE$25 = "createContext() builds a new context every render, so every consumer gets cut off & resets.";
|
|
15044
15257
|
const CONTEXT_MODULES = [
|
|
15045
15258
|
"react",
|
|
15046
15259
|
"use-context-selector",
|
|
@@ -15076,7 +15289,7 @@ const noCreateContextInRender = defineRule({
|
|
|
15076
15289
|
if (!componentOrHookName) return;
|
|
15077
15290
|
context.report({
|
|
15078
15291
|
node,
|
|
15079
|
-
message: `${MESSAGE$
|
|
15292
|
+
message: `${MESSAGE$25} (called inside "${componentOrHookName}")`
|
|
15080
15293
|
});
|
|
15081
15294
|
} })
|
|
15082
15295
|
});
|
|
@@ -15216,7 +15429,7 @@ const noCreateStoreInRender = defineRule({
|
|
|
15216
15429
|
});
|
|
15217
15430
|
//#endregion
|
|
15218
15431
|
//#region src/plugin/rules/react-builtins/no-danger.ts
|
|
15219
|
-
const MESSAGE$
|
|
15432
|
+
const MESSAGE$24 = "`dangerouslySetInnerHTML` is an XSS hole that runs attacker-controlled HTML in your users' browsers.";
|
|
15220
15433
|
const noDanger = defineRule({
|
|
15221
15434
|
id: "no-danger",
|
|
15222
15435
|
title: "Use of dangerouslySetInnerHTML",
|
|
@@ -15229,7 +15442,7 @@ const noDanger = defineRule({
|
|
|
15229
15442
|
if (!propAttribute) return;
|
|
15230
15443
|
context.report({
|
|
15231
15444
|
node: propAttribute.name,
|
|
15232
|
-
message: MESSAGE$
|
|
15445
|
+
message: MESSAGE$24
|
|
15233
15446
|
});
|
|
15234
15447
|
},
|
|
15235
15448
|
CallExpression(node) {
|
|
@@ -15241,7 +15454,7 @@ const noDanger = defineRule({
|
|
|
15241
15454
|
const propertyKey = property.key;
|
|
15242
15455
|
if (isNodeOfType(propertyKey, "Identifier") && propertyKey.name === "dangerouslySetInnerHTML" || isNodeOfType(propertyKey, "Literal") && propertyKey.value === "dangerouslySetInnerHTML") context.report({
|
|
15243
15456
|
node: propertyKey,
|
|
15244
|
-
message: MESSAGE$
|
|
15457
|
+
message: MESSAGE$24
|
|
15245
15458
|
});
|
|
15246
15459
|
}
|
|
15247
15460
|
}
|
|
@@ -15249,7 +15462,7 @@ const noDanger = defineRule({
|
|
|
15249
15462
|
});
|
|
15250
15463
|
//#endregion
|
|
15251
15464
|
//#region src/plugin/rules/react-builtins/no-danger-with-children.ts
|
|
15252
|
-
const MESSAGE$
|
|
15465
|
+
const MESSAGE$23 = "React throws an error when you set both children & `dangerouslySetInnerHTML`.";
|
|
15253
15466
|
const isLineBreak = (child) => {
|
|
15254
15467
|
if (!isNodeOfType(child, "JSXText")) return false;
|
|
15255
15468
|
return child.value.trim().length === 0 && child.value.includes("\n");
|
|
@@ -15319,7 +15532,7 @@ const noDangerWithChildren = defineRule({
|
|
|
15319
15532
|
if (!hasChildrenProp && !hasNestedChildren) return;
|
|
15320
15533
|
if (hasJsxPropIgnoreCase(opening.attributes, "dangerouslySetInnerHTML") || spreadPropsShape.hasDangerously) context.report({
|
|
15321
15534
|
node: opening,
|
|
15322
|
-
message: MESSAGE$
|
|
15535
|
+
message: MESSAGE$23
|
|
15323
15536
|
});
|
|
15324
15537
|
},
|
|
15325
15538
|
CallExpression(node) {
|
|
@@ -15331,7 +15544,7 @@ const noDangerWithChildren = defineRule({
|
|
|
15331
15544
|
if (!propsShape.hasDangerously) return;
|
|
15332
15545
|
if (node.arguments.length >= 3 || propsShape.hasChildren) context.report({
|
|
15333
15546
|
node,
|
|
15334
|
-
message: MESSAGE$
|
|
15547
|
+
message: MESSAGE$23
|
|
15335
15548
|
});
|
|
15336
15549
|
}
|
|
15337
15550
|
})
|
|
@@ -15909,7 +16122,7 @@ const isSetStateCallInLifecycle = (setStateCall, lifecycleNames, options = {}) =
|
|
|
15909
16122
|
//#endregion
|
|
15910
16123
|
//#region src/plugin/rules/react-builtins/no-did-mount-set-state.ts
|
|
15911
16124
|
const LIFECYCLE_NAMES$2 = new Set(["componentDidMount"]);
|
|
15912
|
-
const MESSAGE$
|
|
16125
|
+
const MESSAGE$22 = "Your users see an extra render right after mount when you call `setState` in `componentDidMount`.";
|
|
15913
16126
|
const resolveSettings$20 = (settings) => {
|
|
15914
16127
|
const reactDoctor = settings?.["react-doctor"];
|
|
15915
16128
|
return { mode: (typeof reactDoctor === "object" && reactDoctor !== null ? reactDoctor.noDidMountSetState ?? {} : {}).mode ?? "allowed" };
|
|
@@ -15928,7 +16141,7 @@ const noDidMountSetState = defineRule({
|
|
|
15928
16141
|
if (!isSetStateCallInLifecycle(node, LIFECYCLE_NAMES$2, { disallowInNestedFunctions: mode === "disallow-in-func" })) return;
|
|
15929
16142
|
context.report({
|
|
15930
16143
|
node: node.callee,
|
|
15931
|
-
message: MESSAGE$
|
|
16144
|
+
message: MESSAGE$22
|
|
15932
16145
|
});
|
|
15933
16146
|
} };
|
|
15934
16147
|
}
|
|
@@ -15936,7 +16149,7 @@ const noDidMountSetState = defineRule({
|
|
|
15936
16149
|
//#endregion
|
|
15937
16150
|
//#region src/plugin/rules/react-builtins/no-did-update-set-state.ts
|
|
15938
16151
|
const LIFECYCLE_NAMES$1 = new Set(["componentDidUpdate"]);
|
|
15939
|
-
const MESSAGE$
|
|
16152
|
+
const MESSAGE$21 = "This can loop forever & freeze the component.";
|
|
15940
16153
|
const resolveSettings$19 = (settings) => {
|
|
15941
16154
|
const reactDoctor = settings?.["react-doctor"];
|
|
15942
16155
|
return { mode: (typeof reactDoctor === "object" && reactDoctor !== null ? reactDoctor.noDidUpdateSetState ?? {} : {}).mode ?? "allowed" };
|
|
@@ -15955,7 +16168,7 @@ const noDidUpdateSetState = defineRule({
|
|
|
15955
16168
|
if (!isSetStateCallInLifecycle(node, LIFECYCLE_NAMES$1, { disallowInNestedFunctions: mode === "disallow-in-func" })) return;
|
|
15956
16169
|
context.report({
|
|
15957
16170
|
node: node.callee,
|
|
15958
|
-
message: MESSAGE$
|
|
16171
|
+
message: MESSAGE$21
|
|
15959
16172
|
});
|
|
15960
16173
|
} };
|
|
15961
16174
|
}
|
|
@@ -15978,7 +16191,7 @@ const isStateMemberExpression = (node) => {
|
|
|
15978
16191
|
};
|
|
15979
16192
|
//#endregion
|
|
15980
16193
|
//#region src/plugin/rules/react-builtins/no-direct-mutation-state.ts
|
|
15981
|
-
const MESSAGE$
|
|
16194
|
+
const MESSAGE$20 = "Your users see stale data because mutating `this.state` by hand never redraws & gets overwritten.";
|
|
15982
16195
|
const shouldIgnoreMutation = (node) => {
|
|
15983
16196
|
let isConstructor = false;
|
|
15984
16197
|
let isInsideCallExpression = false;
|
|
@@ -16000,7 +16213,7 @@ const reportIfStateMutation = (context, reportNode, target) => {
|
|
|
16000
16213
|
if (shouldIgnoreMutation(reportNode)) return;
|
|
16001
16214
|
context.report({
|
|
16002
16215
|
node: reportNode,
|
|
16003
|
-
message: MESSAGE$
|
|
16216
|
+
message: MESSAGE$20
|
|
16004
16217
|
});
|
|
16005
16218
|
};
|
|
16006
16219
|
const noDirectMutationState = defineRule({
|
|
@@ -17588,7 +17801,7 @@ const ALLOWED_NAMESPACES = new Set([
|
|
|
17588
17801
|
"ReactDOM",
|
|
17589
17802
|
"ReactDom"
|
|
17590
17803
|
]);
|
|
17591
|
-
const MESSAGE$
|
|
17804
|
+
const MESSAGE$19 = "`findDOMNode` crashes your app in React 19 because it was removed.";
|
|
17592
17805
|
const noFindDomNode = defineRule({
|
|
17593
17806
|
id: "no-find-dom-node",
|
|
17594
17807
|
title: "Use of findDOMNode",
|
|
@@ -17599,7 +17812,7 @@ const noFindDomNode = defineRule({
|
|
|
17599
17812
|
if (isNodeOfType(callee, "Identifier") && callee.name === "findDOMNode") {
|
|
17600
17813
|
context.report({
|
|
17601
17814
|
node: callee,
|
|
17602
|
-
message: MESSAGE$
|
|
17815
|
+
message: MESSAGE$19
|
|
17603
17816
|
});
|
|
17604
17817
|
return;
|
|
17605
17818
|
}
|
|
@@ -17610,7 +17823,7 @@ const noFindDomNode = defineRule({
|
|
|
17610
17823
|
if (callee.property.name !== "findDOMNode") return;
|
|
17611
17824
|
context.report({
|
|
17612
17825
|
node: callee.property,
|
|
17613
|
-
message: MESSAGE$
|
|
17826
|
+
message: MESSAGE$19
|
|
17614
17827
|
});
|
|
17615
17828
|
}
|
|
17616
17829
|
} })
|
|
@@ -18139,6 +18352,51 @@ const noIsMounted = defineRule({
|
|
|
18139
18352
|
} })
|
|
18140
18353
|
});
|
|
18141
18354
|
//#endregion
|
|
18355
|
+
//#region src/plugin/rules/correctness/no-jsx-element-type.ts
|
|
18356
|
+
const MESSAGE$18 = "`JSX.Element` is too narrow: it excludes `null`, strings, numbers, and fragments that components commonly return. Use `React.ReactNode` instead.";
|
|
18357
|
+
const isJsxElementTypeReference = (node) => {
|
|
18358
|
+
if (!isNodeOfType(node, "TSTypeReference")) return false;
|
|
18359
|
+
const typeName = node.typeName;
|
|
18360
|
+
if (!isNodeOfType(typeName, "TSQualifiedName")) return false;
|
|
18361
|
+
return isNodeOfType(typeName.left, "Identifier") && typeName.left.name === "JSX" && isNodeOfType(typeName.right, "Identifier") && typeName.right.name === "Element";
|
|
18362
|
+
};
|
|
18363
|
+
const extractReturnTypeAnnotation = (returnType) => {
|
|
18364
|
+
if (!returnType) return null;
|
|
18365
|
+
if (!isNodeOfType(returnType, "TSTypeAnnotation")) return null;
|
|
18366
|
+
return returnType.typeAnnotation ?? null;
|
|
18367
|
+
};
|
|
18368
|
+
const checkReturnType = (context, returnType) => {
|
|
18369
|
+
const typeAnnotation = extractReturnTypeAnnotation(returnType);
|
|
18370
|
+
if (!typeAnnotation) return;
|
|
18371
|
+
if (isJsxElementTypeReference(typeAnnotation)) context.report({
|
|
18372
|
+
node: typeAnnotation,
|
|
18373
|
+
message: MESSAGE$18
|
|
18374
|
+
});
|
|
18375
|
+
};
|
|
18376
|
+
const noJsxElementType = defineRule({
|
|
18377
|
+
id: "no-jsx-element-type",
|
|
18378
|
+
title: "No JSX.Element",
|
|
18379
|
+
severity: "error",
|
|
18380
|
+
recommendation: "Replace `JSX.Element` with `React.ReactNode`. `JSX.Element` is too narrow: it excludes `null`, strings, numbers, and fragments that components commonly return.",
|
|
18381
|
+
create: (context) => ({
|
|
18382
|
+
FunctionDeclaration(node) {
|
|
18383
|
+
checkReturnType(context, node.returnType);
|
|
18384
|
+
},
|
|
18385
|
+
ArrowFunctionExpression(node) {
|
|
18386
|
+
checkReturnType(context, node.returnType);
|
|
18387
|
+
},
|
|
18388
|
+
FunctionExpression(node) {
|
|
18389
|
+
checkReturnType(context, node.returnType);
|
|
18390
|
+
},
|
|
18391
|
+
TSDeclareFunction(node) {
|
|
18392
|
+
checkReturnType(context, node.returnType);
|
|
18393
|
+
},
|
|
18394
|
+
TSMethodSignature(node) {
|
|
18395
|
+
checkReturnType(context, node.returnType);
|
|
18396
|
+
}
|
|
18397
|
+
})
|
|
18398
|
+
});
|
|
18399
|
+
//#endregion
|
|
18142
18400
|
//#region src/plugin/rules/design/no-justified-text.ts
|
|
18143
18401
|
const noJustifiedText = defineRule({
|
|
18144
18402
|
id: "no-justified-text",
|
|
@@ -25375,6 +25633,26 @@ const preferUseReducer = defineRule({
|
|
|
25375
25633
|
}
|
|
25376
25634
|
});
|
|
25377
25635
|
//#endregion
|
|
25636
|
+
//#region src/plugin/rules/tanstack-query/query-destructure-result.ts
|
|
25637
|
+
const queryDestructureResult = defineRule({
|
|
25638
|
+
id: "query-destructure-result",
|
|
25639
|
+
title: "Destructure TanStack Query result",
|
|
25640
|
+
tags: ["test-noise"],
|
|
25641
|
+
requires: ["tanstack-query"],
|
|
25642
|
+
severity: "error",
|
|
25643
|
+
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.",
|
|
25644
|
+
create: (context) => ({ VariableDeclarator(node) {
|
|
25645
|
+
if (!isNodeOfType(node.id, "Identifier")) return;
|
|
25646
|
+
if (!node.init || !isNodeOfType(node.init, "CallExpression")) return;
|
|
25647
|
+
const calleeName = isNodeOfType(node.init.callee, "Identifier") ? node.init.callee.name : null;
|
|
25648
|
+
if (!calleeName || !TANSTACK_QUERY_HOOKS.has(calleeName)) return;
|
|
25649
|
+
context.report({
|
|
25650
|
+
node: node.id,
|
|
25651
|
+
message: `Destructure ${calleeName}() results instead of assigning the whole query object, so TanStack Query only subscribes to the fields you use.`
|
|
25652
|
+
});
|
|
25653
|
+
} })
|
|
25654
|
+
});
|
|
25655
|
+
//#endregion
|
|
25378
25656
|
//#region src/plugin/rules/tanstack-query/query-mutation-missing-invalidation.ts
|
|
25379
25657
|
const queryMutationMissingInvalidation = defineRule({
|
|
25380
25658
|
id: "query-mutation-missing-invalidation",
|
|
@@ -32917,15 +33195,6 @@ const serverFetchWithoutRevalidate = defineRule({
|
|
|
32917
33195
|
});
|
|
32918
33196
|
//#endregion
|
|
32919
33197
|
//#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
33198
|
const STATIC_IO_FUNCTIONS = new Set([
|
|
32930
33199
|
"readFileSync",
|
|
32931
33200
|
"readFile",
|
|
@@ -35440,6 +35709,28 @@ const reactDoctorRules = [
|
|
|
35440
35709
|
category: "Bugs"
|
|
35441
35710
|
}
|
|
35442
35711
|
},
|
|
35712
|
+
{
|
|
35713
|
+
key: "react-doctor/nextjs-error-boundary-missing-use-client",
|
|
35714
|
+
id: "nextjs-error-boundary-missing-use-client",
|
|
35715
|
+
source: "react-doctor",
|
|
35716
|
+
originallyExternal: false,
|
|
35717
|
+
rule: {
|
|
35718
|
+
...nextjsErrorBoundaryMissingUseClient,
|
|
35719
|
+
framework: "nextjs",
|
|
35720
|
+
category: "Bugs"
|
|
35721
|
+
}
|
|
35722
|
+
},
|
|
35723
|
+
{
|
|
35724
|
+
key: "react-doctor/nextjs-global-error-missing-html-body",
|
|
35725
|
+
id: "nextjs-global-error-missing-html-body",
|
|
35726
|
+
source: "react-doctor",
|
|
35727
|
+
originallyExternal: false,
|
|
35728
|
+
rule: {
|
|
35729
|
+
...nextjsGlobalErrorMissingHtmlBody,
|
|
35730
|
+
framework: "nextjs",
|
|
35731
|
+
category: "Bugs"
|
|
35732
|
+
}
|
|
35733
|
+
},
|
|
35443
35734
|
{
|
|
35444
35735
|
key: "react-doctor/nextjs-image-missing-sizes",
|
|
35445
35736
|
id: "nextjs-image-missing-sizes",
|
|
@@ -35517,6 +35808,28 @@ const reactDoctorRules = [
|
|
|
35517
35808
|
category: "Bugs"
|
|
35518
35809
|
}
|
|
35519
35810
|
},
|
|
35811
|
+
{
|
|
35812
|
+
key: "react-doctor/nextjs-no-default-export-in-route-handler",
|
|
35813
|
+
id: "nextjs-no-default-export-in-route-handler",
|
|
35814
|
+
source: "react-doctor",
|
|
35815
|
+
originallyExternal: false,
|
|
35816
|
+
rule: {
|
|
35817
|
+
...nextjsNoDefaultExportInRouteHandler,
|
|
35818
|
+
framework: "nextjs",
|
|
35819
|
+
category: "Bugs"
|
|
35820
|
+
}
|
|
35821
|
+
},
|
|
35822
|
+
{
|
|
35823
|
+
key: "react-doctor/nextjs-no-edge-og-runtime",
|
|
35824
|
+
id: "nextjs-no-edge-og-runtime",
|
|
35825
|
+
source: "react-doctor",
|
|
35826
|
+
originallyExternal: false,
|
|
35827
|
+
rule: {
|
|
35828
|
+
...nextjsNoEdgeOgRuntime,
|
|
35829
|
+
framework: "nextjs",
|
|
35830
|
+
category: "Bugs"
|
|
35831
|
+
}
|
|
35832
|
+
},
|
|
35520
35833
|
{
|
|
35521
35834
|
key: "react-doctor/nextjs-no-font-link",
|
|
35522
35835
|
id: "nextjs-no-font-link",
|
|
@@ -35528,6 +35841,17 @@ const reactDoctorRules = [
|
|
|
35528
35841
|
category: "Bugs"
|
|
35529
35842
|
}
|
|
35530
35843
|
},
|
|
35844
|
+
{
|
|
35845
|
+
key: "react-doctor/nextjs-no-google-analytics-script",
|
|
35846
|
+
id: "nextjs-no-google-analytics-script",
|
|
35847
|
+
source: "react-doctor",
|
|
35848
|
+
originallyExternal: false,
|
|
35849
|
+
rule: {
|
|
35850
|
+
...nextjsNoGoogleAnalyticsScript,
|
|
35851
|
+
framework: "nextjs",
|
|
35852
|
+
category: "Bugs"
|
|
35853
|
+
}
|
|
35854
|
+
},
|
|
35531
35855
|
{
|
|
35532
35856
|
key: "react-doctor/nextjs-no-head-import",
|
|
35533
35857
|
id: "nextjs-no-head-import",
|
|
@@ -35583,6 +35907,17 @@ const reactDoctorRules = [
|
|
|
35583
35907
|
category: "Bugs"
|
|
35584
35908
|
}
|
|
35585
35909
|
},
|
|
35910
|
+
{
|
|
35911
|
+
key: "react-doctor/nextjs-no-script-in-head",
|
|
35912
|
+
id: "nextjs-no-script-in-head",
|
|
35913
|
+
source: "react-doctor",
|
|
35914
|
+
originallyExternal: false,
|
|
35915
|
+
rule: {
|
|
35916
|
+
...nextjsNoScriptInHead,
|
|
35917
|
+
framework: "nextjs",
|
|
35918
|
+
category: "Bugs"
|
|
35919
|
+
}
|
|
35920
|
+
},
|
|
35586
35921
|
{
|
|
35587
35922
|
key: "react-doctor/nextjs-no-side-effect-in-get-handler",
|
|
35588
35923
|
id: "nextjs-no-side-effect-in-get-handler",
|
|
@@ -35605,6 +35940,17 @@ const reactDoctorRules = [
|
|
|
35605
35940
|
category: "Bugs"
|
|
35606
35941
|
}
|
|
35607
35942
|
},
|
|
35943
|
+
{
|
|
35944
|
+
key: "react-doctor/nextjs-no-vercel-og-import",
|
|
35945
|
+
id: "nextjs-no-vercel-og-import",
|
|
35946
|
+
source: "react-doctor",
|
|
35947
|
+
originallyExternal: false,
|
|
35948
|
+
rule: {
|
|
35949
|
+
...nextjsNoVercelOgImport,
|
|
35950
|
+
framework: "nextjs",
|
|
35951
|
+
category: "Bugs"
|
|
35952
|
+
}
|
|
35953
|
+
},
|
|
35608
35954
|
{
|
|
35609
35955
|
key: "react-doctor/no-access-key",
|
|
35610
35956
|
id: "no-access-key",
|
|
@@ -36155,6 +36501,17 @@ const reactDoctorRules = [
|
|
|
36155
36501
|
category: "Bugs"
|
|
36156
36502
|
}
|
|
36157
36503
|
},
|
|
36504
|
+
{
|
|
36505
|
+
key: "react-doctor/no-jsx-element-type",
|
|
36506
|
+
id: "no-jsx-element-type",
|
|
36507
|
+
source: "react-doctor",
|
|
36508
|
+
originallyExternal: false,
|
|
36509
|
+
rule: {
|
|
36510
|
+
...noJsxElementType,
|
|
36511
|
+
framework: "global",
|
|
36512
|
+
category: "Bugs"
|
|
36513
|
+
}
|
|
36514
|
+
},
|
|
36158
36515
|
{
|
|
36159
36516
|
key: "react-doctor/no-justified-text",
|
|
36160
36517
|
id: "no-justified-text",
|
|
@@ -36980,6 +37337,17 @@ const reactDoctorRules = [
|
|
|
36980
37337
|
category: "Bugs"
|
|
36981
37338
|
}
|
|
36982
37339
|
},
|
|
37340
|
+
{
|
|
37341
|
+
key: "react-doctor/query-destructure-result",
|
|
37342
|
+
id: "query-destructure-result",
|
|
37343
|
+
source: "react-doctor",
|
|
37344
|
+
originallyExternal: false,
|
|
37345
|
+
rule: {
|
|
37346
|
+
...queryDestructureResult,
|
|
37347
|
+
framework: "tanstack-query",
|
|
37348
|
+
category: "Bugs"
|
|
37349
|
+
}
|
|
37350
|
+
},
|
|
36983
37351
|
{
|
|
36984
37352
|
key: "react-doctor/query-mutation-missing-invalidation",
|
|
36985
37353
|
id: "query-mutation-missing-invalidation",
|