preface
Monorepo NPM package release recently, reference (copy) Vite release method. However, when I finished the next day, Vite rewrote its release script (2022/2/11) πππ so I revised my project again and wrote an article about it.
This paper will be divided into two parts:
- Vite publishing from the user’s perspective
- Vite release implementation
Vite publishing from the user’s perspective
After Vite publishing reconstruction, users can publish Vite through a visual interface.
The GitHub Action screen is shown below. You can publish vite and some official Vite plug-ins by manually triggering GitHub actions.
It is important to note that only project members can see and execute the Release workflow. If we want to experience running this workflow, we need to fork the Vite repository and then go to the Action screen
Where Package and type are optional, corresponding to the NPM package to be released and the generation method of version number (such as only adding minior revision number).
Once run, the workflow is automatically executed to publish vite to NPM.
Vite release implementation
Github workflows configuration files are stored in the repository under.github/workflows.
We are running the release workflow, so we need to see the lot/workflows/the yml configuration
release.yml
Let’s break release.yml down into several parts:
- Defines parameters that the user can select
- Run job and publish to NPM
Define optional parameters for the user
Optional parameters are as follows:
- The branch that runs the publication, the main branch by default
- The NPM package to publish, vite by default
- Version number generation mode
name: Release
on:
workflow_dispatch:
inputs:
branch:
description: "branch"
required: true
type: string
default: "main"
package:
description: "package"
required: true
type: choice
options:
- vite
- plugin-legacy
- plugin-vue
- plugin-vue-jsx
- plugin-react
- create-vite
type:
description: "type"
required: true
type: choice
options:
- next
- stable
- minor-beta
- major-beta
- minor
- major
Copy the code
The effect is as follows:
The form entry Use Workflow from, which is not defined in release.yml, is a built-in option when GitHub runs the workflow.
What it does is, which branch release.yml is read, because release.yml may be different from branch to branch.
If the selected branch does not have a release.yml file, these three options are no longer available and the pipeline cannot run.
Run job and publish to NPM
There are mainly the following steps:
- Build using Ubuntu images
- Pull git repository code and pull the selected branch
- Using the 16
- Install the PNPM
- PNPM install, if there is a cache, then use the cache to speed up the installation
- In the project of the package that needs to be published, execute
pnpm run release
jobs:
release:
# prevents this action from running on forks
# Avoid forking the repository to run this workflow
if: github.repository = = 'vitejs/vite'
name: Release
runs-on: The ${{ matrix.os }}
environment: Release
strategy:
matrix:
# pseudo-matrix for convenience, NEVER use more than a single combination
# Pseudo-matrix, for convenience (maybe vite developers copied code from elsewhere and made a few changes), don't use more than one combination
Use the latest Ubuntu system and run the job using Node 16
node: [16]
os: [ubuntu-latest]
steps:
Pull git code
- name: checkout
uses: actions/checkout@v2
with:
# pull the corresponding branch
ref: The ${{ github.event.inputs.branch }}
# fetch-depth set to 0 to get all git commit histories and tags. If this parameter is not set, only the latest commit information is obtained by default.
Get all Git commits to generate Changelog
fetch-depth: 0
Use the previously defined Version of Node 16
- uses: actions/setup-node@v2
with:
node-version: The ${{ matrix.node }}
Set git user to commit code
- run: git config user.name vitebot
- run: git config user.email [email protected]
Install PNPM and YARN. PNPM is used to install dependencies and YARN is used to publish NPM packages
- run: npm i -g pnpm@6
- run: npm i -g yarn # even if the repo is using pnpm, Vite still uses yarn v1 for publishing
- run: yarn config set registry https://registry.npmjs.org # Yarn's default registry proxy doesn't work in CI
# Use Node 16, again to specify the use of caching (PNPM not previously installed)
# cache usage details see: https://github.com/actions/setup-node#caching-packages-dependencies
- uses: actions/setup-node@v2
with:
node-version: The ${{ matrix.node }}
cache: "pnpm"
cache-dependency-path: "**/pnpm-lock.yaml"
# install dependencies
- name: install
run: pnpm install --frozen-lockfile --prefer-offline
# create.npmrc, used to store the secret key published by NPM, automatically read.npmrc secret key when published, avoiding user interaction such as password input
- name: Creating .npmrc
run: | cat << EOF > "$HOME/.npmrc" //registry.npmjs.org/:_authToken=$NPM_TOKEN EOF env:
NPM_TOKEN: The ${{ secrets.NPM_TOKEN }}
# Run the release script in package.json under Packages and pass in the parameters
# --quiet Skips command line interaction. The pipeline cannot interact
# --type Specifies how the version number is generated
- name: Release
run: pnpm --dir packages/${{ github.event.inputs.package }} release -- --quiet --type The ${{ github.event.inputs.type }}
env:
GITHUB_TOKEN: The ${{ secrets.GITHUB_TOKEN }}
NPM_TOKEN: The ${{ secrets.NPM_TOKEN }}
Copy the code
Release the script
Each package directory has a package.json with a release script, such as vite:
/ / of/packages/the vite/package. The json
{
"name": "vite"."version": "2.8.1"."author": "Evan You"."scripts": {
"release": "ts-node .. /.. /scripts/release.ts"}}Copy the code
You actually executed relex.ts in the scripts directory of the vite repository root. All packages in the Vite repository are distributed using this script
The document is 250 lines long, not much, so let’s take a look at the general structure:
async function main() :Promise<void> {
// for now
}
main().catch((err) = > {
console.error(err)
})
Copy the code
The entire file simply executes main and prints out any errors.
The main function consists of the following steps:
- Generate a new version number targetVersion
- Confirm the release again
- Update package.json version number
- Execute a build
- Generate the changelog
- Release NPM package
- Commit to making
Generate a new version number
If no version number is specified when executing the script, a new version number is generated
The generated rules are generated from the command line argument –type. The number of lines is quite large, in fact, most of them are some error handling and prompts, do not need to investigate. It is important to know that the NPM package semver was used to generate the version number.
const currentVersion = '1.0.0'
const inc: (i: ReleaseType) = > string = (i) = >
semver.inc(currentVersion, i, 'beta')!
inc('major') // 2.0.0, if the second argument is not preXXX(premajor, etc.), the third argument is ignored
inc('premajor') / / 2.0.0 - beta. 0
inc('minor') / / 1.1.0
inc('preminor') / / 1.1.0 - beta. 0
inc('patch') / / 1.0.1
inc('prepatch') / / - beta 1.0.1. 0
inc('prerelease') / / - beta 1.0.1. 0
Copy the code
If –type is not passed, the version type is selected through command line interaction. This happens when you manually call PNPM Run Release in your project, which is how you publish before you publish refactoring. The cli interaction modes are as follows:
Here’s the code:
// args is the argument passed in when the script is executed using the command line
// Make the first parameter targetVersion
let targetVersion: string | undefined = args._[0]
// If no targetVersion is passed, it will be generated automatically
if(! targetVersion) {// Read the type argument from the command line, --type XXX
const type: string | undefined = args.type
// Generate a version number based on type
if (type) {
const currentBeta = currentVersion.includes('beta')
if (type= = ='next') {
targetVersion = inc(currentBeta ? 'prerelease' : 'patch')}else if (type= = ='stable') {
// Out of beta
if(! currentBeta) {throw new Error(
`Current version: ${currentVersion} isn't a beta, stable can't be used`
)
}
targetVersion = inc('patch')}else if (type= = ='minor-beta') {
if (currentBeta) {
throw new Error(
`Current version: ${currentVersion} is already a beta, minor-beta can't be used`
)
}
targetVersion = inc('preminor')}else if (type= = ='major-beta') {
if (currentBeta) {
throw new Error(
`Current version: ${currentVersion} is already a beta, major-beta can't be used`
)
}
targetVersion = inc('premajor')}else if (type= = ='minor') {
if (currentBeta) {
throw new Error(
`Current version: ${currentVersion} is a beta, use stable to release it first`
)
}
targetVersion = inc('minor')}else if (type= = ='major') {
if (currentBeta) {
throw new Error(
`Current version: ${currentVersion} is a beta, use stable to release it first`
)
}
targetVersion = inc('major')}else {
throw new Error(
`type: The ${type} isn't a valid type. Use stable, minor-beta, major-beta, or next`)}}else {
// no explicit version or type, offer suggestions
const { release }: { release: string } = await prompts({
type: 'select'.name: 'release'.message: 'Select release type'.choices: versionIncrements
.map((i) = > `${i} (${inc(i)}) `)
.concat(['custom'])
.map((i) = > ({ value: i, title: i }))
})
if (release === 'custom') {
const res: { version: string } = await prompts({
type: 'text'.name: 'version'.message: 'Input custom version'.initial: currentVersion
})
targetVersion = res.version
} else {
targetVersion = release.match(/ / / ((. *) \))! [1]}}}Copy the code
Confirm the release again
Confirm the release again, beta version needs to confirm again.
If the –quiet argument is passed, it will be skipped. This is used when the GitHub CI workflow is executed and the execution script will actively pass –quiet.
if(! args.quiet) {if (targetVersion.includes('beta') && !args.tag) {
const { tagBeta }: { tagBeta: boolean } = await prompts({
type: 'confirm'.name: 'tagBeta'.message: `Publish under dist-tag "beta"? `
})
if (tagBeta) args.tag = 'beta'
}
const { yes }: { yes: boolean } = await prompts({
type: 'confirm'.name: 'yes'.message: `Releasing ${tag}. Confirm? `
})
if(! yes) {return}}else {
if (targetVersion.includes('beta') && !args.tag) {
args.tag = 'beta'}}Copy the code
Update package.json version number
step('\nUpdating package version... ')
updateVersion(targetVersion)
Copy the code
UpdateVersion overwrites the original package.json
function updateVersion(version: string) :void {
const pkg = JSON.parse(readFileSync(pkgPath, 'utf-8'))
pkg.version = version
writeFileSync(pkgPath, JSON.stringify(pkg, null.2) + '\n')}Copy the code
Execute a build
Run PNPM run build
step('\nBuilding package... ')
if(! skipBuild && ! isDryRun) {await run('pnpm'['run'.'build'])}else {
console.log(`(skipped)`)}Copy the code
If the — DRY parameter is passed, it indicates that the run is a DRY Run (also known as a trial run), often used for script debugging
In the dry run of relex.ts, the build and upload of the NPM package is not performed, but the command line statements to be executed are printed out on the command line
Generate the changelog
Run the PNPM run Changelog command. This article does not explain how to generate Changelog due to limited space.
step('\nGenerating changelog... ')
await run('pnpm'['run'.'changelog'])
Copy the code
Release NPM package
step('\nPublishing package... ')
await publishPackage(targetVersion, runIfNotDry)
Copy the code
The publishPackage implementation is as follows:
async function publishPackage(
version: string,
runIfNotDry: RunFn | DryRunFn
) :Promise<void> {
// Parameters of YARN publish
const publicArgs = [
'publish'.'--no-git-tag-version'.'--new-version',
version,
'--access'.'public'
]
if (args.tag) {
publicArgs.push(`--tag`, args.tag)
}
try {
// important: we still use Yarn 1 to publish since we rely on its specific
// behavior
// It is still published with YARN 1 and has not been optimized yet
await runIfNotDry('yarn', publicArgs, {
stdio: 'pipe'
})
console.log(colors.green(`Successfully published ${pkgName}@${version}`))}catch (e: any) {
if (e.stderr.match(/previously published/)) {
console.log(colors.red(`Skipping already published: ${pkgName}`))}else {
throw e
}
}
}
Copy the code
RunIfNotDry implementation:
// Run the command line
const run: RunFn = (bin, args, opts = {}) = >
execa(bin, args, { stdio: 'inherit'. opts })type DryRunFn = (bin: string, args: string[], opts? :any) = > void
// In dry run mode, only command line statements are output
const dryRun: DryRunFn = (bin, args, opts: any) = >
console.log(colors.blue(`[dryrun] ${bin} ${args.join(' ')}`), opts)
const runIfNotDry = isDryRun ? dryRun : run
Copy the code
RunIfNotDry In Dry Run mode, the command line statements are output, but not executed, and only used for local debugging.
Commit to making
const tag = pkgName === 'vite' ? `v${targetVersion}` : `${pkgName}@${targetVersion}`
const { stdout } = await run('git'['diff'] and {stdio: 'pipe' })
// If there is a git discrepancy, commit and label it
// If the version number is changed, package.json is changed
if (stdout) {
step('\nCommitting changes... ')
await runIfNotDry('git'['add'.'-A'])
await runIfNotDry('git'['commit'.'-m'.`release: ${tag}`])
await runIfNotDry('git'['tag', tag])
} else {
console.log('No changes to commit.')
}
step('\nPushing to GitHub... ')
await runIfNotDry('git'['push'.'origin'.`refs/tags/${tag}`])
await runIfNotDry('git'['push'])
if (isDryRun) {
console.log(`\nDry run finished - run git diff to see package changes.`)}Copy the code
Tag generation rules:
For example, if targetVersion is 1.0.1, the tag is v1.0.1 if the package being released is Vite. If the published package is something else, such as plugin-vue, the tag is [email protected]
conclusion
We look at the source, need to have a purpose, for example this time, is to learn the vite source distribution. Looking at the source code with a purpose like this, you’ll see that it’s not that hard; Do not look at the source code warehouse from beginning to end, do not understand, and it is easy to lose the patience and confidence to learn.
Careful you, may find, in fact, a lot of times the source is not very perfect
-
For example, the annotations for release.yml explicitly state that the pseudo-matrix is for convenience, presumably copied from other configuration files with minor modifications.
-
For example, the NPM package is still published using YARN 1 (indicated in the comment). It is estimated that the package management tool of the Vite repository migrates PNPM from YARN, but the publishing mode has not been migrated.
If an open source library can become the mainstream, it must have its outstanding points, but after all, the time of developers is limited, so it cannot be perfect in every aspect. It is normal that such a release process does not affect the core code and does not have high priority for optimization.
Therefore, we are more to learn its essence from the open source code, improve and optimize it, and apply it to their own projects.
If this article is helpful to you, please help to point a thumbs-up π, your encouragement is the biggest power on the way of my creation