From de40373b520847b2139a9dc10034f15433ba4027 Mon Sep 17 00:00:00 2001 From: Clay Mackenthun Date: Wed, 5 Mar 2025 15:41:01 -0600 Subject: [PATCH 01/10] make window check more robust --- src/rules/use-client.test.ts | 14 ++++++++++++++ src/rules/use-client.ts | 33 +++++++++++++++++++++++++++++++-- 2 files changed, 45 insertions(+), 2 deletions(-) diff --git a/src/rules/use-client.test.ts b/src/rules/use-client.test.ts index 603fe4a..20d54fa 100644 --- a/src/rules/use-client.test.ts +++ b/src/rules/use-client.test.ts @@ -48,6 +48,13 @@ describe("use client", () => { return context; }`, }, + { + code: `const HREF = typeof window === 'undefined' ? undefined : window.location.href;` + }, + { + code: `const HREF = typeof window !== 'undefined' ? window.location.href : '';` + }, + ], invalid: [ // DOCUMENT @@ -99,6 +106,13 @@ function Bar() { window.addEventListener('scroll', () => {}) return
; }`, + }, + { + code: `const HREF = typeof window === 'undefined' ? window.location.href : window.location.href.slice(0,10);`, + errors: [{ messageId: "addUseClientBrowserAPI" }], + output: `'use client'; + +const HREF = typeof window === 'undefined' ? window.location.href : window.location.href.slice(0,10);`, }, // OBSERVERS { diff --git a/src/rules/use-client.ts b/src/rules/use-client.ts index 4e39062..0e611c6 100644 --- a/src/rules/use-client.ts +++ b/src/rules/use-client.ts @@ -208,11 +208,40 @@ const create = Components.detect( // @ts-expect-error const name = node.object.name; const scopeType = context.getScope().type; - if ( + + // check if the window usage is behind a typeof window === 'undefined' check + const conditionalExpressionNode = node.parent?.parent; + const isWindowCheck = + conditionalExpressionNode?.type === "ConditionalExpression" && + conditionalExpressionNode.test?.type === "BinaryExpression" && + conditionalExpressionNode.test.left?.type === "UnaryExpression" && + conditionalExpressionNode.test.left.operator === "typeof" && + conditionalExpressionNode.test.left.argument?.type === "Identifier" && + conditionalExpressionNode.test.left.argument?.name === "window" && + conditionalExpressionNode.test.right?.type === "Literal" && + conditionalExpressionNode.test.right.value === "undefined"; + + // checks to see if it's `typeof window !== 'undefined'` or `typeof window === 'undefined'` + const isNegatedWindowCheck = + isWindowCheck && + conditionalExpressionNode.test?.type === "BinaryExpression" && + conditionalExpressionNode.test.operator === "!=="; + + // checks to see if window is being accessed safely behind a window check + const isSafelyBehindWindowCheck = + (isWindowCheck && + !isNegatedWindowCheck && + conditionalExpressionNode.alternate === node?.parent) || + (isNegatedWindowCheck && + conditionalExpressionNode.consequent === node?.parent); + + if ( undeclaredReferences.has(name) && browserOnlyGlobals.has(name) && - (scopeType === "module" || !!util.getParentComponent(node)) + (scopeType === "module" || !!util.getParentComponent(node)) && + !isSafelyBehindWindowCheck ) { + // console.log(name, node.object) instances.push(name); reportMissingDirective("addUseClientBrowserAPI", node.object); } From 558c4bdca70b175578e0e24f81c7be0f955a60c3 Mon Sep 17 00:00:00 2001 From: cmackenthun Date: Wed, 5 Mar 2025 16:00:05 -0600 Subject: [PATCH 02/10] add changeset --- .changeset/honest-peas-peel.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/honest-peas-peel.md diff --git a/.changeset/honest-peas-peel.md b/.changeset/honest-peas-peel.md new file mode 100644 index 0000000..75e1303 --- /dev/null +++ b/.changeset/honest-peas-peel.md @@ -0,0 +1,5 @@ +--- +"eslint-plugin-react-server-components": patch +--- + +Making window check more robust to not require "use client" when safely accessed behind a `typeof window !== undefined` check From 98c1fd6357879b302bf51bdc93360d5290d640b3 Mon Sep 17 00:00:00 2001 From: cmackenthun <159166894+cmackenthun@users.noreply.github.com> Date: Thu, 6 Mar 2025 08:18:14 -0600 Subject: [PATCH 03/10] Update .changeset/honest-peas-peel.md Co-authored-by: Rogin Farrer --- .changeset/honest-peas-peel.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.changeset/honest-peas-peel.md b/.changeset/honest-peas-peel.md index 75e1303..f4d27cc 100644 --- a/.changeset/honest-peas-peel.md +++ b/.changeset/honest-peas-peel.md @@ -1,5 +1,5 @@ --- -"eslint-plugin-react-server-components": patch +"eslint-plugin-react-server-components": minor --- Making window check more robust to not require "use client" when safely accessed behind a `typeof window !== undefined` check From 2996b70aef8237557d167620416732272dc938b4 Mon Sep 17 00:00:00 2001 From: cmackenthun Date: Thu, 6 Mar 2025 08:23:54 -0600 Subject: [PATCH 04/10] quick cleanup --- src/rules/use-client.ts | 61 +++++++++++++++++++++++------------------ 1 file changed, 35 insertions(+), 26 deletions(-) diff --git a/src/rules/use-client.ts b/src/rules/use-client.ts index 0e611c6..dc83352 100644 --- a/src/rules/use-client.ts +++ b/src/rules/use-client.ts @@ -4,6 +4,7 @@ import type { ExpressionStatement, Identifier, ImportSpecifier, + MemberExpression, Node, Program, SpreadElement, @@ -113,6 +114,38 @@ const create = Components.detect( }); } + function getIsSafeWindowCheck(node: Rule.NodeParentExtension) { + + // check if the window usage is behind a typeof window === 'undefined' check + const conditionalExpressionNode = node.parent?.parent; + const isWindowCheck = + conditionalExpressionNode?.type === "ConditionalExpression" && + conditionalExpressionNode.test?.type === "BinaryExpression" && + conditionalExpressionNode.test.left?.type === "UnaryExpression" && + conditionalExpressionNode.test.left.operator === "typeof" && + conditionalExpressionNode.test.left.argument?.type === "Identifier" && + conditionalExpressionNode.test.left.argument?.name === "window" && + conditionalExpressionNode.test.right?.type === "Literal" && + conditionalExpressionNode.test.right.value === "undefined"; + + // checks to see if it's `typeof window !== 'undefined'` or `typeof window === 'undefined'` + const isNegatedWindowCheck = + isWindowCheck && + conditionalExpressionNode.test?.type === "BinaryExpression" && + conditionalExpressionNode.test.operator === "!=="; + + // checks to see if window is being accessed safely behind a window check + const isSafelyBehindWindowCheck = + (isWindowCheck && + !isNegatedWindowCheck && + conditionalExpressionNode.alternate === node?.parent) || + (isNegatedWindowCheck && + conditionalExpressionNode.consequent === node?.parent); + + return isSafelyBehindWindowCheck + + } + const reactImports: Record = { namespace: [], }; @@ -209,31 +242,8 @@ const create = Components.detect( const name = node.object.name; const scopeType = context.getScope().type; - // check if the window usage is behind a typeof window === 'undefined' check - const conditionalExpressionNode = node.parent?.parent; - const isWindowCheck = - conditionalExpressionNode?.type === "ConditionalExpression" && - conditionalExpressionNode.test?.type === "BinaryExpression" && - conditionalExpressionNode.test.left?.type === "UnaryExpression" && - conditionalExpressionNode.test.left.operator === "typeof" && - conditionalExpressionNode.test.left.argument?.type === "Identifier" && - conditionalExpressionNode.test.left.argument?.name === "window" && - conditionalExpressionNode.test.right?.type === "Literal" && - conditionalExpressionNode.test.right.value === "undefined"; - - // checks to see if it's `typeof window !== 'undefined'` or `typeof window === 'undefined'` - const isNegatedWindowCheck = - isWindowCheck && - conditionalExpressionNode.test?.type === "BinaryExpression" && - conditionalExpressionNode.test.operator === "!=="; - - // checks to see if window is being accessed safely behind a window check - const isSafelyBehindWindowCheck = - (isWindowCheck && - !isNegatedWindowCheck && - conditionalExpressionNode.alternate === node?.parent) || - (isNegatedWindowCheck && - conditionalExpressionNode.consequent === node?.parent); + const isSafelyBehindWindowCheck = getIsSafeWindowCheck(node); + if ( undeclaredReferences.has(name) && @@ -241,7 +251,6 @@ const create = Components.detect( (scopeType === "module" || !!util.getParentComponent(node)) && !isSafelyBehindWindowCheck ) { - // console.log(name, node.object) instances.push(name); reportMissingDirective("addUseClientBrowserAPI", node.object); } From 98465009a3bbcdc7c0b212af59444233405a0886 Mon Sep 17 00:00:00 2001 From: cmackenthun Date: Thu, 6 Mar 2025 08:58:17 -0600 Subject: [PATCH 05/10] quick cleanup --- src/rules/use-client.ts | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/src/rules/use-client.ts b/src/rules/use-client.ts index dc83352..4a80c70 100644 --- a/src/rules/use-client.ts +++ b/src/rules/use-client.ts @@ -115,7 +115,6 @@ const create = Components.detect( } function getIsSafeWindowCheck(node: Rule.NodeParentExtension) { - // check if the window usage is behind a typeof window === 'undefined' check const conditionalExpressionNode = node.parent?.parent; const isWindowCheck = @@ -142,8 +141,7 @@ const create = Components.detect( (isNegatedWindowCheck && conditionalExpressionNode.consequent === node?.parent); - return isSafelyBehindWindowCheck - + return isSafelyBehindWindowCheck; } const reactImports: Record = { @@ -243,9 +241,8 @@ const create = Components.detect( const scopeType = context.getScope().type; const isSafelyBehindWindowCheck = getIsSafeWindowCheck(node); - - if ( + if ( undeclaredReferences.has(name) && browserOnlyGlobals.has(name) && (scopeType === "module" || !!util.getParentComponent(node)) && From fe5dec3b096b67e0132e670b26e47c80321fe622 Mon Sep 17 00:00:00 2001 From: cmackenthun Date: Thu, 6 Mar 2025 14:26:29 -0600 Subject: [PATCH 06/10] rework --- src/rules/use-client.test.ts | 102 ++++++++++++++++------ src/rules/use-client.ts | 162 ++++++++++++++++++++++++----------- 2 files changed, 189 insertions(+), 75 deletions(-) diff --git a/src/rules/use-client.test.ts b/src/rules/use-client.test.ts index 20d54fa..53a9d8a 100644 --- a/src/rules/use-client.test.ts +++ b/src/rules/use-client.test.ts @@ -24,37 +24,22 @@ describe("use client", () => { code: 'const foo = "bar"', }, { - code: `import {createContext, useContext, useEffect} from 'react'; - const context = createContext() - export function useTheme() { - const context = useContext(context); - useEffect(() => { - window.setTimeout(() => {}); - }); - return context; - }`, + code: `const HREF = typeof window === 'undefined' ? undefined : window.location.href;`, }, { - code: `import * as React from 'react'; - const context = React.createContext() - export function Foo() { - return
; - } - export function useTheme() { - const context = React.useContext(context); - React.useEffect(() => { - window.setTimeout(() => {}); - }); - return context; - }`, + code: `const HREF = typeof window !== 'undefined' ? window.location.href : '';`, }, { - code: `const HREF = typeof window === 'undefined' ? undefined : window.location.href;` + code: `const el = typeof document === 'undefined' ? undefined : document.createElement('element');`, }, { - code: `const HREF = typeof window !== 'undefined' ? window.location.href : '';` + code: `const foo = "bar"; +function Bar() { + if(typeof document !== 'undefined') + document.addEventListener('scroll', () => {}) + return
; +}`, }, - ], invalid: [ // DOCUMENT @@ -106,6 +91,62 @@ function Bar() { window.addEventListener('scroll', () => {}) return
; }`, + }, + { + code: `import {createContext, useContext, useEffect} from 'react'; + + const context = createContext() + export function useTheme() { + const context = useContext(context); + useEffect(() => { + window.setTimeout(() => {}); + }); + return context; + }`, + errors: [{ messageId: "addUseClientBrowserAPI" }], + output: `'use client'; + +import {createContext, useContext, useEffect} from 'react'; + + const context = createContext() + export function useTheme() { + const context = useContext(context); + useEffect(() => { + window.setTimeout(() => {}); + }); + return context; + }`, + }, + { + code: `import * as React from 'react'; + + const context = React.createContext() + export function Foo() { + return
; + } + export function useTheme() { + const context = React.useContext(context); + React.useEffect(() => { + window.setTimeout(() => {}); + }); + return context; + }`, + errors: [{ messageId: "addUseClientBrowserAPI" }], + output: `'use client'; + +import * as React from 'react'; + + const context = React.createContext() + export function Foo() { + return
; + } + export function useTheme() { + const context = React.useContext(context); + React.useEffect(() => { + window.setTimeout(() => {}); + }); + return context; + }`, }, { code: `const HREF = typeof window === 'undefined' ? window.location.href : window.location.href.slice(0,10);`, @@ -113,6 +154,19 @@ function Bar() { output: `'use client'; const HREF = typeof window === 'undefined' ? window.location.href : window.location.href.slice(0,10);`, + }, + { + code: `let HREF = ''; + if (typeof window === 'undefined') { + HREF = window.location.href; + }`, + errors: [{ messageId: "addUseClientBrowserAPI" }], + output: `'use client'; + +let HREF = ''; + if (typeof window === 'undefined') { + HREF = window.location.href; + }`, }, // OBSERVERS { diff --git a/src/rules/use-client.ts b/src/rules/use-client.ts index 4a80c70..89c36b7 100644 --- a/src/rules/use-client.ts +++ b/src/rules/use-client.ts @@ -1,10 +1,10 @@ import type { Rule } from "eslint"; import type { + BinaryExpression, Expression, ExpressionStatement, Identifier, ImportSpecifier, - MemberExpression, Node, Program, SpreadElement, @@ -27,6 +27,8 @@ const browserOnlyGlobals = Object.keys(globals.browser).reduce< return acc; }, new Set()); +const validGlobalsForServerChecks = new Set(["document", "window"]); + type Options = [ { allowedServerHooks?: string[]; @@ -114,42 +116,97 @@ const create = Components.detect( }); } - function getIsSafeWindowCheck(node: Rule.NodeParentExtension) { - // check if the window usage is behind a typeof window === 'undefined' check - const conditionalExpressionNode = node.parent?.parent; + function findFirstParentOfType( + node: Rule.Node, + type: string + ): Rule.Node | null { + let currentNode: Rule.Node | null = node; + + while (currentNode) { + if (currentNode.type === type) { + return currentNode; + } + currentNode = currentNode?.parent; + } + + return null; + } + + function isNodeInTree(node: Rule.Node, target: Rule.Node): boolean { + let currentNode: Rule.Node | null = node; + + while (currentNode) { + if (currentNode === target) { + return true; + } + currentNode = currentNode.parent; + } + + return false; + } + + function getBinaryBranchExecutedOnServer(node: BinaryExpression): { + isWindowCheck: boolean; + serverBranch: Rule.Node | null; + } { const isWindowCheck = - conditionalExpressionNode?.type === "ConditionalExpression" && - conditionalExpressionNode.test?.type === "BinaryExpression" && - conditionalExpressionNode.test.left?.type === "UnaryExpression" && - conditionalExpressionNode.test.left.operator === "typeof" && - conditionalExpressionNode.test.left.argument?.type === "Identifier" && - conditionalExpressionNode.test.left.argument?.name === "window" && - conditionalExpressionNode.test.right?.type === "Literal" && - conditionalExpressionNode.test.right.value === "undefined"; - - // checks to see if it's `typeof window !== 'undefined'` or `typeof window === 'undefined'` - const isNegatedWindowCheck = - isWindowCheck && - conditionalExpressionNode.test?.type === "BinaryExpression" && - conditionalExpressionNode.test.operator === "!=="; - - // checks to see if window is being accessed safely behind a window check - const isSafelyBehindWindowCheck = - (isWindowCheck && - !isNegatedWindowCheck && - conditionalExpressionNode.alternate === node?.parent) || - (isNegatedWindowCheck && - conditionalExpressionNode.consequent === node?.parent); - - return isSafelyBehindWindowCheck; + node.left?.type === "UnaryExpression" && + node.left.operator === "typeof" && + node.left.argument?.type === "Identifier" && + validGlobalsForServerChecks.has(node.left.argument?.name) && + node.right?.type === "Literal" && + node.right.value === "undefined" && + (node.operator === "===" || node.operator === "!=="); + + let serverBranch = null; + + if (!isWindowCheck) { + return { isWindowCheck, serverBranch }; + } + + //@ts-expect-error + const { parent } = node; + if (!parent) { + return { isWindowCheck, serverBranch }; + } + + if (node.operator === "===") { + serverBranch = + parent.type === "IfStatement" || + parent.type === "ConditionalExpression" + ? parent.alternate + : null; + } else { + serverBranch = + parent.type === "IfStatement" || + parent.type === "ConditionalExpression" + ? parent.consequent + : null; + } + + return { isWindowCheck, serverBranch }; } + const isNodePartOfSafelyExecutedServerBranch = ( + node: Rule.Node + ): boolean => { + let isUsedInServerBranch = false; + serverBranches.forEach((serverBranch) => { + if (isNodeInTree(node, serverBranch)) { + isUsedInServerBranch = true; + } + }); + return isUsedInServerBranch; + }; + const reactImports: Record = { namespace: [], }; const undeclaredReferences = new Set(); + const serverBranches = new Set(); + return { Program(node) { for (const block of node.body) { @@ -226,30 +283,33 @@ const create = Components.detect( }); } }, - MemberExpression(node) { - // Catch uses of browser APIs in module scope - // or React component scope. - // eg: - // const foo = window.foo - // window.addEventListener(() => {}) - // const Foo() { - // const foo = window.foo - // return
; - // } + Identifier(node) { + const name = node.name; // @ts-expect-error - const name = node.object.name; - const scopeType = context.getScope().type; - - const isSafelyBehindWindowCheck = getIsSafeWindowCheck(node); - - if ( - undeclaredReferences.has(name) && - browserOnlyGlobals.has(name) && - (scopeType === "module" || !!util.getParentComponent(node)) && - !isSafelyBehindWindowCheck - ) { - instances.push(name); - reportMissingDirective("addUseClientBrowserAPI", node.object); + if (undeclaredReferences.has(name) && browserOnlyGlobals.has(name)) { + // find the nearest binary expression so we can see if this instance of window is being used in a `typeof window === undefined`-like check + const binaryExpressionNode = findFirstParentOfType( + node, + "BinaryExpression" + ) as BinaryExpression | null; + if (binaryExpressionNode) { + const { isWindowCheck, serverBranch } = + getBinaryBranchExecutedOnServer(binaryExpressionNode); + // if this instance isn't part of a window check we report it + if (!isWindowCheck) { + instances.push(name); + reportMissingDirective("addUseClientBrowserAPI", node); + } else if (isWindowCheck && serverBranch) { + // if it is part of a window check, we don't report it and we save the server branch so we can check if future window instances are a part of the branch of code safely executed on the server + serverBranches.add(serverBranch); + } + } else { + // if the window usage isn't part of the binary expression, we check to see if it's part of a safely checked server branch and report if not + if (!isNodePartOfSafelyExecutedServerBranch(node)) { + instances.push(name); + reportMissingDirective("addUseClientBrowserAPI", node); + } + } } }, ExpressionStatement(node) { From 8a1981981db2a32779b7aa34293eb23fcf4bdc80 Mon Sep 17 00:00:00 2001 From: cmackenthun Date: Thu, 6 Mar 2025 14:31:21 -0600 Subject: [PATCH 07/10] improve changeset --- .changeset/honest-peas-peel.md | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/.changeset/honest-peas-peel.md b/.changeset/honest-peas-peel.md index f4d27cc..89a6971 100644 --- a/.changeset/honest-peas-peel.md +++ b/.changeset/honest-peas-peel.md @@ -2,4 +2,14 @@ "eslint-plugin-react-server-components": minor --- -Making window check more robust to not require "use client" when safely accessed behind a `typeof window !== undefined` check +Making checks for window usage more robust to not require "use client" when safely accessed behind a `typeof window !== 'undefined'` or `typeof document !== 'undefined'`check. + +For example: +``` +const HREF = typeof window !== 'undefined' ? window.location.href : ''; + +const MyComponent = () => { + return
{HREF}
; +} +``` +does not need to be marked with a "use client" because all of it's client only actions are behind a server check. \ No newline at end of file From 5ea4aec026ccea08476b7078f463ad69aa4f4f9b Mon Sep 17 00:00:00 2001 From: cmackenthun Date: Thu, 6 Mar 2025 14:40:44 -0600 Subject: [PATCH 08/10] change variables to generalize --- src/rules/use-client.ts | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/src/rules/use-client.ts b/src/rules/use-client.ts index 89c36b7..ddb46e1 100644 --- a/src/rules/use-client.ts +++ b/src/rules/use-client.ts @@ -146,10 +146,10 @@ const create = Components.detect( } function getBinaryBranchExecutedOnServer(node: BinaryExpression): { - isWindowCheck: boolean; + isGlobalClientPropertyCheck: boolean; serverBranch: Rule.Node | null; } { - const isWindowCheck = + const isGlobalClientPropertyCheck = node.left?.type === "UnaryExpression" && node.left.operator === "typeof" && node.left.argument?.type === "Identifier" && @@ -160,14 +160,14 @@ const create = Components.detect( let serverBranch = null; - if (!isWindowCheck) { - return { isWindowCheck, serverBranch }; + if (!isGlobalClientPropertyCheck) { + return { isGlobalClientPropertyCheck, serverBranch }; } //@ts-expect-error const { parent } = node; if (!parent) { - return { isWindowCheck, serverBranch }; + return { isGlobalClientPropertyCheck, serverBranch }; } if (node.operator === "===") { @@ -184,7 +184,7 @@ const create = Components.detect( : null; } - return { isWindowCheck, serverBranch }; + return { isGlobalClientPropertyCheck, serverBranch }; } const isNodePartOfSafelyExecutedServerBranch = ( @@ -287,24 +287,24 @@ const create = Components.detect( const name = node.name; // @ts-expect-error if (undeclaredReferences.has(name) && browserOnlyGlobals.has(name)) { - // find the nearest binary expression so we can see if this instance of window is being used in a `typeof window === undefined`-like check + // find the nearest binary expression so we can see if this instance is being used in a `typeof window === undefined`-like check const binaryExpressionNode = findFirstParentOfType( node, "BinaryExpression" ) as BinaryExpression | null; if (binaryExpressionNode) { - const { isWindowCheck, serverBranch } = + const { isGlobalClientPropertyCheck, serverBranch } = getBinaryBranchExecutedOnServer(binaryExpressionNode); - // if this instance isn't part of a window check we report it - if (!isWindowCheck) { + // if this instance isn't part of a server check we report it + if (!isGlobalClientPropertyCheck) { instances.push(name); reportMissingDirective("addUseClientBrowserAPI", node); - } else if (isWindowCheck && serverBranch) { - // if it is part of a window check, we don't report it and we save the server branch so we can check if future window instances are a part of the branch of code safely executed on the server + } else if (isGlobalClientPropertyCheck && serverBranch) { + // if it is part of a check, we don't report it and we save the server branch so we can check if future instances are a part of the branch of code safely executed on the server serverBranches.add(serverBranch); } } else { - // if the window usage isn't part of the binary expression, we check to see if it's part of a safely checked server branch and report if not + // if the usage isn't part of the binary expression, we check to see if it's part of a safely checked server branch and report if not if (!isNodePartOfSafelyExecutedServerBranch(node)) { instances.push(name); reportMissingDirective("addUseClientBrowserAPI", node); From 943176a2538c08e920cf0b0de65f9b2ce4a6214f Mon Sep 17 00:00:00 2001 From: cmackenthun Date: Tue, 18 Mar 2025 14:56:02 -0500 Subject: [PATCH 09/10] refactor from pr comments --- src/rules/use-client.ts | 61 ++++++++++++++++++++--------------------- 1 file changed, 30 insertions(+), 31 deletions(-) diff --git a/src/rules/use-client.ts b/src/rules/use-client.ts index ddb46e1..9aba989 100644 --- a/src/rules/use-client.ts +++ b/src/rules/use-client.ts @@ -27,8 +27,6 @@ const browserOnlyGlobals = Object.keys(globals.browser).reduce< return acc; }, new Set()); -const validGlobalsForServerChecks = new Set(["document", "window"]); - type Options = [ { allowedServerHooks?: string[]; @@ -116,34 +114,6 @@ const create = Components.detect( }); } - function findFirstParentOfType( - node: Rule.Node, - type: string - ): Rule.Node | null { - let currentNode: Rule.Node | null = node; - - while (currentNode) { - if (currentNode.type === type) { - return currentNode; - } - currentNode = currentNode?.parent; - } - - return null; - } - - function isNodeInTree(node: Rule.Node, target: Rule.Node): boolean { - let currentNode: Rule.Node | null = node; - - while (currentNode) { - if (currentNode === target) { - return true; - } - currentNode = currentNode.parent; - } - - return false; - } function getBinaryBranchExecutedOnServer(node: BinaryExpression): { isGlobalClientPropertyCheck: boolean; @@ -153,7 +123,7 @@ const create = Components.detect( node.left?.type === "UnaryExpression" && node.left.operator === "typeof" && node.left.argument?.type === "Identifier" && - validGlobalsForServerChecks.has(node.left.argument?.name) && + browserOnlyGlobals.has(node.left.argument?.name as any) && node.right?.type === "Literal" && node.right.value === "undefined" && (node.operator === "===" || node.operator === "!=="); @@ -411,4 +381,33 @@ function isFunction(def: any) { return false; } +function findFirstParentOfType( + node: Rule.Node, + type: string +): Rule.Node | null { + let currentNode: Rule.Node | null = node; + + while (currentNode) { + if (currentNode.type === type) { + return currentNode; + } + currentNode = currentNode?.parent; + } + + return null; +} + +function isNodeInTree(node: Rule.Node, target: Rule.Node): boolean { + let currentNode: Rule.Node | null = node; + + while (currentNode) { + if (currentNode === target) { + return true; + } + currentNode = currentNode.parent; + } + + return false; +} + export const ClientComponents = { meta, create }; From f0474fd2af75ff0f03c7812a32a689c2a9a8f9ba Mon Sep 17 00:00:00 2001 From: Rogin Farrer Date: Tue, 8 Apr 2025 04:51:17 -0700 Subject: [PATCH 10/10] Update honest-peas-peel.md --- .changeset/honest-peas-peel.md | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/.changeset/honest-peas-peel.md b/.changeset/honest-peas-peel.md index 89a6971..8a9e189 100644 --- a/.changeset/honest-peas-peel.md +++ b/.changeset/honest-peas-peel.md @@ -1,15 +1,15 @@ --- -"eslint-plugin-react-server-components": minor +"eslint-plugin-react-server-components": major --- -Making checks for window usage more robust to not require "use client" when safely accessed behind a `typeof window !== 'undefined'` or `typeof document !== 'undefined'`check. +Makes checks for window usage more robust to not require "use client" when safely accessed behind a `typeof window !== 'undefined'` or `typeof document !== 'undefined'`check. For example: -``` -const HREF = typeof window !== 'undefined' ? window.location.href : ''; -const MyComponent = () => { - return
{HREF}
; -} +```jsx +const href = typeof window !== 'undefined' ? window.location.href : ''; + +const MyComponent = () =>
{href}
; ``` -does not need to be marked with a "use client" because all of it's client only actions are behind a server check. \ No newline at end of file + +This does not need to be marked with a "use client" because all of its client-only actions are behind a safety check.