深入理解构建
在 "基础使用" 的部分,我们已经知道可以通过 buildConfig
配置对项目的输出产物进行修改。buildConfig
不仅描述了产物的一些特性,同时还为构建产物提供了一些功能。
TIP
如果你还不了解 buildConfig
的作用,请先阅读 修改输出产物。
而在本章里我们将要深入理解某些构建配置的作用以及了解执行 modern build
命令的时候发生了什么。
bundle
/ bundleless
那么首先我们来了解一下 bundle 和 bundleless。
所谓 bundle 是指对构建产物进行打包,构建产物可能是一个文件,也有可能是基于一定的代码拆分策略得到的多个文件。
而 bundleless 则是指对每个源文件单独进行编译构建,但是并不将它们打包在一起。每一个产物文件都可以找到与之相对应的源码文件。bundleless 构建的过程,也可以理解为仅对源文件进行代码转换的过程。
它们有各自的好处:
- bundle 可以减少构建产物的体积,也可以对依赖预打包,减小安装依赖的体积。提前对库进行打包,可以加快应用项目构建的速度。
- bundleless 则是可以保持原有的文件结构,更有利于调试和 tree shaking。
WARNING
bundleless 是单文件编译模式,因此对于类型的引用和导出你需要加上 type
字段, 例如 import type { A } from './types
,背景参考 esbuild 文档。
在 buildConfig
中可以通过 buildConfig.buildType
来指定当前构建任务是 bundle 还是 bundleless。
input
/ sourceDir
buildConfig.input
用于指定读取源码的文件路径或者目录路径,其默认值在 bundle 和 bundleless 构建过程中有所不同:
- 当
buildType: 'bundle'
的时候,input
默认值为 src/index.(j|t)sx?
- 当
buildType: 'bundleless'
的时候,input
默认值为 ['src']
从默认值上我们可以知道:使用 bundle 模式构建时一般指定一个或多个文件作为构建的入口,而使用 bundleless 构建则是指定一个目录,将目录下所有文件作为入口。
sourceDir
用于指定源码目录,它只与以下两个内容有关系:
因此我们可以得到其最佳实践:
- 在 bundle 构建过程中,只能指定
input
。
- 一般情况下,bundleless 只需要指定
sourceDir
(此时 input
会与 sourceDir
保持一致)。
如果我们想要在 bundleless 里只对一部分文件进行转换,例如只需要转换 src/runtime
目录的文件,此时需要配置 input
:
modern.config.ts
import { defineConfig } from '@modern-js/module-tools';
export default defineConfig({
buildConfig: {
input: ['src/runtime'],
sourceDir: 'src',
},
});
使用 swc
在部分场景下,esbuild 不足以满足我们的需求,此时我们会使用 swc 来做代码转换。
从 2.36.0 版本开始,涉及到以下功能时,Modern.js Module 默认会使用 swc ,但不这意味着不使用 esbuild 了,其余功能还是使用 esbuild:
事实上,我们在 2.16.0 版本开始全量使用 swc 进行代码转换。不过 swc 同样也存在一些限制,为此我们添加了 sourceType 配置,当源码格式为 'commonjs' 时关闭 swc, 但这种方式并不符合用户直觉,另外,swc 格式化输出的 cjs 模式没有给每个导出名称添加注释,这在 node 中使用可能会带来一些问题。
因为我们废弃了此行为,回到了最初的设计 - 只在需要的场景下使用 swc 作为补充。
使用 Hook 介入构建流程
Modern.js Module 提供了 Hook 机制,允许我们在构建流程的不同阶段注入自定义逻辑。
Modern.js Module Hook 使用了 tapable 实现,扩展了 esbuild 的插件机制,若 esbuild plugins 已经满足了你的需求,建议直接使用它。
下面展开说明其用法:
Hook 类型
AsyncSeriesBailHook
串行执行的 hooks,如果某个 tapped function 返回非 undefined 结果,则后续其他的 tapped function 停止执行。
AsyncSeriesWaterFallHooks
串行执行的 hooks,其结果会传递给下一个 tapped function
Hook API
load
interface LoadArgs {
path: string;
namespace: string;
suffix: string;
}
type LoadResult =
| {
contents: string; // 模块内容
map?: SourceMap; // https://esbuild.github.io/api/#sourcemap
loader?: Loader; // https://esbuild.github.io/api/#loader
resolveDir?: string;
}
| undefined;
compiler.hooks.load.tapPromise('load content from memfs', async args => {
const contents = memfs.readFileSync(args.path);
return {
contents: contents,
loader: 'js',
};
});
transform
- AsyncSeriesWaterFallHooks
- 在 esbuild onLoad callbacks 触发,
将 load 阶段获取的模块内容进行转换
- 输入参数(返回参数)
export type Source = {
code: string;
map?: SourceMap;
path: string;
loader?: string;
};
compiler.hooks.transform.tapPromise('6to5', async args => {
const result = babelTransform(args.code, { presets: ['@babel/preset-env'] });
return {
code: result.code,
map: result.map,
};
});
renderChunk
- AsyncSeriesWaterFallHooks
- 在 esbuild onEnd callbacks 触发,
类似于 transform hook,但是作用在 esbuild 生成的产物
- 输入参数(返回参数)
export type AssetChunk = {
type: 'asset';
contents: string | Buffer;
entryPoint?: string;
/**
* absolute file path
*/
fileName: string;
originalFileName?: string;
};
export type JsChunk = {
type: 'chunk';
contents: string;
entryPoint?: string;
/**
* absolute file path
*/
fileName: string;
map?: SourceMap;
modules?: Record<string, any>;
originalFileName?: string;
};
export type Chunk = AssetChunk | JsChunk;
compiler.hooks.renderChunk.tapPromise('minify', async chunk => {
if (chunk.type === 'chunk') {
const code = chunk.contents.toString();
const result = await minify.call(compiler, code);
return {
...chunk,
contents: result.code,
map: result.map,
};
}
return chunk;
});
类型文件生成
buildConfig.dts
配置主要用于类型文件的生成。
关闭类型生成
默认情况下类型生成功能是开启的,如果需要关闭的话,可以按照如下配置:
modern.config.ts
import { defineConfig } from '@modern-js/module-tools';
export default defineConfig({
buildConfig: {
dts: false,
},
});
打包类型文件
在 buildType: 'bundleless'
的时候,类型文件的生成是使用项目的 tsc
命令来完成生产。
Modern.js Module 同时还支持对类型文件进行打包,不过使用该功能的时候需要注意:
- 对类型文件进行打包不会开启类型检查。
- 一些第三方依赖存在错误的语法会导致打包过程失败。因此对于这种情况,需要手动通过
buildConfig.externals
将这类第三方包排除,或者直接关闭dts.respectExternal从而不打包任何三方包类型。
- 对于第三方依赖的类型文件指向的是一个
.ts
文件的情况,目前无法处理。比如第三方依赖的 package.json
中存在这样的内容: {"types": "./src/index.ts"
。
对于上述问题,我们推荐的处理方式是首先使用 tsc
生成 d.ts 文件,然后将 index.d.ts 作为入口进行打包处理,并且关闭 dts.respectExternal
。在之后的演进我们也会逐渐向这种处理方式靠拢。
别名转换
在 bundleless 构建过程中,如果源代码中出现了别名,例如:
./src/index.ts
import utils from '@common/utils';
使用 tsc
生成的产物类型文件也会包含这些别名。不过 Modern.js Module 会对 tsc
生成的类型文件里的别名进行转换处理。
一些示例
import { defineConfig } from '@modern-js/module-tools';
export default defineConfig({
// 此时打包的类型文件输出路径为 `./dist/types`,并且将会读取项目下的 other-tsconfig.json 文件
buildConfig: {
buildType: 'bundle',
dts: {
tsconfigPath: './other-tsconfig.json',
distPath: './types',
},
outDir: './dist',
},
});
import { defineConfig } from '@modern-js/module-tools';
export default defineConfig({
// 此时类型文件没有进行打包,输出路径为 `./dist/types`
buildConfig: [
{
buildType: 'bundle',
dts: false,
outDir: './dist',
},
{
buildType: 'bundleless',
dts: {
only: true,
},
outDir: './dist/types',
},
],
});
构建过程
当执行 modern build
命令的时候,会发生
- 根据
buildConfig.outDir
清理产物目录。
- 编译
js/ts
源代码生成 bundle / bundleless 的 JS 构建产物。
- 使用
tsc
生成 bundle / bundleless 的类型文件。
- 处理
copy
任务。
构建报错
当发生构建报错的时候,基于以上了解到的信息,可以很容易的明白在终端出现的报错内容:
js 或者 ts 构建的报错:
error ModuleBuildError:
╭───────────────────────╮
│ bundle failed: │
│ - format is "cjs" │
│ - target is "esnext" │
╰───────────────────────╯
Detailed Information:
类型文件生成过程的报错:
error ModuleBuildError:
bundle DTS failed:
对于 js/ts
构建错误,我们可以从报错信息中知道:
- 报错的
buildType
- 报错的
format
- 报错的
target
- 其他具体报错信息
调试模式
从 2.36.0 版本开始,为了便于排查问题,Modern.js Module 提供了调试模式,你可以在执行构建时添加 DEBUG=module 环境变量来开启调试模式。
DEBUG=module modern build
调试模式下,你会看到 Shell 中输出更详细的构建日志,这主要以流程日志为主:
module run beforeBuildTask hooks +6ms
module run beforeBuildTask hooks done +0ms
module [DTS] Build Start +139ms
module [CJS] Build Start +1ms
另外,Module 还提供了调试内部工作流程的能力。你可以通过设置环境变量 DEBUG=module:*
来开启更详细的调试日志:
目前只支持了 DEBUG=module:resolve
,可以查看 Module 内部模块解析的详细日志:
module:resolve onResolve args: {
path: './src/hooks/misc.ts',
importer: '',
namespace: 'file',
resolveDir: '/Users/bytedance/modern.js/packages/solutions/module-tools',
kind: 'entry-point',
pluginData: undefined
} +0ms
module:resolve onResolve result: {
path: '/Users/bytedance/modern.js/packages/solutions/module-tools/src/hooks/misc.ts',
external: false,
namespace: 'file',
sideEffects: undefined,
suffix: ''
} +0ms