简体   繁体   English

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

[英]How to correctly build NestJS app for production with node_modules dependencies in bundle?

After nest build or nest build --webpack dist folder does not contain all required modules and I got Error: Cannot find module '@nestjs/core' when trying to run node main.js .nest buildnest build --webpack dist 文件夹不包含所有必需的模块之后,我收到Error: Cannot find module '@nestjs/core'在尝试运行node main.jsError: Cannot find module '@nestjs/core'

I could not find any clear instructions on https://docs.nestjs.com/ on how to correctly build app for production, so maybe I missed something?我在https://docs.nestjs.com/上找不到任何关于如何正确构建生产应用程序的明确说明,所以也许我错过了什么?

Out of the box, nest cli does not support including the node_modules dependencies into the dist bundle.开箱即用,nest cli 不支持将node_modules依赖项包含到dist包中。


However, there are some community examples of custom webpack configs that include the dependencies in the bundle, eg bundled-nest .但是,有一些自定义 webpack 配置的社区示例,包括包中的依赖项,例如bundled-nest As described in this issue , it is necessary to include the webpack.IgnorePlugin to whitelist unused dynamic libraries.本期所述,需要包含webpack.IgnorePlugin以将未使用的动态库列入白名单。

bundle-nest has been archived/discontinued: bundle-nest已归档/停产:

We've concluded that it is not recommended to bundle NestJS, or actually, NodeJS web servers in general.我们得出的结论是,一般不建议捆绑 NestJS,或者实际上是 NodeJS Web 服务器。 This is archived for historical reference during the period of time when the community was attempting to tree-shake, and bundle NestJS apps.这是在社区尝试摇树和捆绑 NestJS 应用程序期间存档的历史参考。 Refer to @kamilmysliwiec comment for details:有关详细信息,请参阅@kamilmysliwiec 评论:

In many real-world scenarios (depending on what libraries are being used), you should not bundle Node.js applications (not only NestJS applications) with all dependencies (external packages located in the node_modules folder).在许多实际场景中(取决于正在使用的库),您不应将 Node.js 应用程序(不仅是 NestJS 应用程序)与所有依赖项(位于 node_modules 文件夹中的外部包)捆绑在一起。 Although this may make your docker images smaller (due to tree-shaking), somewhat reduce the memory consumption, slightly increase the bootstrap time (which is particularly useful in the serverless environments), it won't work in combination with many popular libraries commonly used in the ecosystem.虽然这可能会使你的 docker 镜像变小(由于 tree-shaking),在一定程度上减少了内存消耗,稍微增加了引导时间(这在无服务器环境中特别有用),但它通常无法与许多流行的库结合使用生态系统中使用。 For instance, if you try to build NestJS (or just express) application with MongoDB, you will see the following error in your console:例如,如果您尝试使用 MongoDB 构建 NestJS(或只是 express)应用程序,您将在控制台中看到以下错误:

Error: Cannot find module './drivers/node-mongodb-native/connection' at webpackEmptyContext错误:在 webpackEmptyContext 中找不到模块“./drivers/node-mongodb-native/connection”

Why?为什么? Because mongoose depends on mongodb which depends on kerberos (C++) and node-gyp.因为 mongoose 依赖于 mongodb,而 mongodb 又依赖于 kerberos (C++) 和 node-gyp。

Well, about mongo , you can make some exceptions (leave some modules in node_modules ), can you?好吧,关于mongo ,你可以做一些例外(在node_modules保留一些模块),可以吗? It's not like it's all or nothing.这不像是全有或全无。 But still, I'm not sure you want to follow this path.但是,我仍然不确定您是否愿意遵循这条道路。 I've just succeeded with bundling a nestjs application.我刚刚成功捆绑了一个nestjs应用程序。 It was a proof of concept, I'm not sure if it'll go into production.这是一个概念证明,我不确定它是否会投入生产。 And it was hard, I might have broken something in the process, but at first glance it works.这很难,我可能在这个过程中破坏了一些东西,但乍一看它是有效的。 The most complex part was adminjs .最复杂的部分是adminjs It has rollup and babel as dependencies.它有rollupbabel作为依赖项。 And in the app code they unconditionally call watch for some reason.在应用程序代码中,他们出于某种原因无条件地调用watch Anyways, if you'd like to follow this path you should be ready to debug/inspect your packages' code.无论如何,如果您想遵循此路径,您应该准备好调试/检查您的包的代码。 And you might need to add workarounds as new packages are added to the project.当新包添加到项目时,您可能需要添加变通方法。 But it all depends on your dependencies, it may be easier than in my case.但这一切都取决于您的依赖项,这可能比我的情况更容易。 For a freshly created nestjs + mysql app it was relatively simple.对于新创建的nestjs + mysql应用程序,它相对简单。

The config I ended up with (it overrides the nestjs defaults ):我最终得到的配置(它覆盖了nestjs默认值):

webpack.config.js ( webpack-5.58.2 , @nestjs/cli-8.1.4 ): 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',
        }
    };
};

make-optional-plugin.js : 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;

rewrite-require-loader.js : 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);
};

rewrite-code-loader.js : 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);
};

The supposed way to build the app is:构建应用程序的假设方法是:

$ nest build --webpack

I didn't bother with source maps, since the target is nodejs .我没有打扰源映射,因为目标是nodejs

It's not a config you can just copy-paste, you should figure out what's needed for your project yourself.这不是一个可以复制粘贴的配置,你应该自己弄清楚你的项目需要什么。

One more trick here , but well, you probably won't need it.这里还有一个技巧,但好吧,你可能不需要它。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

相关问题 使用 NestJS + Typescript + Webpack + node_modules 的单个文件包 - Single file bundle with NestJS + Typescript + Webpack + node_modules 如何在 Docker 构建中缓存 node_modules? - How to cache node_modules on Docker build? 使用 Typescript 时将捆绑 node_modules 汇总到应用程序中 - Rollup bundle node_modules into app when using Typescript Webpack似乎将node_modules的node_modules包括在包中? - Webpack seems to be including node_modules of node_modules into bundle? 减少用于生产的node_modules的大小 - Reducing size of node_modules for production npm:缺少node_modules中的传递依赖项 - npm: missing transitive dependencies in node_modules Nuxt 构建失败,在 ./node_modules/bootstrap-vue/:varisouspath 中找不到 core-js 依赖项 - Nuxt build failure with core-js dependencies not found in ./node_modules/bootstrap-vue/:varisouspath rollup 是否将 node_modules 捆绑到 bundle.js 中? - Does rollup bundle node_modules into bundle.js? 无法解决“node_modules \\ expo \\ AppEntry.js”中的“../../App”构建JavaScript包失败 - Unable to resolve “../../App” from “node_modules\expo\AppEntry.js” Failed building JavaScript bundle Angular 2导入node_modules未添加到构建 - Angular 2 importing node_modules not adding to build
 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM