esbuild
esbuild copied to clipboard
Useless repeated imports when using external modules
Setup
npx esbuild --version
# 0.7.17
echo 'export const x = 1' > a.js
echo 'import {x} from "./a.js"; console.log(x);' > b.js
echo 'import {x} from "./a.js"; console.log(x);' > c.js
echo 'import "./b.js"; import "./c.js";' > d.js
Actual behavior
$ npx esbuild --bundle --format=esm --external:"`pwd`/a.js" d.js
// b.js
import {x} from "./a.js";
console.log(x);
// c.js
import {x as x2} from "./a.js";
console.log(x2);
And
$ npx esbuild --bundle --minify --format=esm --external:"`pwd`/a.js" d.js
import{x as o}from"./a.js";console.log(o);import{x as m}from"./a.js";console.log(m);
NB: x
is imported twice, once as x
and once as x2
Expected behavior
$ npx esbuild --bundle --format=esm --external:"`pwd`/a.js" d.js
// b.js
import {x} from "./a.js";
console.log(x);
// c.js
console.log(x);
And
$ npx esbuild --bundle --minify --format=esm --external:"`pwd`/a.js" d.js
import{x as o}from"./a.js";console.log(o);console.log(o);
NB: x
is imported only once
Thanks for reporting this. I have wanted to fix this for a while (and also the similar case for require
). I agree this isn't great but it's also not a correctness issue, so FYI I'm going to prioritize some other things over this. For example, I'm currently working on shipping the plugin API (#111) and fixing some correctness issues (#399 and #465) and work like that will take priority. It may take a while to get to this but I do want to fix it.
Related: #382
I noticed I have an absolutely huge (near 50mb) .map
file for a pretty trivial three.js
project. Indeed, the bundle itself is probably rather larger than necessary, but not enough to worry about... the source map is mind-boggling.
The project is here, esbuild
configuration in watch.js
.
I have to say that in most ways I find this library a joy to work with - very efficient and generally I find that the configuration options make sense etc. Not sure if I just missed something here.
Is there a workaround for this? I really don't want to use another bundler.
@AlvinThorn008 I partially work around this with a plugin specifically geared at adding a shim around my imports to three
. I won't suggest that it is a brilliantly scalable solution, but I'll probably look to expanding my configuration to do similar things for other specific libraries. It wouldn't really work on things imported from three/examples/whatever
, for example (although a more elaborated script could be concocted), and I'm sure various modules may have different ways in which they may need to be adapted, which may not be easy to formally test, etc.
It's made a massive difference to my proejct(s), though.
- I add a
<script>
tag to load it the old-fashioned way, resulting (in the case ofthree
) in a globalTHREE
object. - My build script runs in node where it calls
require('three')
and produces a string representing a module that exports each member by the same name. - A plugin declares a new namespace and loads my shim as appropriate.
The interface for writing plugins is liable to change, but it is still considerably less painful than using WebPack. Or rollup. Or any other bundler I've tried thus far. Sorry to the kind and diligent souls who maintain said projects.
// assuming that THREE is a global object,
// makes any imported reference to three proxy to that instead.
const threeShim = Object.keys(require("three")).map(k => `export const ${k} = window.THREE.${k}`).join('\n');
//https://esbuild.github.io/plugins/#using-plugins
const externaliseThreePlugin = {
name: 'three',
setup(build) {
build.onResolve({ filter: /^three$/ }, args => ({
path: args.path,
namespace: 'three-ns'
}));
build.onLoad({ filter: /.*/, namespace: 'three-ns' }, (args) => ({
contents: threeShim,
loader: 'js'
}));
}
}
I'd previously written that for another project, but you can see it in context in my current version of watch.js
linked above.
@xinaesthete Thanks for this. I'll see how I can integrate this into my project.
Has there been any update on this?
Is this in the roadmap somehow? we would like to adopt esbuild
but since we have different react packages we can't use it because it generate multiple instances of react.
Same issue here. Causes huge problems for projects with sensitive dependencies... wasm, three.js, react etc. Have to go back to other bundlers for now ☹
Is this in the roadmap somehow? we would like to adopt
esbuild
but since we have different react packages we can't use it because it generate multiple instances of react.
My way of avoiding the "multiple instances of react" is to use React CDN, then use inject
to add global React
.
Anyway, it's always a waste of bundle size when there's multiple duplicated imports.
vite
seems to be working fairly well with sane configuration, and running fast using esbuild
under the hood. I didn't get on so well with Snowpack
for some reason.
Using a CDN & inject
also seems like not a bad idea for a specific library like React. Indeed I haven't looked into it, but that sounds like something cleaner than my three shim.
Trying to inject React
globally just results in:
Uncaught SyntaxError: Identifier 'React' has already been declared
Yep, kinda stuck on how to proceed here. We're largely using cjs
and I dunno how feasible it would be to transform the entire codebase to satisfy esm
. We also have other global imports like jQuery
, Promise
, Immutable
and the like, and it seems like if every chunk (when using code splitting) gets fully injected with the import it seems like a huge increase in bundle sizes.
I've got an app that depends on the solid library. Building with esbuild produces a bundle with code sections like this:
// src/app/Main.jsx
init_define_config();
init_web();
init_web();
init_web();
init_solid();
init_dist();
// src/app/parts/Header.jsx
init_define_config();
init_web();
init_web();
init_web();
init_web();
init_web();
init_web();
init_web();
init_web();
// src/app/parts/InfoBox.jsx
init_define_config();
init_web();
init_web();
init_web();
// src/app/parts/ProfileMenu.jsx
init_define_config();
init_web();
init_web();
init_web();
init_web();
init_web();
init_web();
init_web();
init_web();
init_web();
I'm having some difficulty understanding exactly what is going on. Perhaps the JSX transform is producing duplicate imports and esbuild then doesn't consolidate them?
Have any update about this issues? I have same problem(duplicate import) too😅
Indeed, I think this is preventing us from packaging a react component library with esbuild and then being liberal in who can import, e.g., a webpack consumer is giving warnings of more than one copy of React
due to failing pointer equality checks on React
: https://reactjs.org/warnings/invalid-hook-call-warning.html . Hooks seem to require us to make React
external with pointer equality, which makes this bug an issue.
Am working on a minimal test case, but basically:
Library (esbuild):
"module": "dist/index.pure.esm.js",
"scripts": {
"build:esm:js": "esbuild --bundle src/index.js --sourcemap --target=es6 --loader:.js=jsx --format=esm --external:react --external:react-dom --outfile=dist/index.pure.esm.js",
},
"peerDependencies": {
"react": ">=16.8.6",
"react-dom": ">=16.8.6"
},
"devDependencies": {
"react": ">=17.0.2",
"react-dom": ">=17.0.2",
Consumer (CRA):
"dependencies": {
"@mylib": "file:/mylib",
"react": "^17.0.2",
"react-dom": "^17.0.2",
We want to support multiple consumers -- it's an OSS lib -- with at least cjs + esm as officially support. But right now can't get any combo of esbuild component => wp4 CRA consumer. Right now we are trying random --format
etc options to see if we can push anything through.
Edit: Why I think it may be this -- console.log(window.reactDomPatchReact == window.componentPatchReact, window.componentPatchReact == window.appPatchReact )
=> false, false
: React is loaded, but different references
Ah maybe false alarm in our case: https://github.com/facebook/react/issues/13991#issuecomment-435587809
I would love an update on this too, I do have several libraries duplicated :(
Any updates on this? 👀 I have the same issue when building a react component library:
// bundle.js
// src/Button/Button.tsx
import React from "react";
var Button = () => /* @__PURE__ */ React.createElement("p", null, "This is the Button");
// src/Avatar/Avatar.tsx
import React2 from "react";
var Avatar = () => /* @__PURE__ */ React2.createElement("p", null, "This is the avatar");
export {
Avatar,
Button
};
@evanw I saw you've mentioned that this issue has something to do with rewriting the linker that hasn't landed yet.
Is there an issue I can subscribe to in the meantime? :-)
@evanw any updates on this or something that we can help with?
Unless I am missing something or there is a workaround this basically makes it impossible to use esbuild for any react project that makes use of an internal component or common hooks library.
I posted a workaround in https://github.com/remix-run/remix/issues/2987#issuecomment-1155106420
I posted a workaround in remix-run/remix#2987 (comment)
I can't say if that's a workaround since it only works on a single environment.
I posted a workaround in remix-run/remix#2987 (comment)
I can't say if that's a workaround since it only works on a single environment.
This was based on an earlier comment in this issue, you should be able to adapt it to other environments within your esbuild config
Unless I am missing something or there is a workaround this basically makes it impossible to use esbuild for any react project that makes use of an internal component or common hooks library.
Two import statements for the same module does not cause any observable behavior changes (unless your JS runtime is seriously broken). So this is an issue for aesthetics and code size, but it should not impact correctness. If you do have an example case where it impacts correctness, please post it here.
@evanw we were able to pinpoint the issue now, seems like a react shim clashes with some imports. you're right, it doesn't affect correctness, sorry about all the confusion!
Any progress ?
I found in remix app that the generated build/index.js
file contains duplicate symbols, but require()
module is different package:
// a.tsx
var import_react = require('react'), ...
// b.tsx
var import_react = require('@remix-run/react'), ...
// c.tsx
var import_react2 = require('react'), ...
This was fixed for me in [email protected]
https://github.com/remix-run/remix/issues/2987
This was fixed for me in
[email protected]
remix-run/remix#2987
I observed this problem because I upgraded Remix from 1.6.x to 1.7 the day before yesterday