oxlint-plugin-react-doctor 0.4.0 → 0.4.1-dev.7d4b996
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 +380 -0
- package/dist/index.js +1447 -657
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import * as path from "node:path";
|
|
2
|
-
import { analyze } from "eslint-scope";
|
|
3
|
-
import * as eslintVisitorKeys from "eslint-visitor-keys";
|
|
4
2
|
import * as fs from "node:fs";
|
|
5
3
|
import { parseSync } from "oxc-parser";
|
|
4
|
+
import { analyze } from "eslint-scope";
|
|
5
|
+
import * as eslintVisitorKeys from "eslint-visitor-keys";
|
|
6
6
|
//#region src/plugin/utils/is-testlike-filename.ts
|
|
7
7
|
const NON_PRODUCTION_PATH_SEGMENTS = [
|
|
8
8
|
"/test/",
|
|
@@ -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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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",
|
|
@@ -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
|
|
@@ -13170,23 +13367,796 @@ const nextjsNoSideEffectInGetHandler = defineRule({
|
|
|
13170
13367
|
}
|
|
13171
13368
|
});
|
|
13172
13369
|
//#endregion
|
|
13173
|
-
//#region src/plugin/
|
|
13174
|
-
const
|
|
13175
|
-
|
|
13370
|
+
//#region src/plugin/utils/find-exported-function-body.ts
|
|
13371
|
+
const isFunctionLike = (node) => {
|
|
13372
|
+
if (!node) return false;
|
|
13373
|
+
return isNodeOfType(node, "FunctionDeclaration") || isNodeOfType(node, "FunctionExpression") || isNodeOfType(node, "ArrowFunctionExpression");
|
|
13374
|
+
};
|
|
13375
|
+
const findExportedFunctionBody = (programRoot, exportedName) => {
|
|
13376
|
+
if (!isNodeOfType(programRoot, "Program")) return null;
|
|
13377
|
+
const localBindings = /* @__PURE__ */ new Map();
|
|
13378
|
+
const namedExports = /* @__PURE__ */ new Map();
|
|
13379
|
+
let defaultExport = null;
|
|
13380
|
+
let defaultExportIdentifierName = null;
|
|
13381
|
+
const recordVariableDeclaration = (declaration) => {
|
|
13382
|
+
for (const declarator of declaration.declarations ?? []) {
|
|
13383
|
+
if (!isNodeOfType(declarator, "VariableDeclarator")) continue;
|
|
13384
|
+
if (!isNodeOfType(declarator.id, "Identifier")) continue;
|
|
13385
|
+
const initializer = declarator.init ? stripParenExpression(declarator.init) : null;
|
|
13386
|
+
if (initializer && isFunctionLike(initializer)) localBindings.set(declarator.id.name, initializer);
|
|
13387
|
+
}
|
|
13388
|
+
};
|
|
13389
|
+
for (const statement of programRoot.body ?? []) {
|
|
13390
|
+
if (isNodeOfType(statement, "VariableDeclaration")) {
|
|
13391
|
+
recordVariableDeclaration(statement);
|
|
13392
|
+
continue;
|
|
13393
|
+
}
|
|
13394
|
+
if (isNodeOfType(statement, "FunctionDeclaration") && statement.id) {
|
|
13395
|
+
localBindings.set(statement.id.name, statement);
|
|
13396
|
+
continue;
|
|
13397
|
+
}
|
|
13398
|
+
if (isNodeOfType(statement, "ExportNamedDeclaration")) {
|
|
13399
|
+
const declaration = statement.declaration;
|
|
13400
|
+
if (declaration && isNodeOfType(declaration, "VariableDeclaration")) {
|
|
13401
|
+
recordVariableDeclaration(declaration);
|
|
13402
|
+
for (const declarator of declaration.declarations ?? []) {
|
|
13403
|
+
if (!isNodeOfType(declarator, "VariableDeclarator")) continue;
|
|
13404
|
+
if (!isNodeOfType(declarator.id, "Identifier")) continue;
|
|
13405
|
+
namedExports.set(declarator.id.name, declarator.id.name);
|
|
13406
|
+
}
|
|
13407
|
+
} else if (declaration && isNodeOfType(declaration, "FunctionDeclaration") && declaration.id) {
|
|
13408
|
+
localBindings.set(declaration.id.name, declaration);
|
|
13409
|
+
namedExports.set(declaration.id.name, declaration.id.name);
|
|
13410
|
+
}
|
|
13411
|
+
for (const specifier of statement.specifiers ?? []) {
|
|
13412
|
+
if (!isNodeOfType(specifier, "ExportSpecifier")) continue;
|
|
13413
|
+
const local = specifier.local;
|
|
13414
|
+
const exported = specifier.exported;
|
|
13415
|
+
if (!isNodeOfType(local, "Identifier")) continue;
|
|
13416
|
+
const exportedNameSpec = isNodeOfType(exported, "Identifier") ? exported.name : isNodeOfType(exported, "Literal") && typeof exported.value === "string" ? exported.value : null;
|
|
13417
|
+
if (!exportedNameSpec) continue;
|
|
13418
|
+
namedExports.set(exportedNameSpec, local.name);
|
|
13419
|
+
}
|
|
13420
|
+
continue;
|
|
13421
|
+
}
|
|
13422
|
+
if (isNodeOfType(statement, "ExportDefaultDeclaration")) {
|
|
13423
|
+
const declaration = statement.declaration;
|
|
13424
|
+
if (!declaration) continue;
|
|
13425
|
+
if (isNodeOfType(declaration, "FunctionDeclaration") && declaration.id) {
|
|
13426
|
+
localBindings.set(declaration.id.name, declaration);
|
|
13427
|
+
defaultExport = declaration;
|
|
13428
|
+
continue;
|
|
13429
|
+
}
|
|
13430
|
+
if (isFunctionLike(declaration)) {
|
|
13431
|
+
defaultExport = declaration;
|
|
13432
|
+
continue;
|
|
13433
|
+
}
|
|
13434
|
+
if (isNodeOfType(declaration, "Identifier")) {
|
|
13435
|
+
defaultExportIdentifierName = declaration.name;
|
|
13436
|
+
continue;
|
|
13437
|
+
}
|
|
13438
|
+
}
|
|
13439
|
+
}
|
|
13440
|
+
if (exportedName === "default") {
|
|
13441
|
+
if (defaultExport) return defaultExport;
|
|
13442
|
+
if (defaultExportIdentifierName) {
|
|
13443
|
+
const binding = localBindings.get(defaultExportIdentifierName);
|
|
13444
|
+
if (binding) return binding;
|
|
13445
|
+
}
|
|
13446
|
+
}
|
|
13447
|
+
const localName = namedExports.get(exportedName);
|
|
13448
|
+
if (!localName) return null;
|
|
13449
|
+
return localBindings.get(localName) ?? null;
|
|
13450
|
+
};
|
|
13451
|
+
const resolveImportedExportName = (importSpecifier) => {
|
|
13452
|
+
if (isNodeOfType(importSpecifier, "ImportSpecifier")) {
|
|
13453
|
+
const imported = importSpecifier.imported;
|
|
13454
|
+
if (isNodeOfType(imported, "Identifier")) return imported.name;
|
|
13455
|
+
if (isNodeOfType(imported, "Literal") && typeof imported.value === "string") return imported.value;
|
|
13456
|
+
return null;
|
|
13457
|
+
}
|
|
13458
|
+
if (isNodeOfType(importSpecifier, "ImportDefaultSpecifier")) return "default";
|
|
13459
|
+
return null;
|
|
13460
|
+
};
|
|
13461
|
+
const findReExportSourcesForName = (programRoot, exportedName) => {
|
|
13462
|
+
if (!isNodeOfType(programRoot, "Program")) return [];
|
|
13463
|
+
const exportAllSources = [];
|
|
13464
|
+
for (const statement of programRoot.body ?? []) {
|
|
13465
|
+
if (isNodeOfType(statement, "ExportNamedDeclaration") && statement.source) {
|
|
13466
|
+
const sourceValue = statement.source.value;
|
|
13467
|
+
if (typeof sourceValue !== "string") continue;
|
|
13468
|
+
for (const specifier of statement.specifiers ?? []) {
|
|
13469
|
+
if (!isNodeOfType(specifier, "ExportSpecifier")) continue;
|
|
13470
|
+
const exported = specifier.exported;
|
|
13471
|
+
if ((isNodeOfType(exported, "Identifier") ? exported.name : isNodeOfType(exported, "Literal") && typeof exported.value === "string" ? exported.value : null) === exportedName) return [sourceValue];
|
|
13472
|
+
}
|
|
13473
|
+
}
|
|
13474
|
+
if (isNodeOfType(statement, "ExportAllDeclaration") && statement.source) {
|
|
13475
|
+
const sourceValue = statement.source.value;
|
|
13476
|
+
if (typeof sourceValue === "string") exportAllSources.push(sourceValue);
|
|
13477
|
+
}
|
|
13478
|
+
}
|
|
13479
|
+
return exportAllSources;
|
|
13480
|
+
};
|
|
13481
|
+
//#endregion
|
|
13482
|
+
//#region src/plugin/utils/attach-parent-references.ts
|
|
13483
|
+
const attachParentReferences = (root) => {
|
|
13484
|
+
const visit = (node, parent) => {
|
|
13485
|
+
const writableNode = node;
|
|
13486
|
+
writableNode.parent = parent;
|
|
13487
|
+
const nodeRecord = node;
|
|
13488
|
+
for (const key of Object.keys(nodeRecord)) {
|
|
13489
|
+
if (key === "parent") continue;
|
|
13490
|
+
const child = nodeRecord[key];
|
|
13491
|
+
if (Array.isArray(child)) {
|
|
13492
|
+
for (const item of child) if (isAstNode(item)) visit(item, node);
|
|
13493
|
+
} else if (isAstNode(child)) visit(child, node);
|
|
13494
|
+
}
|
|
13495
|
+
};
|
|
13496
|
+
visit(root, null);
|
|
13497
|
+
};
|
|
13498
|
+
//#endregion
|
|
13499
|
+
//#region src/plugin/utils/parse-source-file.ts
|
|
13500
|
+
const FILENAME_TO_LANG = {
|
|
13501
|
+
".ts": "ts",
|
|
13502
|
+
".tsx": "tsx",
|
|
13503
|
+
".js": "js",
|
|
13504
|
+
".jsx": "jsx",
|
|
13505
|
+
".mjs": "js",
|
|
13506
|
+
".cjs": "js",
|
|
13507
|
+
".mts": "ts",
|
|
13508
|
+
".cts": "ts"
|
|
13509
|
+
};
|
|
13510
|
+
const resolveLang = (filename) => {
|
|
13511
|
+
return FILENAME_TO_LANG[path.extname(filename).toLowerCase()] ?? "tsx";
|
|
13512
|
+
};
|
|
13513
|
+
const parseCache = /* @__PURE__ */ new Map();
|
|
13514
|
+
const parseSourceFile = (absoluteFilePath) => {
|
|
13515
|
+
let fileStat;
|
|
13516
|
+
try {
|
|
13517
|
+
fileStat = fs.statSync(absoluteFilePath);
|
|
13518
|
+
} catch {
|
|
13519
|
+
return null;
|
|
13520
|
+
}
|
|
13521
|
+
if (!fileStat.isFile()) return null;
|
|
13522
|
+
if (fileStat.size > 2e6) return null;
|
|
13523
|
+
const cached = parseCache.get(absoluteFilePath);
|
|
13524
|
+
if (cached && cached.mtimeMs === fileStat.mtimeMs && cached.size === fileStat.size) return cached.program;
|
|
13525
|
+
if (absoluteFilePath.endsWith(".d.ts") || absoluteFilePath.endsWith(".d.mts") || absoluteFilePath.endsWith(".d.cts")) {
|
|
13526
|
+
parseCache.set(absoluteFilePath, {
|
|
13527
|
+
mtimeMs: fileStat.mtimeMs,
|
|
13528
|
+
size: fileStat.size,
|
|
13529
|
+
program: null
|
|
13530
|
+
});
|
|
13531
|
+
return null;
|
|
13532
|
+
}
|
|
13533
|
+
let sourceText;
|
|
13534
|
+
try {
|
|
13535
|
+
sourceText = fs.readFileSync(absoluteFilePath, "utf8");
|
|
13536
|
+
} catch {
|
|
13537
|
+
parseCache.set(absoluteFilePath, {
|
|
13538
|
+
mtimeMs: fileStat.mtimeMs,
|
|
13539
|
+
size: fileStat.size,
|
|
13540
|
+
program: null
|
|
13541
|
+
});
|
|
13542
|
+
return null;
|
|
13543
|
+
}
|
|
13544
|
+
let parsedProgram = null;
|
|
13545
|
+
try {
|
|
13546
|
+
const result = parseSync(absoluteFilePath, sourceText, {
|
|
13547
|
+
astType: "ts",
|
|
13548
|
+
lang: resolveLang(absoluteFilePath)
|
|
13549
|
+
});
|
|
13550
|
+
if (!result.errors.some((parseError) => parseError.severity === "Error")) {
|
|
13551
|
+
parsedProgram = result.program;
|
|
13552
|
+
attachParentReferences(parsedProgram);
|
|
13553
|
+
}
|
|
13554
|
+
} catch {
|
|
13555
|
+
parsedProgram = null;
|
|
13556
|
+
}
|
|
13557
|
+
parseCache.set(absoluteFilePath, {
|
|
13558
|
+
mtimeMs: fileStat.mtimeMs,
|
|
13559
|
+
size: fileStat.size,
|
|
13560
|
+
program: parsedProgram
|
|
13561
|
+
});
|
|
13562
|
+
return parsedProgram;
|
|
13563
|
+
};
|
|
13564
|
+
//#endregion
|
|
13565
|
+
//#region src/plugin/utils/parse-export-specifiers.ts
|
|
13566
|
+
const getSpecifierName = (rawName) => rawName.replace(/^type\s+/, "").trim();
|
|
13567
|
+
const parseExportSpecifiers = (specifiersText, declarationIsTypeOnly) => specifiersText.split(",").map((specifierText) => specifierText.trim()).filter(Boolean).map((specifierText) => {
|
|
13568
|
+
const isTypeOnly = declarationIsTypeOnly || specifierText.startsWith("type ");
|
|
13569
|
+
const [rawLocalName, rawExportedName] = specifierText.split(/\s+as\s+/);
|
|
13570
|
+
const localName = getSpecifierName(rawLocalName ?? "");
|
|
13571
|
+
return {
|
|
13572
|
+
localName,
|
|
13573
|
+
exportedName: getSpecifierName(rawExportedName ?? localName),
|
|
13574
|
+
isTypeOnly
|
|
13575
|
+
};
|
|
13576
|
+
});
|
|
13577
|
+
//#endregion
|
|
13578
|
+
//#region src/plugin/utils/strip-js-comments.ts
|
|
13579
|
+
const BLOCK_COMMENT_PATTERN = /\/\*[\s\S]*?\*\//g;
|
|
13580
|
+
const LINE_COMMENT_PATTERN = /^\s*\/\/.*$/gm;
|
|
13581
|
+
const stripJsComments = (sourceText) => sourceText.replace(BLOCK_COMMENT_PATTERN, "").replace(LINE_COMMENT_PATTERN, "");
|
|
13582
|
+
//#endregion
|
|
13583
|
+
//#region src/plugin/utils/does-module-export-name.ts
|
|
13584
|
+
const DEFAULT_EXPORT_DECLARATION_PATTERN = /^\s*export\s+default\b/m;
|
|
13585
|
+
const NAMED_EXPORT_DECLARATION_PATTERN = /^\s*export\s+(?:declare\s+)?(?:(?:async\s+)?function|(?:abstract\s+)?class|const|let|var|enum|interface|type)\s+([\w$]+)/gm;
|
|
13586
|
+
const LOCAL_EXPORT_SPECIFIER_DECLARATION_PATTERN$1 = /^\s*export\s+(?:type\s+)?\{([\s\S]*?)\}(?:\s+from\s+["'][^"']+["'])?\s*;?\s*(?:(?:\/\/[^\n]*)?\s*)/gm;
|
|
13587
|
+
const doesSourceTextExportName = (sourceText, exportedName) => {
|
|
13588
|
+
const strippedSource = stripJsComments(sourceText);
|
|
13589
|
+
if (exportedName === "default" && DEFAULT_EXPORT_DECLARATION_PATTERN.test(strippedSource)) return true;
|
|
13590
|
+
for (const match of strippedSource.matchAll(NAMED_EXPORT_DECLARATION_PATTERN)) if (match[1] === exportedName) return true;
|
|
13591
|
+
for (const match of strippedSource.matchAll(LOCAL_EXPORT_SPECIFIER_DECLARATION_PATTERN$1)) if (parseExportSpecifiers(match[1] ?? "", false).map((specifier) => specifier.exportedName).includes(exportedName)) return true;
|
|
13592
|
+
return false;
|
|
13593
|
+
};
|
|
13594
|
+
const doesModuleExportName = (filePath, exportedName) => {
|
|
13595
|
+
try {
|
|
13596
|
+
return doesSourceTextExportName(fs.readFileSync(filePath, "utf8"), exportedName);
|
|
13597
|
+
} catch {
|
|
13598
|
+
return false;
|
|
13599
|
+
}
|
|
13600
|
+
};
|
|
13601
|
+
//#endregion
|
|
13602
|
+
//#region src/plugin/utils/is-barrel-index-module.ts
|
|
13603
|
+
const INDEX_MODULE_FILE_PATTERN = /^index\.(?:[cm]?[jt]sx?|mjs)$/;
|
|
13604
|
+
const BINDING_IMPORT_DECLARATION_PATTERN = /^\s*import\s+(type\s+)?(?!["'])([^;]*?)\s+from\s+["']([^"']+)["']\s*;?\s*(?:(?:\/\/[^\n]*)?\s*)/gm;
|
|
13605
|
+
const BARREL_REEXPORT_DECLARATION_PATTERN = /^\s*export\s+(type\s+)?(?:\*(?:\s+as\s+([\w$]+))?|\{([\s\S]*?)\})\s+from\s+["']([^"']+)["']\s*;?\s*(?:(?:\/\/[^\n]*)?\s*)/gm;
|
|
13606
|
+
const LOCAL_EXPORT_SPECIFIER_DECLARATION_PATTERN = /^\s*export\s+(type\s+)?\{([\s\S]*?)\}\s*;?\s*(?:(?:\/\/[^\n]*)?\s*)/gm;
|
|
13607
|
+
const barrelIndexModuleInfoCache = /* @__PURE__ */ new Map();
|
|
13608
|
+
const isIndexModuleFilePath = (filePath) => INDEX_MODULE_FILE_PATTERN.test(path.basename(filePath));
|
|
13609
|
+
const createNonBarrelInfo = () => ({
|
|
13610
|
+
isBarrel: false,
|
|
13611
|
+
exportsByName: /* @__PURE__ */ new Map(),
|
|
13612
|
+
starExportSources: []
|
|
13613
|
+
});
|
|
13614
|
+
const addImportedBinding = (importedBindings, binding) => {
|
|
13615
|
+
importedBindings.set(binding.localName, {
|
|
13616
|
+
...binding,
|
|
13617
|
+
didExport: false
|
|
13618
|
+
});
|
|
13619
|
+
};
|
|
13620
|
+
const collectNamedImportBindings = (namedSpecifiersText, source, declarationIsTypeOnly, importedBindings) => {
|
|
13621
|
+
for (const specifier of parseExportSpecifiers(namedSpecifiersText, declarationIsTypeOnly)) addImportedBinding(importedBindings, {
|
|
13622
|
+
localName: specifier.exportedName,
|
|
13623
|
+
importedName: specifier.localName,
|
|
13624
|
+
source,
|
|
13625
|
+
isTypeOnly: specifier.isTypeOnly
|
|
13626
|
+
});
|
|
13627
|
+
};
|
|
13628
|
+
const collectImportBindings = (importClause, source, declarationIsTypeOnly, importedBindings) => {
|
|
13629
|
+
const trimmedImportClause = importClause.trim();
|
|
13630
|
+
const namespaceMatch = trimmedImportClause.match(/(?:^|,\s*)\*\s+as\s+([\w$]+)/);
|
|
13631
|
+
if (namespaceMatch?.[1]) addImportedBinding(importedBindings, {
|
|
13632
|
+
localName: namespaceMatch[1],
|
|
13633
|
+
importedName: "*",
|
|
13634
|
+
source,
|
|
13635
|
+
isTypeOnly: declarationIsTypeOnly
|
|
13636
|
+
});
|
|
13637
|
+
const namedImportMatch = trimmedImportClause.match(/\{([\s\S]*?)\}/);
|
|
13638
|
+
if (namedImportMatch?.[1]) collectNamedImportBindings(namedImportMatch[1], source, declarationIsTypeOnly, importedBindings);
|
|
13639
|
+
const defaultImportName = trimmedImportClause.split(",")[0]?.trim();
|
|
13640
|
+
if (defaultImportName && !defaultImportName.startsWith("{") && !defaultImportName.startsWith("*")) addImportedBinding(importedBindings, {
|
|
13641
|
+
localName: defaultImportName,
|
|
13642
|
+
importedName: "default",
|
|
13643
|
+
source,
|
|
13644
|
+
isTypeOnly: declarationIsTypeOnly
|
|
13645
|
+
});
|
|
13646
|
+
};
|
|
13647
|
+
const replaceKnownDeclarations = (sourceText, importedBindings, exportsByName, starExportSources) => {
|
|
13648
|
+
let withoutKnownDeclarations = sourceText.replace(BINDING_IMPORT_DECLARATION_PATTERN, (_match, typeKeyword, importClause, source) => {
|
|
13649
|
+
collectImportBindings(importClause, source, Boolean(typeKeyword), importedBindings);
|
|
13650
|
+
return "";
|
|
13651
|
+
});
|
|
13652
|
+
withoutKnownDeclarations = withoutKnownDeclarations.replace(BARREL_REEXPORT_DECLARATION_PATTERN, (_match, typeKeyword, namespaceExportName, specifiersText, source) => {
|
|
13653
|
+
const isTypeOnly = Boolean(typeKeyword);
|
|
13654
|
+
if (namespaceExportName) {
|
|
13655
|
+
exportsByName.set(namespaceExportName, {
|
|
13656
|
+
exportedName: namespaceExportName,
|
|
13657
|
+
importedName: "*",
|
|
13658
|
+
source,
|
|
13659
|
+
isTypeOnly
|
|
13660
|
+
});
|
|
13661
|
+
return "";
|
|
13662
|
+
}
|
|
13663
|
+
if (specifiersText) {
|
|
13664
|
+
for (const specifier of parseExportSpecifiers(specifiersText, isTypeOnly)) exportsByName.set(specifier.exportedName, {
|
|
13665
|
+
exportedName: specifier.exportedName,
|
|
13666
|
+
importedName: specifier.localName,
|
|
13667
|
+
source,
|
|
13668
|
+
isTypeOnly: specifier.isTypeOnly
|
|
13669
|
+
});
|
|
13670
|
+
return "";
|
|
13671
|
+
}
|
|
13672
|
+
starExportSources.push(source);
|
|
13673
|
+
return "";
|
|
13674
|
+
});
|
|
13675
|
+
withoutKnownDeclarations = withoutKnownDeclarations.replace(LOCAL_EXPORT_SPECIFIER_DECLARATION_PATTERN, (_match, typeKeyword, specifiersText) => {
|
|
13676
|
+
for (const specifier of parseExportSpecifiers(specifiersText, Boolean(typeKeyword))) {
|
|
13677
|
+
const importedBinding = importedBindings.get(specifier.localName);
|
|
13678
|
+
if (!importedBinding) return _match;
|
|
13679
|
+
importedBinding.didExport = true;
|
|
13680
|
+
exportsByName.set(specifier.exportedName, {
|
|
13681
|
+
exportedName: specifier.exportedName,
|
|
13682
|
+
importedName: importedBinding.importedName,
|
|
13683
|
+
source: importedBinding.source,
|
|
13684
|
+
isTypeOnly: specifier.isTypeOnly || importedBinding.isTypeOnly
|
|
13685
|
+
});
|
|
13686
|
+
}
|
|
13687
|
+
return "";
|
|
13688
|
+
});
|
|
13689
|
+
return withoutKnownDeclarations;
|
|
13690
|
+
};
|
|
13691
|
+
const hasUnexportedRuntimeImport = (importedBindings) => {
|
|
13692
|
+
for (const binding of importedBindings.values()) if (!binding.isTypeOnly && !binding.didExport) return true;
|
|
13693
|
+
return false;
|
|
13694
|
+
};
|
|
13695
|
+
const classifyBarrelModule = (sourceText) => {
|
|
13696
|
+
const strippedSource = stripJsComments(sourceText).trim();
|
|
13697
|
+
if (!strippedSource) return createNonBarrelInfo();
|
|
13698
|
+
const importedBindings = /* @__PURE__ */ new Map();
|
|
13699
|
+
const exportsByName = /* @__PURE__ */ new Map();
|
|
13700
|
+
const starExportSources = [];
|
|
13701
|
+
if (replaceKnownDeclarations(strippedSource, importedBindings, exportsByName, starExportSources).trim() || hasUnexportedRuntimeImport(importedBindings)) return createNonBarrelInfo();
|
|
13702
|
+
return {
|
|
13703
|
+
isBarrel: exportsByName.size > 0 || starExportSources.length > 0,
|
|
13704
|
+
exportsByName,
|
|
13705
|
+
starExportSources
|
|
13706
|
+
};
|
|
13707
|
+
};
|
|
13708
|
+
const getBarrelIndexModuleInfo = (filePath) => {
|
|
13709
|
+
if (!isIndexModuleFilePath(filePath)) return createNonBarrelInfo();
|
|
13710
|
+
const cachedResult = barrelIndexModuleInfoCache.get(filePath);
|
|
13711
|
+
if (cachedResult !== void 0) return cachedResult;
|
|
13712
|
+
let moduleInfo = createNonBarrelInfo();
|
|
13713
|
+
try {
|
|
13714
|
+
moduleInfo = classifyBarrelModule(fs.readFileSync(filePath, "utf8"));
|
|
13715
|
+
} catch {
|
|
13716
|
+
moduleInfo = createNonBarrelInfo();
|
|
13717
|
+
}
|
|
13718
|
+
barrelIndexModuleInfoCache.set(filePath, moduleInfo);
|
|
13719
|
+
return moduleInfo;
|
|
13720
|
+
};
|
|
13721
|
+
const isBarrelIndexModule = (filePath) => getBarrelIndexModuleInfo(filePath).isBarrel;
|
|
13722
|
+
//#endregion
|
|
13723
|
+
//#region src/plugin/utils/resolve-relative-import-path.ts
|
|
13724
|
+
const MODULE_FILE_EXTENSIONS = [
|
|
13725
|
+
".ts",
|
|
13726
|
+
".tsx",
|
|
13727
|
+
".js",
|
|
13728
|
+
".jsx",
|
|
13729
|
+
".mjs",
|
|
13730
|
+
".cjs",
|
|
13731
|
+
".mts",
|
|
13732
|
+
".cts"
|
|
13733
|
+
];
|
|
13734
|
+
const PACKAGE_EXPORT_CONDITIONS = [
|
|
13735
|
+
"import",
|
|
13736
|
+
"default",
|
|
13737
|
+
"module",
|
|
13738
|
+
"browser",
|
|
13739
|
+
"require"
|
|
13740
|
+
];
|
|
13741
|
+
const PACKAGE_ENTRY_FIELDS = [
|
|
13742
|
+
"module",
|
|
13743
|
+
"main",
|
|
13744
|
+
"browser"
|
|
13745
|
+
];
|
|
13746
|
+
const getExistingFilePath = (filePath) => {
|
|
13747
|
+
try {
|
|
13748
|
+
return fs.statSync(filePath).isFile() ? filePath : null;
|
|
13749
|
+
} catch {
|
|
13750
|
+
return null;
|
|
13751
|
+
}
|
|
13752
|
+
};
|
|
13753
|
+
const getExistingDirectoryPath = (directoryPath) => {
|
|
13754
|
+
try {
|
|
13755
|
+
return fs.statSync(directoryPath).isDirectory() ? directoryPath : null;
|
|
13756
|
+
} catch {
|
|
13757
|
+
return null;
|
|
13758
|
+
}
|
|
13759
|
+
};
|
|
13760
|
+
const getModuleFilePathCandidates = (modulePath) => {
|
|
13761
|
+
const extension = path.extname(modulePath);
|
|
13762
|
+
if (!extension) return MODULE_FILE_EXTENSIONS.map((moduleExtension) => `${modulePath}${moduleExtension}`);
|
|
13763
|
+
const modulePathWithoutExtension = modulePath.slice(0, -extension.length);
|
|
13764
|
+
if (extension === ".js") return [
|
|
13765
|
+
modulePath,
|
|
13766
|
+
`${modulePathWithoutExtension}.ts`,
|
|
13767
|
+
`${modulePathWithoutExtension}.tsx`,
|
|
13768
|
+
`${modulePathWithoutExtension}.jsx`
|
|
13769
|
+
];
|
|
13770
|
+
if (extension === ".jsx") return [modulePath, `${modulePathWithoutExtension}.tsx`];
|
|
13771
|
+
if (extension === ".mjs") return [modulePath, `${modulePathWithoutExtension}.mts`];
|
|
13772
|
+
if (extension === ".cjs") return [modulePath, `${modulePathWithoutExtension}.cts`];
|
|
13773
|
+
return [modulePath];
|
|
13774
|
+
};
|
|
13775
|
+
const isObjectRecord$1 = (value) => typeof value === "object" && value !== null;
|
|
13776
|
+
const getConditionalExportEntry = (exportEntry) => {
|
|
13777
|
+
if (typeof exportEntry === "string") return exportEntry;
|
|
13778
|
+
if (Array.isArray(exportEntry)) {
|
|
13779
|
+
for (const fallbackEntry of exportEntry) {
|
|
13780
|
+
const resolvedFallbackEntry = getConditionalExportEntry(fallbackEntry);
|
|
13781
|
+
if (resolvedFallbackEntry) return resolvedFallbackEntry;
|
|
13782
|
+
}
|
|
13783
|
+
return null;
|
|
13784
|
+
}
|
|
13785
|
+
if (!isObjectRecord$1(exportEntry)) return null;
|
|
13786
|
+
for (const condition of PACKAGE_EXPORT_CONDITIONS) {
|
|
13787
|
+
const nestedEntry = getConditionalExportEntry(exportEntry[condition]);
|
|
13788
|
+
if (nestedEntry) return nestedEntry;
|
|
13789
|
+
}
|
|
13790
|
+
return null;
|
|
13791
|
+
};
|
|
13792
|
+
const getPackageExportEntry = (packageJson) => {
|
|
13793
|
+
const exportsField = packageJson.exports;
|
|
13794
|
+
if (!exportsField) return null;
|
|
13795
|
+
const directExportEntry = getConditionalExportEntry(exportsField);
|
|
13796
|
+
if (directExportEntry) return directExportEntry;
|
|
13797
|
+
if (!isObjectRecord$1(exportsField)) return null;
|
|
13798
|
+
return getConditionalExportEntry(exportsField["."]);
|
|
13799
|
+
};
|
|
13800
|
+
const resolveModulePathWithIndexFallback = (modulePath) => {
|
|
13801
|
+
const filePath = resolveModuleFilePath(modulePath);
|
|
13802
|
+
if (filePath) return filePath;
|
|
13803
|
+
return resolveModuleFilePath(path.join(modulePath, "index"));
|
|
13804
|
+
};
|
|
13805
|
+
const resolvePackageDirectoryEntry = (directoryPath) => {
|
|
13806
|
+
const existingDirectoryPath = getExistingDirectoryPath(directoryPath);
|
|
13807
|
+
if (!existingDirectoryPath) return null;
|
|
13808
|
+
const packageJsonPath = path.join(existingDirectoryPath, "package.json");
|
|
13809
|
+
try {
|
|
13810
|
+
const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, "utf8"));
|
|
13811
|
+
const packageEntry = getPackageExportEntry(packageJson) ?? PACKAGE_ENTRY_FIELDS.map((fieldName) => packageJson[fieldName]).find((value) => typeof value === "string");
|
|
13812
|
+
if (!packageEntry) return null;
|
|
13813
|
+
return resolveModulePathWithIndexFallback(path.resolve(existingDirectoryPath, packageEntry));
|
|
13814
|
+
} catch {
|
|
13815
|
+
return null;
|
|
13816
|
+
}
|
|
13817
|
+
};
|
|
13818
|
+
const resolveModuleFilePath = (modulePath) => {
|
|
13819
|
+
const exactFilePath = getExistingFilePath(modulePath);
|
|
13820
|
+
if (exactFilePath) return exactFilePath;
|
|
13821
|
+
for (const candidateFilePath of getModuleFilePathCandidates(modulePath)) {
|
|
13822
|
+
const filePath = getExistingFilePath(candidateFilePath);
|
|
13823
|
+
if (filePath) return filePath;
|
|
13824
|
+
}
|
|
13825
|
+
return null;
|
|
13826
|
+
};
|
|
13827
|
+
const resolveModuleFileFromAbsolutePath = (importPath) => {
|
|
13828
|
+
const directFilePath = resolveModuleFilePath(importPath);
|
|
13829
|
+
if (directFilePath) return directFilePath;
|
|
13830
|
+
const packageEntryFilePath = resolvePackageDirectoryEntry(importPath);
|
|
13831
|
+
if (packageEntryFilePath) return packageEntryFilePath;
|
|
13832
|
+
return resolveModuleFilePath(path.join(importPath, "index"));
|
|
13833
|
+
};
|
|
13834
|
+
const resolveRelativeImportPath = (filename, source) => resolveModuleFileFromAbsolutePath(path.resolve(path.dirname(filename), source));
|
|
13835
|
+
//#endregion
|
|
13836
|
+
//#region src/plugin/utils/resolve-barrel-export-file-path.ts
|
|
13837
|
+
const getUniqueFilePath = (filePaths) => {
|
|
13838
|
+
const uniqueFilePaths = new Set(filePaths);
|
|
13839
|
+
if (uniqueFilePaths.size !== 1) return null;
|
|
13840
|
+
const [filePath] = uniqueFilePaths;
|
|
13841
|
+
return filePath ?? null;
|
|
13842
|
+
};
|
|
13843
|
+
const resolveStarExportFilePath = (barrelFilePath, exportedName, source, visitedFilePaths) => {
|
|
13844
|
+
const resolvedTargetPath = resolveRelativeImportPath(barrelFilePath, source);
|
|
13845
|
+
if (!resolvedTargetPath) return null;
|
|
13846
|
+
const nestedTargetPath = resolveBarrelExportFilePath(resolvedTargetPath, exportedName, new Set(visitedFilePaths));
|
|
13847
|
+
if (nestedTargetPath) return nestedTargetPath;
|
|
13848
|
+
return doesModuleExportName(resolvedTargetPath, exportedName) ? resolvedTargetPath : null;
|
|
13849
|
+
};
|
|
13850
|
+
const resolveBarrelExportFilePath = (barrelFilePath, exportedName, visitedFilePaths = /* @__PURE__ */ new Set()) => {
|
|
13851
|
+
if (visitedFilePaths.has(barrelFilePath)) return null;
|
|
13852
|
+
visitedFilePaths.add(barrelFilePath);
|
|
13853
|
+
const moduleInfo = getBarrelIndexModuleInfo(barrelFilePath);
|
|
13854
|
+
if (!moduleInfo.isBarrel) return null;
|
|
13855
|
+
const target = moduleInfo.exportsByName.get(exportedName);
|
|
13856
|
+
if (target) {
|
|
13857
|
+
const resolvedTargetPath = resolveRelativeImportPath(barrelFilePath, target.source);
|
|
13858
|
+
if (!resolvedTargetPath) return null;
|
|
13859
|
+
return resolveBarrelExportFilePath(resolvedTargetPath, target.importedName, visitedFilePaths) ?? resolvedTargetPath;
|
|
13860
|
+
}
|
|
13861
|
+
if (exportedName === "default") return null;
|
|
13862
|
+
return getUniqueFilePath(moduleInfo.starExportSources.map((source) => resolveStarExportFilePath(barrelFilePath, exportedName, source, visitedFilePaths)).filter((filePath) => Boolean(filePath)));
|
|
13863
|
+
};
|
|
13864
|
+
//#endregion
|
|
13865
|
+
//#region src/plugin/utils/resolve-tsconfig-alias.ts
|
|
13866
|
+
const TSCONFIG_FILE_NAMES = ["tsconfig.json", "jsconfig.json"];
|
|
13867
|
+
const isObjectRecord = (value) => typeof value === "object" && value !== null;
|
|
13868
|
+
const stripJsonComments = (text) => {
|
|
13869
|
+
let output = "";
|
|
13870
|
+
let inString = false;
|
|
13871
|
+
let inLineComment = false;
|
|
13872
|
+
let inBlockComment = false;
|
|
13873
|
+
for (let index = 0; index < text.length; index++) {
|
|
13874
|
+
const character = text[index];
|
|
13875
|
+
const nextCharacter = text[index + 1];
|
|
13876
|
+
if (inLineComment) {
|
|
13877
|
+
if (character === "\n") {
|
|
13878
|
+
inLineComment = false;
|
|
13879
|
+
output += character;
|
|
13880
|
+
}
|
|
13881
|
+
continue;
|
|
13882
|
+
}
|
|
13883
|
+
if (inBlockComment) {
|
|
13884
|
+
if (character === "*" && nextCharacter === "/") {
|
|
13885
|
+
inBlockComment = false;
|
|
13886
|
+
index++;
|
|
13887
|
+
}
|
|
13888
|
+
continue;
|
|
13889
|
+
}
|
|
13890
|
+
if (inString) {
|
|
13891
|
+
output += character;
|
|
13892
|
+
if (character === "\\") {
|
|
13893
|
+
output += nextCharacter ?? "";
|
|
13894
|
+
index++;
|
|
13895
|
+
} else if (character === "\"") inString = false;
|
|
13896
|
+
continue;
|
|
13897
|
+
}
|
|
13898
|
+
if (character === "\"") {
|
|
13899
|
+
inString = true;
|
|
13900
|
+
output += character;
|
|
13901
|
+
continue;
|
|
13902
|
+
}
|
|
13903
|
+
if (character === "/" && nextCharacter === "/") {
|
|
13904
|
+
inLineComment = true;
|
|
13905
|
+
index++;
|
|
13906
|
+
continue;
|
|
13907
|
+
}
|
|
13908
|
+
if (character === "/" && nextCharacter === "*") {
|
|
13909
|
+
inBlockComment = true;
|
|
13910
|
+
index++;
|
|
13911
|
+
continue;
|
|
13912
|
+
}
|
|
13913
|
+
output += character;
|
|
13914
|
+
}
|
|
13915
|
+
return output.replace(/,(\s*[}\]])/g, "$1");
|
|
13916
|
+
};
|
|
13917
|
+
const parseTsconfigFile = (configFilePath) => {
|
|
13918
|
+
let sourceText;
|
|
13919
|
+
try {
|
|
13920
|
+
sourceText = fs.readFileSync(configFilePath, "utf8");
|
|
13921
|
+
} catch {
|
|
13922
|
+
return null;
|
|
13923
|
+
}
|
|
13924
|
+
try {
|
|
13925
|
+
const parsed = JSON.parse(stripJsonComments(sourceText));
|
|
13926
|
+
return isObjectRecord(parsed) ? parsed : null;
|
|
13927
|
+
} catch {
|
|
13928
|
+
return null;
|
|
13929
|
+
}
|
|
13930
|
+
};
|
|
13931
|
+
const resolveExtendsPath = (extendsValue, fromConfigDirectory) => {
|
|
13932
|
+
const withExtension = extendsValue.endsWith(".json") ? extendsValue : `${extendsValue}.json`;
|
|
13933
|
+
if (extendsValue.startsWith("./") || extendsValue.startsWith("../")) return path.resolve(fromConfigDirectory, withExtension);
|
|
13934
|
+
return path.join(fromConfigDirectory, "node_modules", withExtension);
|
|
13935
|
+
};
|
|
13936
|
+
const parsePathsField = (pathsField) => {
|
|
13937
|
+
const paths = /* @__PURE__ */ new Map();
|
|
13938
|
+
if (!isObjectRecord(pathsField)) return paths;
|
|
13939
|
+
for (const [pattern, targets] of Object.entries(pathsField)) {
|
|
13940
|
+
if (!Array.isArray(targets)) continue;
|
|
13941
|
+
const stringTargets = targets.filter((target) => typeof target === "string");
|
|
13942
|
+
if (stringTargets.length > 0) paths.set(pattern, stringTargets);
|
|
13943
|
+
}
|
|
13944
|
+
return paths;
|
|
13945
|
+
};
|
|
13946
|
+
const readResolvedTsconfig = (configFilePath, extendsDepth) => {
|
|
13947
|
+
const parsed = parseTsconfigFile(configFilePath);
|
|
13948
|
+
if (!parsed) return null;
|
|
13949
|
+
const configDirectory = path.dirname(configFilePath);
|
|
13950
|
+
const compilerOptions = isObjectRecord(parsed.compilerOptions) ? parsed.compilerOptions : {};
|
|
13951
|
+
const baseUrlValue = typeof compilerOptions.baseUrl === "string" ? compilerOptions.baseUrl : null;
|
|
13952
|
+
const hasExplicitBaseUrl = baseUrlValue !== null;
|
|
13953
|
+
const baseAbsolutePath = baseUrlValue !== null ? path.resolve(configDirectory, baseUrlValue) : configDirectory;
|
|
13954
|
+
if (isObjectRecord(compilerOptions.paths)) return {
|
|
13955
|
+
baseAbsolutePath,
|
|
13956
|
+
hasExplicitBaseUrl,
|
|
13957
|
+
paths: parsePathsField(compilerOptions.paths)
|
|
13958
|
+
};
|
|
13959
|
+
if (typeof parsed.extends === "string" && extendsDepth < 8) {
|
|
13960
|
+
const parentPath = resolveExtendsPath(parsed.extends, configDirectory);
|
|
13961
|
+
const inherited = parentPath ? readResolvedTsconfig(parentPath, extendsDepth + 1) : null;
|
|
13962
|
+
if (inherited) return inherited;
|
|
13963
|
+
}
|
|
13964
|
+
return hasExplicitBaseUrl ? {
|
|
13965
|
+
baseAbsolutePath,
|
|
13966
|
+
hasExplicitBaseUrl,
|
|
13967
|
+
paths: /* @__PURE__ */ new Map()
|
|
13968
|
+
} : null;
|
|
13969
|
+
};
|
|
13970
|
+
const configByFilePath = /* @__PURE__ */ new Map();
|
|
13971
|
+
const loadTsconfigCached = (configFilePath) => {
|
|
13972
|
+
let fileStat;
|
|
13973
|
+
try {
|
|
13974
|
+
fileStat = fs.statSync(configFilePath);
|
|
13975
|
+
} catch {
|
|
13976
|
+
return null;
|
|
13977
|
+
}
|
|
13978
|
+
const cached = configByFilePath.get(configFilePath);
|
|
13979
|
+
if (cached && cached.mtimeMs === fileStat.mtimeMs) return cached.config;
|
|
13980
|
+
const config = readResolvedTsconfig(configFilePath, 0);
|
|
13981
|
+
configByFilePath.set(configFilePath, {
|
|
13982
|
+
mtimeMs: fileStat.mtimeMs,
|
|
13983
|
+
config
|
|
13984
|
+
});
|
|
13985
|
+
return config;
|
|
13986
|
+
};
|
|
13987
|
+
const findNearestTsconfig = (fromDirectory) => {
|
|
13988
|
+
let currentDirectory = fromDirectory;
|
|
13989
|
+
for (let level = 0; level < 30; level++) {
|
|
13990
|
+
for (const fileName of TSCONFIG_FILE_NAMES) {
|
|
13991
|
+
const candidate = loadTsconfigCached(path.join(currentDirectory, fileName));
|
|
13992
|
+
if (candidate) return candidate;
|
|
13993
|
+
}
|
|
13994
|
+
const parentDirectory = path.dirname(currentDirectory);
|
|
13995
|
+
if (parentDirectory === currentDirectory) break;
|
|
13996
|
+
currentDirectory = parentDirectory;
|
|
13997
|
+
}
|
|
13998
|
+
return null;
|
|
13999
|
+
};
|
|
14000
|
+
const matchPathPattern = (source, pattern) => {
|
|
14001
|
+
const starIndex = pattern.indexOf("*");
|
|
14002
|
+
if (starIndex === -1) return source === pattern ? "" : null;
|
|
14003
|
+
const prefix = pattern.slice(0, starIndex);
|
|
14004
|
+
const suffix = pattern.slice(starIndex + 1);
|
|
14005
|
+
if (source.length >= prefix.length + suffix.length && source.startsWith(prefix) && source.endsWith(suffix)) return source.slice(prefix.length, source.length - suffix.length);
|
|
14006
|
+
return null;
|
|
14007
|
+
};
|
|
14008
|
+
const resolveTsconfigAliasPath = (fromFilename, source) => {
|
|
14009
|
+
const config = findNearestTsconfig(path.dirname(fromFilename));
|
|
14010
|
+
if (!config) return null;
|
|
14011
|
+
let bestPattern = null;
|
|
14012
|
+
let bestCapture = "";
|
|
14013
|
+
let bestPrefixLength = -1;
|
|
14014
|
+
for (const pattern of config.paths.keys()) {
|
|
14015
|
+
const capture = matchPathPattern(source, pattern);
|
|
14016
|
+
if (capture === null) continue;
|
|
14017
|
+
const starIndex = pattern.indexOf("*");
|
|
14018
|
+
const prefixLength = starIndex === -1 ? pattern.length : starIndex;
|
|
14019
|
+
if (prefixLength > bestPrefixLength) {
|
|
14020
|
+
bestPattern = pattern;
|
|
14021
|
+
bestCapture = capture;
|
|
14022
|
+
bestPrefixLength = prefixLength;
|
|
14023
|
+
}
|
|
14024
|
+
}
|
|
14025
|
+
if (bestPattern) for (const target of config.paths.get(bestPattern) ?? []) {
|
|
14026
|
+
const substituted = target.replaceAll("*", bestCapture);
|
|
14027
|
+
const resolved = resolveModuleFileFromAbsolutePath(path.resolve(config.baseAbsolutePath, substituted));
|
|
14028
|
+
if (resolved) return resolved;
|
|
14029
|
+
}
|
|
14030
|
+
if (config.hasExplicitBaseUrl) return resolveModuleFileFromAbsolutePath(path.resolve(config.baseAbsolutePath, source));
|
|
14031
|
+
return null;
|
|
14032
|
+
};
|
|
14033
|
+
//#endregion
|
|
14034
|
+
//#region src/plugin/utils/resolve-module-path.ts
|
|
14035
|
+
const resolveModulePath = (fromFilename, source) => resolveRelativeImportPath(fromFilename, source) ?? resolveTsconfigAliasPath(fromFilename, source);
|
|
14036
|
+
//#endregion
|
|
14037
|
+
//#region src/plugin/utils/resolve-cross-file-function-export.ts
|
|
14038
|
+
const resolveFunctionExportInFile = (filePath, exportedName, visitedFilePaths) => {
|
|
14039
|
+
if (visitedFilePaths.size >= 4) return null;
|
|
14040
|
+
if (visitedFilePaths.has(filePath)) return null;
|
|
14041
|
+
visitedFilePaths.add(filePath);
|
|
14042
|
+
const actualFilePath = resolveBarrelExportFilePath(filePath, exportedName) ?? filePath;
|
|
14043
|
+
const programRoot = parseSourceFile(actualFilePath);
|
|
14044
|
+
if (!programRoot) return null;
|
|
14045
|
+
const exported = findExportedFunctionBody(programRoot, exportedName);
|
|
14046
|
+
if (exported) return exported;
|
|
14047
|
+
for (const reExportSource of findReExportSourcesForName(programRoot, exportedName)) {
|
|
14048
|
+
const nextFilePath = resolveModulePath(actualFilePath, reExportSource);
|
|
14049
|
+
if (!nextFilePath) continue;
|
|
14050
|
+
const resolved = resolveFunctionExportInFile(nextFilePath, exportedName, visitedFilePaths);
|
|
14051
|
+
if (resolved) return resolved;
|
|
14052
|
+
}
|
|
14053
|
+
return null;
|
|
14054
|
+
};
|
|
14055
|
+
const resolveCrossFileFunctionExport = (fromFilename, source, exportedName) => {
|
|
14056
|
+
const resolvedFilePath = resolveModulePath(fromFilename, source);
|
|
14057
|
+
if (!resolvedFilePath) return null;
|
|
14058
|
+
return resolveFunctionExportInFile(resolvedFilePath, exportedName, /* @__PURE__ */ new Set());
|
|
14059
|
+
};
|
|
14060
|
+
//#endregion
|
|
14061
|
+
//#region src/plugin/utils/ast-mentions-suspense.ts
|
|
14062
|
+
const isSuspenseJsxOpeningName = (name) => {
|
|
14063
|
+
if (isNodeOfType(name, "JSXIdentifier")) return name.name === "Suspense";
|
|
14064
|
+
return isNodeOfType(name, "JSXMemberExpression") && isNodeOfType(name.property, "JSXIdentifier") && name.property.name === "Suspense";
|
|
14065
|
+
};
|
|
14066
|
+
const astMentionsSuspense = (programNode) => {
|
|
14067
|
+
let didDetect = false;
|
|
13176
14068
|
walkAst(programNode, (child) => {
|
|
13177
|
-
if (
|
|
13178
|
-
if (isNodeOfType(child, "JSXOpeningElement") &&
|
|
13179
|
-
|
|
14069
|
+
if (didDetect) return false;
|
|
14070
|
+
if (isNodeOfType(child, "JSXOpeningElement") && isSuspenseJsxOpeningName(child.name)) {
|
|
14071
|
+
didDetect = true;
|
|
13180
14072
|
return false;
|
|
13181
14073
|
}
|
|
13182
14074
|
if (isNodeOfType(child, "ImportDeclaration") && child.source?.value === "react") {
|
|
13183
14075
|
if ((child.specifiers ?? []).some((specifier) => isNodeOfType(specifier, "ImportSpecifier") && getImportedName$1(specifier) === "Suspense")) {
|
|
13184
|
-
|
|
14076
|
+
didDetect = true;
|
|
13185
14077
|
return false;
|
|
13186
14078
|
}
|
|
13187
14079
|
}
|
|
13188
14080
|
});
|
|
13189
|
-
return
|
|
14081
|
+
return didDetect;
|
|
14082
|
+
};
|
|
14083
|
+
//#endregion
|
|
14084
|
+
//#region src/plugin/utils/find-ancestor-suspense-layout.ts
|
|
14085
|
+
const LAYOUT_FILE_NAMES = [
|
|
14086
|
+
"layout.tsx",
|
|
14087
|
+
"layout.jsx",
|
|
14088
|
+
"layout.ts",
|
|
14089
|
+
"layout.js"
|
|
14090
|
+
];
|
|
14091
|
+
const hasAncestorSuspenseLayout = (pageFilename) => {
|
|
14092
|
+
const normalizedPage = pageFilename.replaceAll("\\", "/");
|
|
14093
|
+
let currentDirectory = path.dirname(normalizedPage);
|
|
14094
|
+
for (let level = 0; level < 30; level++) {
|
|
14095
|
+
for (const layoutFileName of LAYOUT_FILE_NAMES) {
|
|
14096
|
+
const layoutPath = path.join(currentDirectory, layoutFileName);
|
|
14097
|
+
if (layoutPath.replaceAll("\\", "/") === normalizedPage) continue;
|
|
14098
|
+
const programRoot = parseSourceFile(layoutPath);
|
|
14099
|
+
if (programRoot && astMentionsSuspense(programRoot)) return true;
|
|
14100
|
+
}
|
|
14101
|
+
if (path.basename(currentDirectory) === "app") break;
|
|
14102
|
+
const parentDirectory = path.dirname(currentDirectory);
|
|
14103
|
+
if (parentDirectory === currentDirectory) break;
|
|
14104
|
+
currentDirectory = parentDirectory;
|
|
14105
|
+
}
|
|
14106
|
+
return false;
|
|
14107
|
+
};
|
|
14108
|
+
//#endregion
|
|
14109
|
+
//#region src/plugin/rules/nextjs/nextjs-no-use-search-params-without-suspense.ts
|
|
14110
|
+
const astContainsUseSearchParams = (root) => {
|
|
14111
|
+
let didFind = false;
|
|
14112
|
+
walkAst(root, (child) => {
|
|
14113
|
+
if (didFind) return false;
|
|
14114
|
+
if (isHookCall$1(child, "useSearchParams")) {
|
|
14115
|
+
didFind = true;
|
|
14116
|
+
return false;
|
|
14117
|
+
}
|
|
14118
|
+
});
|
|
14119
|
+
return didFind;
|
|
14120
|
+
};
|
|
14121
|
+
const isSuspenseJsxName = (name, suspenseLocalNames) => {
|
|
14122
|
+
if (isNodeOfType(name, "JSXIdentifier")) return name.name === "Suspense" || suspenseLocalNames.has(name.name);
|
|
14123
|
+
return isNodeOfType(name, "JSXMemberExpression") && isNodeOfType(name.property, "JSXIdentifier") && name.property.name === "Suspense";
|
|
14124
|
+
};
|
|
14125
|
+
const isInsideSuspenseBoundary = (node, suspenseLocalNames) => {
|
|
14126
|
+
let ancestor = node.parent;
|
|
14127
|
+
while (ancestor) {
|
|
14128
|
+
if (isNodeOfType(ancestor, "JSXElement") && isSuspenseJsxName(ancestor.openingElement?.name, suspenseLocalNames)) return true;
|
|
14129
|
+
ancestor = ancestor.parent ?? null;
|
|
14130
|
+
}
|
|
14131
|
+
return false;
|
|
14132
|
+
};
|
|
14133
|
+
const collectSuspenseLocalNames = (programNode) => {
|
|
14134
|
+
const names = /* @__PURE__ */ new Set();
|
|
14135
|
+
for (const statement of programNode.body ?? []) {
|
|
14136
|
+
if (!isNodeOfType(statement, "ImportDeclaration")) continue;
|
|
14137
|
+
if (statement.source?.value !== "react") continue;
|
|
14138
|
+
for (const specifier of statement.specifiers ?? []) if (isNodeOfType(specifier, "ImportSpecifier") && getImportedName$1(specifier) === "Suspense" && specifier.local?.name) names.add(specifier.local.name);
|
|
14139
|
+
}
|
|
14140
|
+
return names;
|
|
14141
|
+
};
|
|
14142
|
+
const collectImportedComponents = (programNode) => {
|
|
14143
|
+
const entries = /* @__PURE__ */ new Map();
|
|
14144
|
+
for (const statement of programNode.body ?? []) {
|
|
14145
|
+
if (!isNodeOfType(statement, "ImportDeclaration")) continue;
|
|
14146
|
+
if (typeof statement.source?.value !== "string") continue;
|
|
14147
|
+
const source = statement.source.value;
|
|
14148
|
+
for (const specifier of statement.specifiers ?? []) {
|
|
14149
|
+
const localName = specifier.local?.name;
|
|
14150
|
+
if (!localName) continue;
|
|
14151
|
+
const exportedName = resolveImportedExportName(specifier);
|
|
14152
|
+
if (!exportedName) continue;
|
|
14153
|
+
entries.set(localName, {
|
|
14154
|
+
source,
|
|
14155
|
+
exportedName
|
|
14156
|
+
});
|
|
14157
|
+
}
|
|
14158
|
+
}
|
|
14159
|
+
return entries;
|
|
13190
14160
|
};
|
|
13191
14161
|
const nextjsNoUseSearchParamsWithoutSuspense = defineRule({
|
|
13192
14162
|
id: "nextjs-no-use-search-params-without-suspense",
|
|
@@ -13196,25 +14166,68 @@ const nextjsNoUseSearchParamsWithoutSuspense = defineRule({
|
|
|
13196
14166
|
severity: "warn",
|
|
13197
14167
|
recommendation: "Wrap the component using useSearchParams: `<Suspense fallback={<Skeleton />}><SearchComponent /></Suspense>`",
|
|
13198
14168
|
create: (context) => {
|
|
14169
|
+
let isPageOrLayoutFile = false;
|
|
14170
|
+
let hasAncestorLayoutSuspense = false;
|
|
13199
14171
|
let hasSuspenseInFile = false;
|
|
14172
|
+
let importedComponents = /* @__PURE__ */ new Map();
|
|
14173
|
+
let suspenseLocalNames = /* @__PURE__ */ new Set();
|
|
13200
14174
|
return {
|
|
13201
14175
|
Program(programNode) {
|
|
13202
|
-
|
|
14176
|
+
const filename = normalizeFilename$1(context.filename ?? "");
|
|
14177
|
+
isPageOrLayoutFile = PAGE_OR_LAYOUT_FILE_PATTERN.test(filename);
|
|
14178
|
+
if (!isPageOrLayoutFile) return;
|
|
14179
|
+
hasAncestorLayoutSuspense = hasAncestorSuspenseLayout(context.filename ?? "");
|
|
14180
|
+
if (hasAncestorLayoutSuspense) return;
|
|
14181
|
+
hasSuspenseInFile = astMentionsSuspense(programNode);
|
|
14182
|
+
importedComponents = collectImportedComponents(programNode);
|
|
14183
|
+
suspenseLocalNames = collectSuspenseLocalNames(programNode);
|
|
13203
14184
|
},
|
|
13204
14185
|
CallExpression(node) {
|
|
13205
|
-
if (hasSuspenseInFile) return;
|
|
14186
|
+
if (!isPageOrLayoutFile || hasAncestorLayoutSuspense || hasSuspenseInFile) return;
|
|
13206
14187
|
if (!isHookCall$1(node, "useSearchParams")) return;
|
|
13207
14188
|
context.report({
|
|
13208
14189
|
node,
|
|
13209
14190
|
message: "useSearchParams() without a <Suspense> boundary forces the whole page into client-side rendering."
|
|
13210
14191
|
});
|
|
14192
|
+
},
|
|
14193
|
+
JSXOpeningElement(node) {
|
|
14194
|
+
if (!isPageOrLayoutFile || hasAncestorLayoutSuspense) return;
|
|
14195
|
+
if (!isNodeOfType(node.name, "JSXIdentifier")) return;
|
|
14196
|
+
const importEntry = importedComponents.get(node.name.name);
|
|
14197
|
+
if (!importEntry) return;
|
|
14198
|
+
const jsxElement = node.parent;
|
|
14199
|
+
if (!jsxElement) return;
|
|
14200
|
+
if (isInsideSuspenseBoundary(jsxElement, suspenseLocalNames)) return;
|
|
14201
|
+
const componentBody = resolveCrossFileFunctionExport(context.filename ?? "", importEntry.source, importEntry.exportedName);
|
|
14202
|
+
if (!componentBody || !astContainsUseSearchParams(componentBody)) return;
|
|
14203
|
+
context.report({
|
|
14204
|
+
node,
|
|
14205
|
+
message: `<${node.name.name}> uses useSearchParams() but is not wrapped in a <Suspense> boundary.`
|
|
14206
|
+
});
|
|
13211
14207
|
}
|
|
13212
14208
|
};
|
|
13213
14209
|
}
|
|
13214
14210
|
});
|
|
13215
14211
|
//#endregion
|
|
14212
|
+
//#region src/plugin/rules/nextjs/nextjs-no-vercel-og-import.ts
|
|
14213
|
+
const nextjsNoVercelOgImport = defineRule({
|
|
14214
|
+
id: "nextjs-no-vercel-og-import",
|
|
14215
|
+
title: "@vercel/og import instead of next/og",
|
|
14216
|
+
tags: ["test-noise"],
|
|
14217
|
+
requires: ["nextjs"],
|
|
14218
|
+
severity: "warn",
|
|
14219
|
+
recommendation: "Use `import { ImageResponse } from \"next/og\"`. The `@vercel/og` package is built into Next.js and should not be imported directly",
|
|
14220
|
+
create: (context) => ({ ImportDeclaration(node) {
|
|
14221
|
+
if (node.source?.value !== "@vercel/og") return;
|
|
14222
|
+
context.report({
|
|
14223
|
+
node,
|
|
14224
|
+
message: "@vercel/og is bundled into Next.js. Import from \"next/og\" instead to avoid duplicate code and version mismatch."
|
|
14225
|
+
});
|
|
14226
|
+
} })
|
|
14227
|
+
});
|
|
14228
|
+
//#endregion
|
|
13216
14229
|
//#region src/plugin/rules/a11y/no-access-key.ts
|
|
13217
|
-
const MESSAGE$
|
|
14230
|
+
const MESSAGE$31 = "Screen reader users can lose their shortcuts because `accessKey` clashes with them, so remove it.";
|
|
13218
14231
|
const isUndefinedIdentifier = (expression) => isNodeOfType(expression, "Identifier") && expression.name === "undefined";
|
|
13219
14232
|
const noAccessKey = defineRule({
|
|
13220
14233
|
id: "no-access-key",
|
|
@@ -13231,7 +14244,7 @@ const noAccessKey = defineRule({
|
|
|
13231
14244
|
if (isNodeOfType(attributeValue, "Literal") && typeof attributeValue.value === "string") {
|
|
13232
14245
|
context.report({
|
|
13233
14246
|
node: accessKey,
|
|
13234
|
-
message: MESSAGE$
|
|
14247
|
+
message: MESSAGE$31
|
|
13235
14248
|
});
|
|
13236
14249
|
return;
|
|
13237
14250
|
}
|
|
@@ -13241,7 +14254,7 @@ const noAccessKey = defineRule({
|
|
|
13241
14254
|
if (isUndefinedIdentifier(expression)) return;
|
|
13242
14255
|
context.report({
|
|
13243
14256
|
node: accessKey,
|
|
13244
|
-
message: MESSAGE$
|
|
14257
|
+
message: MESSAGE$31
|
|
13245
14258
|
});
|
|
13246
14259
|
}
|
|
13247
14260
|
} })
|
|
@@ -13724,7 +14737,7 @@ const noAdjustStateOnPropChange = defineRule({
|
|
|
13724
14737
|
});
|
|
13725
14738
|
//#endregion
|
|
13726
14739
|
//#region src/plugin/rules/a11y/no-aria-hidden-on-focusable.ts
|
|
13727
|
-
const MESSAGE$
|
|
14740
|
+
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
14741
|
const noAriaHiddenOnFocusable = defineRule({
|
|
13729
14742
|
id: "no-aria-hidden-on-focusable",
|
|
13730
14743
|
title: "aria-hidden on focusable element",
|
|
@@ -13751,7 +14764,7 @@ const noAriaHiddenOnFocusable = defineRule({
|
|
|
13751
14764
|
const isImplicitlyFocusable = isInteractiveElement(tag, node);
|
|
13752
14765
|
if (isExplicitlyFocusable || isImplicitlyFocusable) context.report({
|
|
13753
14766
|
node: ariaHidden,
|
|
13754
|
-
message: MESSAGE$
|
|
14767
|
+
message: MESSAGE$30
|
|
13755
14768
|
});
|
|
13756
14769
|
} })
|
|
13757
14770
|
});
|
|
@@ -14119,7 +15132,7 @@ const noArrayIndexAsKey = defineRule({
|
|
|
14119
15132
|
});
|
|
14120
15133
|
//#endregion
|
|
14121
15134
|
//#region src/plugin/rules/react-builtins/no-array-index-key.ts
|
|
14122
|
-
const MESSAGE$
|
|
15135
|
+
const MESSAGE$29 = "Your users can see & submit the wrong data when this list reorders.";
|
|
14123
15136
|
const SECOND_INDEX_METHODS = new Set([
|
|
14124
15137
|
"every",
|
|
14125
15138
|
"filter",
|
|
@@ -14323,7 +15336,7 @@ const noArrayIndexKey = defineRule({
|
|
|
14323
15336
|
}
|
|
14324
15337
|
context.report({
|
|
14325
15338
|
node: keyAttribute,
|
|
14326
|
-
message: MESSAGE$
|
|
15339
|
+
message: MESSAGE$29
|
|
14327
15340
|
});
|
|
14328
15341
|
},
|
|
14329
15342
|
CallExpression(node) {
|
|
@@ -14343,7 +15356,7 @@ const noArrayIndexKey = defineRule({
|
|
|
14343
15356
|
if (propName !== "key") continue;
|
|
14344
15357
|
if (expressionUsesIndex(property.value, indexBinding.name)) context.report({
|
|
14345
15358
|
node: property,
|
|
14346
|
-
message: MESSAGE$
|
|
15359
|
+
message: MESSAGE$29
|
|
14347
15360
|
});
|
|
14348
15361
|
}
|
|
14349
15362
|
}
|
|
@@ -14351,7 +15364,7 @@ const noArrayIndexKey = defineRule({
|
|
|
14351
15364
|
});
|
|
14352
15365
|
//#endregion
|
|
14353
15366
|
//#region src/plugin/rules/a11y/no-autofocus.ts
|
|
14354
|
-
const MESSAGE$
|
|
15367
|
+
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
15368
|
const resolveSettings$21 = (settings) => {
|
|
14356
15369
|
const reactDoctor = settings?.["react-doctor"];
|
|
14357
15370
|
return { ignoreNonDOM: (typeof reactDoctor === "object" && reactDoctor !== null ? reactDoctor.noAutofocus ?? {} : {}).ignoreNonDOM ?? true };
|
|
@@ -14407,7 +15420,7 @@ const noAutofocus = defineRule({
|
|
|
14407
15420
|
}
|
|
14408
15421
|
context.report({
|
|
14409
15422
|
node: autoFocusAttribute,
|
|
14410
|
-
message: MESSAGE$
|
|
15423
|
+
message: MESSAGE$28
|
|
14411
15424
|
});
|
|
14412
15425
|
} };
|
|
14413
15426
|
}
|
|
@@ -14421,306 +15434,6 @@ const createRelativeImportSource = (filename, targetFilePath) => {
|
|
|
14421
15434
|
return relativePath.startsWith(".") ? relativePath : `./${relativePath}`;
|
|
14422
15435
|
};
|
|
14423
15436
|
//#endregion
|
|
14424
|
-
//#region src/plugin/utils/parse-export-specifiers.ts
|
|
14425
|
-
const getSpecifierName = (rawName) => rawName.replace(/^type\s+/, "").trim();
|
|
14426
|
-
const parseExportSpecifiers = (specifiersText, declarationIsTypeOnly) => specifiersText.split(",").map((specifierText) => specifierText.trim()).filter(Boolean).map((specifierText) => {
|
|
14427
|
-
const isTypeOnly = declarationIsTypeOnly || specifierText.startsWith("type ");
|
|
14428
|
-
const [rawLocalName, rawExportedName] = specifierText.split(/\s+as\s+/);
|
|
14429
|
-
const localName = getSpecifierName(rawLocalName ?? "");
|
|
14430
|
-
return {
|
|
14431
|
-
localName,
|
|
14432
|
-
exportedName: getSpecifierName(rawExportedName ?? localName),
|
|
14433
|
-
isTypeOnly
|
|
14434
|
-
};
|
|
14435
|
-
});
|
|
14436
|
-
//#endregion
|
|
14437
|
-
//#region src/plugin/utils/strip-js-comments.ts
|
|
14438
|
-
const BLOCK_COMMENT_PATTERN = /\/\*[\s\S]*?\*\//g;
|
|
14439
|
-
const LINE_COMMENT_PATTERN = /^\s*\/\/.*$/gm;
|
|
14440
|
-
const stripJsComments = (sourceText) => sourceText.replace(BLOCK_COMMENT_PATTERN, "").replace(LINE_COMMENT_PATTERN, "");
|
|
14441
|
-
//#endregion
|
|
14442
|
-
//#region src/plugin/utils/is-barrel-index-module.ts
|
|
14443
|
-
const INDEX_MODULE_FILE_PATTERN = /^index\.(?:[cm]?[jt]sx?|mjs)$/;
|
|
14444
|
-
const BINDING_IMPORT_DECLARATION_PATTERN = /^\s*import\s+(type\s+)?(?!["'])([^;]*?)\s+from\s+["']([^"']+)["']\s*;?\s*(?:(?:\/\/[^\n]*)?\s*)/gm;
|
|
14445
|
-
const BARREL_REEXPORT_DECLARATION_PATTERN = /^\s*export\s+(type\s+)?(?:\*(?:\s+as\s+([\w$]+))?|\{([\s\S]*?)\})\s+from\s+["']([^"']+)["']\s*;?\s*(?:(?:\/\/[^\n]*)?\s*)/gm;
|
|
14446
|
-
const LOCAL_EXPORT_SPECIFIER_DECLARATION_PATTERN$1 = /^\s*export\s+(type\s+)?\{([\s\S]*?)\}\s*;?\s*(?:(?:\/\/[^\n]*)?\s*)/gm;
|
|
14447
|
-
const barrelIndexModuleInfoCache = /* @__PURE__ */ new Map();
|
|
14448
|
-
const isIndexModuleFilePath = (filePath) => INDEX_MODULE_FILE_PATTERN.test(path.basename(filePath));
|
|
14449
|
-
const createNonBarrelInfo = () => ({
|
|
14450
|
-
isBarrel: false,
|
|
14451
|
-
exportsByName: /* @__PURE__ */ new Map(),
|
|
14452
|
-
starExportSources: []
|
|
14453
|
-
});
|
|
14454
|
-
const addImportedBinding = (importedBindings, binding) => {
|
|
14455
|
-
importedBindings.set(binding.localName, {
|
|
14456
|
-
...binding,
|
|
14457
|
-
didExport: false
|
|
14458
|
-
});
|
|
14459
|
-
};
|
|
14460
|
-
const collectNamedImportBindings = (namedSpecifiersText, source, declarationIsTypeOnly, importedBindings) => {
|
|
14461
|
-
for (const specifier of parseExportSpecifiers(namedSpecifiersText, declarationIsTypeOnly)) addImportedBinding(importedBindings, {
|
|
14462
|
-
localName: specifier.exportedName,
|
|
14463
|
-
importedName: specifier.localName,
|
|
14464
|
-
source,
|
|
14465
|
-
isTypeOnly: specifier.isTypeOnly
|
|
14466
|
-
});
|
|
14467
|
-
};
|
|
14468
|
-
const collectImportBindings = (importClause, source, declarationIsTypeOnly, importedBindings) => {
|
|
14469
|
-
const trimmedImportClause = importClause.trim();
|
|
14470
|
-
const namespaceMatch = trimmedImportClause.match(/(?:^|,\s*)\*\s+as\s+([\w$]+)/);
|
|
14471
|
-
if (namespaceMatch?.[1]) addImportedBinding(importedBindings, {
|
|
14472
|
-
localName: namespaceMatch[1],
|
|
14473
|
-
importedName: "*",
|
|
14474
|
-
source,
|
|
14475
|
-
isTypeOnly: declarationIsTypeOnly
|
|
14476
|
-
});
|
|
14477
|
-
const namedImportMatch = trimmedImportClause.match(/\{([\s\S]*?)\}/);
|
|
14478
|
-
if (namedImportMatch?.[1]) collectNamedImportBindings(namedImportMatch[1], source, declarationIsTypeOnly, importedBindings);
|
|
14479
|
-
const defaultImportName = trimmedImportClause.split(",")[0]?.trim();
|
|
14480
|
-
if (defaultImportName && !defaultImportName.startsWith("{") && !defaultImportName.startsWith("*")) addImportedBinding(importedBindings, {
|
|
14481
|
-
localName: defaultImportName,
|
|
14482
|
-
importedName: "default",
|
|
14483
|
-
source,
|
|
14484
|
-
isTypeOnly: declarationIsTypeOnly
|
|
14485
|
-
});
|
|
14486
|
-
};
|
|
14487
|
-
const replaceKnownDeclarations = (sourceText, importedBindings, exportsByName, starExportSources) => {
|
|
14488
|
-
let withoutKnownDeclarations = sourceText.replace(BINDING_IMPORT_DECLARATION_PATTERN, (_match, typeKeyword, importClause, source) => {
|
|
14489
|
-
collectImportBindings(importClause, source, Boolean(typeKeyword), importedBindings);
|
|
14490
|
-
return "";
|
|
14491
|
-
});
|
|
14492
|
-
withoutKnownDeclarations = withoutKnownDeclarations.replace(BARREL_REEXPORT_DECLARATION_PATTERN, (_match, typeKeyword, namespaceExportName, specifiersText, source) => {
|
|
14493
|
-
const isTypeOnly = Boolean(typeKeyword);
|
|
14494
|
-
if (namespaceExportName) {
|
|
14495
|
-
exportsByName.set(namespaceExportName, {
|
|
14496
|
-
exportedName: namespaceExportName,
|
|
14497
|
-
importedName: "*",
|
|
14498
|
-
source,
|
|
14499
|
-
isTypeOnly
|
|
14500
|
-
});
|
|
14501
|
-
return "";
|
|
14502
|
-
}
|
|
14503
|
-
if (specifiersText) {
|
|
14504
|
-
for (const specifier of parseExportSpecifiers(specifiersText, isTypeOnly)) exportsByName.set(specifier.exportedName, {
|
|
14505
|
-
exportedName: specifier.exportedName,
|
|
14506
|
-
importedName: specifier.localName,
|
|
14507
|
-
source,
|
|
14508
|
-
isTypeOnly: specifier.isTypeOnly
|
|
14509
|
-
});
|
|
14510
|
-
return "";
|
|
14511
|
-
}
|
|
14512
|
-
starExportSources.push(source);
|
|
14513
|
-
return "";
|
|
14514
|
-
});
|
|
14515
|
-
withoutKnownDeclarations = withoutKnownDeclarations.replace(LOCAL_EXPORT_SPECIFIER_DECLARATION_PATTERN$1, (_match, typeKeyword, specifiersText) => {
|
|
14516
|
-
for (const specifier of parseExportSpecifiers(specifiersText, Boolean(typeKeyword))) {
|
|
14517
|
-
const importedBinding = importedBindings.get(specifier.localName);
|
|
14518
|
-
if (!importedBinding) return _match;
|
|
14519
|
-
importedBinding.didExport = true;
|
|
14520
|
-
exportsByName.set(specifier.exportedName, {
|
|
14521
|
-
exportedName: specifier.exportedName,
|
|
14522
|
-
importedName: importedBinding.importedName,
|
|
14523
|
-
source: importedBinding.source,
|
|
14524
|
-
isTypeOnly: specifier.isTypeOnly || importedBinding.isTypeOnly
|
|
14525
|
-
});
|
|
14526
|
-
}
|
|
14527
|
-
return "";
|
|
14528
|
-
});
|
|
14529
|
-
return withoutKnownDeclarations;
|
|
14530
|
-
};
|
|
14531
|
-
const hasUnexportedRuntimeImport = (importedBindings) => {
|
|
14532
|
-
for (const binding of importedBindings.values()) if (!binding.isTypeOnly && !binding.didExport) return true;
|
|
14533
|
-
return false;
|
|
14534
|
-
};
|
|
14535
|
-
const classifyBarrelModule = (sourceText) => {
|
|
14536
|
-
const strippedSource = stripJsComments(sourceText).trim();
|
|
14537
|
-
if (!strippedSource) return createNonBarrelInfo();
|
|
14538
|
-
const importedBindings = /* @__PURE__ */ new Map();
|
|
14539
|
-
const exportsByName = /* @__PURE__ */ new Map();
|
|
14540
|
-
const starExportSources = [];
|
|
14541
|
-
if (replaceKnownDeclarations(strippedSource, importedBindings, exportsByName, starExportSources).trim() || hasUnexportedRuntimeImport(importedBindings)) return createNonBarrelInfo();
|
|
14542
|
-
return {
|
|
14543
|
-
isBarrel: exportsByName.size > 0 || starExportSources.length > 0,
|
|
14544
|
-
exportsByName,
|
|
14545
|
-
starExportSources
|
|
14546
|
-
};
|
|
14547
|
-
};
|
|
14548
|
-
const getBarrelIndexModuleInfo = (filePath) => {
|
|
14549
|
-
if (!isIndexModuleFilePath(filePath)) return createNonBarrelInfo();
|
|
14550
|
-
const cachedResult = barrelIndexModuleInfoCache.get(filePath);
|
|
14551
|
-
if (cachedResult !== void 0) return cachedResult;
|
|
14552
|
-
let moduleInfo = createNonBarrelInfo();
|
|
14553
|
-
try {
|
|
14554
|
-
moduleInfo = classifyBarrelModule(fs.readFileSync(filePath, "utf8"));
|
|
14555
|
-
} catch {
|
|
14556
|
-
moduleInfo = createNonBarrelInfo();
|
|
14557
|
-
}
|
|
14558
|
-
barrelIndexModuleInfoCache.set(filePath, moduleInfo);
|
|
14559
|
-
return moduleInfo;
|
|
14560
|
-
};
|
|
14561
|
-
const isBarrelIndexModule = (filePath) => getBarrelIndexModuleInfo(filePath).isBarrel;
|
|
14562
|
-
//#endregion
|
|
14563
|
-
//#region src/plugin/utils/does-module-export-name.ts
|
|
14564
|
-
const DEFAULT_EXPORT_DECLARATION_PATTERN = /^\s*export\s+default\b/m;
|
|
14565
|
-
const NAMED_EXPORT_DECLARATION_PATTERN = /^\s*export\s+(?:declare\s+)?(?:(?:async\s+)?function|(?:abstract\s+)?class|const|let|var|enum|interface|type)\s+([\w$]+)/gm;
|
|
14566
|
-
const LOCAL_EXPORT_SPECIFIER_DECLARATION_PATTERN = /^\s*export\s+(?:type\s+)?\{([\s\S]*?)\}(?:\s+from\s+["'][^"']+["'])?\s*;?\s*(?:(?:\/\/[^\n]*)?\s*)/gm;
|
|
14567
|
-
const doesSourceTextExportName = (sourceText, exportedName) => {
|
|
14568
|
-
const strippedSource = stripJsComments(sourceText);
|
|
14569
|
-
if (exportedName === "default" && DEFAULT_EXPORT_DECLARATION_PATTERN.test(strippedSource)) return true;
|
|
14570
|
-
for (const match of strippedSource.matchAll(NAMED_EXPORT_DECLARATION_PATTERN)) if (match[1] === exportedName) return true;
|
|
14571
|
-
for (const match of strippedSource.matchAll(LOCAL_EXPORT_SPECIFIER_DECLARATION_PATTERN)) if (parseExportSpecifiers(match[1] ?? "", false).map((specifier) => specifier.exportedName).includes(exportedName)) return true;
|
|
14572
|
-
return false;
|
|
14573
|
-
};
|
|
14574
|
-
const doesModuleExportName = (filePath, exportedName) => {
|
|
14575
|
-
try {
|
|
14576
|
-
return doesSourceTextExportName(fs.readFileSync(filePath, "utf8"), exportedName);
|
|
14577
|
-
} catch {
|
|
14578
|
-
return false;
|
|
14579
|
-
}
|
|
14580
|
-
};
|
|
14581
|
-
//#endregion
|
|
14582
|
-
//#region src/plugin/utils/resolve-relative-import-path.ts
|
|
14583
|
-
const MODULE_FILE_EXTENSIONS = [
|
|
14584
|
-
".ts",
|
|
14585
|
-
".tsx",
|
|
14586
|
-
".js",
|
|
14587
|
-
".jsx",
|
|
14588
|
-
".mjs",
|
|
14589
|
-
".cjs",
|
|
14590
|
-
".mts",
|
|
14591
|
-
".cts"
|
|
14592
|
-
];
|
|
14593
|
-
const PACKAGE_EXPORT_CONDITIONS = [
|
|
14594
|
-
"import",
|
|
14595
|
-
"default",
|
|
14596
|
-
"module",
|
|
14597
|
-
"browser",
|
|
14598
|
-
"require"
|
|
14599
|
-
];
|
|
14600
|
-
const PACKAGE_ENTRY_FIELDS = [
|
|
14601
|
-
"module",
|
|
14602
|
-
"main",
|
|
14603
|
-
"browser"
|
|
14604
|
-
];
|
|
14605
|
-
const getExistingFilePath = (filePath) => {
|
|
14606
|
-
try {
|
|
14607
|
-
return fs.statSync(filePath).isFile() ? filePath : null;
|
|
14608
|
-
} catch {
|
|
14609
|
-
return null;
|
|
14610
|
-
}
|
|
14611
|
-
};
|
|
14612
|
-
const getExistingDirectoryPath = (directoryPath) => {
|
|
14613
|
-
try {
|
|
14614
|
-
return fs.statSync(directoryPath).isDirectory() ? directoryPath : null;
|
|
14615
|
-
} catch {
|
|
14616
|
-
return null;
|
|
14617
|
-
}
|
|
14618
|
-
};
|
|
14619
|
-
const getModuleFilePathCandidates = (modulePath) => {
|
|
14620
|
-
const extension = path.extname(modulePath);
|
|
14621
|
-
if (!extension) return MODULE_FILE_EXTENSIONS.map((moduleExtension) => `${modulePath}${moduleExtension}`);
|
|
14622
|
-
const modulePathWithoutExtension = modulePath.slice(0, -extension.length);
|
|
14623
|
-
if (extension === ".js") return [
|
|
14624
|
-
modulePath,
|
|
14625
|
-
`${modulePathWithoutExtension}.ts`,
|
|
14626
|
-
`${modulePathWithoutExtension}.tsx`,
|
|
14627
|
-
`${modulePathWithoutExtension}.jsx`
|
|
14628
|
-
];
|
|
14629
|
-
if (extension === ".jsx") return [modulePath, `${modulePathWithoutExtension}.tsx`];
|
|
14630
|
-
if (extension === ".mjs") return [modulePath, `${modulePathWithoutExtension}.mts`];
|
|
14631
|
-
if (extension === ".cjs") return [modulePath, `${modulePathWithoutExtension}.cts`];
|
|
14632
|
-
return [modulePath];
|
|
14633
|
-
};
|
|
14634
|
-
const isObjectRecord = (value) => typeof value === "object" && value !== null;
|
|
14635
|
-
const getConditionalExportEntry = (exportEntry) => {
|
|
14636
|
-
if (typeof exportEntry === "string") return exportEntry;
|
|
14637
|
-
if (Array.isArray(exportEntry)) {
|
|
14638
|
-
for (const fallbackEntry of exportEntry) {
|
|
14639
|
-
const resolvedFallbackEntry = getConditionalExportEntry(fallbackEntry);
|
|
14640
|
-
if (resolvedFallbackEntry) return resolvedFallbackEntry;
|
|
14641
|
-
}
|
|
14642
|
-
return null;
|
|
14643
|
-
}
|
|
14644
|
-
if (!isObjectRecord(exportEntry)) return null;
|
|
14645
|
-
for (const condition of PACKAGE_EXPORT_CONDITIONS) {
|
|
14646
|
-
const nestedEntry = getConditionalExportEntry(exportEntry[condition]);
|
|
14647
|
-
if (nestedEntry) return nestedEntry;
|
|
14648
|
-
}
|
|
14649
|
-
return null;
|
|
14650
|
-
};
|
|
14651
|
-
const getPackageExportEntry = (packageJson) => {
|
|
14652
|
-
const exportsField = packageJson.exports;
|
|
14653
|
-
if (!exportsField) return null;
|
|
14654
|
-
const directExportEntry = getConditionalExportEntry(exportsField);
|
|
14655
|
-
if (directExportEntry) return directExportEntry;
|
|
14656
|
-
if (!isObjectRecord(exportsField)) return null;
|
|
14657
|
-
return getConditionalExportEntry(exportsField["."]);
|
|
14658
|
-
};
|
|
14659
|
-
const resolveModulePathWithIndexFallback = (modulePath) => {
|
|
14660
|
-
const filePath = resolveModuleFilePath(modulePath);
|
|
14661
|
-
if (filePath) return filePath;
|
|
14662
|
-
return resolveModuleFilePath(path.join(modulePath, "index"));
|
|
14663
|
-
};
|
|
14664
|
-
const resolvePackageDirectoryEntry = (directoryPath) => {
|
|
14665
|
-
const existingDirectoryPath = getExistingDirectoryPath(directoryPath);
|
|
14666
|
-
if (!existingDirectoryPath) return null;
|
|
14667
|
-
const packageJsonPath = path.join(existingDirectoryPath, "package.json");
|
|
14668
|
-
try {
|
|
14669
|
-
const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, "utf8"));
|
|
14670
|
-
const packageEntry = getPackageExportEntry(packageJson) ?? PACKAGE_ENTRY_FIELDS.map((fieldName) => packageJson[fieldName]).find((value) => typeof value === "string");
|
|
14671
|
-
if (!packageEntry) return null;
|
|
14672
|
-
return resolveModulePathWithIndexFallback(path.resolve(existingDirectoryPath, packageEntry));
|
|
14673
|
-
} catch {
|
|
14674
|
-
return null;
|
|
14675
|
-
}
|
|
14676
|
-
};
|
|
14677
|
-
const resolveModuleFilePath = (modulePath) => {
|
|
14678
|
-
const exactFilePath = getExistingFilePath(modulePath);
|
|
14679
|
-
if (exactFilePath) return exactFilePath;
|
|
14680
|
-
for (const candidateFilePath of getModuleFilePathCandidates(modulePath)) {
|
|
14681
|
-
const filePath = getExistingFilePath(candidateFilePath);
|
|
14682
|
-
if (filePath) return filePath;
|
|
14683
|
-
}
|
|
14684
|
-
return null;
|
|
14685
|
-
};
|
|
14686
|
-
const resolveRelativeImportPath = (filename, source) => {
|
|
14687
|
-
const importPath = path.resolve(path.dirname(filename), source);
|
|
14688
|
-
const directFilePath = resolveModuleFilePath(importPath);
|
|
14689
|
-
if (directFilePath) return directFilePath;
|
|
14690
|
-
const packageEntryFilePath = resolvePackageDirectoryEntry(importPath);
|
|
14691
|
-
if (packageEntryFilePath) return packageEntryFilePath;
|
|
14692
|
-
return resolveModuleFilePath(path.join(importPath, "index"));
|
|
14693
|
-
};
|
|
14694
|
-
//#endregion
|
|
14695
|
-
//#region src/plugin/utils/resolve-barrel-export-file-path.ts
|
|
14696
|
-
const getUniqueFilePath = (filePaths) => {
|
|
14697
|
-
const uniqueFilePaths = new Set(filePaths);
|
|
14698
|
-
if (uniqueFilePaths.size !== 1) return null;
|
|
14699
|
-
const [filePath] = uniqueFilePaths;
|
|
14700
|
-
return filePath ?? null;
|
|
14701
|
-
};
|
|
14702
|
-
const resolveStarExportFilePath = (barrelFilePath, exportedName, source, visitedFilePaths) => {
|
|
14703
|
-
const resolvedTargetPath = resolveRelativeImportPath(barrelFilePath, source);
|
|
14704
|
-
if (!resolvedTargetPath) return null;
|
|
14705
|
-
const nestedTargetPath = resolveBarrelExportFilePath(resolvedTargetPath, exportedName, new Set(visitedFilePaths));
|
|
14706
|
-
if (nestedTargetPath) return nestedTargetPath;
|
|
14707
|
-
return doesModuleExportName(resolvedTargetPath, exportedName) ? resolvedTargetPath : null;
|
|
14708
|
-
};
|
|
14709
|
-
const resolveBarrelExportFilePath = (barrelFilePath, exportedName, visitedFilePaths = /* @__PURE__ */ new Set()) => {
|
|
14710
|
-
if (visitedFilePaths.has(barrelFilePath)) return null;
|
|
14711
|
-
visitedFilePaths.add(barrelFilePath);
|
|
14712
|
-
const moduleInfo = getBarrelIndexModuleInfo(barrelFilePath);
|
|
14713
|
-
if (!moduleInfo.isBarrel) return null;
|
|
14714
|
-
const target = moduleInfo.exportsByName.get(exportedName);
|
|
14715
|
-
if (target) {
|
|
14716
|
-
const resolvedTargetPath = resolveRelativeImportPath(barrelFilePath, target.source);
|
|
14717
|
-
if (!resolvedTargetPath) return null;
|
|
14718
|
-
return resolveBarrelExportFilePath(resolvedTargetPath, target.importedName, visitedFilePaths) ?? resolvedTargetPath;
|
|
14719
|
-
}
|
|
14720
|
-
if (exportedName === "default") return null;
|
|
14721
|
-
return getUniqueFilePath(moduleInfo.starExportSources.map((source) => resolveStarExportFilePath(barrelFilePath, exportedName, source, visitedFilePaths)).filter((filePath) => Boolean(filePath)));
|
|
14722
|
-
};
|
|
14723
|
-
//#endregion
|
|
14724
15437
|
//#region src/plugin/rules/bundle-size/no-barrel-import.ts
|
|
14725
15438
|
const getLiteralName = (node) => {
|
|
14726
15439
|
if (node.type === "Identifier" && typeof node.name === "string") return node.name;
|
|
@@ -14932,7 +15645,7 @@ const noChainStateUpdates = defineRule({
|
|
|
14932
15645
|
});
|
|
14933
15646
|
//#endregion
|
|
14934
15647
|
//#region src/plugin/rules/react-builtins/no-children-prop.ts
|
|
14935
|
-
const MESSAGE$
|
|
15648
|
+
const MESSAGE$27 = "Your component can render the wrong children when you pass them through a `children` prop.";
|
|
14936
15649
|
const noChildrenProp = defineRule({
|
|
14937
15650
|
id: "no-children-prop",
|
|
14938
15651
|
title: "Children passed as a prop",
|
|
@@ -14944,7 +15657,7 @@ const noChildrenProp = defineRule({
|
|
|
14944
15657
|
if (node.name.name !== "children") return;
|
|
14945
15658
|
context.report({
|
|
14946
15659
|
node: node.name,
|
|
14947
|
-
message: MESSAGE$
|
|
15660
|
+
message: MESSAGE$27
|
|
14948
15661
|
});
|
|
14949
15662
|
},
|
|
14950
15663
|
CallExpression(node) {
|
|
@@ -14957,7 +15670,7 @@ const noChildrenProp = defineRule({
|
|
|
14957
15670
|
const propertyKey = property.key;
|
|
14958
15671
|
if (isNodeOfType(propertyKey, "Identifier") && propertyKey.name === "children" || isNodeOfType(propertyKey, "Literal") && propertyKey.value === "children") context.report({
|
|
14959
15672
|
node: propertyKey,
|
|
14960
|
-
message: MESSAGE$
|
|
15673
|
+
message: MESSAGE$27
|
|
14961
15674
|
});
|
|
14962
15675
|
}
|
|
14963
15676
|
}
|
|
@@ -14965,7 +15678,7 @@ const noChildrenProp = defineRule({
|
|
|
14965
15678
|
});
|
|
14966
15679
|
//#endregion
|
|
14967
15680
|
//#region src/plugin/rules/react-builtins/no-clone-element.ts
|
|
14968
|
-
const MESSAGE$
|
|
15681
|
+
const MESSAGE$26 = "`React.cloneElement` breaks easily when the cloned element's props change.";
|
|
14969
15682
|
const noCloneElement = defineRule({
|
|
14970
15683
|
id: "no-clone-element",
|
|
14971
15684
|
title: "Use of cloneElement",
|
|
@@ -14978,7 +15691,7 @@ const noCloneElement = defineRule({
|
|
|
14978
15691
|
if (isNodeOfType(callee, "Identifier") && callee.name === "cloneElement") {
|
|
14979
15692
|
if (isImportedFromModule(node, "cloneElement", "react")) context.report({
|
|
14980
15693
|
node: callee,
|
|
14981
|
-
message: MESSAGE$
|
|
15694
|
+
message: MESSAGE$26
|
|
14982
15695
|
});
|
|
14983
15696
|
return;
|
|
14984
15697
|
}
|
|
@@ -14991,7 +15704,7 @@ const noCloneElement = defineRule({
|
|
|
14991
15704
|
if (!isImportedFromModule(node, callee.object.name, "react")) return;
|
|
14992
15705
|
context.report({
|
|
14993
15706
|
node: callee,
|
|
14994
|
-
message: MESSAGE$
|
|
15707
|
+
message: MESSAGE$26
|
|
14995
15708
|
});
|
|
14996
15709
|
}
|
|
14997
15710
|
} })
|
|
@@ -15040,7 +15753,7 @@ const enclosingComponentOrHookName = (node) => {
|
|
|
15040
15753
|
};
|
|
15041
15754
|
//#endregion
|
|
15042
15755
|
//#region src/plugin/rules/state-and-effects/no-create-context-in-render.ts
|
|
15043
|
-
const MESSAGE$
|
|
15756
|
+
const MESSAGE$25 = "createContext() builds a new context every render, so every consumer gets cut off & resets.";
|
|
15044
15757
|
const CONTEXT_MODULES = [
|
|
15045
15758
|
"react",
|
|
15046
15759
|
"use-context-selector",
|
|
@@ -15076,7 +15789,7 @@ const noCreateContextInRender = defineRule({
|
|
|
15076
15789
|
if (!componentOrHookName) return;
|
|
15077
15790
|
context.report({
|
|
15078
15791
|
node,
|
|
15079
|
-
message: `${MESSAGE$
|
|
15792
|
+
message: `${MESSAGE$25} (called inside "${componentOrHookName}")`
|
|
15080
15793
|
});
|
|
15081
15794
|
} })
|
|
15082
15795
|
});
|
|
@@ -15216,7 +15929,7 @@ const noCreateStoreInRender = defineRule({
|
|
|
15216
15929
|
});
|
|
15217
15930
|
//#endregion
|
|
15218
15931
|
//#region src/plugin/rules/react-builtins/no-danger.ts
|
|
15219
|
-
const MESSAGE$
|
|
15932
|
+
const MESSAGE$24 = "`dangerouslySetInnerHTML` is an XSS hole that runs attacker-controlled HTML in your users' browsers.";
|
|
15220
15933
|
const noDanger = defineRule({
|
|
15221
15934
|
id: "no-danger",
|
|
15222
15935
|
title: "Use of dangerouslySetInnerHTML",
|
|
@@ -15229,7 +15942,7 @@ const noDanger = defineRule({
|
|
|
15229
15942
|
if (!propAttribute) return;
|
|
15230
15943
|
context.report({
|
|
15231
15944
|
node: propAttribute.name,
|
|
15232
|
-
message: MESSAGE$
|
|
15945
|
+
message: MESSAGE$24
|
|
15233
15946
|
});
|
|
15234
15947
|
},
|
|
15235
15948
|
CallExpression(node) {
|
|
@@ -15241,7 +15954,7 @@ const noDanger = defineRule({
|
|
|
15241
15954
|
const propertyKey = property.key;
|
|
15242
15955
|
if (isNodeOfType(propertyKey, "Identifier") && propertyKey.name === "dangerouslySetInnerHTML" || isNodeOfType(propertyKey, "Literal") && propertyKey.value === "dangerouslySetInnerHTML") context.report({
|
|
15243
15956
|
node: propertyKey,
|
|
15244
|
-
message: MESSAGE$
|
|
15957
|
+
message: MESSAGE$24
|
|
15245
15958
|
});
|
|
15246
15959
|
}
|
|
15247
15960
|
}
|
|
@@ -15249,7 +15962,7 @@ const noDanger = defineRule({
|
|
|
15249
15962
|
});
|
|
15250
15963
|
//#endregion
|
|
15251
15964
|
//#region src/plugin/rules/react-builtins/no-danger-with-children.ts
|
|
15252
|
-
const MESSAGE$
|
|
15965
|
+
const MESSAGE$23 = "React throws an error when you set both children & `dangerouslySetInnerHTML`.";
|
|
15253
15966
|
const isLineBreak = (child) => {
|
|
15254
15967
|
if (!isNodeOfType(child, "JSXText")) return false;
|
|
15255
15968
|
return child.value.trim().length === 0 && child.value.includes("\n");
|
|
@@ -15319,7 +16032,7 @@ const noDangerWithChildren = defineRule({
|
|
|
15319
16032
|
if (!hasChildrenProp && !hasNestedChildren) return;
|
|
15320
16033
|
if (hasJsxPropIgnoreCase(opening.attributes, "dangerouslySetInnerHTML") || spreadPropsShape.hasDangerously) context.report({
|
|
15321
16034
|
node: opening,
|
|
15322
|
-
message: MESSAGE$
|
|
16035
|
+
message: MESSAGE$23
|
|
15323
16036
|
});
|
|
15324
16037
|
},
|
|
15325
16038
|
CallExpression(node) {
|
|
@@ -15331,7 +16044,7 @@ const noDangerWithChildren = defineRule({
|
|
|
15331
16044
|
if (!propsShape.hasDangerously) return;
|
|
15332
16045
|
if (node.arguments.length >= 3 || propsShape.hasChildren) context.report({
|
|
15333
16046
|
node,
|
|
15334
|
-
message: MESSAGE$
|
|
16047
|
+
message: MESSAGE$23
|
|
15335
16048
|
});
|
|
15336
16049
|
}
|
|
15337
16050
|
})
|
|
@@ -15909,7 +16622,7 @@ const isSetStateCallInLifecycle = (setStateCall, lifecycleNames, options = {}) =
|
|
|
15909
16622
|
//#endregion
|
|
15910
16623
|
//#region src/plugin/rules/react-builtins/no-did-mount-set-state.ts
|
|
15911
16624
|
const LIFECYCLE_NAMES$2 = new Set(["componentDidMount"]);
|
|
15912
|
-
const MESSAGE$
|
|
16625
|
+
const MESSAGE$22 = "Your users see an extra render right after mount when you call `setState` in `componentDidMount`.";
|
|
15913
16626
|
const resolveSettings$20 = (settings) => {
|
|
15914
16627
|
const reactDoctor = settings?.["react-doctor"];
|
|
15915
16628
|
return { mode: (typeof reactDoctor === "object" && reactDoctor !== null ? reactDoctor.noDidMountSetState ?? {} : {}).mode ?? "allowed" };
|
|
@@ -15928,7 +16641,7 @@ const noDidMountSetState = defineRule({
|
|
|
15928
16641
|
if (!isSetStateCallInLifecycle(node, LIFECYCLE_NAMES$2, { disallowInNestedFunctions: mode === "disallow-in-func" })) return;
|
|
15929
16642
|
context.report({
|
|
15930
16643
|
node: node.callee,
|
|
15931
|
-
message: MESSAGE$
|
|
16644
|
+
message: MESSAGE$22
|
|
15932
16645
|
});
|
|
15933
16646
|
} };
|
|
15934
16647
|
}
|
|
@@ -15936,7 +16649,7 @@ const noDidMountSetState = defineRule({
|
|
|
15936
16649
|
//#endregion
|
|
15937
16650
|
//#region src/plugin/rules/react-builtins/no-did-update-set-state.ts
|
|
15938
16651
|
const LIFECYCLE_NAMES$1 = new Set(["componentDidUpdate"]);
|
|
15939
|
-
const MESSAGE$
|
|
16652
|
+
const MESSAGE$21 = "This can loop forever & freeze the component.";
|
|
15940
16653
|
const resolveSettings$19 = (settings) => {
|
|
15941
16654
|
const reactDoctor = settings?.["react-doctor"];
|
|
15942
16655
|
return { mode: (typeof reactDoctor === "object" && reactDoctor !== null ? reactDoctor.noDidUpdateSetState ?? {} : {}).mode ?? "allowed" };
|
|
@@ -15955,7 +16668,7 @@ const noDidUpdateSetState = defineRule({
|
|
|
15955
16668
|
if (!isSetStateCallInLifecycle(node, LIFECYCLE_NAMES$1, { disallowInNestedFunctions: mode === "disallow-in-func" })) return;
|
|
15956
16669
|
context.report({
|
|
15957
16670
|
node: node.callee,
|
|
15958
|
-
message: MESSAGE$
|
|
16671
|
+
message: MESSAGE$21
|
|
15959
16672
|
});
|
|
15960
16673
|
} };
|
|
15961
16674
|
}
|
|
@@ -15978,7 +16691,7 @@ const isStateMemberExpression = (node) => {
|
|
|
15978
16691
|
};
|
|
15979
16692
|
//#endregion
|
|
15980
16693
|
//#region src/plugin/rules/react-builtins/no-direct-mutation-state.ts
|
|
15981
|
-
const MESSAGE$
|
|
16694
|
+
const MESSAGE$20 = "Your users see stale data because mutating `this.state` by hand never redraws & gets overwritten.";
|
|
15982
16695
|
const shouldIgnoreMutation = (node) => {
|
|
15983
16696
|
let isConstructor = false;
|
|
15984
16697
|
let isInsideCallExpression = false;
|
|
@@ -16000,7 +16713,7 @@ const reportIfStateMutation = (context, reportNode, target) => {
|
|
|
16000
16713
|
if (shouldIgnoreMutation(reportNode)) return;
|
|
16001
16714
|
context.report({
|
|
16002
16715
|
node: reportNode,
|
|
16003
|
-
message: MESSAGE$
|
|
16716
|
+
message: MESSAGE$20
|
|
16004
16717
|
});
|
|
16005
16718
|
};
|
|
16006
16719
|
const noDirectMutationState = defineRule({
|
|
@@ -17588,7 +18301,7 @@ const ALLOWED_NAMESPACES = new Set([
|
|
|
17588
18301
|
"ReactDOM",
|
|
17589
18302
|
"ReactDom"
|
|
17590
18303
|
]);
|
|
17591
|
-
const MESSAGE$
|
|
18304
|
+
const MESSAGE$19 = "`findDOMNode` crashes your app in React 19 because it was removed.";
|
|
17592
18305
|
const noFindDomNode = defineRule({
|
|
17593
18306
|
id: "no-find-dom-node",
|
|
17594
18307
|
title: "Use of findDOMNode",
|
|
@@ -17599,7 +18312,7 @@ const noFindDomNode = defineRule({
|
|
|
17599
18312
|
if (isNodeOfType(callee, "Identifier") && callee.name === "findDOMNode") {
|
|
17600
18313
|
context.report({
|
|
17601
18314
|
node: callee,
|
|
17602
|
-
message: MESSAGE$
|
|
18315
|
+
message: MESSAGE$19
|
|
17603
18316
|
});
|
|
17604
18317
|
return;
|
|
17605
18318
|
}
|
|
@@ -17610,7 +18323,7 @@ const noFindDomNode = defineRule({
|
|
|
17610
18323
|
if (callee.property.name !== "findDOMNode") return;
|
|
17611
18324
|
context.report({
|
|
17612
18325
|
node: callee.property,
|
|
17613
|
-
message: MESSAGE$
|
|
18326
|
+
message: MESSAGE$19
|
|
17614
18327
|
});
|
|
17615
18328
|
}
|
|
17616
18329
|
} })
|
|
@@ -18139,6 +18852,51 @@ const noIsMounted = defineRule({
|
|
|
18139
18852
|
} })
|
|
18140
18853
|
});
|
|
18141
18854
|
//#endregion
|
|
18855
|
+
//#region src/plugin/rules/correctness/no-jsx-element-type.ts
|
|
18856
|
+
const MESSAGE$18 = "`JSX.Element` is too narrow: it excludes `null`, strings, numbers, and fragments that components commonly return. Use `React.ReactNode` instead.";
|
|
18857
|
+
const isJsxElementTypeReference = (node) => {
|
|
18858
|
+
if (!isNodeOfType(node, "TSTypeReference")) return false;
|
|
18859
|
+
const typeName = node.typeName;
|
|
18860
|
+
if (!isNodeOfType(typeName, "TSQualifiedName")) return false;
|
|
18861
|
+
return isNodeOfType(typeName.left, "Identifier") && typeName.left.name === "JSX" && isNodeOfType(typeName.right, "Identifier") && typeName.right.name === "Element";
|
|
18862
|
+
};
|
|
18863
|
+
const extractReturnTypeAnnotation = (returnType) => {
|
|
18864
|
+
if (!returnType) return null;
|
|
18865
|
+
if (!isNodeOfType(returnType, "TSTypeAnnotation")) return null;
|
|
18866
|
+
return returnType.typeAnnotation ?? null;
|
|
18867
|
+
};
|
|
18868
|
+
const checkReturnType = (context, returnType) => {
|
|
18869
|
+
const typeAnnotation = extractReturnTypeAnnotation(returnType);
|
|
18870
|
+
if (!typeAnnotation) return;
|
|
18871
|
+
if (isJsxElementTypeReference(typeAnnotation)) context.report({
|
|
18872
|
+
node: typeAnnotation,
|
|
18873
|
+
message: MESSAGE$18
|
|
18874
|
+
});
|
|
18875
|
+
};
|
|
18876
|
+
const noJsxElementType = defineRule({
|
|
18877
|
+
id: "no-jsx-element-type",
|
|
18878
|
+
title: "No JSX.Element",
|
|
18879
|
+
severity: "error",
|
|
18880
|
+
recommendation: "Replace `JSX.Element` with `React.ReactNode`. `JSX.Element` is too narrow: it excludes `null`, strings, numbers, and fragments that components commonly return.",
|
|
18881
|
+
create: (context) => ({
|
|
18882
|
+
FunctionDeclaration(node) {
|
|
18883
|
+
checkReturnType(context, node.returnType);
|
|
18884
|
+
},
|
|
18885
|
+
ArrowFunctionExpression(node) {
|
|
18886
|
+
checkReturnType(context, node.returnType);
|
|
18887
|
+
},
|
|
18888
|
+
FunctionExpression(node) {
|
|
18889
|
+
checkReturnType(context, node.returnType);
|
|
18890
|
+
},
|
|
18891
|
+
TSDeclareFunction(node) {
|
|
18892
|
+
checkReturnType(context, node.returnType);
|
|
18893
|
+
},
|
|
18894
|
+
TSMethodSignature(node) {
|
|
18895
|
+
checkReturnType(context, node.returnType);
|
|
18896
|
+
}
|
|
18897
|
+
})
|
|
18898
|
+
});
|
|
18899
|
+
//#endregion
|
|
18142
18900
|
//#region src/plugin/rules/design/no-justified-text.ts
|
|
18143
18901
|
const noJustifiedText = defineRule({
|
|
18144
18902
|
id: "no-justified-text",
|
|
@@ -18410,11 +19168,14 @@ const noLongTransitionDuration = defineRule({
|
|
|
18410
19168
|
} })
|
|
18411
19169
|
});
|
|
18412
19170
|
//#endregion
|
|
19171
|
+
//#region src/plugin/utils/is-boolean-prefixed-prop-name.ts
|
|
19172
|
+
const BOOLEAN_PROP_PREFIX_PATTERN = /^(?:is|has|should|can|show|hide|enable|disable|with)[A-Z]/;
|
|
19173
|
+
const isBooleanPrefixedPropName = (propName) => BOOLEAN_PROP_PREFIX_PATTERN.test(propName);
|
|
19174
|
+
//#endregion
|
|
18413
19175
|
//#region src/plugin/utils/is-component-declaration.ts
|
|
18414
19176
|
const isComponentDeclaration = (node) => isNodeOfType(node, "FunctionDeclaration") && node.id !== null && Boolean(node.id?.name) && isUppercaseName(node.id.name);
|
|
18415
19177
|
//#endregion
|
|
18416
19178
|
//#region src/plugin/rules/architecture/no-many-boolean-props.ts
|
|
18417
|
-
const BOOLEAN_PROP_PREFIX_PATTERN = /^(?:is|has|should|can|show|hide|enable|disable|with)[A-Z]/;
|
|
18418
19179
|
const collectBooleanLikePropsFromBody = (componentBody, propsParamName) => {
|
|
18419
19180
|
const found = /* @__PURE__ */ new Set();
|
|
18420
19181
|
if (!componentBody) return found;
|
|
@@ -18424,7 +19185,7 @@ const collectBooleanLikePropsFromBody = (componentBody, propsParamName) => {
|
|
|
18424
19185
|
if (!isNodeOfType(child.object, "Identifier")) return;
|
|
18425
19186
|
if (child.object.name !== propsParamName) return;
|
|
18426
19187
|
if (!isNodeOfType(child.property, "Identifier")) return;
|
|
18427
|
-
if (!
|
|
19188
|
+
if (!isBooleanPrefixedPropName(child.property.name)) return;
|
|
18428
19189
|
found.add(child.property.name);
|
|
18429
19190
|
});
|
|
18430
19191
|
return found;
|
|
@@ -18450,7 +19211,7 @@ const noManyBooleanProps = defineRule({
|
|
|
18450
19211
|
if (!isNodeOfType(property, "Property")) continue;
|
|
18451
19212
|
const keyName = isNodeOfType(property.key, "Identifier") ? property.key.name : null;
|
|
18452
19213
|
if (!keyName) continue;
|
|
18453
|
-
if (
|
|
19214
|
+
if (isBooleanPrefixedPropName(keyName)) booleanLikePropNames.push(keyName);
|
|
18454
19215
|
}
|
|
18455
19216
|
reportIfMany(booleanLikePropNames, componentName, reportNode);
|
|
18456
19217
|
return;
|
|
@@ -19020,224 +19781,7 @@ const isLodashMutatorCall = (callExpression) => {
|
|
|
19020
19781
|
return false;
|
|
19021
19782
|
};
|
|
19022
19783
|
//#endregion
|
|
19023
|
-
//#region src/plugin/utils/find-exported-function-body.ts
|
|
19024
|
-
const isFunctionLike = (node) => {
|
|
19025
|
-
if (!node) return false;
|
|
19026
|
-
return isNodeOfType(node, "FunctionDeclaration") || isNodeOfType(node, "FunctionExpression") || isNodeOfType(node, "ArrowFunctionExpression");
|
|
19027
|
-
};
|
|
19028
|
-
const findExportedFunctionBody = (programRoot, exportedName) => {
|
|
19029
|
-
if (!isNodeOfType(programRoot, "Program")) return null;
|
|
19030
|
-
const localBindings = /* @__PURE__ */ new Map();
|
|
19031
|
-
const namedExports = /* @__PURE__ */ new Map();
|
|
19032
|
-
let defaultExport = null;
|
|
19033
|
-
let defaultExportIdentifierName = null;
|
|
19034
|
-
const recordVariableDeclaration = (declaration) => {
|
|
19035
|
-
for (const declarator of declaration.declarations ?? []) {
|
|
19036
|
-
if (!isNodeOfType(declarator, "VariableDeclarator")) continue;
|
|
19037
|
-
if (!isNodeOfType(declarator.id, "Identifier")) continue;
|
|
19038
|
-
const initializer = declarator.init ? stripParenExpression(declarator.init) : null;
|
|
19039
|
-
if (initializer && isFunctionLike(initializer)) localBindings.set(declarator.id.name, initializer);
|
|
19040
|
-
}
|
|
19041
|
-
};
|
|
19042
|
-
for (const statement of programRoot.body ?? []) {
|
|
19043
|
-
if (isNodeOfType(statement, "VariableDeclaration")) {
|
|
19044
|
-
recordVariableDeclaration(statement);
|
|
19045
|
-
continue;
|
|
19046
|
-
}
|
|
19047
|
-
if (isNodeOfType(statement, "FunctionDeclaration") && statement.id) {
|
|
19048
|
-
localBindings.set(statement.id.name, statement);
|
|
19049
|
-
continue;
|
|
19050
|
-
}
|
|
19051
|
-
if (isNodeOfType(statement, "ExportNamedDeclaration")) {
|
|
19052
|
-
const declaration = statement.declaration;
|
|
19053
|
-
if (declaration && isNodeOfType(declaration, "VariableDeclaration")) {
|
|
19054
|
-
recordVariableDeclaration(declaration);
|
|
19055
|
-
for (const declarator of declaration.declarations ?? []) {
|
|
19056
|
-
if (!isNodeOfType(declarator, "VariableDeclarator")) continue;
|
|
19057
|
-
if (!isNodeOfType(declarator.id, "Identifier")) continue;
|
|
19058
|
-
namedExports.set(declarator.id.name, declarator.id.name);
|
|
19059
|
-
}
|
|
19060
|
-
} else if (declaration && isNodeOfType(declaration, "FunctionDeclaration") && declaration.id) {
|
|
19061
|
-
localBindings.set(declaration.id.name, declaration);
|
|
19062
|
-
namedExports.set(declaration.id.name, declaration.id.name);
|
|
19063
|
-
}
|
|
19064
|
-
for (const specifier of statement.specifiers ?? []) {
|
|
19065
|
-
if (!isNodeOfType(specifier, "ExportSpecifier")) continue;
|
|
19066
|
-
const local = specifier.local;
|
|
19067
|
-
const exported = specifier.exported;
|
|
19068
|
-
if (!isNodeOfType(local, "Identifier")) continue;
|
|
19069
|
-
const exportedNameSpec = isNodeOfType(exported, "Identifier") ? exported.name : isNodeOfType(exported, "Literal") && typeof exported.value === "string" ? exported.value : null;
|
|
19070
|
-
if (!exportedNameSpec) continue;
|
|
19071
|
-
namedExports.set(exportedNameSpec, local.name);
|
|
19072
|
-
}
|
|
19073
|
-
continue;
|
|
19074
|
-
}
|
|
19075
|
-
if (isNodeOfType(statement, "ExportDefaultDeclaration")) {
|
|
19076
|
-
const declaration = statement.declaration;
|
|
19077
|
-
if (!declaration) continue;
|
|
19078
|
-
if (isNodeOfType(declaration, "FunctionDeclaration") && declaration.id) {
|
|
19079
|
-
localBindings.set(declaration.id.name, declaration);
|
|
19080
|
-
defaultExport = declaration;
|
|
19081
|
-
continue;
|
|
19082
|
-
}
|
|
19083
|
-
if (isFunctionLike(declaration)) {
|
|
19084
|
-
defaultExport = declaration;
|
|
19085
|
-
continue;
|
|
19086
|
-
}
|
|
19087
|
-
if (isNodeOfType(declaration, "Identifier")) {
|
|
19088
|
-
defaultExportIdentifierName = declaration.name;
|
|
19089
|
-
continue;
|
|
19090
|
-
}
|
|
19091
|
-
}
|
|
19092
|
-
}
|
|
19093
|
-
if (exportedName === "default") {
|
|
19094
|
-
if (defaultExport) return defaultExport;
|
|
19095
|
-
if (defaultExportIdentifierName) {
|
|
19096
|
-
const binding = localBindings.get(defaultExportIdentifierName);
|
|
19097
|
-
if (binding) return binding;
|
|
19098
|
-
}
|
|
19099
|
-
}
|
|
19100
|
-
const localName = namedExports.get(exportedName);
|
|
19101
|
-
if (!localName) return null;
|
|
19102
|
-
return localBindings.get(localName) ?? null;
|
|
19103
|
-
};
|
|
19104
|
-
const resolveImportedExportName = (importSpecifier) => {
|
|
19105
|
-
if (isNodeOfType(importSpecifier, "ImportSpecifier")) {
|
|
19106
|
-
const imported = importSpecifier.imported;
|
|
19107
|
-
if (isNodeOfType(imported, "Identifier")) return imported.name;
|
|
19108
|
-
if (isNodeOfType(imported, "Literal") && typeof imported.value === "string") return imported.value;
|
|
19109
|
-
return null;
|
|
19110
|
-
}
|
|
19111
|
-
if (isNodeOfType(importSpecifier, "ImportDefaultSpecifier")) return "default";
|
|
19112
|
-
return null;
|
|
19113
|
-
};
|
|
19114
|
-
const findReExportSourcesForName = (programRoot, exportedName) => {
|
|
19115
|
-
if (!isNodeOfType(programRoot, "Program")) return [];
|
|
19116
|
-
const exportAllSources = [];
|
|
19117
|
-
for (const statement of programRoot.body ?? []) {
|
|
19118
|
-
if (isNodeOfType(statement, "ExportNamedDeclaration") && statement.source) {
|
|
19119
|
-
const sourceValue = statement.source.value;
|
|
19120
|
-
if (typeof sourceValue !== "string") continue;
|
|
19121
|
-
for (const specifier of statement.specifiers ?? []) {
|
|
19122
|
-
if (!isNodeOfType(specifier, "ExportSpecifier")) continue;
|
|
19123
|
-
const exported = specifier.exported;
|
|
19124
|
-
if ((isNodeOfType(exported, "Identifier") ? exported.name : isNodeOfType(exported, "Literal") && typeof exported.value === "string" ? exported.value : null) === exportedName) return [sourceValue];
|
|
19125
|
-
}
|
|
19126
|
-
}
|
|
19127
|
-
if (isNodeOfType(statement, "ExportAllDeclaration") && statement.source) {
|
|
19128
|
-
const sourceValue = statement.source.value;
|
|
19129
|
-
if (typeof sourceValue === "string") exportAllSources.push(sourceValue);
|
|
19130
|
-
}
|
|
19131
|
-
}
|
|
19132
|
-
return exportAllSources;
|
|
19133
|
-
};
|
|
19134
|
-
//#endregion
|
|
19135
|
-
//#region src/plugin/utils/attach-parent-references.ts
|
|
19136
|
-
const attachParentReferences = (root) => {
|
|
19137
|
-
const visit = (node, parent) => {
|
|
19138
|
-
const writableNode = node;
|
|
19139
|
-
writableNode.parent = parent;
|
|
19140
|
-
const nodeRecord = node;
|
|
19141
|
-
for (const key of Object.keys(nodeRecord)) {
|
|
19142
|
-
if (key === "parent") continue;
|
|
19143
|
-
const child = nodeRecord[key];
|
|
19144
|
-
if (Array.isArray(child)) {
|
|
19145
|
-
for (const item of child) if (isAstNode(item)) visit(item, node);
|
|
19146
|
-
} else if (isAstNode(child)) visit(child, node);
|
|
19147
|
-
}
|
|
19148
|
-
};
|
|
19149
|
-
visit(root, null);
|
|
19150
|
-
};
|
|
19151
|
-
//#endregion
|
|
19152
|
-
//#region src/plugin/utils/parse-source-file.ts
|
|
19153
|
-
const FILENAME_TO_LANG = {
|
|
19154
|
-
".ts": "ts",
|
|
19155
|
-
".tsx": "tsx",
|
|
19156
|
-
".js": "js",
|
|
19157
|
-
".jsx": "jsx",
|
|
19158
|
-
".mjs": "js",
|
|
19159
|
-
".cjs": "js",
|
|
19160
|
-
".mts": "ts",
|
|
19161
|
-
".cts": "ts"
|
|
19162
|
-
};
|
|
19163
|
-
const resolveLang = (filename) => {
|
|
19164
|
-
return FILENAME_TO_LANG[path.extname(filename).toLowerCase()] ?? "tsx";
|
|
19165
|
-
};
|
|
19166
|
-
const parseCache = /* @__PURE__ */ new Map();
|
|
19167
|
-
const parseSourceFile = (absoluteFilePath) => {
|
|
19168
|
-
let fileStat;
|
|
19169
|
-
try {
|
|
19170
|
-
fileStat = fs.statSync(absoluteFilePath);
|
|
19171
|
-
} catch {
|
|
19172
|
-
return null;
|
|
19173
|
-
}
|
|
19174
|
-
if (!fileStat.isFile()) return null;
|
|
19175
|
-
if (fileStat.size > 2e6) return null;
|
|
19176
|
-
const cached = parseCache.get(absoluteFilePath);
|
|
19177
|
-
if (cached && cached.mtimeMs === fileStat.mtimeMs && cached.size === fileStat.size) return cached.program;
|
|
19178
|
-
if (absoluteFilePath.endsWith(".d.ts") || absoluteFilePath.endsWith(".d.mts") || absoluteFilePath.endsWith(".d.cts")) {
|
|
19179
|
-
parseCache.set(absoluteFilePath, {
|
|
19180
|
-
mtimeMs: fileStat.mtimeMs,
|
|
19181
|
-
size: fileStat.size,
|
|
19182
|
-
program: null
|
|
19183
|
-
});
|
|
19184
|
-
return null;
|
|
19185
|
-
}
|
|
19186
|
-
let sourceText;
|
|
19187
|
-
try {
|
|
19188
|
-
sourceText = fs.readFileSync(absoluteFilePath, "utf8");
|
|
19189
|
-
} catch {
|
|
19190
|
-
parseCache.set(absoluteFilePath, {
|
|
19191
|
-
mtimeMs: fileStat.mtimeMs,
|
|
19192
|
-
size: fileStat.size,
|
|
19193
|
-
program: null
|
|
19194
|
-
});
|
|
19195
|
-
return null;
|
|
19196
|
-
}
|
|
19197
|
-
let parsedProgram = null;
|
|
19198
|
-
try {
|
|
19199
|
-
const result = parseSync(absoluteFilePath, sourceText, {
|
|
19200
|
-
astType: "ts",
|
|
19201
|
-
lang: resolveLang(absoluteFilePath)
|
|
19202
|
-
});
|
|
19203
|
-
if (!result.errors.some((parseError) => parseError.severity === "Error")) {
|
|
19204
|
-
parsedProgram = result.program;
|
|
19205
|
-
attachParentReferences(parsedProgram);
|
|
19206
|
-
}
|
|
19207
|
-
} catch {
|
|
19208
|
-
parsedProgram = null;
|
|
19209
|
-
}
|
|
19210
|
-
parseCache.set(absoluteFilePath, {
|
|
19211
|
-
mtimeMs: fileStat.mtimeMs,
|
|
19212
|
-
size: fileStat.size,
|
|
19213
|
-
program: parsedProgram
|
|
19214
|
-
});
|
|
19215
|
-
return parsedProgram;
|
|
19216
|
-
};
|
|
19217
|
-
//#endregion
|
|
19218
19784
|
//#region src/plugin/rules/state-and-effects/utils/resolve-reducer-function.ts
|
|
19219
|
-
const resolveFunctionExportInFile = (filePath, exportedName, visitedFilePaths) => {
|
|
19220
|
-
if (visitedFilePaths.size >= 4) return null;
|
|
19221
|
-
if (visitedFilePaths.has(filePath)) return null;
|
|
19222
|
-
visitedFilePaths.add(filePath);
|
|
19223
|
-
const actualFilePath = resolveBarrelExportFilePath(filePath, exportedName) ?? filePath;
|
|
19224
|
-
const programRoot = parseSourceFile(actualFilePath);
|
|
19225
|
-
if (!programRoot) return null;
|
|
19226
|
-
const exported = findExportedFunctionBody(programRoot, exportedName);
|
|
19227
|
-
if (exported) return exported;
|
|
19228
|
-
for (const reExportSource of findReExportSourcesForName(programRoot, exportedName)) {
|
|
19229
|
-
const nextFilePath = resolveRelativeImportPath(actualFilePath, reExportSource);
|
|
19230
|
-
if (!nextFilePath) continue;
|
|
19231
|
-
const resolved = resolveFunctionExportInFile(nextFilePath, exportedName, visitedFilePaths);
|
|
19232
|
-
if (resolved) return resolved;
|
|
19233
|
-
}
|
|
19234
|
-
return null;
|
|
19235
|
-
};
|
|
19236
|
-
const resolveCrossFileFunctionExport = (fromFilename, source, exportedName) => {
|
|
19237
|
-
const resolvedFilePath = resolveRelativeImportPath(fromFilename, source);
|
|
19238
|
-
if (!resolvedFilePath) return null;
|
|
19239
|
-
return resolveFunctionExportInFile(resolvedFilePath, exportedName, /* @__PURE__ */ new Set());
|
|
19240
|
-
};
|
|
19241
19785
|
const resolveReducerFunction = (node, currentFilename) => {
|
|
19242
19786
|
if (!node) return null;
|
|
19243
19787
|
const unwrappedNode = stripParenExpression(node);
|
|
@@ -19259,7 +19803,6 @@ const resolveReducerFunction = (node, currentFilename) => {
|
|
|
19259
19803
|
if (!importDeclaration || !isNodeOfType(importDeclaration, "ImportDeclaration")) return null;
|
|
19260
19804
|
const sourceValue = importDeclaration.source?.value;
|
|
19261
19805
|
if (typeof sourceValue !== "string") return null;
|
|
19262
|
-
if (!sourceValue.startsWith(".") && !sourceValue.startsWith("/")) return null;
|
|
19263
19806
|
const exportedName = resolveImportedExportName(initializer);
|
|
19264
19807
|
if (!exportedName) return null;
|
|
19265
19808
|
const crossFileFunction = resolveCrossFileFunctionExport(currentFilename, sourceValue, exportedName);
|
|
@@ -23259,6 +23802,7 @@ const noUnknownProperty = defineRule({
|
|
|
23259
23802
|
create: (context) => {
|
|
23260
23803
|
const { ignore = [], requireDataLowercase = false } = resolveSettings$10(context.settings);
|
|
23261
23804
|
const ignoreSet = new Set(ignore);
|
|
23805
|
+
if (isNextjsMetadataImageRouteFilename(context.filename)) ignoreSet.add("tw");
|
|
23262
23806
|
let fileIsNonReactJsx = false;
|
|
23263
23807
|
return {
|
|
23264
23808
|
Program(node) {
|
|
@@ -23953,6 +24497,8 @@ const ENTRY_POINT_BASENAMES = new Set([
|
|
|
23953
24497
|
"global-error.jsx",
|
|
23954
24498
|
"route.tsx",
|
|
23955
24499
|
"route.jsx",
|
|
24500
|
+
"_layout.tsx",
|
|
24501
|
+
"_layout.jsx",
|
|
23956
24502
|
"_app.tsx",
|
|
23957
24503
|
"_app.jsx",
|
|
23958
24504
|
"_document.tsx",
|
|
@@ -24661,6 +25207,128 @@ const preferEs6Class = defineRule({
|
|
|
24661
25207
|
}
|
|
24662
25208
|
});
|
|
24663
25209
|
//#endregion
|
|
25210
|
+
//#region src/plugin/utils/is-jsx-element-or-fragment.ts
|
|
25211
|
+
/**
|
|
25212
|
+
* Type-guard for the two single-node JSX output forms: `JSXElement`
|
|
25213
|
+
* (`<Foo />`) and `JSXFragment` (`<>…</>`). Canonical home for the
|
|
25214
|
+
* `isNodeOfType(x, "JSXElement") || isNodeOfType(x, "JSXFragment")` check
|
|
25215
|
+
* that many rules otherwise inline. Does NOT unwrap parens / TS wrappers —
|
|
25216
|
+
* callers that need the semantic expression should `stripParenExpression`
|
|
25217
|
+
* first.
|
|
25218
|
+
*/
|
|
25219
|
+
const isJsxElementOrFragment = (node) => Boolean(node && (isNodeOfType(node, "JSXElement") || isNodeOfType(node, "JSXFragment")));
|
|
25220
|
+
//#endregion
|
|
25221
|
+
//#region src/plugin/rules/architecture/prefer-explicit-variants.ts
|
|
25222
|
+
const resolveBooleanPropTestName = (testNode, booleanPropBindings) => {
|
|
25223
|
+
let identifierNode = stripParenExpression(testNode);
|
|
25224
|
+
if (isNodeOfType(identifierNode, "UnaryExpression") && identifierNode.operator === "!") identifierNode = stripParenExpression(identifierNode.argument);
|
|
25225
|
+
if (!isNodeOfType(identifierNode, "Identifier")) return null;
|
|
25226
|
+
return booleanPropBindings.has(identifierNode.name) ? identifierNode.name : null;
|
|
25227
|
+
};
|
|
25228
|
+
const CROSS_CUTTING_STATE_BOOLEAN_NAMES = new Set([
|
|
25229
|
+
"isLoading",
|
|
25230
|
+
"isPending",
|
|
25231
|
+
"isFetching",
|
|
25232
|
+
"isRefetching",
|
|
25233
|
+
"isSubmitting",
|
|
25234
|
+
"isError",
|
|
25235
|
+
"isSuccess",
|
|
25236
|
+
"isEmpty",
|
|
25237
|
+
"isReady",
|
|
25238
|
+
"isDirty",
|
|
25239
|
+
"isValid",
|
|
25240
|
+
"isInvalid",
|
|
25241
|
+
"isOpen",
|
|
25242
|
+
"isClosed",
|
|
25243
|
+
"isVisible",
|
|
25244
|
+
"isHidden",
|
|
25245
|
+
"isActive",
|
|
25246
|
+
"isInactive",
|
|
25247
|
+
"isExpanded",
|
|
25248
|
+
"isCollapsed",
|
|
25249
|
+
"isSelected",
|
|
25250
|
+
"isChecked",
|
|
25251
|
+
"isDisabled",
|
|
25252
|
+
"isEnabled",
|
|
25253
|
+
"isFocused",
|
|
25254
|
+
"isHovered",
|
|
25255
|
+
"isDragging",
|
|
25256
|
+
"isFullscreen",
|
|
25257
|
+
"isMobile",
|
|
25258
|
+
"isDesktop",
|
|
25259
|
+
"isTablet",
|
|
25260
|
+
"isOnline",
|
|
25261
|
+
"isOffline",
|
|
25262
|
+
"isLoggedIn",
|
|
25263
|
+
"isAuthenticated",
|
|
25264
|
+
"isAuthorized",
|
|
25265
|
+
"isDark",
|
|
25266
|
+
"isLight"
|
|
25267
|
+
]);
|
|
25268
|
+
const collectBooleanPropBindings = (param) => {
|
|
25269
|
+
const bindings = /* @__PURE__ */ new Set();
|
|
25270
|
+
if (!param || !isNodeOfType(param, "ObjectPattern")) return bindings;
|
|
25271
|
+
for (const property of param.properties ?? []) {
|
|
25272
|
+
if (!isNodeOfType(property, "Property")) continue;
|
|
25273
|
+
if (property.computed) continue;
|
|
25274
|
+
if (!isNodeOfType(property.key, "Identifier")) continue;
|
|
25275
|
+
if (!isBooleanPrefixedPropName(property.key.name)) continue;
|
|
25276
|
+
if (CROSS_CUTTING_STATE_BOOLEAN_NAMES.has(property.key.name)) continue;
|
|
25277
|
+
const propertyValue = property.value;
|
|
25278
|
+
if (isNodeOfType(propertyValue, "Identifier")) bindings.add(propertyValue.name);
|
|
25279
|
+
else if (isNodeOfType(propertyValue, "AssignmentPattern") && isNodeOfType(propertyValue.left, "Identifier")) bindings.add(propertyValue.left.name);
|
|
25280
|
+
}
|
|
25281
|
+
return bindings;
|
|
25282
|
+
};
|
|
25283
|
+
const collectVariantBranchProps = (body, booleanPropBindings) => {
|
|
25284
|
+
const variantBranchProps = /* @__PURE__ */ new Set();
|
|
25285
|
+
if (!body) return variantBranchProps;
|
|
25286
|
+
walkAst(body, (current) => {
|
|
25287
|
+
if (isNodeOfType(current, "FunctionDeclaration") || isInlineFunctionExpression(current)) return false;
|
|
25288
|
+
if (!isNodeOfType(current, "ConditionalExpression")) return;
|
|
25289
|
+
const propName = resolveBooleanPropTestName(current.test, booleanPropBindings);
|
|
25290
|
+
if (!propName) return;
|
|
25291
|
+
const consequent = stripParenExpression(current.consequent);
|
|
25292
|
+
const alternate = stripParenExpression(current.alternate);
|
|
25293
|
+
if (!isJsxElementOrFragment(consequent) || !isJsxElementOrFragment(alternate)) return;
|
|
25294
|
+
variantBranchProps.add(propName);
|
|
25295
|
+
});
|
|
25296
|
+
return variantBranchProps;
|
|
25297
|
+
};
|
|
25298
|
+
const preferExplicitVariants = defineRule({
|
|
25299
|
+
id: "prefer-explicit-variants",
|
|
25300
|
+
title: "Prefer explicit variant components",
|
|
25301
|
+
severity: "warn",
|
|
25302
|
+
tags: ["test-noise", "react-jsx-only"],
|
|
25303
|
+
recommendation: "Replace boolean props that switch whole subtrees with explicit variant components, like `<ThreadComposer />` and `<EditMessageComposer />`, so each variant renders one clear path.",
|
|
25304
|
+
create: (context) => {
|
|
25305
|
+
const checkComponent = (param, body, componentName, reportNode) => {
|
|
25306
|
+
const booleanPropBindings = collectBooleanPropBindings(param);
|
|
25307
|
+
if (booleanPropBindings.size < 2) return;
|
|
25308
|
+
const variantBranchProps = collectVariantBranchProps(body, booleanPropBindings);
|
|
25309
|
+
if (variantBranchProps.size < 2) return;
|
|
25310
|
+
const propList = [...variantBranchProps].slice(0, 3).join(", ");
|
|
25311
|
+
const overflow = variantBranchProps.size > 3 ? "…" : "";
|
|
25312
|
+
context.report({
|
|
25313
|
+
node: reportNode,
|
|
25314
|
+
message: `Component "${componentName}" picks which component to render from ${variantBranchProps.size} boolean props (${propList}${overflow}), which multiplies untestable variants. Split it into explicit variant components so each renders one clear path.`
|
|
25315
|
+
});
|
|
25316
|
+
};
|
|
25317
|
+
return {
|
|
25318
|
+
FunctionDeclaration(node) {
|
|
25319
|
+
if (!isComponentDeclaration(node) || !node.id) return;
|
|
25320
|
+
checkComponent(node.params?.[0], node.body, node.id.name, node.id);
|
|
25321
|
+
},
|
|
25322
|
+
VariableDeclarator(node) {
|
|
25323
|
+
if (!isComponentAssignment(node)) return;
|
|
25324
|
+
if (!isNodeOfType(node.id, "Identifier")) return;
|
|
25325
|
+
if (!isInlineFunctionExpression(node.init)) return;
|
|
25326
|
+
checkComponent(node.init.params?.[0], node.init.body, node.id.name, node.id);
|
|
25327
|
+
}
|
|
25328
|
+
};
|
|
25329
|
+
}
|
|
25330
|
+
});
|
|
25331
|
+
//#endregion
|
|
24664
25332
|
//#region src/plugin/rules/react-builtins/prefer-function-component.ts
|
|
24665
25333
|
const MESSAGE$7 = "This class component is harder to maintain than a function component.";
|
|
24666
25334
|
const resolveSettings$4 = (settings) => {
|
|
@@ -25375,6 +26043,26 @@ const preferUseReducer = defineRule({
|
|
|
25375
26043
|
}
|
|
25376
26044
|
});
|
|
25377
26045
|
//#endregion
|
|
26046
|
+
//#region src/plugin/rules/tanstack-query/query-destructure-result.ts
|
|
26047
|
+
const queryDestructureResult = defineRule({
|
|
26048
|
+
id: "query-destructure-result",
|
|
26049
|
+
title: "Destructure TanStack Query result",
|
|
26050
|
+
tags: ["test-noise"],
|
|
26051
|
+
requires: ["tanstack-query"],
|
|
26052
|
+
severity: "error",
|
|
26053
|
+
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.",
|
|
26054
|
+
create: (context) => ({ VariableDeclarator(node) {
|
|
26055
|
+
if (!isNodeOfType(node.id, "Identifier")) return;
|
|
26056
|
+
if (!node.init || !isNodeOfType(node.init, "CallExpression")) return;
|
|
26057
|
+
const calleeName = isNodeOfType(node.init.callee, "Identifier") ? node.init.callee.name : null;
|
|
26058
|
+
if (!calleeName || !TANSTACK_QUERY_HOOKS.has(calleeName)) return;
|
|
26059
|
+
context.report({
|
|
26060
|
+
node: node.id,
|
|
26061
|
+
message: `Destructure ${calleeName}() results instead of assigning the whole query object, so TanStack Query only subscribes to the fields you use.`
|
|
26062
|
+
});
|
|
26063
|
+
} })
|
|
26064
|
+
});
|
|
26065
|
+
//#endregion
|
|
25378
26066
|
//#region src/plugin/rules/tanstack-query/query-mutation-missing-invalidation.ts
|
|
25379
26067
|
const queryMutationMissingInvalidation = defineRule({
|
|
25380
26068
|
id: "query-mutation-missing-invalidation",
|
|
@@ -32884,6 +33572,7 @@ const serverFetchWithoutRevalidate = defineRule({
|
|
|
32884
33572
|
id: "server-fetch-without-revalidate",
|
|
32885
33573
|
title: "Fetch without revalidate",
|
|
32886
33574
|
severity: "warn",
|
|
33575
|
+
disabledBy: ["nextjs:15"],
|
|
32887
33576
|
recommendation: "Pass `{ next: { revalidate: <seconds> } }` (or `cache: \"no-store\"`) so old data doesn't stick around.",
|
|
32888
33577
|
create: (context) => {
|
|
32889
33578
|
let isServerSideFile = false;
|
|
@@ -32917,15 +33606,6 @@ const serverFetchWithoutRevalidate = defineRule({
|
|
|
32917
33606
|
});
|
|
32918
33607
|
//#endregion
|
|
32919
33608
|
//#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
33609
|
const STATIC_IO_FUNCTIONS = new Set([
|
|
32930
33610
|
"readFileSync",
|
|
32931
33611
|
"readFile",
|
|
@@ -35440,6 +36120,28 @@ const reactDoctorRules = [
|
|
|
35440
36120
|
category: "Bugs"
|
|
35441
36121
|
}
|
|
35442
36122
|
},
|
|
36123
|
+
{
|
|
36124
|
+
key: "react-doctor/nextjs-error-boundary-missing-use-client",
|
|
36125
|
+
id: "nextjs-error-boundary-missing-use-client",
|
|
36126
|
+
source: "react-doctor",
|
|
36127
|
+
originallyExternal: false,
|
|
36128
|
+
rule: {
|
|
36129
|
+
...nextjsErrorBoundaryMissingUseClient,
|
|
36130
|
+
framework: "nextjs",
|
|
36131
|
+
category: "Bugs"
|
|
36132
|
+
}
|
|
36133
|
+
},
|
|
36134
|
+
{
|
|
36135
|
+
key: "react-doctor/nextjs-global-error-missing-html-body",
|
|
36136
|
+
id: "nextjs-global-error-missing-html-body",
|
|
36137
|
+
source: "react-doctor",
|
|
36138
|
+
originallyExternal: false,
|
|
36139
|
+
rule: {
|
|
36140
|
+
...nextjsGlobalErrorMissingHtmlBody,
|
|
36141
|
+
framework: "nextjs",
|
|
36142
|
+
category: "Bugs"
|
|
36143
|
+
}
|
|
36144
|
+
},
|
|
35443
36145
|
{
|
|
35444
36146
|
key: "react-doctor/nextjs-image-missing-sizes",
|
|
35445
36147
|
id: "nextjs-image-missing-sizes",
|
|
@@ -35517,6 +36219,28 @@ const reactDoctorRules = [
|
|
|
35517
36219
|
category: "Bugs"
|
|
35518
36220
|
}
|
|
35519
36221
|
},
|
|
36222
|
+
{
|
|
36223
|
+
key: "react-doctor/nextjs-no-default-export-in-route-handler",
|
|
36224
|
+
id: "nextjs-no-default-export-in-route-handler",
|
|
36225
|
+
source: "react-doctor",
|
|
36226
|
+
originallyExternal: false,
|
|
36227
|
+
rule: {
|
|
36228
|
+
...nextjsNoDefaultExportInRouteHandler,
|
|
36229
|
+
framework: "nextjs",
|
|
36230
|
+
category: "Bugs"
|
|
36231
|
+
}
|
|
36232
|
+
},
|
|
36233
|
+
{
|
|
36234
|
+
key: "react-doctor/nextjs-no-edge-og-runtime",
|
|
36235
|
+
id: "nextjs-no-edge-og-runtime",
|
|
36236
|
+
source: "react-doctor",
|
|
36237
|
+
originallyExternal: false,
|
|
36238
|
+
rule: {
|
|
36239
|
+
...nextjsNoEdgeOgRuntime,
|
|
36240
|
+
framework: "nextjs",
|
|
36241
|
+
category: "Bugs"
|
|
36242
|
+
}
|
|
36243
|
+
},
|
|
35520
36244
|
{
|
|
35521
36245
|
key: "react-doctor/nextjs-no-font-link",
|
|
35522
36246
|
id: "nextjs-no-font-link",
|
|
@@ -35528,6 +36252,17 @@ const reactDoctorRules = [
|
|
|
35528
36252
|
category: "Bugs"
|
|
35529
36253
|
}
|
|
35530
36254
|
},
|
|
36255
|
+
{
|
|
36256
|
+
key: "react-doctor/nextjs-no-google-analytics-script",
|
|
36257
|
+
id: "nextjs-no-google-analytics-script",
|
|
36258
|
+
source: "react-doctor",
|
|
36259
|
+
originallyExternal: false,
|
|
36260
|
+
rule: {
|
|
36261
|
+
...nextjsNoGoogleAnalyticsScript,
|
|
36262
|
+
framework: "nextjs",
|
|
36263
|
+
category: "Bugs"
|
|
36264
|
+
}
|
|
36265
|
+
},
|
|
35531
36266
|
{
|
|
35532
36267
|
key: "react-doctor/nextjs-no-head-import",
|
|
35533
36268
|
id: "nextjs-no-head-import",
|
|
@@ -35583,6 +36318,17 @@ const reactDoctorRules = [
|
|
|
35583
36318
|
category: "Bugs"
|
|
35584
36319
|
}
|
|
35585
36320
|
},
|
|
36321
|
+
{
|
|
36322
|
+
key: "react-doctor/nextjs-no-script-in-head",
|
|
36323
|
+
id: "nextjs-no-script-in-head",
|
|
36324
|
+
source: "react-doctor",
|
|
36325
|
+
originallyExternal: false,
|
|
36326
|
+
rule: {
|
|
36327
|
+
...nextjsNoScriptInHead,
|
|
36328
|
+
framework: "nextjs",
|
|
36329
|
+
category: "Bugs"
|
|
36330
|
+
}
|
|
36331
|
+
},
|
|
35586
36332
|
{
|
|
35587
36333
|
key: "react-doctor/nextjs-no-side-effect-in-get-handler",
|
|
35588
36334
|
id: "nextjs-no-side-effect-in-get-handler",
|
|
@@ -35605,6 +36351,17 @@ const reactDoctorRules = [
|
|
|
35605
36351
|
category: "Bugs"
|
|
35606
36352
|
}
|
|
35607
36353
|
},
|
|
36354
|
+
{
|
|
36355
|
+
key: "react-doctor/nextjs-no-vercel-og-import",
|
|
36356
|
+
id: "nextjs-no-vercel-og-import",
|
|
36357
|
+
source: "react-doctor",
|
|
36358
|
+
originallyExternal: false,
|
|
36359
|
+
rule: {
|
|
36360
|
+
...nextjsNoVercelOgImport,
|
|
36361
|
+
framework: "nextjs",
|
|
36362
|
+
category: "Bugs"
|
|
36363
|
+
}
|
|
36364
|
+
},
|
|
35608
36365
|
{
|
|
35609
36366
|
key: "react-doctor/no-access-key",
|
|
35610
36367
|
id: "no-access-key",
|
|
@@ -36155,6 +36912,17 @@ const reactDoctorRules = [
|
|
|
36155
36912
|
category: "Bugs"
|
|
36156
36913
|
}
|
|
36157
36914
|
},
|
|
36915
|
+
{
|
|
36916
|
+
key: "react-doctor/no-jsx-element-type",
|
|
36917
|
+
id: "no-jsx-element-type",
|
|
36918
|
+
source: "react-doctor",
|
|
36919
|
+
originallyExternal: false,
|
|
36920
|
+
rule: {
|
|
36921
|
+
...noJsxElementType,
|
|
36922
|
+
framework: "global",
|
|
36923
|
+
category: "Bugs"
|
|
36924
|
+
}
|
|
36925
|
+
},
|
|
36158
36926
|
{
|
|
36159
36927
|
key: "react-doctor/no-justified-text",
|
|
36160
36928
|
id: "no-justified-text",
|
|
@@ -36881,6 +37649,17 @@ const reactDoctorRules = [
|
|
|
36881
37649
|
category: "Maintainability"
|
|
36882
37650
|
}
|
|
36883
37651
|
},
|
|
37652
|
+
{
|
|
37653
|
+
key: "react-doctor/prefer-explicit-variants",
|
|
37654
|
+
id: "prefer-explicit-variants",
|
|
37655
|
+
source: "react-doctor",
|
|
37656
|
+
originallyExternal: false,
|
|
37657
|
+
rule: {
|
|
37658
|
+
...preferExplicitVariants,
|
|
37659
|
+
framework: "global",
|
|
37660
|
+
category: "Maintainability"
|
|
37661
|
+
}
|
|
37662
|
+
},
|
|
36884
37663
|
{
|
|
36885
37664
|
key: "react-doctor/prefer-function-component",
|
|
36886
37665
|
id: "prefer-function-component",
|
|
@@ -36980,6 +37759,17 @@ const reactDoctorRules = [
|
|
|
36980
37759
|
category: "Bugs"
|
|
36981
37760
|
}
|
|
36982
37761
|
},
|
|
37762
|
+
{
|
|
37763
|
+
key: "react-doctor/query-destructure-result",
|
|
37764
|
+
id: "query-destructure-result",
|
|
37765
|
+
source: "react-doctor",
|
|
37766
|
+
originallyExternal: false,
|
|
37767
|
+
rule: {
|
|
37768
|
+
...queryDestructureResult,
|
|
37769
|
+
framework: "tanstack-query",
|
|
37770
|
+
category: "Bugs"
|
|
37771
|
+
}
|
|
37772
|
+
},
|
|
36983
37773
|
{
|
|
36984
37774
|
key: "react-doctor/query-mutation-missing-invalidation",
|
|
36985
37775
|
id: "query-mutation-missing-invalidation",
|