Skip to content
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

Check window first in setting global object #48

Open
wants to merge 2 commits into
base: master
Choose a base branch
from

Conversation

yanatan16
Copy link

This is a proposal to satisfy browserify/browserify#1189

@zertosh
Copy link
Member

zertosh commented Jun 2, 2015

@jmm, ignore what I said in browserify/browserify#1282. I'm ok with this order. if you're cool with it too, then I'll merge it

@jmm
Copy link

jmm commented Jun 2, 2015

This is certainly better for the browser, but I think it can easily trigger a false positive in another environment, like if there's window = ... in node. So I think it's worth considering a more specific test like I suggested in browserify/browserify#1189 (comment) / browserify/browserify#1282 (comment), something along the lines of:

window && window.document && window.document.implementation ? window

@zertosh
Copy link
Member

zertosh commented Jun 2, 2015

@jmm that test is too specific, and too big - remember, this gets added to every module that references global. if there's a global object called window, then you've got bigger problems 😛

The umd wrapper checks for window -> global -> self. Though, it does check for the nodeism typeof exports === "object" && typeof module !== "undefined" before all that.

@jmm
Copy link

jmm commented Jun 2, 2015

@zertosh

if there's a global object called window, then you've got bigger problems 😛

I lack the confidence to say that the possibility for there to be global.window in node or some other environment can be disregarded, so if you're confident of that I defer to you.

@jmm that test is too specific, and too big - remember, this gets added to every module that references global

What makes it too specific (disregarding length)? It could be less specific than that yet still more specific than just window (e.g. window && window.document). (For that matter, window.document is listed as an Unforgeable attribute, so document && document.something could be a relevant test.)

The umd wrapper checks for window -> global -> self. Though, it does check for the nodeism typeof exports === "object" && typeof module !== "undefined" before all that.

That seems like a significant difference. But a moot point if the premise is that global.window in some non-browser environment is "invalid" and it's ok for it to just result in yielding the wrong object as the global.

@zertosh
Copy link
Member

zertosh commented Jun 5, 2015

What makes it too specific (disregarding length)? It could be less specific than that yet still more specific than just window (e.g. window && window.document)

I don't feel it's correct to look for anything besides a global variable called window - anything else is trying to infer that something is the global object because it's DOM like. I'd say that in most cases it is true that a window object that is DOM-like is in fact the global object - but so is just a plain window object.

I take back my take-back. The correct thing to do is to leave the order as it is because global is the node global object, and there's a presumption that browserify is packaging node code that should still work in node first.

NW.js creating a fake global is deliberate. I just tried @adam-lynch's browserify-nw-global-problem with a node-main in his _public/package.json instead of a <script> and it works perfectly. As for <div id="global">, just don't do that. If it's a third-party script then (1) submit a PR there, or (2) do global = undefined before or after the node is added to the DOM (both before and after worked in Chrome).

@jmm
Copy link

jmm commented Jun 5, 2015

I don't feel it's correct to look for anything besides a global variable called window - anything else is trying to infer that something is the global object because it's DOM like

My interpretation is that it's all just duck typing the environment. The current test, in whatever order (global|self|window), is (rather error-prone) duck typing. Testing for a property of one of those objects that is indicative of it being the global object in the current environment is duck typing.

This isn't related to the DOM except in the most tangential sense. The name window itself is a DOM thing anyway. To the best of my knowledge they go hand in hand. To me the "DOM like-ness" of window or window.whatever are equal. Since the DOM angle concerns you, there are other properties 110% unrelated to DOM (but perhaps less indicative of the environment being the browser, which is the actual point) that can be considered: Array|Object|parseInt|...

I'd say that in most cases it is true that a window object that is DOM-like is in fact the global object - but so is just a plain window object.

Not when there's a random global window in any environment where window isn't the global, like node. Conversely, in environments where window is the correct global, what property is standard enough that it's a suitable test for that? I don't believe the answer is "none".

