-
Notifications
You must be signed in to change notification settings - Fork 4.3k
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
Guard access to browser globals #16227
Conversation
This allows the module to be used from environments other than the browser, like plain node, opening up server side rendering.
I'm wondering why |
Found a bunch more and updated the PR. I found instances by running |
@gziolo looks like updates because I ran |
@@ -15,7 +15,7 @@ import { Component } from '@wordpress/element'; | |||
* @return {Component} The bound ScrollLock component. | |||
*/ | |||
export function createScrollLockComponent( { | |||
htmlDocument = document, | |||
htmlDocument = typeof document !== 'undefined' ? document : undefined, |
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 one is only required because we create a "default" scroll lock component at the end of the file. Perhaps we could create it on-demand instead or creating it during module init?
import 'mousetrap/plugins/global-bind/mousetrap-global-bind'; | ||
if ( typeof window !== 'undefined' ) { | ||
// only initialize the global bind if we're in a browser environment | ||
require( 'mousetrap/plugins/global-bind/mousetrap-global-bind' ); |
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 hate this, but I'm unsure how else to handle it. Mousetrap returns early when window
isn't present, but the plugin has no such protection.
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.
Should probably be fixed upstream?
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.
Should probably be fixed upstream?
If there was an issue, I would suggest so.
Looking at the code, it's not immediately obvious to me what the issue is.
There's an alternative that we copy and adapt the code from the plugin directly in this file.
I don't have a strong opinion on approach, even as implemented here. Would be good to have a better understanding of the problem this is addressing though.
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.
Worth starting by opening an issue at upstream? https://github.com/ccampbell/mousetrap/issues
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.
Should probably be fixed upstream?
If there was an issue, I would suggest so.
Looking at the code, it's not immediately obvious to me what the issue is.
There's an alternative that we copy and adapt the code from the plugin directly in this file.
I don't have a strong opinion on approach, even as implemented here. Would be good to have a better understanding of the problem this is addressing though.
My (admittedly blunt) take is that if it's available as an npm, it should at least not break things when used on the server side (read: node) but fall back to a no-op.
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.
And as Ben points out, mousetrap
itself takes precautions to prevent that from happening -- it's just the global-bind
plugin that doesn't.
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.
Precautions against what? I see no references to browser globals in the global-bind
plugin code.
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 plugin goes after the Mousetrap
global and the Mousetrap.prototype
, but mousetrap
returns early when window
is not present and never sets the Mousetrap
global. So it's not a browser global that's missing, it's Mousetrap
.
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.
filed ccampbell/mousetrap#465 upstream
@@ -35,7 +29,7 @@ class FocusableIframe extends Component { | |||
return; | |||
} | |||
|
|||
const focusEvent = new FocusEvent( 'focus', { bubbles: true } ); | |||
const focusEvent = new window.FocusEvent( 'focus', { bubbles: true } ); |
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 don't technically need the window
here, but it was failing lint without it.
@@ -15,6 +15,9 @@ | |||
* @return {void} | |||
*/ | |||
const domReady = function( callback ) { | |||
if ( typeof document === 'undefined' ) { | |||
return; |
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.
should we call the callback instead? warn? assume folks using this are using it in an environment that supports the dom?
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.
should we call the callback instead? warn? assume folks using this are using it in an environment that supports the dom?
Hm. I think the callback should be called, yes. I don't feel strongly about it, but I think a warning message would be reasonable as well to inform the developer that a document was not found.
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.
Hm. I think the callback should be called, yes. I don't feel strongly about it, but I think a warning message would be reasonable as well to inform the developer that a document was not found.
We should implement this.
@@ -40,11 +28,11 @@ function isSelectionForward( selection ) { | |||
/* eslint-disable no-bitwise */ | |||
// Compare whether anchor node precedes focus node. If focus node (where | |||
// end of selection occurs) is after the anchor node, it is forward. | |||
if ( position & DOCUMENT_POSITION_PRECEDING ) { | |||
if ( position & anchorNode.DOCUMENT_POSITION_PRECEDING ) { |
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'll see this pattern a lot through this PR. Instead of using the constant hanging off window.Node
, use the same constant which is present on anything that implements Node.
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.
Nice! There are many more instances of this across the codebase. Could they all be replaced in this PR?
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 wanted to make sure this was an acceptable approach before pushing it further. The alternative would be to use Node.DOCUMENT_POSITION_PRECEDING
, just not as a module level variable.
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 a great approach!
if ( typeof window !== 'undefined' ) { | ||
return window.requestIdleCallback ? window.requestIdleCallback : window.requestAnimationFrame; | ||
} | ||
return setTimeout; |
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'm just assuming falling back to setTimeout is ok here. The API to setTimeout includes an optional second parameter, the timeout, which we are not specifying at the call site, but which defaults to 0.
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'm just assuming falling back to setTimeout is ok here. The API to setTimeout includes an optional second parameter, the timeout, which we are not specifying at the call site, but which defaults to 0.
I think it should be fine. Not super problematic since it's internal-only, but we could wrap setTimeout
if we wanted to avoid issues of whether the interface is capable of handling a second argument.
return setTimeout; | |
return ( callback ) => setTimeout( callback, 0 ); |
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 could wrap
setTimeout
if we wanted to avoid issues of whether the interface is capable of handling a second argument.
I would suggest this be implemented.
Based on the specific changes, I suspect more likely because you're not running the latest version of NPM. https://github.com/WordPress/gutenberg/blob/master/docs/contributors/getting-started.md |
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.
For some of this, it seems to give us safety only on the relatively fragile basis that the code is less likely but to be run as in the case where it occurs at top-level scope. I suppose it's an improvement, but it's not a guarantee, and it could be argued we sacrifice some readability in the process.
Granted, I'm not sure what the alternative is, other than not referencing these properties, or some pattern of shimming values to guarantee they're at least defined (jsdom
-like, or as we had before with modules importing element-closest
if they used Element#closest
).
@@ -15,7 +15,7 @@ import { Component } from '@wordpress/element'; | |||
* @return {Component} The bound ScrollLock component. | |||
*/ | |||
export function createScrollLockComponent( { | |||
htmlDocument = document, | |||
htmlDocument = typeof document !== 'undefined' ? document : undefined, |
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.
An issue I see here is that it's prone to thrown errors, since we don't guard property reference in its use later in this function (and, as documented by the JSDoc, it's not nullable/"undefined"-able).
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.
should we throw 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.
ah, right, I remember why I didn't add a throw: We create a default scroll lock component at the bottom of this file, so it's guaranteed to throw. Maybe we don't create that default component when window
is missing? I'm not sure.
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.
Maybe we don't create that default component when
window
is missing? I'm not sure.
Yeah, in a non-DOM context, I think changing the behavior here to be basically "return a noop function" would be least problematic.
Something like:
if ( typeof document === 'undefined' ) {
return function ScrollLock() {
return null;
};
}
Replying here for @hypest since he asked me to have a look Changes look good and will not cause any problem to mobile native 👍 |
This was just enough to get the code to load without throwing in a non-DOM environment. If we want real safety, that's a much larger PR and probably a change to how Gutenberg is approaching development. |
This is blocking work in Calypso that would allow us to break out more components into isolated packages (Automattic/wp-calypso#34277), so I'd be highly interested in getting this merged 😬 @aduth Since Ben is out for the near future -- anything I can do to get this landed? Seems like a few of these changes should be fairly uncontroversial; for the rest, it'd probably be good to consider the circumstances under which the affected code is called (or isn't). Specifically -- is this good enough as a first iteration, or is there anything you'd like to see changed specifically? |
@ockham I added a few comments for things which should be implemented here, if you can help take them on. I think it might also be worth doing a rebase to see if there are more recent additions of |
This PR seems to have become stale. Can it be closed @blowery? |
Shutting this one down. It's quite stale. |
Would I be correct in assuming that the goal of this pull request is still desirable (i.e. allowing code to at least be imported without errors in Node)? |
@aduth yes, I think that's still a worthwhile goal |
Description
This allows the module to be used from environments other than the browser, like plain node, opening up server side rendering.
The problem occurs because we try to reference browser globals during module initialization. This PR defers usage to when the the references are actually needed, which at least lets the modules initialize. Many will still fail when run in a non-browser-like environment, but this may be good enough to allow some basic components to server-side render with React.
How has this been tested?
Just the existing unit tests.
Types of changes
Bug fix
Checklist:
Fixes #9265