本篇主要讲述 umi 脚手架的npm script命令

参考

文档

前置知识

$ 怎么来的

1
2
// globals 函数增加了 global的$等等属性
import 'zx/globals';

模板字符串的函数调用

参考

pnpm release

关于 umi-scripts 命令

精妙的workspace:*

pnpm 的 workspace:* 可以轻松的将源码 以npm link的方式映射到 node_module 上,
极大地提高了开发和调试效率。

1
2
3
4
5
6
7
8
9

"scripts": {
"release": "umi-scripts release",
},


"devDependencies": {
"umi-scripts": "workspace:*",
},

使用 esno 执行 ts文件

每个命令将最终被使用 esno 执行 ts文件

1
2
3
4
5
6
7
8
9
10
11
12
// scripts 就是 umi-scripts 包
// scripts\bin\umi-scripts.js
const spawn = sync(
'esno',
[scriptsPath, ...argv.slice(1)],
{
env: process.env,
cwd: process.cwd(),
stdio: 'inherit',
shell: true
}
)

通过readdirSync读取 packages 下的包

1
2
3
4
5
6
export function getPkgs(opts?: { base?: string }): string[] {
const base = opts?.base || PATHS.PACKAGES;
return readdirSync(base).filter((dir) => {
return !dir.startsWith('.') && existsSync(join(base, dir, 'package.json'));
});
}

获取当前的branch

1
2
import getGitRepoInfo from 'git-repo-info';
const { branch } = getGitRepoInfo();

查看是否有未git add 文件

1
2
const isGitClean = (await $`git status --porcelain`).stdout.trim().length;
assert(!isGitClean, 'git status is not clean');

assert错误判断与退出程序的妙用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// 关于logger.error
import chalk from '../compiled/chalk';

export const prefixes = {
error: chalk.red('error') + ' -',
};
export function error(...message: any[]) {
console.error(prefixes.error, ...message);
}

// 关于assert
export function assert(v: unknown, message: string) {
if (!v) {
logger.error(message);
process.exit(1);
}
}

// assert 使用
const isGitClean = (await $`git status --porcelain`).stdout.trim().length;
assert(!isGitClean, 'git status is not clean');

判断你没有拉取最新的远程提交

1
2
3
await $`git fetch`;
const gitStatus = (await $`git status --short --branch`).stdout.trim();
assert(!gitStatus.includes('behind'), `git status is behind remote`);

判断npm源是否为要发布的源

1
2
3
4
5
6
7
// check npm registry
logger.event('check npm registry');
const registry = (await $`npm config get registry`).stdout.trim();
assert(
registry === 'https://registry.npmjs.org/',
'npm registry is not https://registry.npmjs.org/',
);

(待研究)lerna判断packages下是否有改动

1
2
3
4
// check package changed
logger.event('check package changed');
const changed = (await $`lerna changed --loglevel error`).stdout.trim();
assert(changed, `no package is changed`);

是否在npm包的owner上

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// check npm ownership
logger.event('check npm ownership');
const whoami = (await $`npm whoami`).stdout.trim();
await Promise.all(
['umi', '@umijs/core'].map(async (pkg) => {
const owners = (await $`npm owner ls ${pkg}`).stdout
.trim()
.split('\n')
.map((line) => {
return line.split(' ')[0];
});
assert(owners.includes(whoami), `${pkg} is not owned by ${whoami}`);
}),
);

检测文件(目录)是否符合发布check:packageFiles

检测 packages 和 examples 下的包或示例,其下面的每个文件或目录是否符合约定的 npm publish 标准,
会查看 pkgJson.files

1
await $`npm run check:packageFiles`;
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
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
  // check package.json
logger.event('check package.json info');


// clean
logger.event('clean');
eachPkg(pkgs, ({ dir, name }) => {
logger.info(`clean dist of ${name}`);
rimraf.sync(join(dir, 'dist'));
});

// build packages
logger.event('build packages');
await $`npm run build:release`;
await $`npm run build:extra`;
await $`npm run build:client`;

logger.event('check client code change');
const isGitCleanAfterClientBuild = (
await $`git status --porcelain`
).stdout.trim().length;
assert(!isGitCleanAfterClientBuild, 'client code is updated');

// generate changelog
// TODO
logger.event('generate changelog');

// bump version
logger.event('bump version');
await $`lerna version --exact --no-commit-hooks --no-git-tag-version --no-push --loglevel error`;
const version = require(PATHS.LERNA_CONFIG).version;
let tag = 'latest';
if (
version.includes('-alpha.') ||
version.includes('-beta.') ||
version.includes('-rc.')
) {
tag = 'next';
}
if (version.includes('-canary.')) tag = 'canary';

// update example versions
logger.event('update example versions');
const examplesDir = PATHS.EXAMPLES;
const examples = fs.readdirSync(examplesDir).filter((dir) => {
return (
!dir.startsWith('.') && existsSync(join(examplesDir, dir, 'package.json'))
);
});
examples.forEach((example) => {
const pkg = require(join(examplesDir, example, 'package.json'));
pkg.scripts['start'] = 'npm run dev';
// change deps version
setDepsVersion({
pkg,
version,
deps: [
'umi',
'@umijs/max',
'@umijs/plugins',
'@umijs/bundler-vite',
'@umijs/preset-vue',
],
// for mfsu-independent example update dep version
devDeps: ['@umijs/mfsu'],
});
delete pkg.version;
fs.writeFileSync(
join(examplesDir, example, 'package.json'),
`${JSON.stringify(pkg, null, 2)}\n`,
);
});

