-1

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

x-yuri
  • 16,722
  • 15
  • 114
  • 161
  • Yes, there is. And the webpacks docs cover how to do that, so you should be able to just look that up. See the plugin-related answer on [How can I replace files at compile time using webpack?](https://stackoverflow.com/questions/51338574/how-can-i-replace-files-at-compile-time-using-webpack) – Mike 'Pomax' Kamermans Nov 20 '21 at 01:56
  • @Mike'Pomax'Kamermans There's a difference between replacing `require('some string')` and `require(someVar)`. The title might be improved probably though. – x-yuri Nov 21 '21 at 11:06
  • Can you be more specific such as how you decide what to import and if it's at compile time or run time? Also if you're really using require and not es6 imports and why. – Dominic Nov 21 '21 at 12:35
  • @Dominic I want to replace `require(moduleName)` in [`node_modules/sequelize/lib/dialects/abstract/connection-manager.js`](https://github.com/sequelize/sequelize/blob/v6.11.0/lib/dialects/abstract/connection-manager.js#L74) with `require('mysql2')` (since I'm using a single `mysql` database). Or rather make `webpack` think the line reads `require('mysql2')`, not `require(packageName)`. – x-yuri Nov 21 '21 at 12:46
  • Question though: bundles are for clients. Why would your client code ever need to directly talk to your database? – Mike 'Pomax' Kamermans Nov 21 '21 at 15:59
  • @Mike'Pomax'Kamermans I'm experimenting with an idea of using `webpack` to tree shake a lambda function. – x-yuri Nov 21 '21 at 16:30
  • That sounds like a rather different topic, making this an [XY question](https://meta.stackexchange.com/questions/66377/what-is-the-xy-problem). Please don't post XY questions. Please edit your post to ask about the thing you're trying to do (not just the solution you're trying to make work. That's just "the thing you tried" that turned out to be problematic, which is an indicator that the solution you thought of is probably not the right one). – Mike 'Pomax' Kamermans Nov 21 '21 at 17:35
  • @Mike'Pomax'Kamermans Yeah, I know what an XY question is. And Y is not problematic. The answer to Y is in the question (for `nestjs` and `sequelize`). And that's what I'm gonna use. But working on it, I got curious, if I can do it the hard way. Like you know, when you don't have the option like you have with `nestjs` and `sequelize`. Who knows, maybe that's what awaits me down the road. So in a way, there's no X. And actually the X itself is questionable. Those few who tried, those I know of, they gave up. I'd like to give it a try. Maybe I'll follow them. Or not. We'll see. – x-yuri Nov 22 '21 at 06:24
  • I haven't been asked about the problem X for a while now, but okay, hopefully my new phrasing resolves your concerns. – x-yuri Nov 23 '21 at 09:38

1 Answers1

0

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;
x-yuri
  • 16,722
  • 15
  • 114
  • 161