ReferenceError: require is not defined is ESM Node projects
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
- Check out / download Place at the following tag: https://github.com/small-tech/place/releases/tag/esbuild-issue-946
npm inpm 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.
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 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.
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')
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 That’s excellent, thank you :)
the most elegant solution I've found is via use of the
banneroption
@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);'
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 thisevaluator(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
createRequireis evaluating JavaScript in memory and thereforerequirebecomes 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.
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.
Just a heads up that the build was choking on the
\nfor 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 Thanks, Karl. (In my case, it was failing on Linux.)
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,
@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.
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 therequirestatement and throws a runtime error), the most elegant solution I've found is via use of thebanneroption: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
createRequireduring 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?
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;
`
},
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;
^