oxlint-plugin-react-doctor 0.7.2-dev.aa519e5 → 0.7.2-dev.b97a92f
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 +2350 -1563
- package/dist/index.js +73 -27
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -13508,7 +13508,7 @@ const jsTosortedImmutable = defineRule({
|
|
|
13508
13508
|
title: "Spread copy before sort()",
|
|
13509
13509
|
tags: ["test-noise"],
|
|
13510
13510
|
severity: "warn",
|
|
13511
|
-
|
|
13511
|
+
disabledWhen: ["react-native", "pre-es2023"],
|
|
13512
13512
|
recommendation: "Use `array.toSorted()` (ES2023) instead of `[...array].sort()` so you sort without copying the array first",
|
|
13513
13513
|
create: (context) => ({ CallExpression(node) {
|
|
13514
13514
|
if (!isMemberProperty(node.callee, "sort")) return;
|
|
@@ -14755,7 +14755,7 @@ const jsxNoConstructedContextValues = defineRule({
|
|
|
14755
14755
|
title: "Unstable context provider value",
|
|
14756
14756
|
tags: ["react-jsx-only"],
|
|
14757
14757
|
severity: "warn",
|
|
14758
|
-
|
|
14758
|
+
disabledWhen: ["react-compiler"],
|
|
14759
14759
|
recommendation: "Wrap the context value in `useMemo` or move it outside the component so consumers do not redraw every render.",
|
|
14760
14760
|
category: "Performance",
|
|
14761
14761
|
create: (context) => {
|
|
@@ -15123,7 +15123,7 @@ const jsxNoJsxAsProp = defineRule({
|
|
|
15123
15123
|
title: "JSX element passed as a prop",
|
|
15124
15124
|
tags: ["react-jsx-only"],
|
|
15125
15125
|
severity: "warn",
|
|
15126
|
-
|
|
15126
|
+
disabledWhen: ["react-compiler"],
|
|
15127
15127
|
recommendation: "Move the JSX outside the component or wrap it in `useMemo` so memoized children do not redraw every render.",
|
|
15128
15128
|
category: "Performance",
|
|
15129
15129
|
create: (context) => {
|
|
@@ -15511,7 +15511,7 @@ const jsxNoNewArrayAsProp = defineRule({
|
|
|
15511
15511
|
title: "New array passed as a prop",
|
|
15512
15512
|
tags: ["react-jsx-only"],
|
|
15513
15513
|
severity: "warn",
|
|
15514
|
-
|
|
15514
|
+
disabledWhen: ["react-compiler"],
|
|
15515
15515
|
recommendation: "Wrap the array in `useMemo` or move it outside the component so memoized children do not redraw every render.",
|
|
15516
15516
|
category: "Performance",
|
|
15517
15517
|
create: (context) => {
|
|
@@ -15975,7 +15975,7 @@ const jsxNoNewFunctionAsProp = defineRule({
|
|
|
15975
15975
|
title: "New function passed as a prop",
|
|
15976
15976
|
tags: ["react-jsx-only"],
|
|
15977
15977
|
severity: "warn",
|
|
15978
|
-
|
|
15978
|
+
disabledWhen: ["react-compiler"],
|
|
15979
15979
|
recommendation: "Wrap the callback in `useCallback` or move it outside the component so memoized children do not redraw every render.",
|
|
15980
15980
|
category: "Performance",
|
|
15981
15981
|
create: (context) => {
|
|
@@ -16282,7 +16282,7 @@ const jsxNoNewObjectAsProp = defineRule({
|
|
|
16282
16282
|
title: "New object passed as a prop",
|
|
16283
16283
|
tags: ["react-jsx-only"],
|
|
16284
16284
|
severity: "warn",
|
|
16285
|
-
|
|
16285
|
+
disabledWhen: ["react-compiler"],
|
|
16286
16286
|
recommendation: "Wrap the object in `useMemo` or move it outside the component so memoized children do not redraw every render.",
|
|
16287
16287
|
category: "Performance",
|
|
16288
16288
|
create: (context) => {
|
|
@@ -18106,6 +18106,7 @@ const getReactDoctorStringArraySetting = (settings, settingName) => {
|
|
|
18106
18106
|
if (!Array.isArray(settingValue)) return [];
|
|
18107
18107
|
return settingValue.filter((entry) => typeof entry === "string" && entry.length > 0);
|
|
18108
18108
|
};
|
|
18109
|
+
const hasCapability = (settings, capability) => getReactDoctorStringArraySetting(settings, "capabilities").includes(capability);
|
|
18109
18110
|
//#endregion
|
|
18110
18111
|
//#region src/plugin/utils/is-in-project-directory.ts
|
|
18111
18112
|
const isInProjectDirectory = (context, directoryPath) => {
|
|
@@ -18560,6 +18561,7 @@ const describeClientSideNavigation = (node) => {
|
|
|
18560
18561
|
}
|
|
18561
18562
|
return null;
|
|
18562
18563
|
};
|
|
18564
|
+
const STATIC_EXPORT_RECOMMENDATION = "Avoid redirects inside useEffect — they flash the wrong page first. Use an event handler (e.g. onClick), or call redirect() from next/navigation during render (it prerenders a client-side redirect under output: \"export\"). Middleware and getServerSideProps redirects aren't available in a static export.";
|
|
18563
18565
|
const nextjsNoClientSideRedirect = defineRule({
|
|
18564
18566
|
id: "nextjs-no-client-side-redirect",
|
|
18565
18567
|
title: "Client-side redirect for navigation",
|
|
@@ -18567,6 +18569,7 @@ const nextjsNoClientSideRedirect = defineRule({
|
|
|
18567
18569
|
requires: ["nextjs"],
|
|
18568
18570
|
severity: "warn",
|
|
18569
18571
|
recommendation: "Avoid redirects inside useEffect. Use an event handler, middleware, or server-side redirect (App Router: redirect() from next/navigation; Pages Router: getServerSideProps redirect)",
|
|
18572
|
+
recommendationFor: (hasCapability) => hasCapability("nextjs:static-export") ? STATIC_EXPORT_RECOMMENDATION : void 0,
|
|
18570
18573
|
create: (context) => {
|
|
18571
18574
|
return { CallExpression(node) {
|
|
18572
18575
|
if (!isHookCall$2(node, EFFECT_HOOK_NAMES$1)) return;
|
|
@@ -28928,7 +28931,7 @@ const noJsonParseStringifyClone = defineRule({
|
|
|
28928
28931
|
id: "no-json-parse-stringify-clone",
|
|
28929
28932
|
title: "JSON parse/stringify deep clone",
|
|
28930
28933
|
severity: "warn",
|
|
28931
|
-
|
|
28934
|
+
disabledWhen: ["react-native"],
|
|
28932
28935
|
recommendation: "Replace `JSON.parse(JSON.stringify(value))` with `structuredClone(value)`. It is faster and preserves Dates, Maps, Sets, and cyclic references.",
|
|
28933
28936
|
create: (context) => ({ CallExpression(node) {
|
|
28934
28937
|
if (!isJsonMethodCall(node, "parse")) return;
|
|
@@ -31752,12 +31755,8 @@ const noPolymorphicChildren = defineRule({
|
|
|
31752
31755
|
//#endregion
|
|
31753
31756
|
//#region src/plugin/rules/correctness/no-prevent-default.ts
|
|
31754
31757
|
const PREVENT_DEFAULT_ELEMENTS = new Map([["form", ["onSubmit"]], ["a", ["onClick"]]]);
|
|
31755
|
-
const SERVER_CAPABLE_FRAMEWORKS = new Set([
|
|
31756
|
-
"nextjs",
|
|
31757
|
-
"tanstack-start",
|
|
31758
|
-
"remix"
|
|
31759
|
-
]);
|
|
31760
31758
|
const FORM_MESSAGE_SERVER_CAPABLE = "Your users can't submit this <form> without JavaScript because onSubmit calls preventDefault(), so use a server action like `<form action={serverAction}>` to make it work either way.";
|
|
31759
|
+
const FORM_MESSAGE_GENERIC = "Your users can't submit this <form> because onSubmit calls preventDefault().";
|
|
31761
31760
|
const ANCHOR_MESSAGE = "Your users click this <a> & nothing navigates because onClick calls preventDefault(), so use a <button> or a routing component instead.";
|
|
31762
31761
|
const collectPreventDefaultCalls = (node) => {
|
|
31763
31762
|
const preventDefaultCalls = [];
|
|
@@ -31894,15 +31893,18 @@ const noPreventDefault = defineRule({
|
|
|
31894
31893
|
recommendation: "Use `<form action>` where your framework supports it (it works without JS), or use a `<button>` instead of an `<a>` with preventDefault.",
|
|
31895
31894
|
create: (context) => {
|
|
31896
31895
|
const framework = getReactDoctorStringSetting(context.settings, "framework");
|
|
31897
|
-
const
|
|
31896
|
+
const isClientOnlyFramework = hasCapability(context.settings, "client-only");
|
|
31897
|
+
const isServerActionsFramework = hasCapability(context.settings, "server-actions");
|
|
31898
|
+
const formMessage = isServerActionsFramework ? FORM_MESSAGE_SERVER_CAPABLE : FORM_MESSAGE_GENERIC;
|
|
31898
31899
|
return { JSXOpeningElement(node) {
|
|
31899
31900
|
const elementName = isNodeOfType(node.name, "JSXIdentifier") ? node.name.name : null;
|
|
31900
31901
|
if (!elementName) return;
|
|
31901
31902
|
const targetEventProps = PREVENT_DEFAULT_ELEMENTS.get(elementName);
|
|
31902
31903
|
if (!targetEventProps) return;
|
|
31903
31904
|
if (elementName === "form") {
|
|
31904
|
-
if (
|
|
31905
|
-
if (framework === "
|
|
31905
|
+
if (isClientOnlyFramework) return;
|
|
31906
|
+
if (framework === void 0 || framework === "unknown") return;
|
|
31907
|
+
if (framework === "nextjs" && isServerActionsFramework) {
|
|
31906
31908
|
const programRoot = findProgramRoot(node);
|
|
31907
31909
|
if (!programRoot || !hasDirective(programRoot, "use client")) return;
|
|
31908
31910
|
}
|
|
@@ -31926,7 +31928,7 @@ const noPreventDefault = defineRule({
|
|
|
31926
31928
|
}
|
|
31927
31929
|
context.report({
|
|
31928
31930
|
node,
|
|
31929
|
-
message: elementName === "form" ?
|
|
31931
|
+
message: elementName === "form" ? formMessage : ANCHOR_MESSAGE
|
|
31930
31932
|
});
|
|
31931
31933
|
return;
|
|
31932
31934
|
}
|
|
@@ -33196,11 +33198,41 @@ const isIdentifierLikeKeyNameValue = (literalValue) => {
|
|
|
33196
33198
|
if (wordSegments.length < 2) return false;
|
|
33197
33199
|
return wordSegments.every((segment) => /^[a-z]+$/.test(segment));
|
|
33198
33200
|
};
|
|
33201
|
+
const FRAMEWORK_ENV_ADVICE = [
|
|
33202
|
+
[
|
|
33203
|
+
"nextjs",
|
|
33204
|
+
"Next.js",
|
|
33205
|
+
"NEXT_PUBLIC_*"
|
|
33206
|
+
],
|
|
33207
|
+
[
|
|
33208
|
+
"vite",
|
|
33209
|
+
"Vite",
|
|
33210
|
+
"VITE_*"
|
|
33211
|
+
],
|
|
33212
|
+
[
|
|
33213
|
+
"tanstack-start",
|
|
33214
|
+
"TanStack Start",
|
|
33215
|
+
"VITE_*"
|
|
33216
|
+
],
|
|
33217
|
+
[
|
|
33218
|
+
"cra",
|
|
33219
|
+
"Create React App",
|
|
33220
|
+
"REACT_APP_*"
|
|
33221
|
+
],
|
|
33222
|
+
[
|
|
33223
|
+
"gatsby",
|
|
33224
|
+
"Gatsby",
|
|
33225
|
+
"GATSBY_*"
|
|
33226
|
+
]
|
|
33227
|
+
];
|
|
33199
33228
|
const noSecretsInClientCode = defineRule({
|
|
33200
33229
|
id: "no-secrets-in-client-code",
|
|
33201
33230
|
title: "Secret in client code",
|
|
33202
33231
|
severity: "warn",
|
|
33203
33232
|
recommendation: "Move secrets to server-only code. Anything in client env variables gets shipped to the browser, so it can't hold secrets.",
|
|
33233
|
+
recommendationFor: (hasCapability) => {
|
|
33234
|
+
for (const [frameworkToken, frameworkName, publicEnvPrefix] of FRAMEWORK_ENV_ADVICE) if (hasCapability(frameworkToken)) return `Move secrets to server-only code. In ${frameworkName}, only \`${publicEnvPrefix}\` env vars are exposed to the browser, and they must not contain secrets`;
|
|
33235
|
+
},
|
|
33204
33236
|
create: (context) => {
|
|
33205
33237
|
const filename = normalizeFilename(context.filename ?? "");
|
|
33206
33238
|
const framework = getReactDoctorStringSetting(context.settings, "framework");
|
|
@@ -33238,7 +33270,7 @@ const noSecretsInClientCode = defineRule({
|
|
|
33238
33270
|
const isServerOnlyScope = isInsideServerOnlyScope(node);
|
|
33239
33271
|
const trailingSuffix = getIdentifierTrailingWord(variableName);
|
|
33240
33272
|
const isUiConstant = SECRET_FALSE_POSITIVE_SUFFIXES.has(trailingSuffix);
|
|
33241
|
-
if (shouldUseVariableNameHeuristic && !isServerOnlyScope && SECRET_VARIABLE_PATTERN.test(variableName) && !isUiConstant && !isPublicUrlValue(literalValue) && !isPlaceholderValueForVariableHeuristic && !isSelfReferentialSentinelValue(variableName, literalValue) && !isIdentifierLikeKeyNameValue(literalValue) && literalValue.length > 24) {
|
|
33273
|
+
if (shouldUseVariableNameHeuristic && !isServerOnlyScope && SECRET_VARIABLE_PATTERN.test(variableName) && !isUiConstant && !isPublicUrlValue(literalValue) && !isPlaceholderValueForVariableHeuristic && !isSelfReferentialSentinelValue(variableName, literalValue) && !isIdentifierLikeKeyNameValue(literalValue) && !isSelfReferentialSentinelValue(variableName, literalValue) && !isIdentifierLikeKeyNameValue(literalValue) && literalValue.length > 24) {
|
|
33242
33274
|
context.report({
|
|
33243
33275
|
node,
|
|
33244
33276
|
message: `Hardcoding "${variableName}" in client code is a security vulnerability: the secret ships to the browser where anyone can read it.`
|
|
@@ -37962,7 +37994,7 @@ const preferModuleScopeStaticValue = defineRule({
|
|
|
37962
37994
|
tags: ["test-noise"],
|
|
37963
37995
|
severity: "warn",
|
|
37964
37996
|
category: "Architecture",
|
|
37965
|
-
|
|
37997
|
+
disabledWhen: ["react-compiler"],
|
|
37966
37998
|
recommendation: "Move the value above the component, at the top of the file. It doesn't use local state, so rebuilding it each update is wasted and makes it look new every time.",
|
|
37967
37999
|
create: (context) => ({ VariableDeclarator(node) {
|
|
37968
38000
|
if (!isNodeOfType(node.id, "Identifier")) return;
|
|
@@ -38032,7 +38064,7 @@ const preferStableEmptyFallback = defineRule({
|
|
|
38032
38064
|
tags: ["react-jsx-only", "test-noise"],
|
|
38033
38065
|
severity: "warn",
|
|
38034
38066
|
category: "Performance",
|
|
38035
|
-
|
|
38067
|
+
disabledWhen: ["react-compiler"],
|
|
38036
38068
|
recommendation: "Make a `const EMPTY = []` (or `{}`) at module scope and use it as the `||` / `??` fallback, so the value stays the same each render.",
|
|
38037
38069
|
create: (context) => {
|
|
38038
38070
|
let memoRegistry = null;
|
|
@@ -39361,7 +39393,7 @@ const reduxUseselectorInlineDerivation = defineRule({
|
|
|
39361
39393
|
title: "useSelector derives data inline",
|
|
39362
39394
|
severity: "warn",
|
|
39363
39395
|
category: "Performance",
|
|
39364
|
-
|
|
39396
|
+
disabledWhen: ["react-compiler"],
|
|
39365
39397
|
recommendation: "Select the raw slice and memoize derivation so Redux actions do not rebuild a collection and redraw this component.",
|
|
39366
39398
|
create: (context) => {
|
|
39367
39399
|
let aliases = /* @__PURE__ */ new Set();
|
|
@@ -39421,7 +39453,7 @@ const reduxUseselectorReturnsNewCollection = defineRule({
|
|
|
39421
39453
|
title: "useSelector returns a new collection",
|
|
39422
39454
|
severity: "warn",
|
|
39423
39455
|
category: "Performance",
|
|
39424
|
-
|
|
39456
|
+
disabledWhen: ["react-compiler"],
|
|
39425
39457
|
recommendation: "Return a stable selected value, split selectors, or pass `shallowEqual` so every Redux action does not redraw this component.",
|
|
39426
39458
|
create: (context) => {
|
|
39427
39459
|
let aliases = /* @__PURE__ */ new Set();
|
|
@@ -39721,7 +39753,7 @@ const renderingHydrationMismatchTime = defineRule({
|
|
|
39721
39753
|
title: "Time or random value in JSX",
|
|
39722
39754
|
severity: "warn",
|
|
39723
39755
|
category: "Correctness",
|
|
39724
|
-
|
|
39756
|
+
disabledWhen: ["vite", "cra"],
|
|
39725
39757
|
recommendation: "Move time or random values into useEffect+useState so they only run in the browser, or add suppressHydrationWarning to the parent if it's intentional",
|
|
39726
39758
|
create: (context) => {
|
|
39727
39759
|
const isTestlikeFile = isTestlikeFilename(context.filename);
|
|
@@ -41688,7 +41720,7 @@ const rnListCallbackPerRow = defineRule({
|
|
|
41688
41720
|
tags: ["test-noise"],
|
|
41689
41721
|
requires: ["react-native"],
|
|
41690
41722
|
severity: "warn",
|
|
41691
|
-
|
|
41723
|
+
disabledWhen: ["react-compiler"],
|
|
41692
41724
|
recommendation: "Move the handler out with useCallback at list scope and pass the row id as a prop. Then memo() rows skip redrawing when their data has not changed.",
|
|
41693
41725
|
create: (context) => {
|
|
41694
41726
|
const inspect = (node) => {
|
|
@@ -42316,7 +42348,7 @@ const rnNoInlineFlatlistRenderitem = defineRule({
|
|
|
42316
42348
|
tags: ["test-noise"],
|
|
42317
42349
|
requires: ["react-native"],
|
|
42318
42350
|
severity: "warn",
|
|
42319
|
-
|
|
42351
|
+
disabledWhen: ["react-compiler"],
|
|
42320
42352
|
recommendation: "Move renderItem to a named function or wrap it in useCallback so it is not rebuilt every time the screen redraws.",
|
|
42321
42353
|
create: (context) => ({ JSXAttribute(node) {
|
|
42322
42354
|
if (!isNodeOfType(node.name, "JSXIdentifier") || node.name.name !== "renderItem") return;
|
|
@@ -42341,7 +42373,7 @@ const rnNoInlineObjectInListItem = defineRule({
|
|
|
42341
42373
|
tags: ["test-noise"],
|
|
42342
42374
|
requires: ["react-native"],
|
|
42343
42375
|
severity: "warn",
|
|
42344
|
-
|
|
42376
|
+
disabledWhen: ["react-compiler"],
|
|
42345
42377
|
recommendation: "Move style and object props out of renderItem (StyleSheet.create, useMemo at list scope, or pass primitives) so memo() rows stop redrawing when their data has not changed.",
|
|
42346
42378
|
create: (context) => {
|
|
42347
42379
|
const renderPropStack = [];
|
|
@@ -48452,7 +48484,7 @@ const serverFetchWithoutRevalidate = defineRule({
|
|
|
48452
48484
|
id: "server-fetch-without-revalidate",
|
|
48453
48485
|
title: "Fetch without revalidate",
|
|
48454
48486
|
severity: "warn",
|
|
48455
|
-
|
|
48487
|
+
disabledWhen: ["nextjs:15", "nextjs:static-export"],
|
|
48456
48488
|
recommendation: "Pass `{ next: { revalidate: <seconds> } }` (or `cache: \"no-store\"`) so old data doesn't stick around.",
|
|
48457
48489
|
create: (context) => {
|
|
48458
48490
|
let isServerSideFile = false;
|
|
@@ -56227,9 +56259,23 @@ const classifySecurityScanFile = (relativePath) => {
|
|
|
56227
56259
|
};
|
|
56228
56260
|
const shouldReadSecurityScanContent = (relativePath, isGeneratedBundle) => isGeneratedBundle || isProbablyTextFile(relativePath) || isConfigOrCiPath(relativePath) || isRepositorySecretFilePath(relativePath);
|
|
56229
56261
|
//#endregion
|
|
56262
|
+
//#region src/plugin/utils/capability.ts
|
|
56263
|
+
const FRAMEWORK_TOKENS = [
|
|
56264
|
+
"nextjs",
|
|
56265
|
+
"vite",
|
|
56266
|
+
"cra",
|
|
56267
|
+
"remix",
|
|
56268
|
+
"gatsby",
|
|
56269
|
+
"expo",
|
|
56270
|
+
"react-native",
|
|
56271
|
+
"tanstack-start",
|
|
56272
|
+
"preact",
|
|
56273
|
+
"unknown"
|
|
56274
|
+
];
|
|
56275
|
+
//#endregion
|
|
56230
56276
|
//#region src/index.ts
|
|
56231
56277
|
var src_default = plugin;
|
|
56232
56278
|
//#endregion
|
|
56233
|
-
export { ALL_REACT_DOCTOR_RULES, ALL_REACT_DOCTOR_RULE_KEYS, CROSS_FILE_DEPENDENCY_COLLECTORS, CROSS_FILE_RULE_IDS, EXTERNAL_RULES, FRAMEWORK_SPECIFIC_RULE_KEYS, MOTION_LIBRARY_PACKAGES, NEXTJS_RULES, PREACT_RULES, REACT_COMPILER_RULES, REACT_DOCTOR_RULES, REACT_NATIVE_DEPENDENCY_NAMES, REACT_NATIVE_DEPENDENCY_PREFIXES, REACT_NATIVE_RULES, RECOMMENDED_RULES, RULES, TANSTACK_QUERY_RULES, TANSTACK_START_RULES, UNBOUNDED_CROSS_FILE_RULE_IDS, classifySecurityScanFile, collectCrossFileDependencyProbes, src_default as default, isReactNativeDependencyName, resetManifestCaches, shouldReadSecurityScanContent };
|
|
56279
|
+
export { ALL_REACT_DOCTOR_RULES, ALL_REACT_DOCTOR_RULE_KEYS, CROSS_FILE_DEPENDENCY_COLLECTORS, CROSS_FILE_RULE_IDS, EXTERNAL_RULES, FRAMEWORK_SPECIFIC_RULE_KEYS, FRAMEWORK_TOKENS, MOTION_LIBRARY_PACKAGES, NEXTJS_RULES, PREACT_RULES, REACT_COMPILER_RULES, REACT_DOCTOR_RULES, REACT_NATIVE_DEPENDENCY_NAMES, REACT_NATIVE_DEPENDENCY_PREFIXES, REACT_NATIVE_RULES, RECOMMENDED_RULES, RULES, TANSTACK_QUERY_RULES, TANSTACK_START_RULES, UNBOUNDED_CROSS_FILE_RULE_IDS, classifySecurityScanFile, collectCrossFileDependencyProbes, src_default as default, isReactNativeDependencyName, resetManifestCaches, shouldReadSecurityScanContent };
|
|
56234
56280
|
|
|
56235
56281
|
//# sourceMappingURL=index.js.map
|