esbuild icon indicating copy to clipboard operation
esbuild copied to clipboard

Useless repeated imports when using external modules

Open zaygraveyard opened this issue 3 years ago • 39 comments

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

zaygraveyard avatar Oct 20 '20 10:10 zaygraveyard

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.

evanw avatar Oct 22 '20 09:10 evanw

Related: #382

lxsmnsyc avatar Oct 23 '20 11:10 lxsmnsyc

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.

xinaesthete avatar Dec 16 '20 15:12 xinaesthete

Is there a workaround for this? I really don't want to use another bundler.

AlvinThorn008 avatar Dec 18 '20 21:12 AlvinThorn008

@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 of three) in a global THREE 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 avatar Dec 19 '20 21:12 xinaesthete

@xinaesthete Thanks for this. I'll see how I can integrate this into my project.

AlvinThorn008 avatar Dec 22 '20 09:12 AlvinThorn008

Has there been any update on this?

lucas-jones avatar Jun 01 '21 12:06 lucas-jones

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.

huvber avatar Jun 17 '21 14:06 huvber

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 ☹

andrewmunro avatar Jul 03 '21 14:07 andrewmunro

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.

osddeitf avatar Aug 28 '21 04:08 osddeitf

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.

xinaesthete avatar Sep 01 '21 11:09 xinaesthete

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.

neutraali avatar Oct 19 '21 14:10 neutraali

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?

specious avatar Nov 08 '21 18:11 specious

Have any update about this issues? I have same problem(duplicate import) too😅

izcream avatar Nov 14 '21 19:11 izcream

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

lmeyerov avatar Dec 04 '21 03:12 lmeyerov

Ah maybe false alarm in our case: https://github.com/facebook/react/issues/13991#issuecomment-435587809

lmeyerov avatar Dec 04 '21 08:12 lmeyerov

I would love an update on this too, I do have several libraries duplicated :(

Hideman85 avatar Dec 09 '21 19:12 Hideman85

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
};

iampava avatar Feb 10 '22 13:02 iampava

@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? :-)

prichodko avatar Mar 07 '22 12:03 prichodko

@evanw any updates on this or something that we can help with?

agcty avatar May 30 '22 07:05 agcty

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.

jaschaio avatar May 30 '22 08:05 jaschaio

I posted a workaround in https://github.com/remix-run/remix/issues/2987#issuecomment-1155106420

jaschaio avatar Jun 14 '22 12:06 jaschaio

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.

lxsmnsyc avatar Jun 15 '22 01:06 lxsmnsyc

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

jaschaio avatar Jun 15 '22 05:06 jaschaio

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 avatar Jun 15 '22 20:06 evanw

@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!

agcty avatar Jun 15 '22 21:06 agcty

Any progress ?

Valexr avatar Aug 23 '22 11:08 Valexr

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'), ...

FlatMapIO avatar Aug 27 '22 15:08 FlatMapIO

This was fixed for me in [email protected] https://github.com/remix-run/remix/issues/2987

jaschaio avatar Aug 27 '22 17:08 jaschaio

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

FlatMapIO avatar Aug 28 '22 01:08 FlatMapIO