Home > Enterprise >  Make webpack resolve context dependencies of the follow kind: require(someVariable)
Make webpack resolve context dependencies of the follow kind: require(someVariable)

Time:11-24

I was trying to bundle a nestjs application to run it in a lambda. But gave up. After a while I tried to do the same with a freshly created nestjs application that connects to mysql. The problem was that sequelize requires mysql2 this way:

require(moduleName);

Well, it requires the dialect I'm asking it to require, but in this case it was mysql2. Apparently webpack can't do much about it on its own and bails out. I resolved it as they suggested. But then I thought, "I wonder if webpack can replace a specific line in a specific file?" Or rather pretend that it differs. Pretend that instead of require(moduleName) it reads require('mysql2').

There's a similar question, but I'm specifically targeting nodejs. And I'm concerned about what one might call a context dependency in webpack-speak. A dependency whose request is an expression, not a string.

ContextReplacementPlugin can't be applied here, because for a single identifier the request is always . (such requests are indistinguishible). NormalReplacementPlugin isn't applicable because that's a context dependency. And DefinePlugin doesn't seem to be up to the task, because it doesn't let you replace whatever you like, particularly local variables, and function parameters.

If you're interested in bundling a nodejs application, you might want to check out this question. Here I'm concerned with webpack and context dependencies.

P.S. Although I found a solution that doesn't require resolving context dependencies, I might run into it down the road. Or somebody else.

UPD And here's a case that can't be worked around like nestjs sequelize mysql2. sequelize-typescript loads models at runtime:

https://github.com/RobinBuschmann/sequelize-typescript/blob/v2.1.1/src/sequelize/sequelize/sequelize-service.ts#L51

CodePudding user response:

Disclaimer. The provided implementation might not work for you as is. You might need to amend it to work with your version of webpack. Also, I'm targeting nodejs (not a browser), as such I'm ignoring source maps.

Let's say you have src/index.js:

const mysql2 = require('whatever');

and the following packages:

{
  "dependencies": {
    "mysql2": "2.3.3",
    "webpack": "5.64.2",
    "webpack-cli": "4.9.1"
  }
}

and webpack.config.js:

const path = require('path');
const RewriteRequirePlugin = require('./rewrite-require-plugin');
module.exports = {
    mode: 'development',
    target: 'node',
    module: {
        rules: [
            // {test: path.resolve('src/index.js'),
            // use: [
            //     {loader: path.resolve('rewrite-require-loader.js'),
            //     options: {
            //         search: "'whatever'",
            //         replace: JSON.stringify('mysql2'),
            //     }},
            // ]}
        ],
    },
    plugins: [
        // new RewriteRequirePlugin([
        //     [path.resolve('src/index.js'),
        //         "'whatever'",
        //         JSON.stringify('mysql2')],
        // ])
    ],
    stats: {
        modulesSpace: Infinity,
        groupModulesByPath: false,
    }
};

It won't build. But if you uncomment the plugin or the loader it will.

rewrite-require-loader.js:

// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Regular_Expressions#escaping
function escapeRegExp(string) {
  return string.replace(/[.* ?^${}()|[\]\\]/g, '\\$&'); // $& means the whole matched string
}

function processFile(source, search, replace) {
    const re = `require\\(${escapeRegExp(search)}\\)`;
    return source.replace(
        new RegExp(re, 'g'),
        `require(${replace})`);
}

module.exports = function(source) {
    const options = this.getOptions();
    return processFile(source, options.search, options.replace);
};

rewrite-require-plugin.js:

const path = require('path');
const NormalModule = require('webpack/lib/NormalModule');

// https://github.com/webpack/loader-runner/blob/v4.2.0/lib/LoaderRunner.js#L9-L16
function utf8BufferToString(buf) {
    var str = buf.toString("utf-8");
    if(str.charCodeAt(0) === 0xFEFF) {
        return str.substr(1);
    } else {
        return str;
    }
}

// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Regular_Expressions#escaping
function escapeRegExp(string) {
  return string.replace(/[.* ?^${}()|[\]\\]/g, '\\$&'); // $& means the whole matched string
}

function processFile(source, search, replace) {
    source = Buffer.isBuffer(source) ? utf8BufferToString(source) : source;
    const re = `require\\(${escapeRegExp(search)}\\)`;
    return source.replace(
        new RegExp(re, 'g'),
        `require(${replace})`);
}

class RewriteRequirePlugin {
    constructor(rewrites) {
        this.rewrites = rewrites.map(r => [path.resolve(r[0]), r[1], r[2]]);
    }

    apply(compiler) {
        compiler.hooks.compilation.tap('RewriteRequirePlugin', compilation => {
            // https://github.com/webpack/webpack/blob/v5.64.2/lib/schemes/FileUriPlugin.js#L36-L43
            const hooks = NormalModule.getCompilationHooks(compilation);
            hooks.readResource
                .for(undefined)
                .tapAsync("FileUriPlugin", (loaderContext, callback) => {
                    const { resourcePath } = loaderContext;
                    loaderContext.addDependency(resourcePath);
                    loaderContext.fs.readFile(resourcePath, (err, data) => {
                        if (err) return callback(err, data);
                        callback(
                            err,
                            this.rewrites.reduce(
                                (prev, cur) =>
                                    resourcePath == cur[0]
                                        ? processFile(data, cur[1], cur[2])
                                        : data,
                                data));
                    });
                });
        });
    }
};

module.exports = RewriteRequirePlugin;
  • Related