如何使用捆绑中的 node_modules 依赖项正确构建用于生产的 NestJS 应用程序?

Dim*_*oid 15 javascript node.js typescript webpack nestjs

Afternest buildnest build --webpackdist 文件夹不包含所有必需的模块,我Error: Cannot find module '@nestjs/core'在尝试运行node main.js.

我在https://docs.nestjs.com/上找不到任何关于如何正确构建生产应用程序的明确说明,所以也许我错过了什么?

Kim*_*ern 7

Nest cli 开箱即用,不支持将node_modules依赖项包含到dist捆绑包中。


但是,有一些自定义 webpack 配置的社区示例,其中包含捆绑包中的依赖项,例如bundled-nest如本期所述,有必要将webpack.IgnorePlugin未使用的动态库列入白名单。


Xtr*_*ica 6

对于任何感兴趣的人来说,ncc在将完整的 NestJs 应用程序捆绑到单个 js 文件中方面做得非常出色:

ncc build src/main.ts --out dist/main.js

尽管您可能需要修复某些import路径,但我不需要进一步的配置。它会进行树摇动,甚至检测绑定并将它们复制到单独的文件夹中。


x-y*_*uri 5

bundle-nest已存档/停止:

我们得出的结论是,一般不建议捆绑 NestJS,或者实际上是 NodeJS Web 服务器。当社区尝试进行 tree-shake 并捆绑 NestJS 应用程序时,将其存档以供历史参考。详情请参阅@kamilmysliwiec 评论:

在许多现实场景中(取决于所使用的库),您不应将 Node.js 应用程序(不仅是 NestJS 应用程序)与所有依赖项(位于 node_modules 文件夹中的外部包)捆绑在一起。虽然这可能会使您的 docker 映像更小(由于树摇动),在一定程度上减少内存消耗,稍微增加引导时间(这在无服务器环境中特别有用),但它通常无法与许多流行的库结合使用在生态系统中使用。例如,如果您尝试使用 MongoDB 构建 NestJS(或 Express)应用程序,您将在控制台中看到以下错误:

错误:在 webpackEmptyContext 找不到模块“./drivers/node-mongodb-native/connection”

为什么?因为 mongoose 依赖于 mongodb,而 mongodb 又依赖于 kerberos (C++) 和 node-gyp。

好吧,关于mongo,你可以做一些例外(在 中保留一些模块node_modules),可以吗?这并不是全有或全无。但我仍然不确定您是否愿意走这条路。我刚刚成功捆绑了一个nestjs应用程序。这是一个概念验证,我不确定它是否会投入生产。这很困难,我可能在这个过程中破坏了一些东西,但乍一看它是有效的。最复杂的部分是adminjs。它具有rollupbabel作为依赖项。在应用程序代码中,它们watch出于某种原因无条件调用(生产中的UDP noop)。无论如何,如果您想遵循此路径,您应该准备好调试/检查包的代码。当新包添加到项目中时,您可能需要添加解决方法。但这一切都取决于您的依赖关系,它可能比我的情况更容易。对于一个新创建的nestjs应用mysql程序来说,它相对简单。

我最终得到的配置(它覆盖nestjs默认值):

webpack.config.js( webpack-5.58.2, @nestjs/cli-8.1.4):

