I have a TypeScript project, and I use Webpack to bundle the code to single JavaScript file. I'm using ESM only. When I try to run the distribution file by running: node ./dist/index.js
I get this error:
const external_minimist_namespaceObject = require("minimist");
^
ReferenceError: require is not defined in ES module scope, you can use import instead
This file is being treated as an ES module because it has a '.js' file extension and 'package.json' contains "type": "module". To treat it as a CommonJS script, rename it to use the '.cjs' file extension.
Indeed, I configured "type": "module"
in the package.json
file.
I have this script to run the Webpack: "dist": "node --loader ts-node/esm node_modules/webpack-cli/bin/cli.js -c ./webpack.config.ts",
in my package.json
file.
This is my webpack.config.ts
file:
import path from 'node:path';
import fs from 'node:fs/promises';
import { fileURLToPath } from 'node:url';
import webpack from 'webpack';
import nodeExternals from 'webpack-node-externals';
import TsconfigPathsPlugin from 'tsconfig-paths-webpack-plugin';
import WebpackShellPluginNext from 'webpack-shell-plugin-next';
const packageJsonData = await fs.readFile('package.json', 'utf-8');
const packageJsonObject = JSON.parse(packageJsonData);
const version = packageJsonObject.version;
const __dirname = path.dirname(fileURLToPath(import.meta.url));
const configuration: webpack.Configuration = {
context: __dirname,
mode: 'production',
target: 'node',
entry: './src/index.ts',
// * https://mcmap.net/q/343992/-should-javascript-npm-packages-be-minified
optimization: { minimize: false },
externals: [nodeExternals({ modulesDir: path.join(__dirname, 'node_modules') })],
experiments: { outputModule: true },
module: {
rules: [
{
test: /\.ts$/,
use: [
{
loader: 'ts-loader',
options: { configFile: 'tsconfig.build.json' },
},
],
exclude: /node_modules/,
},
],
},
plugins: [
new WebpackShellPluginNext({
onBuildStart: {
scripts: ['rimraf dist'],
blocking: true,
},
safe: true,
}),
new webpack.DefinePlugin({
__PACKAGE_VERSION__: JSON.stringify(version),
}),
],
resolve: {
extensions: ['.ts'],
plugins: [
new TsconfigPathsPlugin({
configFile: './tsconfig.base.json',
extensions: ['.ts'],
}),
],
},
output: {
filename: 'index.js',
path: path.resolve(__dirname, 'dist'),
library: { type: 'module' },
chunkFormat: 'module',
},
};
export default configuration;
And this is my tsconfig.base.json
file which is the important one:
{
"extends": "./tsconfig.paths.json",
"compilerOptions": {
"target": "ES2018",
"module": "ESNext",
"moduleResolution": "bundler",
"noEmit": true,
"baseUrl": "./",
"allowJs": false,
"skipLibCheck": true,
"strict": true,
"forceConsistentCasingInFileNames": true,
"esModuleInterop": true,
"resolveJsonModule": true,
"isolatedModules": true,
"incremental": true,
"removeComments": true,
"allowUnreachableCode": false,
"allowUnusedLabels": false,
"alwaysStrict": true,
"noFallthroughCasesInSwitch": true,
"noImplicitAny": true,
"noImplicitOverride": true,
"noImplicitReturns": true,
"noImplicitThis": true,
"noPropertyAccessFromIndexSignature": true,
"noUncheckedIndexedAccess": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"typeRoots": ["./node_modules/@types", "./@types"]
}
}
Webpack completes successfully, but I cannot run the distribution file with NodeJS (node ./dist/index.js
). Why does the distribution file contain require
statements instead of import
to successfully run with ESM
?
src/index.ts
to reproduce it, like,import path from 'path'; import minim from 'minimist'; console.log(path, minim);
. I notice that for the output it has this for "path":__WEBPACK_EXTERNAL_createRequire(import.meta.url)("path")
, but somehow didn't do the same automatically for "minimist". (I have no idea how this happens, but hopefully this comment can help others figure out the reason) – Salutationnode_modules/webpack/lib/ExternalModule.js
. In the_getSourceData
method, when therequest = "minimist"
, it detects the external type asexternalType = "commonjs"
. Then it usesgetSourceForCommonJsExternal(request)
which in turn outputs the require. Node's path is treated differently becauseexternalType = "node-commonjs"
in this case. – Salutationreturn getSourceForCommonJsExternal(request);
so that it falls-through to the "node-commonjs". The replaced version also makes sense (whenthis.buildInfo.module
is true, it should usegetSourceForCommonJsExternalInNodeModule
), but then modifying a script innode_modules
means that it will go wrong when you re-install it (and it can easily go wrong when someone else tries to reproduce your project). I hope someone else has a better idea. – Salutation