esbuild icon indicating copy to clipboard operation
esbuild copied to clipboard

How to fix "Dynamic require of "os" is not supported"

Open enricoschaaf opened this issue 3 years ago • 77 comments

How can we use dependencies that still use require while shipping esm? Is that possible?

Minimal reproduction: https://github.com/enricoschaaf/esbuild-issue Just run node index.mjs. This was generated from running build which executes esbuild to bundle this file.

Problem: The problem is that node internal modules can of course not bet bundled so I expected the require syntax would get written to import syntax if the target is esm but they are still require.

Error message: Error: Dynamic require of "os" is not supported at file:///home/enrico/work/esbuild-issue/index.mjs:1:450 at file:///home/enrico/work/esbuild-issue/index.mjs:1:906 at file:///home/enrico/work/esbuild-issue/index.mjs:1:530 at file:///home/enrico/work/esbuild-issue/index.mjs:1:950 at ModuleJob.run (node:internal/modules/esm/module_job:195:25) at async Promise.all (index 0) at async ESMLoader.import (node:internal/modules/esm/loader:337:24) at async loadESM (node:internal/process/esm_loader:88:5) at async handleMainPromise (node:internal/modules/run_main:65:12)

enricoschaaf avatar Jan 07 '22 14:01 enricoschaaf

As far as I know, esbuild cannot convert external "static" commonjs require statements to static esm imports - not even for the side effect free node platform built in modules. This would be a useful feature for esbuild to have, as it's a fairly common use case for NodeJS applications and libraries. One could write an esbuild plugin for that, but a comprehensive solution would require reparsing each JS source file and the build would be considerably slower.

kzc avatar Jan 09 '22 17:01 kzc

Ok, I thought about writing one but I had the same reservations you had. But indeed that would be a super nice feature for node apps.

enricoschaaf avatar Jan 10 '22 22:01 enricoschaaf

I create a new issue #1927 and I think it may be related.

hronro avatar Jan 11 '22 10:01 hronro

Although this functionality would be better in esbuild itself, here's a super hacky script to transform node built-in __require()s in esbuild esm output to import statements. It only works with non-minified esbuild output.

$ cat importify-esbuild-output.js 
var fs = require("fs");
var arg = process.argv[2];
var data = !arg || arg == "-" ? fs.readFileSync(0, "utf-8") : fs.readFileSync(arg, "utf-8");;
var rx = /\b__require\("(_http_agent|_http_client|_http_common|_http_incoming|_http_outgoing|_http_server|_stream_duplex|_stream_passthrough|_stream_readable|_stream_transform|_stream_wrap|_stream_writable|_tls_common|_tls_wrap|assert|async_hooks|buffer|child_process|cluster|console|constants|crypto|dgram|diagnostics_channel|dns|domain|events|fs|http|http2|https|inspector|module|net|os|path|perf_hooks|process|punycode|querystring|readline|repl|stream|string_decoder|sys|timers|tls|trace_events|tty|url|util|v8|vm|wasi|worker_threads|zlib)"\)/gm;
var modules = new Map;
var out = data.replace(rx, function(req, mod) {
    var id = "__import_" + mod.toUpperCase();
    modules.set(mod, id);
    return id;
});
modules.forEach(function(val, key) {
    console.log("import %s from %s;", val, JSON.stringify(key));
});
console.log("\n%s", out);

Example input:

$ cat 0.js
console.log(require("path").extname("foo/bar.hello"));
var os = require("os");
console.log(require("path").extname("ok.world"));

Expected output:

$ cat 0.js | node
.hello
.world

Unmodified esbuild esm bundle output:

$ cat 0.js | esbuild --bundle --platform=node --format=esm
var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require : typeof Proxy !== "undefined" ? new Proxy(x, {
  get: (a, b) => (typeof require !== "undefined" ? require : a)[b]
}) : x)(function(x) {
  if (typeof require !== "undefined")
    return require.apply(this, arguments);
  throw new Error('Dynamic require of "' + x + '" is not supported');
});

// <stdin>
console.log(__require("path").extname("foo/bar.hello"));
var os = __require("os");
console.log(__require("path").extname("ok.world"));

...piped through importify-esbuild-output.js:

$ cat 0.js | esbuild --bundle --platform=node --format=esm | node importify-esbuild-output.js 
import __import_PATH from "path";
import __import_OS from "os";