// update pnpm lockfile
logger.event('update pnpm lockfile');
$.verbose = false;
await $`pnpm i`;
$.verbose = true;

// commit
logger.event('commit');
await $`git commit --all --message "release: ${version}"`;

// git tag
if (tag !== 'canary') {
logger.event('git tag');
await $`git tag v${version}`;
}

// git push
logger.event('git push');
await $`git push origin ${branch} --tags`;

// npm publish
logger.event('pnpm publish');
$.verbose = false;
const innerPkgs = pkgs.filter(
// do not publish father
(pkg) => !['umi', 'max', 'father'].includes(pkg),
);

// check 2fa config
let otpArg: string[] = [];
if (
(await $`npm profile get "two-factor auth"`).toString().includes('writes')
) {
let code = '';
do {
// get otp from user
code = await question('This operation requires a one-time password: ');
// generate arg for zx command
// why use array? https://github.com/google/zx/blob/main/docs/quotes.md
otpArg = ['--otp', code];
} while (code.length !== 6);
}

await Promise.all(
innerPkgs.map(async (pkg) => {
await $`cd packages/${pkg} && npm publish --tag ${tag} ${otpArg}`;
logger.info(`+ ${pkg}`);
}),
);
await $`cd packages/umi && npm publish --tag ${tag} ${otpArg}`;
logger.info(`+ umi`);
await $`cd packages/max && npm publish --tag ${tag} ${otpArg}`;
logger.info(`+ @umijs/max`);
$.verbose = true;

// sync tnpm
logger.event('sync tnpm');
$.verbose = false;
await Promise.all(
pkgs.map(async (pkg) => {
const { name } = require(path.join(PATHS.PACKAGES, pkg, 'package.json'));
logger.info(`sync ${name}`);
await $`tnpm sync ${name}`;
}),
);
$.verbose = true;
})();

function setDepsVersion(opts: {
deps: string[];
devDeps: string[];
pkg: Record<string, any>;
version: string;
}) {
const { deps, devDeps, pkg, version } = opts;
pkg.dependencies ||= {};
deps.forEach((dep) => {
if (pkg.dependencies[dep]) {
pkg.dependencies[dep] = version;
}
});
devDeps.forEach((dep) => {
if (pkg?.devDependencies?.[dep]) {
pkg.devDependencies[dep] = version;
}
});
return pkg;
}

删除package下的dist

1
2
3
4
eachPkg(pkgs, ({ dir, name }) => {
logger.info(`clean dist of ${name}`);
rimraf.sync(join(dir, 'dist'));
});

给每个包进行build生产dist

1
2
3
4
5
// build packages
logger.event('build packages');
await $`npm run build:release`;
await $`npm run build:extra`;
await $`npm run build:client`;

再次检查是否有未git add 文件

因为上面的 build命令,有些包会改变 client 目录

1
2
3
4
5
logger.event('check client code change');
const isGitCleanAfterClientBuild = (
await $`git status --porcelain`
).stdout.trim().length;
assert(!isGitCleanAfterClientBuild, 'client code is updated');

lerna 批量更新版本号

1
2
3
4
5
6
7
8
9
10
11
12
13
// bump version
logger.event('bump version');
await $`lerna version --exact --no-commit-hooks --no-git-tag-version --no-push --loglevel error`;
const version = require(PATHS.LERNA_CONFIG).version;
let tag = 'latest';
if (
version.includes('-alpha.') ||
version.includes('-beta.') ||
version.includes('-rc.')
) {
tag = 'next';
}
if (version.includes('-canary.')) tag = 'canary';

更新 example versions

1
2
logger.event('update example versions');
...

更新 lockfile 文件

1
2
3
4
5
// update pnpm lockfile
logger.event('update pnpm lockfile');
$.verbose = false;
await $`pnpm i`;
$.verbose = true;

commit提交

1
2
3
// commit
logger.event('commit');
await $`git commit --all --message "release: ${version}"`;

设置tag,并提高分支以及tag

1
2
3
4
5
6
7
8
9
// git tag
if (tag !== 'canary') {
logger.event('git tag');
await $`git tag v${version}`;
}

// git push
logger.event('git push');
await $`git push origin ${branch} --tags`;

pnpm publish发布前检测密码(可忽略)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// check 2fa config
let otpArg: string[] = [];
if (
(await $`npm profile get "two-factor auth"`).toString().includes('writes')
) {
let code = '';
do {
// get otp from user
code = await question('This operation requires a one-time password: ');
// generate arg for zx command
// why use array? https://github.com/google/zx/blob/main/docs/quotes.md
otpArg = ['--otp', code];
} while (code.length !== 6);
}

pnpm publish

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// npm publish
logger.event('pnpm publish');
$.verbose = false;
const innerPkgs = pkgs.filter(
// do not publish father
(pkg) => !['magicbird', 'max', 'father'].includes(pkg),
);


// 这里有上面的步骤 pnpm publish发布前检测密码(可忽略),可以不用管

await Promise.all(
innerPkgs.map(async (pkg) => {
await $`cd packages/${pkg} && npm publish --tag ${tag} ${otpArg}`;
logger.info(`+ ${pkg}`);
}),
);
await $`cd packages/magicbird && npm publish --tag ${tag} ${otpArg}`;
logger.info(`+ magicbird`);
await $`cd packages/max && npm publish --tag ${tag} ${otpArg}`;
logger.info(`+ @magicbirdjs/max`);