Skip to content

feat: make module window check more robust #14

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 10 commits into
base: main
Choose a base branch
from

Conversation

cmackenthun
Copy link

@cmackenthun cmackenthun commented Mar 5, 2025

We started using this rule in one of our repos (and are loving it so far!) and we noticed it was marking files that have patterns like this as needing the client directive

const HREF = typeof window === 'undefined' ? undefined : window.location.href;

This is fine to execute on the server or the client because we are checking to make sure the window exists before accessing it.

So I took a stab at updating the lint rule to account for this case. This is my first time dabbling in writing a lint rule, so please feel free to make suggestions! Overall we just want to get this case accounted for so we don't run into more issues with files being improperly marked 🙂

Copy link

changeset-bot bot commented Mar 5, 2025

🦋 Changeset detected

Latest commit: f0474fd

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 1 package
Name Type
eslint-plugin-react-server-components Major

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@cmackenthun cmackenthun marked this pull request as ready for review March 5, 2025 22:01
Copy link
Owner

@roginfarrer roginfarrer left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice, thanks for creating a PR!

Comment on lines 219 to 220
conditionalExpressionNode.test.left.argument?.type === "Identifier" &&
conditionalExpressionNode.test.left.argument?.name === "window" &&
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does this catch other places where we might make this check? Like in an if statement:

if (typeof window !== 'undefined') {
}

We could use Identifier as the node entrypoint, and checking whether it's 'window'. Then navigate the tree to see if it's used safely. Should this also work with other browser globals?

I think for legibility I might recommend extracting this logic into a "checkIfSafeBrowserGlobalUse" function outside of this function (where it takes node as an argument).

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah I'm not sure about the if statement or not, but I don't think this would cover that. I was mostly going for the simplest solution to the one edge case we were seeing. I can look into using the Identifier as an entry point but am still pretty new to writing a lint rule, so that might take a bit more effort for me. Would we still need some kind of check under the MemberExpression entrypoint to make sure we skip erroring on these safe checks?

Agreed on the legibility part 👍 I can do that for sure.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh nvm, I think you're advocating for removing the MemberExpression entrypoint in favor of a starting at the Identifier level. I'll investigate that a bit 👍

Comment on lines 234 to 236
conditionalExpressionNode.alternate === node?.parent) ||
(isNegatedWindowCheck &&
conditionalExpressionNode.consequent === node?.parent);
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What is conditionalExpressionNode.consequent === node?.parent meant to check? Not sure what this is for

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is basically to cover either typeof window === undefined ? <server code> : <client code> OR typeof window !== undefined ? <client code> : <server code>. Basically just checking if the alternate or the consequent is the client code based on the isNegatedWindowCheck value

@@ -99,6 +106,13 @@ function Bar() {
window.addEventListener('scroll', () => {})
return <div />;
}`,
},
{
code: `const HREF = typeof window === 'undefined' ? window.location.href : window.location.href.slice(0,10);`,
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good edge case to catch!

Comment on lines -38 to -49
code: `import * as React from 'react';
const context = React.createContext()
export function Foo() {
return <div />;
}
export function useTheme() {
const context = React.useContext(context);
React.useEffect(() => {
window.setTimeout(() => {});
});
return context;
}`,
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I moved these to the invalid section cause honestly I'm not sure why they were valid to being with. From my local testing if I have const context = React.createContext() at the top of a component file and use that component in a server context I get a runtime error. If there's something I'm missing please let me know!

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we do move forward with it and you feel like this warrants a major LMK 👍

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it would probably be best as a major. Happy to leave this in and cut the next release as a major

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@roginfarrer When you say "leave this in" are you saying I should leave my changeset as is? Or update that to a major? Or just remove it all together?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@roginfarrer just wondering if you have a chance to answer ☝️ and then I can make the changes and get this merged

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do you know why they pass in both valid and invalid sections? Or did your changes make them invalid?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah my changes made them invalid. And when I tested the scenario in a small next project, I would get runtime client only errors in the dev server related to the React Context so I think having them be invalid is correct. Please feel free to double check that though!

@cmackenthun cmackenthun requested a review from roginfarrer March 10, 2025 13:21
Comment on lines +167 to +169
//@ts-expect-error
const { parent } = node;
if (!parent) {
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does this fix the error?

Suggested change
//@ts-expect-error
const { parent } = node;
if (!parent) {
const parent = node?.parent
if (!parent) {

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No, the error comes from the parent field apparently not being a part of the BinaryExpression type 🤔

Property 'parent' does not exist on type 'BinaryExpression'.

Comment on lines -38 to -49
code: `import * as React from 'react';
const context = React.createContext()
export function Foo() {
return <div />;
}
export function useTheme() {
const context = React.useContext(context);
React.useEffect(() => {
window.setTimeout(() => {});
});
return context;
}`,
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it would probably be best as a major. Happy to leave this in and cut the next release as a major

@cmackenthun cmackenthun requested a review from roginfarrer March 18, 2025 19:56
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants