-
Notifications
You must be signed in to change notification settings - Fork 196
Description
Hi folks! Thanks for all of your hard work on Plot, I've really been enjoying using the library and learning more about the philosophy behind its design! This bug report comes from an issue I encountered while building a small Next.js application using some of the APIs from Plot.
Minimal Reproduction
To make the bug easier to see, I created a minimal reproduction. The README has instructions for how to run the project and see the errors; getting going just involves running two yarn commands. Let me know if anything is unclear in how to get the reproduction working.
Bug Description
When serving a production build of the minimal reproduction above, we hit the following runtime error:
ReferenceError: can't access lexical declaration 'd' before initialization (Firefox)
ReferenceError: Cannot access 'd' before initialization (Chrome)
After doing some digging using production source maps, I believe the core issue is that Plot is trying to extend from the Mark class in facet.js before Mark has been declared in the built Webpack chunk.
Running a development build of this project does not throw any runtime errors.
Stack Trace
The stack trace of the reported error is as follows (Chrome):
ReferenceError: Cannot access 'd' before initialization
at Object.vc (497-35515fda497bb06a.js:1:665)
at Object.27 (facet.js:13:21)
at r (bootstrap:21:23)
at Object.2186 (497-35515fda497bb06a.js:1:1422)
at r (bootstrap:21:23)
at Object.3962 (histogram-6827f696cfad69e9.js:1:297)
at r (bootstrap:21:23)
at (index):5:16
at route-loader.js:236:51
Following the stack trace is a bit difficult since the production build is minified. However, I did turn on source maps using productionBrowserSourceMaps: true in next.config.js in the reproduction. This is what allows us to trace that the error arose from L13, C21 in facet.js.
What does the stack trace suggest?
L13, C21 of facet.js pertains to the Mark class imported from mark.js. The issue appears to be that, at the time class Facet is being initialized to extend Mark, Mark itself has not yet been initialized. This is what leads to the ReferenceError. d in the minified build pertains to class Mark.
These types of ReferenceErrors in production Webpack builds tend, based on this comment from Webpack's core author Tobias Koppers, to be caused by cyclic dependencies.
Discussion
Does @observablehq/plot have cyclic dependencies?
Yes. Adding the no-cycle rule from eslint-plugin-import to @observablehq/plot resulted in 52 detected cycles.
To be clear, cyclic dependencies are allowed by the ESM spec; they can just (under some circumstances) lead to these kinds of temporal deadzone errors in Webpack. In addition, tree shaking in Webpack makes this even more tricky to track down, because, depending on which parts of the Plot API you use, the chunk Webpack generates for @observablehq/plot will resolve these cycles in different orders. This means you may use a set of APIs that result in a perfectly fine production build with no runtime errors.
What could be done to alleviate this error?
The solution with the highest probability of success would be to eliminate all cycles from the source. However, this is likely prohibitively expensive due to how many cycles currently exist and the challenges associated with such large-scale refactoring.
The specific error here appears to be the result of the following cycle:
facet.jsimportsMarkfrommark.jsmark.jsimportsplotfromplot.jsplot.jsimportsfacetsfromfacets.js
Resolving this cycle could help to alleviate this problem, but I'm not fully certain. It appears to be the most likely candidate based on the stack trace, but I have noticed that this error appears to be mark type dependent. For example, in another project where I use the Plot.barY and Plot.dot marks APIs in the same project, I don't encounter this error. This suggests that the troublesome cycle may be related to some of the transforms APIs (e.g. binX, normalizeX) that the histogram and strip plots use.
Additional Snags
One interesting note is that, if you remove either of the charts in the minimal reproduction (e.g. by deleting histogram.js or strip-plot.js), the error disappears on the page that is left behind. I'm still trying to understand how this affects the resolution of cycles in the built chunk that includes the Plot source.
Final Thoughts
I recognize this may not be a high priority fix for the team, and that using Plot in Webpack-based applications in general may be outside of the core use case of the library. It also may well be that this bug is better directed to the Next or Webpack teams to address; I'm happy to escalate it up to them if you all agree. I mostly opened this bug because I spent a fair bit of time (out of my own curiosity) hunting down its origins myself and wanted to share in case it's helpful to the team, or in the event that someone else encounters this same issue. I'd be happy to try to come up with a draft solution as well, it may just be quite slow with the semester about to get underway for me.
Again, thank you for all the hard work on Plot and I hope to keep contributing!
