组件库打包配置参考-esm&cjs打包
组件库打包配置参考-esm&cjs打包
今天简单讲讲关于组件库打包的esm
和cjs
打包,这里拿arco-design的打包工具arco-cli的1.0
版本来讲解。
开始前
arco-cli
使用的是gulp来组织任务执行的,他能极大的简化构建任务,生态也是及其的庞大,基本业务中的情况都能找到对应的插件。
简单的一些知识可以看看这里。
下面展示的代码可能是笔者更改过的,请勿过分较真(`へ´*)ノ。
因为代码用的是同一套,所以就一起讲,下面主要以
esm
的角度分析,cjs
其实只是部分不同,各位可以酌情甄别一下🙏🏻。
开始
1 |
|
接着看compileTS
方法
1 |
|
接着就两个编译方法都做一下简单的解析
withTSC
1 |
|
我们按照顺序一一解析。
-
const { compilerOptions } = getTSConfig();
getTSConfig
方法是处理ts
配置合并问题的。- 首先查找项目根目录下面的
tsconfig.json
文件,接着和自定义配置进行合并(后者覆盖前者) - 接着根据上面的
tsconfig.json
中的extends
字段,递归向上查找继承的ts
配置,同样按照上面的步骤进行合并合并。
下面是代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26const getTSConfig = (
// 项目根目录的ts配置文件路径
tsconfigPath = path.resolve(process.cwd(), 'tsconfig.json'),
// 自定义配置
subConfig = { compilerOptions: {} }
) => {
// 如果项目存在ts配置文件则使用配置文件
if (fs.pathExistsSync(tsconfigPath)) {
const config = fs.readJsonSync(tsconfigPath);
const compilerOptions = (config && config.compilerOptions) || {};
const subCompilerOptions = (subConfig && subConfig.compilerOptions) || {};
// 编译配置合并
// Avoid overwriting of the compilation options of subConfig
subConfig.compilerOptions = { ...compilerOptions, ...subCompilerOptions };
Object.assign(config, subConfig);
// 存在外部继承的配置则递归获取合并配置
if (config.extends) {
return getTSConfig(path.resolve(path.dirname(tsconfigPath), config.extends), config);
}
return config;
}
return { ...subConfig };
}; - 首先查找项目根目录下面的
-
if(type === 'es')
讲这个之前先说一下上面代码中出现的tscConfig
,源码当中绕了几个圈,说白点就是。
项目约定了一个目录.config
,当中存放一些项目自定义的配置,包括'jest' | 'webpack' | 'babel' | 'docgen' | 'style' | 'tsc'
,
打包时,代码会去查找.config/xx.config.js
文件,文件应该默认导出一个方法,参数接收默认配置,并返回经过处理后的自定义配置供后续打包使用。而
tsConfig
是查找.config/tsc.config.js
。
查看arco-design
源码目录可以发现,暂时还没有使用到这个。接着我们继续来看上面的
if
逻辑。1
2
3
4
5
6
7
8
9
10
11if (type === 'es') {
const regexpES = /^es/i;
if (typeof tscConfig.module === 'string' && regexpES.test(tscConfig.module)) {
module = tscConfig.module;
} else if (
typeof compilerOptions?.module === 'string' &&
regexpES.test(compilerOptions.module)
) {
module = compilerOptions.module;
}
}其实也不难看出,只是为了确定最终的编译的
module
。
所以不出意外的话,module
的值还是原来的es6
-
tsc.compile({})
这里用到了一个第三方的包node-typescript-compiler来讲
ts
编译成js
。1
2
3
4
5
6
7return tsc.compile({
...tscConfig,
module,
outDir,
watch: !!watch,
declaration: type === 'es',
})
withBabel
1 |
|
我们按照上面1.2.3.4
的顺序来看具体的代码
-
1
的话和上面的withTSC
中是一样的,获取tsconfig
配置。 -
2
是目标编译输出的目录(cwd/es/xx
)。 -
3
是根据1
的配置中的include
字段来找到需要编译的目录
具体代码如下1
2
3
4
5
6
7
8
9let srcPath = '';
// ["components/**/*.ts", "components/**/*.tsx"]
for (const pattern of tsconfig.include) {
// match 'src/**/*.ts` or 'src/**/*.{ts,tsx}' or 'src/**/*.t{s,sx}'
if (/\/\*{2}\/\*\.{?t{?s/.test(pattern)) {
srcPath = pattern.split('/**/')[0];
break;
}
}所以
srcPath
其实就是组件库的目录components
-
4
具体的编译流程就是这里。- 首先是定义了目标目录当中一些不需要编译的文件(下面数组当中以
!
为前缀的)
1
2
3
4
5
6
7const patterns = [
...tsconfig.include,
`!${path.resolve(srcPath, '**/demo{,/**}')}`,
`!${path.resolve(srcPath, '**/__test__{,/**}')}`,
`!${path.resolve(srcPath, '**/*.md')}`,
`!${path.resolve(srcPath, '**/*.mdx')}`,
]这些文件其实就是组件库的测试文件
__test__
,组件演示的示例demo
,组件库文档文件md(x)
-
createStream
接着看一下createStream
方法。下面用到了几个包
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50const createStream = (src) => {
// vinyl-fs
// 解析相应的文件 得到相应的元数据
return vfs
.src(src, {
allowEmpty: true,
base: srcPath,
})
// 将文件数据转换成 对象模式
// 方便下面任务对文件的处理
.pipe(through.obj())
.pipe(
// 文件是 .ts 或者 .tsx 的则执行此任务
// 其实就是 ts文件 的编译
gulpIf(
({ path }) => {
return /\.tsx?$/.test(path);
},
// Delete outDir to avoid static resource resolve errors during the babel compilation of next step
// 拿到前面拿到的tsconfig配置
// 如果 type 是 es(esm模式),输出类型声明文件
// 这里把 outDir 设置成了 undefined,根据上面英文解释可以知道,是为了避免在下面的任务中的编译发生冲突
gulpTS({ ...tsconfig.compilerOptions, declaration: type === 'es', outDir: undefined })
)
)
.pipe(
gulpIf(
// 编译 ts js tsx jsx 文件,且非.d.ts文件
({ path }) => {
return !path.endsWith('.d.ts') && /\.(t|j)sx?$/.test(path);
},
through.obj((file, _, cb) => {
try {
// 使用babel编译文件,并将文件格式转成 buffer
// transform 会在下面介绍
file.contents = Buffer.from(transform(file));
// .jsx -> .js
file.path = file.path.replace(path.extname(file.path), '.js');
cb(null, file);
} catch (error) {
console.error(error);
cb(null);
}
})
)
)
// 输出文件到指定目录
.pipe(vfs.dest(targetPath));
};transform
最后看一下withBabel
的最后的babel
编译方法。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37const transform = (file) => {
// Avoid directly modifying the original presets array, it will cause errors when withBabel is called multiple times
// babelConfig 和前面提到的 withTSC中的 tscConfig 类似,当然这里他是一些默认值的配置的,具体可以查看下面的config
// 这里对默认配置的 @babel/preset-env 做一下处理,关于转换模式的
babelConfig.presets = babelConfig.presets.map((preset) => {
const strPresetEnv = '@babel/preset-env';
// 如果 type 是 es 那么就不需要做转换,否则就是转换成 commonjs 模式
// 这里具体可以看下官网的解释
// https://www.babeljs.cn/docs/babel-preset-env#modules
const presetOptions = { modules: type === 'es' ? false : 'cjs' };
// 第一种情况是没有默认配置,则直接赋值
if (preset === strPresetEnv) {
return [strPresetEnv, presetOptions];
}
// 第二种情况是有默认配置,那么就是合并配置
if (Array.isArray(preset) && preset[0] === strPresetEnv) {
const _preset = preset.slice();
_preset[1] = {
...(_preset[1] || {}),
...presetOptions,
};
return _preset;
}
return preset;
});
// 最终的babel 编译
return babelTransform(file.contents, {
...babelConfig,
filename: file.path,
// Ignore the external babel.config.js and directly use the current incoming configuration
configFile: false,
}).code;
}1
2
3
4
5
6
7
8
9
10
11
12const config = {
// TODO Solve babel error when there is no [filename]
filename: '',
presets: ['@babel/preset-env', '@babel/preset-typescript', '@babel/preset-react'],
plugins: [
'@babel/plugin-proposal-export-default-from',
'@babel/plugin-transform-runtime',
'@babel/plugin-syntax-dynamic-import',
'@babel/plugin-proposal-class-properties',
'@babel/plugin-transform-react-jsx-source',
],
}; - 首先是定义了目标目录当中一些不需要编译的文件(下面数组当中以
结束
关于上面的代码,可以参考下简化的代码,其实就是cv
了arco-scripts
的代码🌶。
结束 🔚。
参考链接
本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!