var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require : typeof Proxy !== "undefined" ? new Proxy(x, {
  get: (a, b) => (typeof require !== "undefined" ? require : a)[b]
}) : x)(function(x) {
  if (typeof require !== "undefined")
    return require.apply(this, arguments);
  throw new Error('Dynamic require of "' + x + '" is not supported');
});

// <stdin>
console.log(__import_PATH.extname("foo/bar.hello"));
var os = __import_OS;
console.log(__import_PATH.extname("ok.world"));

... and then piped through esbuild again to perform some DCE:

$ cat 0.js | esbuild --bundle --platform=node --format=esm | node importify-esbuild-output.js | esbuild --bundle --platform=node --format=esm
// <stdin>
import __import_PATH from "path";
console.log(__import_PATH.extname("foo/bar.hello"));
console.log(__import_PATH.extname("ok.world"));

to produce:

$ cat 0.js | esbuild --bundle --platform=node --format=esm | node importify-esbuild-output.js | esbuild --bundle --platform=node --format=esm | node --input-type=module
.hello
.world

kzc avatar Jan 12 '22 00:01 kzc

In addition to the workaround above:

esbuild actually will print a warning when converting require to esm format (see it live) like:

▲ [WARNING] Converting "require" to "esm" is currently not supported

    <stdin>:1:0:
      1 │ require('a')
        ╵ ~~~~~~~

Maybe we can use that info to write a plugin: https://gist.github.com/hyrious/7120a56c593937457c0811443563e017

One note: the modification actually turns a commonjs module into mixed module, however it seems esbuild can handle this case correctly:

import xxx from 'xxx'
module.exports = xxx

hyrious avatar Jan 13 '22 02:01 hyrious

I'm getting this with require("buffer/") and from what I can gather, libs use the trailing slash to bypass core module and lookup in node_modules folder. I believe this is because they provide the polyfill for the browser as part of the package.

For now, I've added @esbuild-plugins/node-globals-polyfill and @esbuild-plugins/node-modules-polyfill, and then patched the packages to use require("buffer") so that they reference the esbuild polyfill instead but wondering if esbuild should be able to handle trailing slashes here?

This is all a bit over my head tbh, so apologies in advance if I'm not being clear 😅

jjenzz avatar Jan 15 '22 15:01 jjenzz

require("buffer/") is a different issue and appears to be working as intended - see https://github.com/evanw/esbuild/commit/b2d7329774fd9c42cb5922a9b5825498b26a28f3.

kzc avatar Jan 16 '22 15:01 kzc

@kzc in my case, it seems the trailing slash is being used to lookup a buffer polyfill for the browser so i am getting this error client-side when --platform=browser.

jjenzz avatar Jan 17 '22 13:01 jjenzz

@jjenzz If you're seeing a bug you should probably open a separate issue for that with a reproducible test case, as it doesn't appear to be related to this issue. This issue is concerned with --format=esm --platform=node bundles producing require() instead of import for NodeJS built-in modules. Your issue appears to be related to a NodeJS module polyfill not being inlined into your bundle for --format=esm --platform=browser.

$ rm -rf node_modules/buffer
$ echo 'console.log(require("buffer/"));' | esbuild --bundle --format=esm | node --input-type=module
✘ [ERROR] Could not resolve "buffer/"

    <stdin>:1:20:
      1 │ console.log(require("buffer/"));
        ╵                     ~~~~~~~~~

  You can mark the path "buffer/" as external to exclude it from the bundle, which will remove this
  error. You can also surround this "require" call with a try/catch block to handle this failure at
  run-time instead of bundle-time.

1 error
$ mkdir -p node_modules/buffer
$ echo "module.exports = {abc: 42};" > node_modules/buffer/index.js
$ echo 'console.log(require("buffer/"));' | esbuild --bundle --format=esm | node --input-type=module
{ abc: 42 }
$ echo 'console.log(require("buffer/"));' | esbuild --bundle --format=esm
var __getOwnPropNames = Object.getOwnPropertyNames;
var __commonJS = (cb, mod) => function __require() {
  return mod || (0, cb[__getOwnPropNames(cb)[0]])((mod = { exports: {} }).exports, mod), mod.exports;
};

// node_modules/buffer/index.js
var require_buffer = __commonJS({
  "node_modules/buffer/index.js"(exports, module) {
    module.exports = { abc: 42 };
  }
});

// <stdin>
console.log(require_buffer());

Notice that require("buffer/") was inlined as expected once node_modules/buffer/ was in place. node --input-type=module would have errored out had it encountered a require().

The trailing backslash did not appear to matter for the browser platform if node_modules/buffer existed - identical esbuild output was produced:

$ cat node_modules/buffer/index.js 
module.exports = {abc: 42};
$ echo 'console.log(require("buffer/"));' | esbuild --bundle --format=esm --platform=browser | shasum
a4d2d5027a1ab792dcf6bfb6f0ddc63e273a2b11  -
$ echo 'console.log(require("buffer"));' | esbuild --bundle --format=esm --platform=browser | shasum
a4d2d5027a1ab792dcf6bfb6f0ddc63e273a2b11  -
$ echo 'console.log(require("buffer"));' | esbuild --bundle --format=esm --platform=browser | node --input-type=module
{ abc: 42 }
$ echo 'console.log(require("buffer/"));' | esbuild --bundle --format=esm --platform=browser | node --input-type=module
{ abc: 42 }

kzc avatar Jan 17 '22 15:01 kzc

@kzc thanks for all of that. I'll see if I can create a minimal repro of the issue I am experiencing and create a separate issue if so.

jjenzz avatar Jan 19 '22 11:01 jjenzz

Related: https://stackoverflow.com/questions/68423950/when-using-esbuild-with-external-react-i-get-dynamic-require-of-react-is-not-s?rq=1 It has no solution either

eric-burel avatar Jan 21 '22 10:01 eric-burel

To share some details about impact, it looks like this is:

  • blocking Netlify's migration to ESM: https://answers.netlify.com/t/netlify-serverless-typescript-functions-fail-with-dynamic-requires/52379/13
  • blocking Shopify from dropping CJS support: https://github.com/Shopify/hydrogen/issues/1155

benmccann avatar Feb 25 '22 17:02 benmccann

FYI, I created a PR that addresses this issue for Node built-ins specifically: https://github.com/evanw/esbuild/pull/2067

eduardoboucas avatar Mar 01 '22 00:03 eduardoboucas

@eduardoboucas @jjenzz @kzc @enricoschaaf hey do you have any idea why this same Dynamic require of {module} is not supported error is coming up even when I've specified --format=iife --platform=browser in my esbuild command? My webpage uses a file that has the umd snippet at the beginning, and trying to minify it with esbuild causes this error to come up. I can see that esbuild is inserting a check for require at the start of the file, but I'm not sure why or how to prevent that. My full command is

esbuild test/**/*.js --bundle --minify --tree-shaking=false --format=iife --platform=browser --external:@most --outdir=./build

and the full error is Dynamic require of "@most/prelude" is not supported

Any ideas?

CodeWithOz avatar Mar 31 '22 18:03 CodeWithOz

This is breaking on AWS lambda with crypto module

{
  "errorType": "Error",
  "errorMessage": "Dynamic require of \"crypto\" is not supported",
  "trace": [
    "Error: Dynamic require of \"crypto\" is not supported",
    "    at file:///var/task/dbStreamHandler.js:29:9",
    "    at ../node_modules/.pnpm/[email protected]/node_modules/uuid/dist/rng.js (file:///var/task/dbStreamHandler.js:10354:42)",
    "    at __require2 (file:///var/task/dbStreamHandler.js:44:50)",
    "    at ../node_modules/.pnpm/[email protected]/node_modules/uuid/dist/v1.js (file:///var/task/dbStreamHandler.js:10439:39)",
    "    at __require2 (file:///var/task/dbStreamHandler.js:44:50)",
    "    at ../node_modules/.pnpm/[email protected]/node_modules/uuid/dist/index.js (file:///var/task/dbStreamHandler.js:10822:37)",
    "    at __require2 (file:///var/task/dbStreamHandler.js:44:50)",
    "    at ../node_modules/.pnpm/@[email protected]/node_modules/@aws-sdk/middleware-retry/dist-cjs/StandardRetryStrategy.js (file:///var/task/dbStreamHandler.js:10930:18)",
    "    at __require2 (file:///var/task/dbStreamHandler.js:44:50)",
    "    at ../node_modules/.pnpm/@[email protected]/node_modules/@aws-sdk/middleware-retry/dist-cjs/AdaptiveRetryStrategy.js (file:///var/task/dbStreamHandler.js:11023:35)"
  ]
}

any workarounds?

ShivamJoker avatar Jun 01 '22 12:06 ShivamJoker

@ShivamJoker we have a dependency that references os but doesn't get used in our code. We worked around it with this in our package.json:

{
  "browser": {
    "os": "os-browserify",
  },
}

geoffharcourt avatar Jun 01 '22 18:06 geoffharcourt

