从零学源码 | vue-release

1,048 阅读3分钟

从零学源码_vue-release.png

一、vue-relase是什么?

vue-releasevue-next/scripts/release.js,主要执行vue版本发布前的一些操作,包括确认版本、执行测试用例、更新依赖、构建、提交git、发布包和推送Github等。

二、前置

1、克隆代码
git clone https://github.com/vuejs/vue-next
2、安装依赖、构建代码
$ npm install --global yarn

$ yarn install

$ yarn build

三、vscode调试

1、js打断点
2、开启调试

打开package.json,然后搜索scripts,在它的上方有个“调试”(或是Debug)按钮,点击它,然后选择scripts里需要调试的命令,此处需要调试的是release命令。

{
    "scripts": {
        "dev": "bide scripts/dev.js",
        "build": "node scripts/build.js",
        "release": "node scripts/release.js"
        ......
    }
    ......
}
3、控制台输出
Running tests

Updating cross dependencies

    @vue/compiler-core -> dependencies -> @vue/shared@3.2.7
    @vue/compiler-dom -> dependencies -> @vue/shared@3.2.7
    @vue/compiler-dom -> dependencies -> @vue/compiler-core@3.2.7
    @vue/compiler-sfc -> dependencies -> @vue/compiler-core@3.2.7
    @vue/compiler-sfc -> dependencies -> @vue/compiler-dom@3.2.7
    @vue/compiler-sfc -> dependencies -> @vue/compiler-ssr@3.2.7
    @vue/compiler-sfc -> dependencies -> @vue/ref-transform@3.2.7
    @vue/compiler-sfc -> dependencies -> @vue/shared@3.2.7
    @vue/compiler-ssr -> dependencies -> @vue/shared@3.2.7
    @vue/compiler-ssr -> dependencies -> @vue/compiler-dom@3.2.7
    @vue/reactivity -> dependencies -> @vue/shared@3.2.7
    @vue/ref-transform -> dependencies -> @vue/compiler-core@3.2.7
    @vue/ref-transform -> dependencies -> @vue/shared@3.2.7
    @vue/runtime-core -> dependencies -> @vue/shared@3.2.7
    @vue/runtime-core -> dependencies -> @vue/reactivity@3.2.7
    @vue/runtime-dom -> dependencies -> @vue/shared@3.2.7
    @vue/runtime-dom -> dependencies -> @vue/runtime-core@3.2.7
    @vue/runtime-test -> dependencies -> @vue/shared@3.2.7
    @vue/runtime-test -> dependencies -> @vue/runtime-core@3.2.7
    @vue/server-renderer -> dependencies -> @vue/shared@3.2.7
    @vue/server-renderer -> dependencies -> @vue/compiler-ssr@3.2.7
    @vue/server-renderer -> peerDependencies -> vue@3.2.7
    vue -> dependencies -> @vue/shared@3.2.7
    vue -> dependencies -> @vue/compiler-dom@3.2.7
    vue -> dependencies -> @vue/runtime-dom@3.2.7
    @vue/compat -> peerDependencies -> vue@3.2.7

Building all packages

Committing changes

Publishing packages

Publishing compiler-core

Publishing compiler-dom

Publishing compiler-sfc

Publishing compiler-ssr

Publishing reactivity

Publishing ref-transform

Publishing runtime-core

Publishing runtime-dom

Publishing server-renderer

Publishing shared

Publishing vue

Publishing vue-compat

Pushing to GitHub

四、源码阅读

1、main方法

main方法是release.js中的入口方法。通过调用js中的其他方法,完成以下操作。

async function main() {
......
}

main().catch(err => {
    console.error(err);
})

(1)确认目标版本

第一步,判断targetVersion是否存在,如果没有值,则提示选择,并增加一个自定义的选项。

let targetVersion = [];
if (!targetVersion) {
    const { release } = await prompt({
        type: 'select',
        name: 'release',
        message: 'Select release type',
        choices: versionIncrements.map(i => `${i} (${inc(i)})`).concat(['custom']);
    })
    
    // 除可以选择的版本以外,增加一个自定义选项
    if (release === 'custom') {
        targetVersion = (
            await prompt({
                type: 'input',
                name: 'version',
                message: 'Input custom version',
                initial: currentVersion
            })
        ).version;
    } else {
        targetVersion = release.match(/\((.*)\)/)[1]
    }
}

