-
-
Notifications
You must be signed in to change notification settings - Fork 5.9k
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
Support SSR #9317
Support SSR #9317
Conversation
@IvanSanchez Unfortunately optional chaining does not work if the window is undefined. The current pattern I'm using is to check via typeof const pointer = typeof window === 'undefined' ? false : !!window.PointerEvent; |
To get back to the discussion under #9316.
Whilst I understand this concern and we want to make using Leaflet as user-friendly as possible, I am left wondering what the implications of this are? I am not sure about adding support for SSR when Leaflet is very much not made to ever be used in a server-side context. What will this PR accomplish exactly? Will this cause Leaflet code be executed on the server? If so, this would yield additional overhead of code execution that does essentially nothing. I cannot imagine this will actually get as far as to render the map on the server and re-hydrate it on the client. In which case I might even consider it a feature that Leaflet doesn't work in such a context. Could you provide a minimal example of how Leaflet would be integrated in a Remix application, what the roadblocks are you are running into and how this will resolve said problem without introducing unnecessary overhead? |
@jonkoops I have a lot of thoughts about Remix, but the bigger picture is that this is a problem that would need a unique solution across all of the current fad of server-rendered UI frameworks, NextJS, SvelteKit, Remix, Nuxt, etc... (I swear some of these framework names are made up 😅 ) In all of these style of frameworks, there is a server that does a first pass of the UI, and because the server doesn't have global DOM variables, SSR support puts the onus on library devs to handle nullish global dom variables. It's not ideal, but SSR support removes a big painpoint for people using libraries in these scenarios. I take the mental load for maintainers seriously, so I understand wanting to avoid any unnecessary features. Looking at the diff in this PR, I'm cautiously convinced this is minimal lift and minimal compute for a high impact use-case. That said, the final call falls on the library devs. Thank you for the work you do 🙏🏻 |
I see it like this too. Over time we got a few request for ssr support and if this few changes are all that are needed, then I would add them. But I would still not publish that we support ssr. It are just a few adjustments that makes it a little bit easier without offical support. Other SSR references: |
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 changes looks good to me! Fairly minimal for enabling a rare but increasing common use case. The only thing I'd suggest is seeing if we can add some kind of a test so that we don't accidentally regress on this in future PRs (which seems easy).
I am still not convinced we should be landing this at all. My questions still remains, why execute Leaflet code on the server when it cannot actually do anything useful? What are the problems that will occur if we don't land this change? From what I am seeing here we're simply introducing more variations in code branching, and taking on supporting various server-side run-times while we're at it. Which implicates that we now need to take this on as additional maintenance should these run-times have different interpretations or expediencies of how the code should be run. For example, Deno has a
I understand that different SSR frameworks have different ways of dealing with client-side only code, but they all have escape hatches that allow users to run code on the client only (see Next.js, Remix and SvelteKit documentation). If a user is committing to use a framework, I think it's a valid expectancy they learn how to use it. |
@jonkoops I'm not familiar with various nuances of SSR frameworks, but we had a similar situation in Mapbox GL JS — tons of requests from users to make it possible to load the bundle in a server-side environment for who knows what. Eventually we gave up and merged a similar PR and added a test which simply checked that Requiring Leaflet in some server script might not be a use case that makes a lot of sense for us (without much experience in this kind of SSR projects), but people will keep requesting it, and IMO it's a small price to pay.
Even if we don't consider actually rendering something on the server, they could at least use some utility functions Leaflet provides without resorting to another tool like Turf — geometric / geographical calculations, projections, etc. |
Very well, if we have to land this I'll give this a proper pass. Let me take a look and put in a review. |
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 are still some globals referred to that might cause issues in a server-side run-time, but they will only create issues if DOM related APIs are used on the server, or if a Map
is initialized.
Not sure what we want to do with those, handling them on a server is not really possible, and the guard code would be hundreds of lines of code.
src/core/Browser.js
Outdated
@@ -40,15 +40,18 @@ const touch = touchNative || pointer; | |||
|
|||
// @property retina: Boolean | |||
// `true` for browsers on a high-resolution "retina" screen or on any screen when browser's display zoom is more than 100%. | |||
const retina = (window.devicePixelRatio || (window.screen.deviceXDPI / window.screen.logicalXDPI)) > 1; | |||
const retina = typeof window === 'undefined' ? false : (window.devicePixelRatio || (window.screen.deviceXDPI / window.screen.logicalXDPI)) > 1; |
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 should take into account that window.devicePixelRatio
and window.screen
might be undefined
in some run-times that do support a global window
, such as Deno. Since window.devicePixelRatio
is supported in all browsers we want to support for v2, I propose we drop the fallback here.
const retina = typeof window === 'undefined' || typeof window.devicePixelRatio === 'undefined' ? false : window.devicePixelRatio > 1;
src/core/Browser.js
Outdated
|
||
// @property mac: Boolean; `true` when the browser is running in a Mac platform | ||
const mac = navigator.platform.startsWith('Mac'); | ||
const mac = typeof navigator === 'undefined' ? false : (navigator?.platform ?? '').startsWith('Mac'); |
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 personal preference, but I prefer to guard the clause rather than to fall back to default values.
const mac = typeof navigator === 'undefined' ? false : (navigator?.platform ?? '').startsWith('Mac'); | |
const mac = typeof navigator === 'undefined' || typeof navigator.platform === 'undefined' ? false : navigator.platform.startsWith('Mac'); |
src/core/Browser.js
Outdated
|
||
// @property mac: Boolean; `true` when the browser is running in a Linux platform | ||
const linux = navigator.platform.startsWith('Linux'); | ||
const linux = typeof navigator === 'undefined' ? false : (navigator?.platform ?? '').startsWith('Linux'); |
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.
Same here, I'd prefer a guard clause than a fallback.
const linux = typeof navigator === 'undefined' ? false : (navigator?.platform ?? '').startsWith('Linux'); | |
const linux = typeof navigator === 'undefined' || typeof navigator.plaform === 'undefined' : navigator.platfom.startsWith('Linux'); |
src/core/Browser.js
Outdated
|
||
function userAgentContains(str) { | ||
if (typeof navigator === '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.
Although supported in Deno, I am not sure navigator.userAgent
might be defined in other run-times, so we might as well guard this code.
if (typeof navigator === 'undefined') { | |
if (typeof navigator === 'undefined' || typeof navigator.userAgent === 'undefined') { |
src/core/Util.js
Outdated
const requestFn = typeof window === 'undefined' ? falseFn : window.requestAnimationFrame; | ||
const cancelFn = typeof window === 'undefined' ? falseFn : window.cancelAnimationFrame; |
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 guard here should also be more extensive, as window
might be defined, but it's members not.
const requestFn = typeof window === 'undefined' ? falseFn : window.requestAnimationFrame; | |
const cancelFn = typeof window === 'undefined' ? falseFn : window.cancelAnimationFrame; | |
const requestFn = typeof window === 'undefined' || typeof window.requestAnimationFrame === 'undefined' ? falseFn : window.requestAnimationFrame; | |
const cancelFn = typeof window === 'undefined' || typeof window.cancelAnimationFrame === 'undefined' ? falseFn : window.cancelAnimationFrame; |
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.
@jonkoops I don't think we should be that thorough here — IMO we should do the minimum possible change that makes the bundle itself execute without errors, disregarding how it will be used in such environment. Putting too much guards just bloats the code for cases no one will run into.
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.
Fair, just want to prevent that in this case things become polymorphic. In this case function
and undefined
.
src/dom/DomUtil.js
Outdated
@@ -78,7 +78,7 @@ export function getPosition(el) { | |||
return positions.get(el) ?? new Point(0, 0); | |||
} | |||
|
|||
const documentStyle = document.documentElement.style; | |||
const documentStyle = typeof document === 'undefined' ? {} : document.documentElement.style; |
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'd rather fall back to undefined
here than give the impression there is a valid object with a fallback, and then use a guard clause wherever documentStyle
is accessed.
@jonkoops I agree with the sentiment. All that this will do is waste some RAM in JS web servers, fail some checks, and produce nothing. On the other hand, I think of this the same way I think of old MSIE compatibility code: we don't need it for the "normal" use case, it shouldn't be there, it's ugly but, but makes things work in some minority scenario. |
In summary, I think we shouldn't anticipate any possible needs on this front by doing premature changes — instead let's accept the most minimal changes possible, as they look harmless and should cover immediate bases. Users who use Leaflet in SSR contexts can always follow-up with small fixes later, which can be reviewed separately. |
Ok, if the issues that would occur under Deno SSR are fixed this can be merged as far as I am concerned. |
Thanks for considering these changes ! 🥹 This is also a common discussion in packages such as Nuxt Leaflet which I work on. We do not exactly want to run Leaflet on the server, but just having it's instance loaded without throwing errors, so SSR environments can play with it (and most of the time, send it to the client on demand). I can try a kind of canary version of Leaflet on my module before merging if you want. |
PLease make this work :-) |
I accidentally closed this PR by adding the wanted changes ... I reopened it (#9385) I added the changes from @jonkoops and made it work with Deno. @Gugustinette can you please test the version of #9385? |
This PR is an alternative to #9316
It doesn't enforce nullish DOM access via lint, but, it does work 😅👍