Skip to content
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

Using --format=esm and --external:package produces code containing require() calls for package #566

Closed
remorses opened this issue Nov 29, 2020 · 9 comments

Comments

@remorses
Copy link
Contributor

Using --format=esm and --external:package produces code containing require() calls for package

I would like to have require calls on the external package to be converted to ES imports instead of require calls, to make the bundle valid ESM code

Reproduction: https://github.com/remorses/reproduction-esbuild-require-in-esm-with-externals

@evanw
Copy link
Owner

evanw commented Nov 30, 2020

This transformation isn't done automatically because it's impossible in the general case to preserve the semantics of the original code when you do this. It's a lossy transformation. Evaluation order would be changed and conditional imports would be changed into unconditional imports.

One way to fix this if you're running the code in node is to bring back the require function before your code is run. It's unfortunate that the require function is missing in node by default. You can get it back by using a stub file like this to load your code:

import { createRequire } from 'module'
global.require = createRequire(import.meta.url)
import('./your-code.mjs')

If you are ok with changing the semantics of your code, another way to work around this is to use a plugin to convert require calls to import statements:

const externalCjsToEsmPlugin = external => ({
  name: 'external',
  setup(build) {
    let escape = text => `^${text.replace(/[-\/\\^$*+?.()|[\]{}]/g, '\\$&')}$`
    let filter = new RegExp(external.map(escape).join('|'))
    build.onResolve({ filter: /.*/, namespace: 'external' }, args => ({
      path: args.path, external: true
    }))
    build.onResolve({ filter }, args => ({
      path: args.path, namespace: 'external'
    }))
    build.onLoad({ filter: /.*/, namespace: 'external' }, args => ({
      contents: `export * from ${JSON.stringify(args.path)}`
    }))
  },
})

You would use it like this:

require('esbuild').build({
  bundle: true,
  outdir: 'bundle',
  format: 'esm',
  target: 'es2017',
  entryPoints: ['./src/index.ts'],
  plugins: [externalCjsToEsmPlugin(['react', 'react-dom'])],
})

@lxsmnsyc
Copy link

Interesting, although the package in the repro (react-storage-hooks) doesn't have an ESM bundle and so esbuild probably fallbacks to its only dist format which is the CJS format?

@remorses
Copy link
Contributor Author

I tried the plugin solution here and it works well

My use case was creating a bundle without any dependency apart from peer dependencies

@buronnie
Copy link

buronnie commented Feb 9, 2021

Hi @evanw , could you elaborate how this plugin can "convert require calls to import statements"? I thought it would change

const URI = require('uri-js');

to

import URI from 'uri-js';

but the plugin seems to only change the content of the dependency lib to export statement. I don't quite understand how this would help to solve the problem in this thread. Thanks!

@remorses
Copy link
Contributor Author

remorses commented Feb 9, 2021

@buronnie, basically esbuild replaces the lib contents with an esm export from the external package, this means that there won't be any require calls in the bundle

If you want to try this yourself i published a plugin to do this https://github.com/remorses/esbuild-plugins/tree/master/esm-externals

@buronnie
Copy link

Thank you @remorses. This works well for both "require cjs module" and "import es module". But it doesn't work for "import cjs module" (e.g. import _ from lodash). The reason i need to consider different cases is because i dynamically traverse the dep tree and whenever an external lib is found, i will bundle it separately. I wonder if onResolve callback can provide info about how module is imported (with import or require), so at least i can apply this solution to only require statement.

@buronnie
Copy link

I also notice that this plugin doesn't seem to work for lodash.

const lodash = require('lodash');
console.log(lodash.upperCase('abc'));

will be transpiled to

import * as default2 from "lodash";
import * as lodash_star from "lodash";
var require_lodash = __commonJS((exports) => {
  __markAsModule(exports);
  __export(exports, {
    default: () => default2
  });
  __exportStar(exports, lodash_star);
});

// src/index.js
var lodash = require_lodash();
console.log(lodash.upperCase("abc"));

instead lodash.default.default.upperCase("abc") will work.

@aral
Copy link

aral commented Feb 27, 2021

Just a note that this also affects requires of Node standard libraries in builds where --platform=node --target=node.

@forthealllight
Copy link

I also notice that this plugin doesn't seem to work for lodash.

const lodash = require('lodash');
console.log(lodash.upperCase('abc'));

will be transpiled to

import * as default2 from "lodash";
import * as lodash_star from "lodash";
var require_lodash = __commonJS((exports) => {
  __markAsModule(exports);
  __export(exports, {
    default: () => default2
  });
  __exportStar(exports, lodash_star);
});

// src/index.js
var lodash = require_lodash();
console.log(lodash.upperCase("abc"));

instead lodash.default.default.upperCase("abc") will work.

I have the same problem. Absolutely, it's " instead lodash.default.upperCase("abc") will work" ,but I want " "lodash.upperCase("abc") " . . how to achieve it .

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

6 participants