esbuild icon indicating copy to clipboard operation
esbuild copied to clipboard

ReferenceError: require is not defined is ESM Node projects

Open aral opened this issue 4 years ago • 15 comments

In my ESM Node project, Place that also imports CommonJS modules (any any Node project these days probably does given ESM modules are not the norm yet), I get the following error when I run the bundle:

ReferenceError: require is not defined

If I add the following line to the top of the bundle to create the require function, it runs:

const require = createRequire(import.meta.url)

This is not a small, reproducible case but filing it anyway in case it helps without one.

To reproduce

  1. Check out / download Place at the following tag: https://github.com/small-tech/place/releases/tag/esbuild-issue-946
  2. npm i
  3. npm run build

Try to run the generated place.js with, e.g., ./place.js evanw.small-web.org and you should see it fail with the error above. If you add the line mentioned above, it should run.

aral avatar Mar 10 '21 13:03 aral

I ran into something similar to this when playing with const evaluator = new Function(code). I didn’t know you could do this but I found the technique here: https://github.com/digital-loukoum/esrun/blob/main/src/main.js#L63.

I don’t know how relevant this is for you but given I encountered the same error, I needed to do this: const evaluator = new Function("require", code) and then invoke the evaluator like this evaluator(require). This seemed hacky to me so I’ve since changed my implementation, but I thought it was worth sharing.

So my guess is what createRequire is evaluating JavaScript in memory and therefore require becomes undefined as a side-effect. This is my best guess as to what’s going on.

Edit: My point is that I don’t think this has anything to do w/ esbuild. I think this has to do with evaluating JavaScript programmatically / in-memory.

zaydek avatar Mar 10 '21 19:03 zaydek

@zaydek Thank you for the additional information… interesting. If that is the case, it doesn’t affect good ’ol eval() itself as JSDB uses it and that’s not creating a problem, so it must be limited to the new Function() method. Hmm…

Update: here’s a link to the relevant code in Node, in case it helps: https://github.com/nodejs/node/blob/831f4c755d2270e7b44c075176ab4a05698d21d9/lib/internal/modules/cjs/helpers.js#L48

Given that it’s a single line of code that needs to be added, I’m just going to inject that as part of my build process for the time being.

aral avatar Mar 10 '21 21:03 aral

My hacky workaround for the moment is running this after the esbuild script in the npm build task (the app is a CLI app, hence the hashbang at the start:

import fs from 'fs'
import path from 'path'
const __dirname = new URL('.', import.meta.url).pathname

const build = path.join(__dirname, '..', 'place.js')
const unpatchedBuild = fs.readFileSync(build, 'utf-8')
const patchedBuild = unpatchedBuild.replace('#!/usr/bin/env node', '#!/usr/bin/env node\n\nconst require = createRequire(import.meta.url)\n')
fs.writeFileSync(build, patchedBuild, 'utf-8')

aral avatar Mar 11 '21 11:03 aral

In case anyone else gets tripped up by this error when converting cjs to esm (where require('native-node-module or module-marked-as-external') preserves the require statement and throws a runtime error), the most elegant solution I've found is via use of the banner option:

esbuild.build({
  banner: {
    js: "import { createRequire as topLevelCreateRequire } from 'module';\n const require = topLevelCreateRequire(import.meta.url);"
  },
  ...
})

Note: it is a good idea to rename createRequire during import in case another module already imports the same, since esbuild will not be able to deduplicate the reference

popeindustries avatar Apr 07 '21 08:04 popeindustries

@popeindustries That’s excellent, thank you :)

aral avatar Apr 09 '21 08:04 aral

the most elegant solution I've found is via use of the banner option

@popeindustries amazing, thanks!

This can also be passed in on the command line:

esbuild index.ts --bundle --outfile=out.mjs --platform=node --target=node16.8 --format=esm  --banner:js='import { createRequire as topLevelCreateRequire } from \"module\"; const require = topLevelCreateRequire(import.meta.url);'

karlhorky avatar Sep 02 '21 16:09 karlhorky

I ran into something similar to this when playing with const evaluator = new Function(code). I didn’t know you could do this but I found the technique here: https://github.com/digital-loukoum/esrun/blob/main/src/main.js#L63.

I don’t know how relevant this is for you but given I encountered the same error, I needed to do this: const evaluator = new Function("require", code) and then invoke the evaluator like this evaluator(require). This seemed hacky to me so I’ve since changed my implementation, but I thought it was worth sharing.

So my guess is what createRequire is evaluating JavaScript in memory and therefore require becomes undefined as a side-effect. This is my best guess as to what’s going on.