const path = require('path');
const MakeOptionalPlugin = require('./make-optional-plugin');
module.exports = (defaultOptions, webpack) => {
    return {
        externals: {},  // make it not exclude `node_modules`
                        // https://github.com/nestjs/nest-cli/blob/v7.0.1/lib/compiler/defaults/webpack-defaults.ts#L24
        resolve: {
            ...defaultOptions.resolve,
            extensions: [...defaultOptions.resolve.extensions, '.json'], // some packages require json files
                                                                         // https://unpkg.com/browse/babel-plugin-polyfill-corejs3@0.4.0/core-js-compat/data.js
                                                                         // https://unpkg.com/browse/core-js-compat@3.19.1/data.json
            alias: {
                // an issue with rollup plugins
                // https://github.com/webpack/enhanced-resolve/issues/319
                '@rollup/plugin-json': '/app/node_modules/@rollup/plugin-json/dist/index.js',
                '@rollup/plugin-replace': '/app/node_modules/@rollup/plugin-replace/dist/rollup-plugin-replace.cjs.js',
                '@rollup/plugin-commonjs': '/app/node_modules/@rollup/plugin-commonjs/dist/index.js',
            },
        },
        module: {
            ...defaultOptions.module,
            rules: [
                ...defaultOptions.module.rules,

                // a context dependency
                // https://github.com/RobinBuschmann/sequelize-typescript/blob/v2.1.1/src/sequelize/sequelize/sequelize-service.ts#L51
                {test: path.resolve('node_modules/sequelize-typescript/dist/sequelize/sequelize/sequelize-service.js'),
                use: [
                    {loader: path.resolve('rewrite-require-loader.js'),
                    options: {
                        search: 'fullPath',
                        context: {
                            directory: path.resolve('src'),
                            useSubdirectories: true,
                            regExp: '/\\.entity\\.ts$/',
                            transform: ".replace('/app/src', '.').replace(/$/, '.ts')",
                        },
                    }},
                ]},

                // adminjs resolves some files using stack (relative to the requiring module)
                // and actually it needs them in the filesystem at runtime
                // so you need to leave node_modules/@adminjs/upload
                // I failed to find a workaround
                // it bundles them to `$prj_root/.adminjs` using `rollup`, probably on production too
                // https://github.com/SoftwareBrothers/adminjs-upload/blob/v2.0.1/src/features/upload-file/upload-file.feature.ts#L92-L100
                {test: path.resolve('node_modules/@adminjs/upload/build/features/upload-file/upload-file.feature.js'),
                use: [
                    {loader: path.resolve('rewrite-code-loader.js'),
                    options: {
                        replacements: [
                            {search: /adminjs_1\.default\.bundle\('\.\.\/\.\.\/\.\.\/src\/features\/upload-file\/components\/edit'\)/,
                            replace: "adminjs_1.default.bundle('/app/node_modules/@adminjs/upload/src/features/upload-file/components/edit')"},

                            {search: /adminjs_1\.default\.bundle\('\.\.\/\.\.\/\.\.\/src\/features\/upload-file\/components\/list'\)/,
                            replace: "adminjs_1.default.bundle('/app/node_modules/@adminjs/upload/src/features/upload-file/components/list')"},

                            {search: /adminjs_1\.default\.bundle\('\.\.\/\.\.\/\.\.\/src\/features\/upload-file\/components\/show'\)/,
                            replace: "adminjs_1.default.bundle('/app/node_modules/@adminjs/upload/src/features/upload-file/components/show')"},
                        ],
                    }},
                ]},

                // not sure what babel does here
                // I made it return standardizedName
                // https://github.com/babel/babel/blob/v7.16.4/packages/babel-core/src/config/files/plugins.ts#L100
                {test: path.resolve('node_modules/@babel/core/lib/config/files/plugins.js'),
                use: [
                    {loader: path.resolve('rewrite-code-loader.js'),
                    options: {
                        replacements: [
                            {search: /const standardizedName = [^;]+;/,
                            replace: match => `${match} return standardizedName;`},
                        ],
                    }},
                ]},

                // a context dependency
                // https://github.com/babel/babel/blob/v7.16.4/packages/babel-core/src/config/files/module-types.ts#L51
                {test: path.resolve('node_modules/@babel/core/lib/config/files/module-types.js'),
                use: [
                    {loader: path.resolve('rewrite-require-loader.js'),
                    options: {
                        search: 'filepath',
                        context: {
                            directory: path.resolve('node_modules/@babel'),
                            useSubdirectories: true,
                            regExp: '/(preset-env\\/lib\\/index\\.js|preset-react\\/lib\\/index\\.js|preset-typescript\\/lib\\/index\\.js)$/',
                            transform: ".replace('./node_modules/@babel', '.')",
                        },
                    }},
                ]},
            ],
        },
        plugins: [
            ...defaultOptions.plugins,
            // some optional dependencies, like this:
            // https://github.com/nestjs/nest/blob/master/packages/core/nest-application.ts#L45-L52
            // `webpack` detects optional dependencies when they are in try/catch
            // https://github.com/webpack/webpack/blob/main/lib/dependencies/CommonJsImportsParserPlugin.js#L152
            new MakeOptionalPlugin([
                '@nestjs/websockets/socket-module',
                '@nestjs/microservices/microservices-module',
                'class-transformer/storage',
                'fastify-swagger',
                'pg-native',
            ]),
        ],

        // to have have module names in the bundle, not some numbers
        // although numbers are sometimes useful
        // not really needed
        optimization: {
            moduleIds: 'named',
        }
    };
};
Run Code Online (Sandbox Code Playgroud)

make-optional-plugin.js:

class MakeOptionalPlugin {
    constructor(deps) {
        this.deps = deps;
    }

    apply(compiler) {
        compiler.hooks.compilation.tap('HelloCompilationPlugin', compilation => {
            compilation.hooks.succeedModule.tap(
                'MakeOptionalPlugin', (module) => {
                    module.dependencies.forEach(d => {
                        this.deps.forEach(d2 => {
                            if (d.request == d2)
                                d.optional = true;
                        });
                    });
                }
            );
        });
    }
}

module.exports = MakeOptionalPlugin;
Run Code Online (Sandbox Code Playgroud)

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})`);
}

function processFileContext(source, search, context) {
    const re = `require\\(${escapeRegExp(search)}\\)`;
    const _d = JSON.stringify(context.directory);
    const _us = JSON.stringify(context.useSubdirectories);
    const _re = context.regExp;
    const _t = context.transform || '';
    const r = source.replace(
        new RegExp(re, 'g'),
        match => `require.context(${_d}, ${_us}, ${_re})(${search}${_t})`);
    return r;
}

module.exports = function(source) {
    const options = this.getOptions();
    return options.context
        ? processFileContext(source, options.search, options.context)
        : processFile(source, options.search, options.replace);
};
Run Code Online (Sandbox Code Playgroud)

rewrite-code-loader.js:

function processFile(source, search, replace) {
    return source.replace(search, replace);
}

module.exports = function(source) {
    const options = this.getOptions();
    return options.replacements.reduce(
        (prv, cur) => {
            return prv.replace(cur.search, cur.replace);
        },
        source);
};
Run Code Online (Sandbox Code Playgroud)

构建应用程序的假定方法是:

$ nest build --webpack
Run Code Online (Sandbox Code Playgroud)

我没有理会源映射,因为目标是nodejs.

这不是一个可以复制粘贴的配置,您应该自己弄清楚您的项目需要什么。

这里还有一个技巧,但是,你可能不需要它。

UPD adminjs似乎附带了预构建的捆绑包,因此此配置可能要简单得多。