This is only relevant if the first test is for something related to window. Where window isn't the global, then typeof window !== "undefined" is the least specific test you can have. So if it's the first test, window.anything lowers the risk of false positive in other environments. Even window.window would be better (if widely supported enough in browsers). I think letting a random global|self variable|element displace the global is a bigger hazard to code running in the browser than choosing a property of window to test for. And I think the hazard that would present in other environments is more of an edge case than the current hazards in the browser.

The correct thing to do is to leave the order as it is because global is the node global object, and there's a presumption that browserify is packaging node code that should still work in node first.

  1. The browser environment isn't a second class citizen in this scenario.
  2. Sometimes it's used exclusively to package code for the browser, without respect to it running in node.
  3. There is probably some compromise that makes for the overall best outcome of supporting both environments. IMO the current test fails too easily in the browser and just checking for window first fails too easily in node. Checking for certain properties of window or global would IMO strike a better balance and reduce failure modes to certain edge cases that are less of a problem than the current failure modes. Even doing something like global && global.setTimeout ? global would be better. At least then a random element added to the document wouldn't break it.

As for <div id="global">, just don't do that. If it's a third-party script then (1) submit a PR there, or (2) do global = undefined before or after the node is added to the DOM (both before and after worked in Chrome).

The current mechaism breaks too easily considering the way web pages are composed in the real world. To me it doesn't make sense to say that it's ok for a JS application that passes a test suite, is deployed, and humming along to suddenly just break because a random attribute value unrelated to the application gets added to the document.

Related:
https://esdiscuss.org/topic/putting-global-reference-in-specs
nodejs/node#1043

@yanatan16
Copy link
Author

Checking for certain properties of window or global would IMO strike a better balance and reduce failure modes to certain edge cases that are less of a problem than the current failure modes. Even doing something like global && global.setTimeout ? global would be better. At least then a random element added to the document wouldn't break it.

I have to 👍 @jmm. Testing for things that have to be on the global variable in any javascript environment is a great way to identify a global object. It seems very unlikely someone will start inserting Array and Object and setTimeout on something called global or window in various environments.

As for <div id="global">, just don't do that. If it's a third-party script then (1) submit a PR there, or (2) do global = undefined before or after the node is added to the DOM (both before and after worked in Chrome).

I originally submitted the ticket because code outside my control inserted <div id="global"> onto the DOM after our script had loaded. Then we lazy-loaded another into the page and it started using a different global object. Javascript is a wild west when it comes to including third party, and its also a necessity. Browserify shouldn't break because a dom element was inserted, which does happen out in the wild far too often.

there's a presumption that browserify is packaging node code that should still work in node first

It says "Browserify lets you require('modules') in the browser by bundling up all of your dependencies." on http://browserify.org/. Why is node the default target environment?

@tilgovi
Copy link

tilgovi commented Sep 23, 2015

I'm having trouble imagining a real use case for insert-module-globals where window wouldn't be a safe first check.

@tilgovi
Copy link

tilgovi commented Sep 23, 2015

In a browser, window shouldn't be redefined globally, so it should always be the global object. In node, it wouldn't be there and the fallback to global would work fine. If anyone consuming insert-module-globals downstream really knows they are bundling inside some wrapper where window is defined and is not the proper global object then they could put this inside a wrapper function that has an undefined local window symbol, or we could expose an option for it.

How does that sound? Switch the default and expose a flag?

@tilgovi
Copy link

tilgovi commented Sep 23, 2015

In other words, do a simple thing that's likely to work without testing or being overly robust and provide an escape valve for special integrations.

@jmm
Copy link

jmm commented Sep 23, 2015

@tilgovi

If anyone consuming insert-module-globals downstream really knows they are bundling inside some wrapper where window is defined and is not the proper global object then they could put this inside a wrapper function that has an undefined local window symbol, or we could expose an option for it.

This is used by Browserify with the purpose of making bundles that will run (and utilize the correct global) in any of the "target" environments (e.g. browser, node, web worker).

If anyone consuming insert-module-globals downstream really knows they are bundling inside some wrapper where window is defined and is not the proper global object

