esbuild: Incorrect import order with code splitting and multiple entry points
For the workaround, see https://github.com/evanw/esbuild/issues/399#issuecomment-1458680887.
Consider the following code:
// entry1.js
import "./init-dep-1.js";
import "./run-dep.js";
// entry2.js
import "./init-dep-2.js";
import "./run-dep.js";
// init-dep-1.js
global.foo = {
log: () => console.log("foo.log() (from entry 1) called"),
};
// init-dep-2.js
global.foo = {
log: () => console.log("foo.log() (from entry 2) called"),
};
// run-dep.js
global.foo.log();
When you bundle this code with esbuild --bundle --outdir=build --format=esm --splitting --platform=node --out-extension:.js=.mjs ./entry1.js ./entry2.js
, ESBuild discovers that run-dep.js
is a module shared between both entry points. Because code splitting is enabled, ESBuild moves it into a separate chunk:
// build/entry1.js
import "./chunk.P2RPFHF3.js";
global.foo = {
log: () => console.log("foo.log() (from entry 1) called")
};
However, by doing so, ESBuild puts run-dep.js
above the init-dep-1.js
or init-dep-2.js
code. This changes the import order – and breaks the code:
$ esbuild --bundle --outdir=build --format=esm --splitting --platform=node --out-extension:.js=.mjs ./entry1.js ./entry2.js
$ node build/entry1.mjs
global.foo.log();
^
TypeError: Cannot read property 'log' of undefined
About this issue
- Original URL
- State: open
- Created 4 years ago
- Reactions: 37
- Comments: 19 (8 by maintainers)
I’m deep in the middle of working on this at the moment. I just discovered an issue with my approach. It’s a problem similar to the one I discovered in #465: external imports (imports to code that is not included in the bundle) must not be hoisted past internal code or the ordering of the code changes, which is invalid. The problem is that this requires the generation of extra chunks instead of just generating a single chunk of code.
Consider this input file:
It would be invalid to transform that to something like this:
That reorders the side effects of the internal file past the side effects of the package. Instead you have to do something like this for correctness:
where
./chunk.HSYX7.js
is an automatically-generated chunk containing all of the code from before the external import. The external import means it is impossible to bundle this code into a single file if you care about import order. I don’t think you can replace this withawait import()
to get around this because there could be multiple external imports involved in a cycle that need to be able to reference each other’s exports.I have been just doing what other bundlers do so I haven’t considered this case before. But other bundlers such as Rollup appear to get this wrong too. This also gets massively more complicated with top-level await and different parallel execution graphs. Not sure what to do about this new level of complexity yet.
@tmcconechy Not sure about other cases, but
However, there’s actually a workaround for this issue (which we’re using at Framer as well). If you convince esbuild to code-split the code that lives in entrypoints, you’ll get a chance to order things correctly.
Like, let’s take the repro above. Right now, it throws an error. But if you add one more file:
and make that file the first entrypoint (first is important):
then esbuild will move
init-dep-1.js
andinit-dep-2.js
into separate chunks and import these chunks before therun-dep.js
chunk. As a result, the bundle will start working as expected:Note that you don’t actually need to load the produced
build/_entrypoints-workaround.mjs
– it’s only needed during the build, to convince esbuild thatinit-dep-1.js
andinit-dep-2.js
are used more than in one entrypoint.I just discovered that I think Rollup also has similar ordering bugs with code splitting. Here is an example (link):
With this input Rollup generates this code, which incorrectly causes the second entry point to throw:
I wonder if this is a known bug with Rollup or not. A quick search of existing issues didn’t uncover anything.
My plan for handling this in esbuild is to make code in non-entry point chunks lazily-evaluated (wrapped in callbacks) and then have the entry points trigger the lazy evaluation (call the callbacks) in the right order. This has the additional benefit of avoiding conflating the binding and evaluation phases in non-esm output formats. If evaluation is deferred, then all binding among chunks happens first followed by all evaluation afterward.
Another possible approach could be to generate many more output chunks for each unique atomic piece of code. But that seems like it’d be undesirable because it would generate too many output chunks. I would also still have to deal with the binding/evaluation issues for non-esm output formats if I went with that approach.
Also faced with a similar problem.
I have a code:
I build these files via command:
esbuild main.ts --bundle --format=esm --splitting --outdir=dist --platform=node --sourcemap
.I also tried
esbuild main.ts --bundle --outdir=dist --platform=node --sourcemap
. The result is the same, despite that resulting bundles are a little bit different.The resulting log is:
So the problem is that I do not have dotenv configured inside dependency, because dependency is initialized before. This is applicable to any other scenario with dependencies.
@evanw I just wanted to check in regarding the status of this issue. It’s been a while since we’ve heard any updates on it, and I’m curious if there are any plans to address this in the near future. As far as I can tell, it effectively prevents a lot of use-cases of splitting, especially when dealing with larger projects with many co-dependent imports. It also is worrying because it’s hard to know while you continue to develop on a project with splitting enabled, whether a new import will break the build.
I’m also curious about workarounds in the meantime. I found this reply as the most recent method to deal with potential issues with import order, but I’m not actually quite sure why it works. Could you expand on that, or provide a more general workaround for dealing with the ordering bug?
As always, thank you so much for continuing to work on this project.
https://github.com/parcel-bundler/parcel/blob/v2/packages/transformers/js/hoist.md is a quite detailed description of the Parcel changes
Looks like Parcel also faced this issue: https://github.com/parcel-bundler/parcel/issues/5659, it was fixed in https://github.com/parcel-bundler/parcel/pull/6230. It’s hard to detect which was the actual fix for this issue though, since that PR is really huge.