第二步,使用semver.valid校验版本的合法性。

if (!semver.valid(targetVersion)) {
    throw new Error(`invalid target version:${targetVersion}`);
}

第三步,确认版本。

const { yes } = await prompt({
    type: 'confirm',
    name: 'yes',
    message: `Releasing v${targetVersion}. Confirm`
})

if (!yes) {
    return;
}

(2)执行测试用例

const bin = name => path.resolve(__dirname, '../node_modules/.bin/' + name);
const run = (bin, args, opts = {}) => 
    execa(bin, args, { stdio: 'inherit', ...opts});
if (!skipTests && !isDryRun) {
    await run(bin('jest'), ['--clearCache']);
    await run('yarn', ['test', '--bail']);
} else {
    console.log(`(skipped)`);
}

(3)更新依赖

updateVersion(targetVersion);

(4)构建

if (!skipBuild && !isDryRun) {
    await run('yarn', ['build', '--release']);
    await run('yarn', ['test-dts-only']);
} else {
    console.log(`(skipped)`);
}

(5)提交git

await run(`yarn`, ['changelog']);

const { stdout } = await run('git', ['diff'], { stdio: 'pipe' });
if (stdout) {
    await runIfNotDry('git', ['add', '-A']);
    await runIfNotDry('git', ['commit', '-m', `release:v${targetVersion}`]);
} else {
    console.log('No changes to commit');
}

(6)发布包

for (const pkg of packages) {
    await publishPackage(pkg, targetVersion, runIfNotDry);
}

(7)推送到Github

await runIfNotDry('git', `v${targetVersion}`);
await runIfNotDry('git', ['push', 'origin', `refs/tags/v${targetVersion}`]);
await runIfNotDry('git', ['push']);
2、updateVersion方法

作用: main方法中调用,传入目标版本,用于更新package.json中的版本。

第一步,调用updatePackage方法,更新package.json

updatePackage(path.resolve(__dirname, '..'), version);

注意: path.resolve(__dirname, '..')传入的是根目录。

第二步,遍历所有packages,更新它们的package.json

const fs = require('fs')
const packages = fs
    .readdirSync(path.resolve(__dirname), '../packages')
    .filter(p => !p.endsWith('.ts') && !p.startsWith('.'))
.....
packages.forEach(p => updatePackage(getPkRoot(p), version));

注意: 此处获取packages用到了fs.readdirSync方法。

3、updatePackage方法

作用: updateVersion方法中调用,传入路径pkgRoot和版本version,用于更新包。

第一步,获取package的路径。

const pkgPath = path.resolve(pkgRoot, 'package.json');

第二步,使用fs.readFileSync方法读取package中的内容,然后更新pkgversion值。

const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf-8'));
pkg.version = version;

第三步,调用updateDeps方法更新依赖。

updateDeps(pkg, 'dependencies', version);
updateDeps(pkg, 'peerDependencies', version);

第四步,使用fs.writeFileSync方法将更新后的内容写入文件。

fs.writeFileSync(pkgPath, JSON.stringify(pkg, null, 2) + '\n');
4、updateDeps方法

作用: updatePackage方法中调用,传入包pkg、依赖类型depType和版本version,用于更新依赖。

第一步,获取依赖。

const deps = pkg[depType];
// 如果没有依赖,则直接返回
if (!deps) return;

第二步,遍历依赖,判断依赖满足以下条件时更新依赖的版本号。

(1)是vue

(2)以@vue开头;

(3)包含@vue

