![](/img/trans.png)
[英]Single file bundle with NestJS + Typescript + Webpack + node_modules
[英]How to correctly build NestJS app for production with node_modules dependencies in bundle?
在nest build
或nest build --webpack
dist 文件夹不包含所有必需的模块之后,我收到Error: Cannot find module '@nestjs/core'
在尝试运行node main.js
时Error: Cannot find module '@nestjs/core'
。
我在https://docs.nestjs.com/上找不到任何关于如何正确构建生产应用程序的明确说明,所以也许我错过了什么?
开箱即用,nest cli 不支持将node_modules
依赖项包含到dist
包中。
但是,有一些自定义 webpack 配置的社区示例,包括包中的依赖项,例如bundled-nest 。 如本期所述,需要包含webpack.IgnorePlugin
以将未使用的动态库列入白名单。
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
。 它有rollup
和babel
作为依赖项。 在应用程序代码中,他们出于某种原因无条件地调用watch
。 无论如何,如果您想遵循此路径,您应该准备好调试/检查您的包的代码。 当新包添加到项目时,您可能需要添加变通方法。 但这一切都取决于您的依赖项,这可能比我的情况更容易。 对于新创建的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',
}
};
};
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
。
这不是一个可以复制粘贴的配置,你应该自己弄清楚你的项目需要什么。
这里还有一个技巧,但好吧,你可能不需要它。
声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.