I'm not sure what you mean by "inside some wrapper". This will make a bundle use the wrong global in node (the assignment could be anywhere -- not necessarily "next to" the require() call for the bundle):

window = {};
require('./bundle');

then they could put this inside a wrapper function that has an undefined local window symbol, or we could expose an option for it.

Do you think anyone would really do that with a Browserify bundle? (I don't.) I don't think an opt-in flag is a good way to keep it from being so brittle and maybe working one execution and failing the next because some new script or module or HTML element set a random variable.

But of course if there's a better solution than what's already been discussed here, that'd be great; I don't mean to discourage creativity. 😃

@tilgovi
Copy link

tilgovi commented Sep 23, 2015

Right, sorry. I wasn't thinking clearly. window could be defined anywhere.

Would typeof(window) !== 'undefined' && window.window === window be a good test? If it's the global object then this should be true. It seems to be in a quick test of FF, Chrome and IE10. That might be enough to catch most cases of window being defined but not the global.

@jmm
Copy link

jmm commented Sep 23, 2015

@tilgovi No worries. But yeah, it's really easy for it to happen, and in code outside your control even.

Would typeof(window) !== 'undefined' && window.window === window be a good test?

It would cut down the risk of false positive a great deal in my opinion. I suggested pretty much the same thing too in an earlier comment:

Even window.window would be better (if widely supported enough in browsers).

@dos1
Copy link

dos1 commented Nov 12, 2015

What's the status? We just got bitten by this bug. We use Browserify to build a bundle that's supposed to be attached as a third party script to client's websites (just like Google Analytics).

Turns out somebody has a variable named global on their website, so anything using global inside our bundle fails, as it expects a reference to window instead of some custom object.

@tilgovi
Copy link

tilgovi commented Nov 12, 2015

I'm still in favor of window.window or such to guard this and switching the order.

Anyone find that exceptionally scary?

@parshap @substack anyone have feelings?

@parshap
Copy link
Collaborator

parshap commented Nov 14, 2015

I'm wondering if there's a better way to get the global object without testing window, global, self, etc. Is the only issue with Function('return this')() CSP?

Also wondering if we can use global and solve this non-trivial problem there.

@jmm
Copy link

jmm commented Nov 20, 2015

See also https://github.com/tc39/proposal-global.

@hgwood
Copy link

hgwood commented Feb 19, 2016

I've come across this problem, using Node Webkit, and I read all the comments from the issue and this PR.

It seems to me that NW is an interesting case to study. The Webkit context of NW is an environment where

  • both global (from the Node part) and window (from the Webkit part) exist
  • the correct global is window because it is a browser environment (remember I'm talking about the Webkit context)
  • window has all the properties of global
  • global.global === global holds
  • window.window === window holds
  • global.window === window holds (this seems weird, but verified the console of a blank NW app)
  • window.global === global holds

So I'd say global and window are both completely valid globals. However, only window is correct, because the Webkit context runs client-side code.

Now, what should browserify do? It has to select window to do the correct thing. Since there is no way to tell that global is not the global, then browserify has to test for window first.

The argument I've seen against this in this thread is "there's a presumption that browserify is packaging node code that should still work in node first". I'm not sure I agree. Browserify is a way to transform Node code so that it runs in the browser. The main target platform is the browser. If you want to run that code in Node, you use the unbrowserified version. I know people are also using browserify to package standalone Node packages, but I'd say that's not the main use case (it wasn't supported before 5.0.3). However, that's for browserify authors to decide.

The remaining problem is the case when global.window exists in a Node environment. The window.window mitigation technique mentioned above is quite good I think.

To sum up, IMHO:

  • this is a seriously-hard-to-debug bug that impacts the main use case, AFAIK, of browserify
  • the fix would switch the impact to a secondary use case with a super rare edge case (Node env with global.window.window defined, that's a lot more rare than browser env with window.global defined, don't you think?)

So, what could be done to resume progress on the matter? :)

Workaround for NW

As a side-node, the following workaround can help, in the case of NW: Object.setPrototypeOf(global, window).

@hmdhk
Copy link

hmdhk commented May 12, 2016

What is the status on this?

@Zarel
Copy link

Zarel commented Aug 23, 2016

This is still a problem for me... Any progress?

@Zarel
Copy link

Zarel commented Aug 23, 2016

I'd also propose:

        return 'typeof window !== "undefined" && typeof global !== "undefined" && window.global === global ? window : '
            + 'typeof global !== "undefined" ? global : '
            + 'typeof self !== "undefined" ? self : '
            + 'typeof window !== "undefined" ? window : {}'

which may be more robust in terms of the case where the window variable is defined in Node.

@insightfuls
Copy link

I'm experiencing this problem trying to use Mocha with Karma in NW.js. I like @Zarel's solution above (#48 (comment)) as it is targetted specifically to the breaking case and is unlikely to break other things. Is there any way I can help to get something merged?

@insightfuls
Copy link

@jmm's proposal (#48 (comment)) also seems pretty appropriate to me.

@insightfuls
Copy link

Regarding some of @hgwood's comments:

window has all the properties of global

I do not believe this is the case since NW.js 0.13: http://docs.nwjs.io/en/latest/For%20Users/Advanced/JavaScript%20Contexts%20in%20NW.js/#access-nodejs-and-nwjs-api-in-browser-context

global.window === window holds (this seems weird, but verified the console of a blank NW app)

This is false for me. Again, it may be an NW.js 0.13+ thing. I believe global.window references the invisible window in which NW.js' Chrome extension runs. Although it is defined, it is not === any open browser window's window (where any browserified code would run).

Note, of course, that when you require some browserified module it will load in NW.js' Node context. In this case window (actually global.window) is not the correct global, and I believe both solutions I liked will fail in this case, and I can't think of anything yet that wouldn't. It would be best if browserified modules would load correctly in both contexts, however, IMHO, the lesser or two evils is if you have to require a non-browserified version of the code if you want to load it in NW.js' Node context, so I'd still like to see a change made here.

@insightfuls
Copy link

insightfuls commented May 17, 2017

Sorry for so many comments.

I've done some further testing. In NW.js < 0.13 (I think; only tested 0.12.3):

  • window.window === window
  • global.window === window (strange but true)
  • window.global === global
  • global.global === global
  • window.self === window
  • global.self is undefined

In NW.js ≥ 0.13 (I think; only tested 0.22.1):

  • window.window === window
  • global.window === global, a significant improvement IMHO
  • window.global === global
  • global.global === global
  • window.self === window
  • global.self === global, another significant improvement IMHO

Therefore:

  • Prioritising either window or self above global would work in both contexts in NW.js ≥ 0.13, or pretty much any of the tests provided earlier in this task.
  • Prioritising self above global but NOT above window would work in both contexts in NW.js < 0.13; solutions which prioritise window above global would NOT work.

Purely from an NW.js perspective, then, the best order would be:

        return 'typeof self !== "undefined" ? self : '
            + 'typeof global !== "undefined" ? global : '
            + 'typeof window !== "undefined" ? window : {}'

However, it would be wise to avoid situations where a variable called self exists in Node's global that isn't global. I also surmise that, although completely unnecessary, it is more likely for a non-window object to have a self-referential self member than a self-referential window member. I humbly suggest, therefore:

        return 'typeof self === "object" && self.window === self ? self : '
            + 'typeof global !== "undefined" ? global : '
            + 'typeof self !== "undefined" ? self : '
            + 'typeof window !== "undefined" ? window : {}'

This avoids any need to wrap a primitive if self happens to be one. It only introduces one additional check (two comparisons). Browsers (including in NW.js) would mostly then succeed after two comparisons (returning self), NodeJS (including in NW.js) would succeed after three (returning global), and web workers after four (returning self); the window fallback would rarely be used. This code does not run often: only once per module, at module load time, and only for modules that actually use global, which isn't many (we aim not to pollute the global namespace, don't we?).

@hgwood
Copy link

hgwood commented May 17, 2017

@insightfuls I was indeed referring to NW.js 0.12. Thanks for clarifying.

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.

10 participants