出现的原因是项目的中各种依赖的版本根本不受semver的控制,说到semver的话,回顾下semver规范
semver规范
版本号格式
MAJOR.MINOR.PATCH
-
MAJOR:非常大的改动,可能是是api等完全不兼容
-
MINOR:以向后兼容的方式添加功能时
-
PATCH:以向后兼容的方式修复bug时
另外发布版本的时候,通常会看到带有这些字母的,比如1.0.0-alpha.1
,1.0.0-rc.1
,他们的顺序应该是这样的
js
Alpha < Beta < RC < Canary < Stable
-
Alpha:Alpha 版本是软件或系统的内部测试版本,仅供内部人员使用。这个阶段的软件通常会有很多 Bug,一般不向外部发布。Alpha 是希腊字母的第一位,表示最初级的版本
-
Beta:Beta 版本是公开测试版,这一版本通常是在 Alpha 版本后推出。相对于 Alpha 版本,Beta 版本已有了很大的改进,消除了严重的错误,但仍可能存在一些缺陷,需要经过多次测试来进一步消除。在这个阶段,软件会一直加入新的功能
-
RC(Release Candidate) :RC 是发行候选版本。与 Beta 版本最大的差别在于,Beta 阶段会一直加入新的功能,但是到了 RC 版本,几乎就不会加入新的功能了,而主要着重于除错。RC 版本是最终发放给用户的最接近正式版的版本,发行后改正 bug 就是正式版了,就是正式版之前的最后一个测试版
-
Canary:即"金丝雀发布"。在软件开发中,Canary 版本通常是指向一小部分用户推出的新版本,用于测试新功能的稳定性和可用性。如果在这一小部分用户中没有发现问题,那么新版本就会推向所有用户。
package.json 中的^和~
~1.2.2 只能安装1.2.x, 更新当前的补丁版本, 1.2.2 <=n<1.3.0
^1.2.2 只能安装1.x.x, 更新当前的次要和补丁版本,1.2.2 <=n<2.0.0
日常工作有时我们会删除 lock文件,然后再pnpm i, 这个时候就会根据规则自动更新, 然而有些依赖是不会遵守这些规则的,
对于这种问题,社区有不少解决方案
长期: 锁依赖
临时:
1、patch-package: 代码引入式修复问题依赖
2、 锁定指定依赖版本等
js
npm overrides (>= 8.3.0)
yarn resolutions
pnpm overrides/resolutions
而umi采用了依赖预打包
依赖预打包
中间商锁依赖,定期主动更新,并对此负责
从# SEE Conf: Umi 4 设计思路文字稿看来, 分为代码和类型两部分,通过ncc 和dts-packer 实现的
这个命令"build:deps": "umi-scripts bundleDeps",
, 在umi源码每个package下面几乎都有, 随便拉一个出来看看bundler-webpack, 之前也做过简单的认识【umi】02 如何在cjs 环境用esm包, 确实是转化为cjs了
可以看到许多版本都是写死了,这不太符合我们的习惯,我们一般都是用插入符^ 或者~。
然后升级的时候,我看到好像也不是批量去升级的,比如这些pr feat: vite deps upgrade to 4.2, dep: webpack upgrade to 5.88.2等, 不知道对于这些写死版本的依赖,是怎么做到定期更新的?而且并不是每一个直接依赖都是写死的版本,有些为啥是"postcss": "^8.4.21",
, 什么样的包才能预编译?
还是先看看预编译的核心代码吧,这下看不懂也要看了
这段对于package.json的处理还是比较好理解,就是拿到package.json
下面的compiledConfig
做一些初始化处理,compiledConfig
会传入这六个参数
js
import { readWantedLockfile } from '@pnpm/lockfile-file';
// @ts-ignore
import ncc from '@vercel/ncc';
import { Package } from 'dts-packer';
import resolve from 'resolve';
import 'zx/globals';
import { PATHS } from './.internal/constants';
// @ts-ignore
// import { Package } from '/Users/chencheng/code/github.com/sorrycc/dts-packer/dist/Package.js';
// 编译打包 package.json 文件中 compiledConfig 配置的依赖库
(async () => {
const base = process.cwd();
const pkg = fs.readJSONSync(path.join(base, 'package.json'));
const pkgDeps = pkg.dependencies || {};
const {
deps,
externals = {},
noMinify = [],
extraDtsDeps = [],
extraDtsExternals = [],
excludeDtsDeps = [],
} = pkg.compiledConfig;
const webpackExternals: Record<string, string> = {};
const dtsExternals = [...extraDtsDeps, ...extraDtsExternals];
// 这里就是用$LOCAL这个标识区分下,如果有这个标识的话就是从当前包引入,
// 没有的话从@umijs/bundler-utils 引入, https://github.com/umijs/umi/tree/master/packages/bundler-utils/compiled
Object.keys(externals).forEach((name) => {
const val = externals[name];
if (val === '$$LOCAL') {
dtsExternals.push(name);
webpackExternals[name] = `${pkg.name}/compiled/${name}`;
} else {
webpackExternals[name] = val;
}
});
// 遍历dep,判断有没有argv.dep,没有走argv['extra-dts-only'],如果还是没有走`deps.concat(extraDtsDeps)`
for (const dep of argv.dep
? [argv.dep]
: argv['extra-dts-only']
? extraDtsDeps
: deps.concat(extraDtsDeps)) {
const isDep = dep.charAt(0) !== '.';
await buildDep({
...(isDep ? { pkgName: dep } : { file: dep }),
// path.basename(path.dirname('./bundles/webpack/bundle')) 这句话是返回目录名字,
// 即这个路径的目录名(即`'./bundles/webpack'`,`path.basename`方法返回一个路径的最后一部分,即`webpack`
target: `compiled/${isDep ? dep : path.basename(path.dirname(dep))}`, // 编译输出的文件目录
base,
webpackExternals,
dtsExternals,
clean: argv.clean,
minify: !noMinify.includes(dep), // 排除不需要压缩的
dtsOnly: extraDtsDeps.includes(dep),
noDts: excludeDtsDeps.includes(dep), // 不需要生成d.tsde的依赖
isDependency: dep in pkgDeps,
});
}
})();
deps.concat(extraDtsDeps) 打印出,只是没有想通为啥有file的, 因为是一个目录
js
export async function buildDep(opts: any) {
console.log(chalk.green(`Build dep ${opts.pkgName || opts.file}`));
// /Users/nanlan/Documents/com-project/umi/packages/bundler-webpack/node_modules
const nodeModulesPath = path.join(opts.base, 'node_modules');
// /Users/nanlan/Documents/com-project/umi/packages/bundler-webpack/compiled/pkgName
const target = path.join(opts.base, opts.target);
if (opts.clean) {
fs.removeSync(target);
}
let entry;
if (opts.pkgName) {
let resolvePath = opts.pkgName;
// mini-css-extract-plugin 用 dist/cjs 为入口会有问题
if (opts.pkgName === 'mini-css-extract-plugin') {
resolvePath = 'mini-css-extract-plugin/dist/index';
}
// entry = /Users/nanlan/Documents/com-project/umi/packages/bundler-webpack/node_modules/less-loader/dist/cjs.js,
// 很神奇,为啥会拼上/dist/cjs.js
entry = resolve.sync(resolvePath, {
basedir: nodeModulesPath,
});
} else {
entry = path.join(opts.base, opts.file);
}
if (!opts.dtsOnly) {
if (opts.isDependency) {
fs.ensureDirSync(target);
fs.writeFileSync(
path.join(target, 'index.js'),
`
const exported = require("${opts.pkgName}");
Object.keys(exported).forEach(function (key) {
if (key === "default" || key === "__esModule") return;
if (key in exports && exports[key] === exported[key]) return;
Object.defineProperty(exports, key, {
enumerable: true,
get: function get() {
return exported[key];
}
});
});
`.trim() + '\n',
'utf-8',
);
} else {
const filesToCopy: string[] = [];
if (opts.file === './bundles/webpack/bundle') {
delete opts.webpackExternals['webpack'];
}
// babel pre rewrite
if (opts.file === './bundles/babel/bundle') {
// See https://github.com/umijs/umi/issues/10356
// The inherited `browserslist` config is dynamic loaded
const babelCorePkg = require.resolve('@babel/core/package.json', {
paths: [path.join(PATHS.PACKAGES, './bundler-utils')],
});
// And need overrides a consistent version of `browserslist` in `packages.json#pnpm.overrides`
const browserslistPkg = require.resolve('browserslist/package.json', {
paths: [path.dirname(babelCorePkg)],
});
const nodePartFile = path.join(
path.dirname(browserslistPkg),
'node.js',
);
const originContent = fs.readFileSync(nodePartFile, 'utf-8');
// https://github.com/browserslist/browserslist/blob/fc5fc088c640466df62a6b6c86154b19be3de821/node.js#L176
fs.writeFileSync(
nodePartFile,
originContent.replace(
/require\(require\.resolve/g,
'eval("require")(require.resolve',
),
'utf-8',
);
}
// 利用ncc 转化为 单个的es5文件
let { code, assets } = await ncc(entry, {
externals: opts.webpackExternals,
minify: !!opts.minify,
target: 'es5',
assetBuilds: false,
customEmit(filePath: string, { id }: any) {
if (
(opts.file === './bundles/webpack/bundle' &&
filePath.endsWith('.runtime.js')) ||
(opts.pkgName === 'terser-webpack-plugin' &&
filePath.endsWith('./utils') &&
id.endsWith('terser-webpack-plugin/dist/index.js')) ||
(opts.pkgName === 'css-minimizer-webpack-plugin' &&
filePath.endsWith('./utils') &&
id.endsWith('css-minimizer-webpack-plugin/dist/index.js'))
) {
filesToCopy.push(
resolve.sync(filePath, {
basedir: path.dirname(id),
}),
);
return `'./${path.basename(filePath)}'`;
}
},
});
// assets
// console.log('filesToCopy', filesToCopy);
for (const key of Object.keys(assets)) {
const asset = assets[key];
const data = asset.source;
const filePath = path.join(target, key);
fs.ensureDirSync(path.dirname(filePath));
fs.writeFileSync(path.join(target, key), data);
}
// filesToCopy
for (const fileToCopy of filesToCopy) {
let content = fs.readFileSync(fileToCopy, 'utf-8');
for (const key of Object.keys(opts.webpackExternals)) {
content = content.replace(
new RegExp(`require\\\(['"]${key}['"]\\\)`, 'gm'),
`require('${opts.webpackExternals[key]}')`,
);
content = content.replace(
new RegExp(`require\\\(['"]${key}/package(\.json)?['"]\\\)`, 'gm'),
`require('${opts.webpackExternals[key]}/package.json')`,
);
}
fs.writeFileSync(
path.join(target, path.basename(fileToCopy)),
content,
'utf-8',
);
}
// entry code
fs.ensureDirSync(target);
//省略代码, 一些特殊模块的处理
}
写入license 和 package.json ,利用new Package
提取有.d.ts
的依赖
js
// license & package.json
if (opts.pkgName) {
// 如果依赖是dependencies内,则写入index.d.ts,着实有点懵?
if (opts.isDependency) {
fs.ensureDirSync(target);
fs.writeFileSync(
path.join(target, 'index.d.ts'),
`export * from '${opts.pkgName}';\n`,
'utf-8',
);
} else {
fs.ensureDirSync(target);
const pkgRoot = path.dirname(
resolve.sync(`${opts.pkgName}/package.json`, {
basedir: opts.base,
}),
);
// 写入LICENSE
if (fs.existsSync(path.join(pkgRoot, 'LICENSE'))) {
fs.writeFileSync(
path.join(target, 'LICENSE'),
fs.readFileSync(path.join(pkgRoot, 'LICENSE'), 'utf-8'),
'utf-8',
);
}
const { name, author, license, types, typing, typings, version } =
JSON.parse(
fs.readFileSync(path.join(pkgRoot, 'package.json'), 'utf-8'),
);
// 写入package.json
fs.writeJSONSync(path.join(target, 'package.json'), {
...{},
...{ name },
...{ version },
...(author ? { author } : undefined),
...(license ? { license } : undefined),
...(types ? { types } : undefined),
...(typing ? { typing } : undefined),
...(typings ? { typings } : undefined),
});
// dts
if (opts.noDts) {
console.log(chalk.yellow(`Do not build dts for ${opts.pkgName}`));
} else {
// import { Package } from 'dts-packer';
// 这个是作者自己写的包,貌似是提取.d.ts文件
new Package({
cwd: opts.base,
name: opts.pkgName,
typesRoot: target,
externals: opts.dtsExternals,
});
// patch
if (opts.pkgName === 'webpack-5-chain') {
const filePath = path.join(target, 'types/index.d.ts');
// 替换成从umi引入
fs.writeFileSync(
filePath,
fs
.readFileSync(filePath, 'utf-8')
.replace(
`} from 'webpack';`,
`} from '@umijs/bunder-webpack/compiled/webpack';`,
),
'utf-8',
);
}
// for bundler-utils
if (opts.pkgName === 'less') {
const dtsPath = path.join(opts.base, 'compiled/less/index.d.ts');
fs.writeFileSync(
dtsPath,
fs
.readFileSync(dtsPath, 'utf-8')
.replace(
'declare module "less"',
'declare module "@umijs/bundler-utils/compiled/less"',
),
'utf-8',
);
}
}
}
}
// copy files in packages
if (opts.file && !opts.dtsOnly) {
const packagesDir = path.join(
opts.base,
path.dirname(opts.file),
'packages',
);
if (fs.existsSync(packagesDir)) {
const files = fs.readdirSync(packagesDir);
files.forEach((file) => {
if (file.charAt(0) === '.') return;
if (!fs.statSync(path.join(packagesDir, file)).isFile()) return;
fs.copyFileSync(path.join(packagesDir, file), path.join(target, file));
});
}
}
依赖预打包的关键代码就是这些, 主要使用了ncc 和dts-packer从node_modules获取源码来转化的。那么看完其实有几个问题的
1、怎么做到定期更新的
2、哪些包需要预打包,哪些包不需要?
3、既然大部分依赖预打包是从node_modules获取,那么webpack和babael 本地有大量的代码,难道是源码?
希望在后续的学习中能破解这谜题