Webpack bundling emits "require" statements in the distribution file
Asked Answered
G

1

0

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?

Gin answered 12/4, 2023 at 17:57 Comment(4)
I created a simple content for 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)Salutation
I managed to find a reason in node_modules/webpack/lib/ExternalModule.js. In the _getSourceData method, when the request = "minimist", it detects the external type as externalType = "commonjs". Then it uses getSourceForCommonJsExternal(request) which in turn outputs the require. Node's path is treated differently because externalType = "node-commonjs" in this case.Salutation
We could technically manually delete line 556: return getSourceForCommonJsExternal(request); so that it falls-through to the "node-commonjs". The replaced version also makes sense (when this.buildInfo.module is true, it should use getSourceForCommonJsExternalInNodeModule), but then modifying a script in node_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
@Salutation there is on going thread regarding this issue here: github.com/webpack/webpack-cli/issues/3728Gin
G
1

The solution is to do as follows:

    externals: [
        nodeExternals({
            modulesDir: path.join(__dirname, 'node_modules'),
            importType: (moduleName) => `import ${moduleName}`,
        }),
    ],

This will inject import statements instead of requires

Gin answered 27/4, 2023 at 18:40 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.