-
Notifications
You must be signed in to change notification settings - Fork 32
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
feat: add eslint rules for SSR #107
Conversation
5c077e6
to
f724eea
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This looks great to me!
'captureEvents', | ||
'chrome', | ||
'clientInformation', | ||
// 'close', |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We chatted about this on Slack, but also leaving this here for other reviewers & for posterity:
In the future, we can use ESlint's scope API to determine whether a variable is locally scoped or a global. When that is in place, we can uncomment these global variables with common names.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In the future, we can use ESlint's scope API to determine whether a variable is locally scoped or a global.
IMO, it's something we should have from day 1. If an ESLint rule reports noise, developers will certainly disable this rule globally and never enable it again.
I am ok with merging this PR to get the ball rolling, but I don't think those rules should be used until we resolve this scoping issue.
schema: [], | ||
messages: { | ||
prohibitedBrowserAPIUsage: | ||
'`{{identifier}}`, like most browser APIs, is not accessible during SSR.', |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
'`{{identifier}}`, like most browser APIs, is not accessible during SSR.', | |
'Invalid usage of a browser global API during SSR. Consider moving `{{identifier}}` to the `renderedCallback` ', |
const { noReferenceDuringSSR } = require('../rule-helpers'); | ||
const { docUrl } = require('../util/doc-url'); | ||
|
||
const prohibitedGlobalVariables = new Set([ |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The list of known browser APIs is consistently evolving. We should avoid having to maintain this list.
ESLint is using internal the globals
npm package to determine which global is available per environment. I think we should do the same.
'captureEvents', | ||
'chrome', | ||
'clientInformation', | ||
// 'close', |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In the future, we can use ESlint's scope API to determine whether a variable is locally scoped or a global.
IMO, it's something we should have from day 1. If an ESLint rule reports noise, developers will certainly disable this rule globally and never enable it again.
I am ok with merging this PR to get the ball rolling, but I don't think those rules should be used until we resolve this scoping issue.
|
||
const tester = new RuleTester(ESLINT_TEST_CONFIG); | ||
|
||
const disallowedProperties = [ |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ditto. Let's avoid having to maintain multiple disallow lists.
errors: [ | ||
{ | ||
message: | ||
'You should not use `this.template.querySelector` in methods that will execute during SSR. Use `lwc:ref` instead.', |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This error message is misleading. Even with refs, you can't access the rendered element in the connectedCallback
(on the client nor the server).
errors: [ | ||
{ | ||
message: | ||
'You should not access any DOM properties on `this` in methods that will execute during SSR.', |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You should not access any DOM properties on
this
in methods that will execute during SSR.
Is this statement really true? As far as I know, we do implement at least this.classList
, this.getAttribute
, and this.setAttribute
. Does it mean that we should retire those APIs?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Reading is fine. Writing is not fine and causes hydration errors. An improvement would be to restrict the developer only from using APIs that change the DOM. Would you be okay with this going into a follow-up PR, @pmdartus?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sure. I don't see any issue with doing this work in a follow-up PR as long as we do it prior to socializing those new rules.
const { noPropertyAccessDuringSSR } = require('../rule-helpers'); | ||
const { docUrl } = require('../util/doc-url'); | ||
|
||
const disallowedProperties = [ |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Only a subset of those APIs is implemented by LightningElement
today. Could we be more selective?
@@ -0,0 +1,48 @@ | |||
# Disallow access of properties on this during SSR (`lwc/no-this-property-during-ssr`) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I would like to better understand the motivation behind this rule. It appears to me that we are bundling multiple potential SSR errors in a single rule:
- Access to certain DOM APIs that aren't implemented by the LWC engine server: eg.
dispatchEvent
- Access to rendered content prior to the component being rendered
this[.template].querySelector
- Access to the parent node via the template
this.template.host
- Access to DOM APIs poly filled on the LWC engine server: eg.
this.getAttribute
,this.setAttribute
,this.classList
Could we tease those use cases apart and evaluate the usefulness of each of them individually?
node.test.left.type === 'UnaryExpression' && | ||
node.test.left.operator === 'typeof' && | ||
node.test.left.argument.type === 'Identifier' && | ||
(node.test.left.argument.name === 'document' || node.test.left.argument.name === 'window') |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is also something we should parameterize. I would expect LWR to come with a more elegant option than type 'window'
checks.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think if we land imports.meta.env
, we can change this to check for that.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sound good to me. Could you add a comment here, to make sure we track this in the future?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There is one bug I would recommend addressing, and a couple of comments. Otherwise looks good.
const { 'restricted-globals': restrictedGlobals } = context.options[0] || { | ||
'restricted-globals': {}, | ||
}; | ||
for (const [global, isRestricted] of Object.entries(restrictedGlobals)) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Since rules can be applied on a per-file basis, we shouldn't mutate the forbiddenGlobalNames
singleton object. The forbiddenGlobalNames
should be copied first before applying the configuration.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We are not modifying the singleton. In line 44, we're cloning it and modifying the clone.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You are right. I haven't read the code carefully enough.
node.test.left.type === 'UnaryExpression' && | ||
node.test.left.operator === 'typeof' && | ||
node.test.left.argument.type === 'Identifier' && | ||
(node.test.left.argument.name === 'document' || node.test.left.argument.name === 'window') |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sound good to me. Could you add a comment here, to make sure we track this in the future?
@divmain is the main author of this commit. I'm moving them from his repo this.
This PR introduces five new rules that check that browser APIs aren't used anywhere where SSR is done.
this.template.querySelector
inrenderedCallback
but not inconnectedCallback
.connectedCallback
callsthis.foo()
andfoo
tries to accessdocument
, the linter will complain.connectedCallback
might calldoSomething()
, which is defined in the same module like sofunction doSomething() { document.write("<div/>") }
. In this case, an error will be surfaced.doSomething
is defined in a different module than your LWC, you're on your own and the linter can't help you.this.template
in SSR. You can't touch most browser global variables that aren't also available in Node.if(document !== undefined)
are ignored. We don't check the else blocks as well.