【问题标题】:How to correctly build NestJS app for production with node_modules dependencies in bundle?如何使用捆绑包中的 node_modules 依赖项正确构建用于生产的 NestJS 应用程序?
【发布时间】:2020-04-25 08:45:15
【问题描述】:

nest buildnest build --webpack 之后,dist 文件夹不包含所有必需的模块,我在尝试运行node main.js 时得到了Error: Cannot find module '@nestjs/core'

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

【问题讨论】:

  • 您是否尝试直接部署 artifact/dist 文件夹?您应该注意,某些库具有特定于机器的代码,并且必须在目标机器上构建,例如bcrypt。当我部署生产应用程序时,我在目标服务器上运行nest build(在npm install 之后)。
  • 问题是缺少代码,机器特定与否。例如,如果您将生成的 dist 移动到同一台机器上的其他位置,即使使用由 nest new my_project 生成的简单应用程序,您也会遇到相同的错误。
  • node_modules 没有捆绑,没有。不过,这应该可以通过 webpack 实现。我假设您想删除源代码并只保留 dist 文件夹,对吗?为什么?
  • 奇怪的问题。人们为什么要构建“二进制文件”?最大限度地减少依赖关系、大小和要部署的文件数量。如果需要与 nest start 相同的复杂环境,那么构建的好处是什么?
  • 好的,谢谢!您能否将其格式化为简短答案,以便我接受。可能这会为其他人节省一些时间......

标签: javascript node.js typescript webpack nestjs


【解决方案1】:

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


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

【讨论】:

    【解决方案2】:

    bundle-nest 已存档/停产:

    我们已经得出结论,不建议捆绑 NestJS,或者实际上是一般的 NodeJS Web 服务器。这是在社区尝试摇树和捆绑 NestJS 应用程序期间存档以供历史参考。详情参考@kamilmysliwiec 评论:

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

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

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

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

    我最终得到的配置(它覆盖了nestjs defaults):

    webpack.config.jswebpack-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:

    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:

    // 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:

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

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

    $ nest build --webpack
    

    我没有打扰源地图,因为目标是nodejs

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

    还有一个技巧here,不过你可能不需要它。

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

    【讨论】:

      猜你喜欢
      • 2020-01-07
      • 2010-09-11
      • 1970-01-01
      • 1970-01-01
      • 2019-02-14
      • 2019-11-15
      • 2018-08-23
      • 1970-01-01
      相关资源
      最近更新 更多