@ShivamJoker Have you found a solution for this? Experiencing the same issue with AWS lambda.

florianbepunkt avatar Jun 03 '22 23:06 florianbepunkt

For AWS Lambda with serverless-esbuild plugin this config work for me

esbuild:
  minify: true
  platform: node
  target: esnext
  sourcemap: true
  sourcesContent: false
  format: esm
  outExtension:
    .js: .mjs
  banner:
    js: import { createRequire } from 'module';const require = createRequire(import.meta.url);
`

fospitia avatar Jun 11 '22 09:06 fospitia

@fospitia can you please share more details about how you are using this config with esbuild?

ShivamJoker avatar Jun 11 '22 18:06 ShivamJoker

@ShivamJoker the cause of the bug is that function 'require' not exists in an ES module of nodejs. The banner option add the 'require' function at the beginning of generated file.

If you use the serverless framework with the serverles-esbuild plugin use below config.

esbuild:
  minify: true
  platform: node
  target: esnext
  sourcemap: true
  sourcesContent: false
  format: esm
  outExtension:
    .js: .mjs
  banner:
    js: import { createRequire } from 'module';const require = createRequire(import.meta.url);

With esbuid API you canuse the BuildOptions below

const buildOptions = {
  bundle: true,
  minify: true,
  format: "esm",
  target: "esnext",
  platform: "node",
  banner: {
    js: 'import { createRequire } from 'module';const require = createRequire(import.meta.url);'
  },
  outExtension: {
    .js: '.mjs'
  }
};
esbuild(buildOptions);

This configs work with AWS Lambda with node16 runtime.

fospitia avatar Jun 11 '22 20:06 fospitia

Thanks @fospitia I got it to work. Here is my full script file which I am importing in CDK

const { build } = require("esbuild");
const { sync } = require("fast-glob");
const { join } = require("path");

const paths = sync("**/*.ts", {
  cwd: join(process.cwd(), "./lambda"),
  absolute: true,
});

// console.log(paths);

console.log("Bundling lambdas to ESM modules");

build({
  absWorkingDir: process.cwd(),
  bundle: true,
  logLevel: "info",
  entryPoints: paths,
  outdir: join(process.cwd(), "./lambda/dist"),
  minify: true,
  format: "esm",
  target: "esnext",
  platform: "node",
  banner: {
    js: "import { createRequire } from 'module';const require = createRequire(import.meta.url);",
  },
  outExtension: {
    ".js": ".mjs",
  },
})
  .catch(() => process.exit(1));

ShivamJoker avatar Jun 25 '22 13:06 ShivamJoker

@ShivamJoker the cause of the bug is that function 'require' not exists in an ES module of nodejs. The banner option add the 'require' function at the beginning of generated file.

If you use the serverless framework with the serverles-esbuild plugin use below config.

esbuild:
  minify: true
  platform: node
  target: esnext
  sourcemap: true
  sourcesContent: false
  format: esm
  outExtension:
    .js: .mjs
  banner:
    js: import { createRequire } from 'module';const require = createRequire(import.meta.url);

With esbuid API you canuse the BuildOptions below

const buildOptions = {
  bundle: true,
  minify: true,
  format: "esm",
  target: "esnext",
  platform: "node",
  banner: {
    js: 'import { createRequire } from 'module';const require = createRequire(import.meta.url);'
  },
  outExtension: {
    .js: '.mjs'
  }
};
esbuild(buildOptions);

This configs work with AWS Lambda with node16 runtime.

thanks man, saved us !!!

ionscorobogaci avatar Jul 28 '22 10:07 ionscorobogaci

it worked without addtional import

import { build } from "esbuild";

/** @type {import('esbuild').BuildOptions} */
const options = {
    minify: true,
    bundle: true,
    target: "node16",
    banner: {
        js: `