// 遍历依赖
Object.keys(deps).forEach(dep => {
    if (dep === 'vue' || 
        (dep.startWith('@vue') && packages.includes(dep.replace(/^@vue\//, '')))
        ) {
            console.log(
                chalk.yellow(`${pkg.name} -> ${depType} -> ${dep}@${version}`)
                deps[dep] = version;
            )
        }
})
5、publishPackage方法

作用:main方法中调用,传入包pkgName,版本version,是否执行更新操作runIfNotDry,用于发布包。

第一步,判断是否是需要忽略更新的包。

if (skippedPackage.includes(pkgName)) {
    return;
}

第二步,获取包的信息。

const pkgRoot = getPkRoot(pkgName);
const pkgPath = path.resolve(pkgRoot, 'package.json');
const pkg = JSON.parse(fs.readFileSync(pkgPathm 'utf-8'));
// 判断包是否是私有的
if (pkg.private) {
    return;
}

第三步,判断包的releaseTag

(1)如果传入的tag参数有值,则releaseTagtag的值;

(2)如果传入的版本号中带有alpha,则releaseTag的值为alpha

(3)如果传入的版本号中带有beta,则releaseTag的值为beta

(4)如果传入的版本号中带有rc,则releaseTag的值为rc

(4)如果包名为vue,则releaseTag的值为next

let releaseTag = null;
if (arg.tag) {
    releaseTag = args.tag;
} else if (version.includes('alpha')) {
    releaseTag = 'alpha';
} else if (version.includes('beta')) {
    releaseTag = 'beta';
} else if (version.includes('rc')) {
    releaseTag = 'rc';
} else if (pkgName === 'vue') {
    releaseTag = 'next';
}

第四步,根据传入的runIfNotDry,执行更新操作或只做控制台输出。

// 获取是否执行更新操作的标识
const idDryRun = args.dry;
// 执行更新操作
const run = (bin, args, opts = {}) => 
    execa(bin, args, { stdio: 'inherit', ...opts});
// 只做控制台输出    
const dryRun = (bin, args, opts = {}) => 
    console.log(chalk.blue(`[dryRun] ${bin} ${args.join(' ')}`), opts);
const runIfNotDry = isDryRun ? dryRun : run; 

async function publishPackage(pkgName, version, runIfNotDry) {
    ......
    try {
        await runIfNotDry(
            'yarn',
            [
                'publish',
                '--new-version',
                version,
                --(releaseTag ? ['--tag', releaseTag] : []),
                '--access',
                'public'
            ],
            {
                cwd: pkgRoot,
                stdio: 'pipe'
            }
        )
        console.log(chalk.green(`Successfully published ${pkgName}@${version}`));
    } catch (e) {
        // 此处捕获异常,判断是否是为该版本已发布,如果是,则做出相应提示
        if (e.stderr.match(/previously published/)) {
            console.log(chalk.red(`Skipping already published: ${pkgName}`));
        } else {
            throw e;
        }
    }
}

五、常用类库

1、args

作用: 用于解析参数

const args = require('minimist')(process.argv.slice(2)) 
2、fs

作用: 用于文件读写

const fs = require('fs') 

// 示例
const packages = fs.readdirSync(path.resolve(__dirname), '../packages');

fs.writeFileSync(pkgPath, JSON.stringify(pkg, null, 2));

const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf-8'));
3、path

作用: 用于获取路径

const path = require('path') 
4、chalk

作用: 用于控制台高亮输出

const chalk = require('chalk') 

// 示例
chalk.blue(`[dryRun] ${bin} ${args.join(' ')}`), opts)

chalk.cyan(msg);

chalk.yellow(
    `The following packages ar skipped and NOT published:\n- ${skippedPackages.join('\n- ')}`
)

chalk.green(`Successfully published ${pkgName}@${version}`);

chalk.red(`Skipping already published:${pkgName}`);
5、semver

作用: 用于版本号处理

const semver = require('semver') 

// 示例
// 解析版本号
semver.prerelease('1.2.3-alpha.1') -> ['alpha', 1]

// 版本号加1
semver.inc('1.2.3', 'prerelease', 'beta') -> '1.2.4-beta.0'

// 校验版本号
semver.valid(targetVersion);
6、enquirer

作用: 用于询问确认

const { prompt } = require('enquirer') 

// 示例
// 选择
prompt({
    type: 'select',
    name: 'release',
    message: 'Select release type',
    choices: versionIncrements.map(i => 
        `${i} (${inc(i)})`
    ).concat(['custom'])
})

// 输入
prompt({
    type: 'input',
    name: 'version',
    message: 'Input custom version',
    initial: currentVersion
})

// 确认
prompt({
    type: 'confirm',
    name: 'yes',
    message: `Releasing v${targetVersion}. Confirm?`
})
7、execa

作用: 用于执行脚本命令

const execa = require('execa')

六、收获

(1)熟悉了vue-release的大致发布流程;

(2)在release.js中支持传入不同的参数,对可能出现的几种情况都进行了处理,包括是否执行更新操作,是否跳过测试,是否跳过构建等等。

(3)在release.js中用到了ES6语法中的asyncawait,使代码更加简洁易懂。