Skip to content

Commit 0f63701

Browse files
committed
add my publish script
1 parent 7f74d2c commit 0f63701

1 file changed

Lines changed: 181 additions & 0 deletions

File tree

script/publish

Lines changed: 181 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,181 @@
1+
#!/usr/bin/env node
2+
3+
/**
4+
* This script is used to publish a new version of the package.
5+
* It can automatically increment the patch version if no version is specified in the package.json and commit the changes.
6+
* It can also automatically tag and push the tags.
7+
* Using GH CLI, it can also create a release.
8+
*
9+
* Fucking awesome, right?
10+
*
11+
* With love, @stamat
12+
*/
13+
import readline from 'node:readline'
14+
import { exec, execFile, spawn } from 'node:child_process'
15+
import fs from 'node:fs'
16+
import path from 'node:path'
17+
18+
const argVersion = process.argv[2]
19+
const packageJson = JSON.parse(fs.readFileSync(path.join(process.cwd(), 'package.json'), 'utf-8'))
20+
21+
function question(prompt) {
22+
const rl = readline.createInterface({
23+
input: process.stdin,
24+
output: process.stdout
25+
})
26+
27+
return new Promise((resolve) => {
28+
rl.question(prompt, (answer) => {
29+
rl.close()
30+
resolve(answer)
31+
})
32+
})
33+
}
34+
35+
function isValidVersion(version) {
36+
return /^(\d+)\.(\d+)\.(\d+)(?:-([\w-]+(?:\.[\w-]+)*))?(?:\+([\w-]+(?:\.[\w-]+)*))?$/.test(version)
37+
}
38+
39+
function incrementPatchVersion(version) {
40+
// can't auto-bump prerelease/build versions (e.g. 1.2.3-beta.1), ask for manual entry instead
41+
if (version.includes('-') || version.includes('+')) return null
42+
const parts = version.split('.')
43+
const patch = parseInt(parts[2]) + 1
44+
return `${parts[0]}.${parts[1]}.${patch}`
45+
}
46+
47+
function runPromise(child) {
48+
// stdout/stderr are null when stdio is inherited (interactive commands)
49+
child.stdout?.on('data', (data) => {
50+
console.log(data.toString())
51+
})
52+
53+
child.stderr?.on('data', (data) => {
54+
console.log(data.toString())
55+
})
56+
57+
return new Promise(function(resolve, reject) {
58+
child.addListener('error', reject)
59+
child.addListener('exit', (code) => {
60+
if (code === 0) {
61+
resolve()
62+
} else {
63+
reject(new Error(`Command exited with code ${code}`))
64+
}
65+
})
66+
})
67+
}
68+
69+
async function run(cmd, exitOnError = true) {
70+
console.log(cmd)
71+
try {
72+
await runPromise(exec(cmd))
73+
} catch (e) {
74+
console.error(e.message)
75+
if (exitOnError) process.exit(1)
76+
}
77+
}
78+
79+
// like run(), but inherits stdio so interactive prompts (npm OTP) reach the terminal
80+
async function runInteractive(cmd, args, exitOnError = true) {
81+
console.log([cmd, ...args].join(' '))
82+
try {
83+
await runPromise(spawn(cmd, args, { stdio: 'inherit', shell: true }))
84+
} catch (e) {
85+
console.error(e.message)
86+
if (exitOnError) process.exit(1)
87+
}
88+
}
89+
90+
// like run(), but takes an args array so user input never touches the shell
91+
async function runFile(cmd, args, exitOnError = true) {
92+
console.log([cmd, ...args].join(' '))
93+
try {
94+
await runPromise(execFile(cmd, args))
95+
} catch (e) {
96+
console.error(e.message)
97+
if (exitOnError) process.exit(1)
98+
}
99+
}
100+
101+
async function publish(version) {
102+
packageJson.version = version
103+
fs.writeFileSync('package.json', JSON.stringify(packageJson, null, 2) + '\n')
104+
105+
await run('git add package.json')
106+
await run(`git commit -m "Bump version to ${version}"`)
107+
108+
if (fs.existsSync(path.join(process.cwd(), 'script/build'))) {
109+
await run('script/build')
110+
await run('git add example/dist')
111+
await run(`git commit -m "Build version ${version}"`)
112+
}
113+
114+
await run(`git tag v${version}`)
115+
116+
// log in to npm first if needed
117+
try {
118+
await runPromise(exec('npm whoami'))
119+
} catch {
120+
await runInteractive('npm', ['login'])
121+
}
122+
123+
// publish to npm before pushing, so a failed publish leaves nothing on the remote
124+
await runInteractive('npm', ['publish'])
125+
126+
await run('git push')
127+
await run('git push --tags')
128+
129+
const answer = await question('Do you want to create a GitHub release? (y/n): ')
130+
if (answer === 'y') {
131+
const notes = await question('Enter notes for the release (optional): ')
132+
const args = ['release', 'create', `v${version}`, '--title', `v${version}`, '--latest']
133+
if (notes.trim() !== '') {
134+
args.push('--notes', notes)
135+
} else {
136+
args.push('--generate-notes')
137+
}
138+
await runFile('gh', args)
139+
}
140+
}
141+
142+
async function init() {
143+
if (!packageJson.version) {
144+
console.log('No version found in package.json')
145+
process.exit(1)
146+
}
147+
148+
if (argVersion && !isValidVersion(argVersion)) {
149+
console.log('Invalid version: ', argVersion)
150+
console.log('Current version: ', packageJson.version)
151+
process.exit(1)
152+
}
153+
154+
if (argVersion && packageJson.version === argVersion) {
155+
console.log('Version is already ', argVersion)
156+
process.exit(1)
157+
}
158+
159+
if (!argVersion) {
160+
console.log('Current version: ', packageJson.version)
161+
const version = incrementPatchVersion(packageJson.version)
162+
const answer = version ? await question(`Do you want to increment the version to ${version}? (y/n) `) : 'n'
163+
if (answer === 'y') {
164+
await publish(version)
165+
} else {
166+
const desiredVersion = await question(`Enter the desired version: (or press enter to use ${packageJson.version})`)
167+
if (desiredVersion.trim() === '') {
168+
await publish(packageJson.version)
169+
} else {
170+
if (!isValidVersion(desiredVersion)) {
171+
console.log('Invalid version: ', desiredVersion)
172+
process.exit(1)
173+
}
174+
await publish(desiredVersion)
175+
}
176+
}
177+
} else {
178+
await publish(argVersion)
179+
}
180+
}
181+
init()

0 commit comments

Comments
 (0)