import { createRequire } from 'module';
import path from 'path';
import { fileURLToPath } from 'url';
const require = createRequire(import.meta.url);
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
`
    },
    platform: "node",
    format: "esm",
  };

const funcDir = ["HttpTrigger1"]

for (const dir of funcDir) {
    /** @type {import('esbuild').BuildOptions} */
    const opts = {
        ...options,
        entryPoints: [`./${dir}/index.ts`],
        outfile: `./dist/${dir}/index.mjs`,
    }

    build(opts).catch((err) => {
        process.stderr.write(err.stderr);
        process.exit(1);
    });
}

arukiidou avatar Aug 30 '22 23:08 arukiidou

I think esbuild could automatically add --banner:js="import {createRequire} from 'module';const require=createRequire(import.meta.url);" when --platform is node. It's needed for pretty much all dependencies that require node core modules.

silverwind avatar Sep 23 '22 05:09 silverwind

I don't know where else to put this same but I'm getting this error with react while attempting to build a component library. I'm using tsup and this is my config file

import { defineConfig } from "tsup";

export default defineConfig((options) => {
   return {
      entry: ["src/index.ts"],
      format: ["cjs", "esm"],
      dts: true,
      external: ["react"],
      minify: !options.watch
   };
});

Removing the external entry fixes the problem but that's not an option.

Adding a banner as someone suggested above did not fix the issue and instead it brought out another error saying

✘ [ERROR] Expected "}" but found "file"

    tsup.config.ts:12:80:
      12 │ ...obal.require = createRequire("file:///home/myusername/apath...
         │                                  ~~~~
         ╵                                  }

I can't find anything else about these errors so I have no idea how to fix them...

milovangudelj avatar Oct 17 '22 20:10 milovangudelj

I have a local script file that I want to import but I am getting the same error. I am not sure why it must be so difficult to import some external script using the import keyword in the browser. The functionality is already built into the browser.

bluebrown avatar Nov 07 '22 19:11 bluebrown

For ESM, node16, this one should address the problem well, even when combining builds with the "same" banner and including support for handling subsequent import/requires in 3rd party libs of user code and such cases...:

import { BuildOptions } from "esbuild";

/** quasi-random identifier generator, good for less than 10k values in series */
export const getRandomBase32Identifier = (length: number = 9) => 
    `i${Math.random().toString(36).substring(2, length)}`
    
export const getEsmCjsCompatOptions = (): BuildOptions => {
    const dirnameAlias = getRandomBase32Identifier();
    const pathToFileURLAlias = getRandomBase32Identifier();
    const createRequireAlias = getRandomBase32Identifier();
    const esmCjsCompatScript = `
        import { pathToFileURL as ${pathToFileURLAlias} } from 'url'; 
        import { createRequire as ${createRequireAlias} } from 'module';
        import ${dirnameAlias} from 'es-dirname'; 
        import.meta.url = ${pathToFileURLAlias}(${dirnameAlias}());
        const require = ${createRequireAlias}(import.meta.url);
   `
    
    return {
        target: 'node16',
        platform: "node",
        format: "esm",
        banner: {
            js: esmCjsCompatScript
        }
    }
}

This implementation makes sure that when userland code imports dirname, etc. top-level, it wouldn't break with "duplicate identifier" error, as all identifiers are randomly generated, which is an issue with the import {createRequire} from 'module';const require=createRequire(import.meta.url); approach. Someone imports "createRequire" in userland code and it breaks. Also: depending on the runtime, import.meta.url won't be defined. That's why the 3rd party module es-dirname is used, which uses a hack to retrieve the dirname by parsing an artificially thrown Node.js stacktrace... (yeah yeah.. I know... what the heck...)

Apply it to your esbuild bundle config as you like:

import { build } from "esbuild"

build({
    ...getEsmCjsCompatOptions(),
    ...your other options here...
}) 

krymel avatar Jan 06 '23 18:01 krymel

This should not be closed. I had to hack it like this:

const sharedConfig =
{
  entryPoints: ['src/main/cli.ts'],
  bundle: true,
  //minify: true,
  sourcemap: 'linked',
  target: ['esnext'],
  platform: 'node',
  external: Object.keys(dependencies),
}

console.log('compile start')

// build the code - esm style
console.log(buildSync({
  ...sharedConfig,
  format: 'esm',
  outfile:'dist/main.esm.js',
  banner: {
    js:`import { createRequire } from 'module';const require = createRequire(import.meta.url);`
  }
}))

I couldn't believe it, but it actually solved the problem.

There should be a parameter that says:

makeItWorkInNode:true

that does this without me having to blindly hack like this. Aside from this, absolutely love the tool!

jason-henriksen avatar Jan 25 '23 05:01 jason-henriksen

I think a good "fix" for this issue would be to show the offending dependency. Adding that dependency to external is the next step to solve the problem for the user.

haikyuu avatar Jan 28 '23 20:01 haikyuu

Folks using AWS JS SDK V2 ("aws-sdk") running into this might consider switching to the V3 JS SDK (e.g. "@aws-sdk/client-dynamodb") as a solution

(I realize there is work in that, just wanted to mention it as an option)

ottokruse avatar Feb 17 '23 15:02 ottokruse