Edit: My point is that I don’t think this has anything to do w/ esbuild. I think this has to do with evaluating JavaScript programmatically / in-memory.

I don't think it's related to the JS engine. require is defined for CJS modules only and you should use import in ESM modules instead (or use createRequire from module).

The side effect of ESM modules is that require becomes undefined. This is expected behavior.

Since Function requires text as the function's code, esbuild can't guess and convert any require to import. I'm sure esbuild replaced your evaluator(require) with evaluator(createRequire) when converting to ESM.


Unfortunately, this does not explain the problem with esbuild. Why isn't require defined? Probably because it is not using createRequire inside these ESM files.

We should take a better look at the output.

RealAlphabet avatar Nov 09 '21 06:11 RealAlphabet

This can also be passed in on the command line:

esbuild index.ts --bundle --outfile=out.mjs --platform=node --target=node16.8 --format=esm  --banner:js='import { createRequire as topLevelCreateRequire } from \"module\";\n const require = topLevelCreateRequire(import.meta.url);'

Just a heads up that the build was choking on the \n for me (wasn’t being substituted). Removing it worked.

aral avatar Jan 19 '22 14:01 aral

Just a heads up that the build was choking on the \n for me (wasn’t being substituted). Removing it worked.

Thanks, I've edited my comment above. I think the \n works on macOS, fails on Windows.

karlhorky avatar Jan 19 '22 15:01 karlhorky

@karlhorky Thanks, Karl. (In my case, it was failing on Linux.)

aral avatar Jan 19 '22 16:01 aral

This might be the answer for many, especially for those using ESM with AWS SDK v3.

https://github.com/aws/aws-sdk-js-v3/issues/4217#issuecomment-1356119155

bundling: {
	format: aws_lambda_nodejs.OutputFormat.ESM,
	mainFields: ["module", "main"],
},
architecture: aws_lambda.Architecture.ARM_64,

hanneswidrig avatar Jun 26 '23 14:06 hanneswidrig

@evanw It would be amazing if we could have a solution to the current errors that didn't require including a banner to separately define require and other node built-ins. That would improve experience in environments like AWS Lambdas where you'll get an error like Dynamic require of "buffer" is not supported when certain cjs dependencies pull in node built-ins.

hanneswidrig avatar Jun 29 '23 12:06 hanneswidrig

In case anyone else gets tripped up by this error when converting cjs to esm (where require('native-node-module or module-marked-as-external') preserves the require statement and throws a runtime error), the most elegant solution I've found is via use of the banner option:

esbuild.build({ banner: { js: "import { createRequire as topLevelCreateRequire } from 'module';\n const require = topLevelCreateRequire(import.meta.url);" }, ... }) Note: it is a good idea to rename createRequire during import in case another module already imports the same, since esbuild will not be able to deduplicate the reference

I tried this first:

banner: {
        js: "import { createRequire } from 'module'; const require = createRequire(import.meta.url);"   
    },

which came up as

SyntaxError: Identifier 'createRequire' has already been declared

So I tried yours next, and that worked. But the next error that now pops up is:

ReferenceError: __dirname is not defined in ES module scope

Does anyone have a banner for this as well for me?

mesqueeb avatar Mar 01 '25 07:03 mesqueeb

this is what fixed the issues for me:

    /** @see https://github.com/evanw/esbuild/issues/946#issuecomment-814703190 */
    banner: {
        js: `
import { createRequire as topLevelCreateRequire } from 'module';
import { fileURLToPath as topLevelFileURLToPath } from 'url';
import { dirname as topLevelDirname, join as topLevelJoin } from 'path';
const __bundleRequire = topLevelCreateRequire(import.meta.url);
const __bundleFilename = topLevelFileURLToPath(import.meta.url);
const __bundleDirname = topLevelDirname(__bundleFilename);
const __bundleJoin = topLevelJoin;

// Make these variables available globally for compatibility
globalThis.require = __bundleRequire;
globalThis.__filename = __bundleFilename;
globalThis.__dirname = __bundleDirname;
globalThis.join = __bundleJoin;
`
    },

mesqueeb avatar Mar 01 '25 07:03 mesqueeb

didn't work for me, it was giving a different error all together, when I used outdir it started the streak of another new error

"build": "npx esbuild ./src/index.ts --bundle --platform=node --outfile=dist/index.mjs --format=esm --banner:js='import { createRequire as topLevelCreateRequire } from \"module\"; const require = topLevelCreateRequire(import.meta.url);"
X [ERROR] Must use "outdir" when there are multiple input files

1 error
node:child_process:929
    throw err;
    ^

pantharshit007 avatar May 26 '25 19:05 pantharshit007