Simplify publish process
This commit is contained in:
136
scripts/publish.mjs
Normal file
136
scripts/publish.mjs
Normal file
@@ -0,0 +1,136 @@
|
||||
#!/usr/bin/env node
|
||||
import { execSync } from 'node:child_process'
|
||||
import { readdirSync, readFileSync } from 'node:fs'
|
||||
import { join } from 'node:path'
|
||||
import readline from 'node:readline'
|
||||
|
||||
const rootDir = process.cwd()
|
||||
const rootPackagePath = join(rootDir, 'package.json')
|
||||
const packageLockPath = join(rootDir, 'package-lock.json')
|
||||
const changesPath = join(rootDir, 'CHANGES.md')
|
||||
|
||||
function readJson (path) {
|
||||
return JSON.parse(readFileSync(path, 'utf8'))
|
||||
}
|
||||
|
||||
async function prompt (question) {
|
||||
const rl = readline.createInterface({
|
||||
input: process.stdin,
|
||||
output: process.stdout
|
||||
})
|
||||
const answer = await new Promise(resolve => rl.question(question, resolve))
|
||||
rl.close()
|
||||
return answer.trim()
|
||||
}
|
||||
|
||||
function expandWorkspaces (patterns) {
|
||||
const expanded = []
|
||||
for (const pattern of patterns) {
|
||||
if (!pattern.includes('*')) {
|
||||
expanded.push(pattern)
|
||||
continue
|
||||
}
|
||||
const [prefix, suffix] = pattern.split('*')
|
||||
const baseDir = join(rootDir, prefix || '.')
|
||||
const entries = readdirSync(baseDir, { withFileTypes: true })
|
||||
for (const entry of entries) {
|
||||
if (entry.isDirectory()) {
|
||||
expanded.push(join(prefix, entry.name, suffix))
|
||||
}
|
||||
}
|
||||
}
|
||||
return expanded
|
||||
}
|
||||
|
||||
function loadWorkspacePackagePaths () {
|
||||
const rootPackage = readJson(rootPackagePath)
|
||||
const workspacePatterns = Array.isArray(rootPackage.workspaces)
|
||||
? rootPackage.workspaces
|
||||
: rootPackage.workspaces?.packages ?? []
|
||||
return expandWorkspaces(workspacePatterns).map(path => join(rootDir, path, 'package.json'))
|
||||
}
|
||||
|
||||
function inGitRepo () {
|
||||
try {
|
||||
execSync('git rev-parse --is-inside-work-tree', { stdio: 'ignore', cwd: rootDir })
|
||||
return true
|
||||
} catch (error) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
function confirmYes (answer) {
|
||||
return /^y(es)?$/i.test(answer)
|
||||
}
|
||||
|
||||
function quoteArg (value) {
|
||||
return JSON.stringify(value)
|
||||
}
|
||||
|
||||
function run (command) {
|
||||
execSync(command, { stdio: 'inherit', cwd: rootDir })
|
||||
}
|
||||
|
||||
async function main () {
|
||||
const rootPackage = readJson(rootPackagePath)
|
||||
const workspacePackages = loadWorkspacePackagePaths()
|
||||
const releaseVersion = rootPackage.version
|
||||
const defaultReleaseName = `v${releaseVersion}`
|
||||
|
||||
console.log(`Preparing to publish ${rootPackage.name}@${releaseVersion}\n`)
|
||||
|
||||
const versionBumped = await prompt(`Has the version bump been completed (current version ${releaseVersion})? [y/N]: `)
|
||||
if (!confirmYes(versionBumped)) {
|
||||
console.log('Please run version bump before publishing. Exiting.')
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
const changelogUpdated = await prompt('Has CHANGES.md been updated? [y/N]: ')
|
||||
if (!confirmYes(changelogUpdated)) {
|
||||
console.log('Please update CHANGES.md before publishing. Exiting.')
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
console.log('\nRunning tests, docs, and builds (test-build)...')
|
||||
try {
|
||||
run('npm run test-build')
|
||||
} catch (error) {
|
||||
console.error('Tests/builds failed. Aborting publish.')
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
const doCommit = await prompt('\nCreate git commit and tag for this release before publishing? [y/N]: ')
|
||||
if (!confirmYes(doCommit)) {
|
||||
console.log('Publish aborted by user.')
|
||||
process.exit(0)
|
||||
}
|
||||
|
||||
if (!inGitRepo()) {
|
||||
console.error('Git repository not detected; cannot create commit/tag.')
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
const releaseNameInput = await prompt(`Release name for commit/tag (default ${defaultReleaseName}): `)
|
||||
const releaseName = releaseNameInput || defaultReleaseName
|
||||
|
||||
console.log('\nCreating commit and tag...')
|
||||
const filesToStage = [
|
||||
changesPath,
|
||||
rootPackagePath,
|
||||
packageLockPath,
|
||||
...workspacePackages
|
||||
]
|
||||
run(`git add ${filesToStage.map(quoteArg).join(' ')}`)
|
||||
run(`git commit -m ${quoteArg(releaseName)}`)
|
||||
run(`git tag ${quoteArg(releaseName)}`)
|
||||
|
||||
console.log('\nPublishing packages to npm...')
|
||||
run('npm publish --workspaces --include-workspace-root')
|
||||
|
||||
console.log(`\nDone. Published ${releaseName}.`)
|
||||
}
|
||||
|
||||
main().catch(err => {
|
||||
console.error(err)
|
||||
process.exit(1)
|
||||
})
|
||||
@@ -37,6 +37,14 @@ const ensureBrowser = async () => {
|
||||
}
|
||||
}
|
||||
|
||||
const ensureBuild = async () => {
|
||||
const distIndex = join(process.cwd(), 'dist', 'editor', 'index.html')
|
||||
if (existsSync(distIndex)) return
|
||||
|
||||
console.log('Building dist/editor for Playwright preview (missing build output)...')
|
||||
await run('npm', ['run', 'build'])
|
||||
}
|
||||
|
||||
const seedNycFromVitest = async () => {
|
||||
const vitestCoverage = join(process.cwd(), 'coverage', 'coverage-final.json')
|
||||
if (existsSync(vitestCoverage)) {
|
||||
@@ -48,6 +56,7 @@ const seedNycFromVitest = async () => {
|
||||
|
||||
if (await hasPlaywright()) {
|
||||
await ensureBrowser()
|
||||
await ensureBuild()
|
||||
await run('rimraf', ['.nyc_output/*'], { shell: true })
|
||||
await seedNycFromVitest()
|
||||
await run('npx', ['playwright', 'test'])
|
||||
|
||||
153
scripts/version-bump.mjs
Normal file
153
scripts/version-bump.mjs
Normal file
@@ -0,0 +1,153 @@
|
||||
#!/usr/bin/env node
|
||||
import { execSync } from 'node:child_process'
|
||||
import { readdirSync, readFileSync, writeFileSync } from 'node:fs'
|
||||
import { join } from 'node:path'
|
||||
import readline from 'node:readline'
|
||||
|
||||
const rootDir = process.cwd()
|
||||
const rootPackagePath = join(rootDir, 'package.json')
|
||||
|
||||
function readJson (path) {
|
||||
return JSON.parse(readFileSync(path, 'utf8'))
|
||||
}
|
||||
|
||||
function writeJson (path, data) {
|
||||
writeFileSync(path, JSON.stringify(data, null, 2) + '\n')
|
||||
}
|
||||
|
||||
function parseSemver (version) {
|
||||
const match = /^(\d+)\.(\d+)\.(\d+)$/.exec(version.trim())
|
||||
if (!match) return null
|
||||
return match.slice(1).map(Number)
|
||||
}
|
||||
|
||||
function bumpVersion (version, type) {
|
||||
const parsed = parseSemver(version)
|
||||
if (!parsed) throw new Error(`Invalid semver: ${version}`)
|
||||
const [major, minor, patch] = parsed
|
||||
if (type === 'patch') return `${major}.${minor}.${patch + 1}`
|
||||
if (type === 'minor') return `${major}.${minor + 1}.0`
|
||||
if (type === 'major') return `${major + 1}.0.0`
|
||||
throw new Error(`Unknown bump type: ${type}`)
|
||||
}
|
||||
|
||||
function isGreaterVersion (a, b) {
|
||||
const pa = parseSemver(a)
|
||||
const pb = parseSemver(b)
|
||||
if (!pa || !pb) return false
|
||||
for (let i = 0; i < 3; i += 1) {
|
||||
if (pa[i] > pb[i]) return true
|
||||
if (pa[i] < pb[i]) return false
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
function expandWorkspaces (patterns) {
|
||||
const expanded = []
|
||||
for (const pattern of patterns) {
|
||||
if (!pattern.includes('*')) {
|
||||
expanded.push(pattern)
|
||||
continue
|
||||
}
|
||||
const [prefix, suffix] = pattern.split('*')
|
||||
const baseDir = join(rootDir, prefix || '.')
|
||||
const entries = readdirSync(baseDir, { withFileTypes: true })
|
||||
for (const entry of entries) {
|
||||
if (entry.isDirectory()) {
|
||||
expanded.push(join(prefix, entry.name, suffix))
|
||||
}
|
||||
}
|
||||
}
|
||||
return expanded
|
||||
}
|
||||
|
||||
async function prompt (question) {
|
||||
const rl = readline.createInterface({
|
||||
input: process.stdin,
|
||||
output: process.stdout
|
||||
})
|
||||
const answer = await new Promise(resolve => rl.question(question, resolve))
|
||||
rl.close()
|
||||
return answer.trim()
|
||||
}
|
||||
|
||||
function loadPackages () {
|
||||
const rootPackage = readJson(rootPackagePath)
|
||||
const workspacePatterns = Array.isArray(rootPackage.workspaces)
|
||||
? rootPackage.workspaces
|
||||
: rootPackage.workspaces?.packages ?? []
|
||||
const workspacePaths = expandWorkspaces(workspacePatterns)
|
||||
const workspaces = workspacePaths.map(path => {
|
||||
const packagePath = join(rootDir, path, 'package.json')
|
||||
return { path, packagePath, pkg: readJson(packagePath) }
|
||||
})
|
||||
return { rootPackage, workspaces }
|
||||
}
|
||||
|
||||
async function chooseVersion (current) {
|
||||
const suggestions = {
|
||||
patch: bumpVersion(current, 'patch'),
|
||||
minor: bumpVersion(current, 'minor'),
|
||||
major: bumpVersion(current, 'major')
|
||||
}
|
||||
console.log('\nSuggested bumps:')
|
||||
console.log(`- patch: ${suggestions.patch}`)
|
||||
console.log(`- minor: ${suggestions.minor}`)
|
||||
console.log(`- major: ${suggestions.major}`)
|
||||
while (true) {
|
||||
const answer = await prompt(`\nNew version [p/m/M/custom] (default patch: ${suggestions.patch}): `)
|
||||
const trimmed = answer.trim()
|
||||
const lower = trimmed.toLowerCase()
|
||||
const chosen = !trimmed
|
||||
? suggestions.patch
|
||||
: lower === 'p' || lower === 'patch'
|
||||
? suggestions.patch
|
||||
: lower === 'm' || lower === 'minor'
|
||||
? suggestions.minor
|
||||
: trimmed === 'M' || lower === 'major'
|
||||
? suggestions.major
|
||||
: trimmed
|
||||
if (!parseSemver(chosen)) {
|
||||
console.log(`Invalid semver: ${chosen}. Expected format x.y.z`)
|
||||
continue
|
||||
}
|
||||
if (!isGreaterVersion(chosen, current)) {
|
||||
console.log(`Version must be greater than current (${current}).`)
|
||||
continue
|
||||
}
|
||||
return chosen
|
||||
}
|
||||
}
|
||||
|
||||
async function main () {
|
||||
const { rootPackage, workspaces } = loadPackages()
|
||||
console.log('Current versions:')
|
||||
console.log(`- ${rootPackage.name} (root): ${rootPackage.version}`)
|
||||
for (const ws of workspaces) {
|
||||
console.log(`- ${ws.pkg.name} (${ws.path}): ${ws.pkg.version}`)
|
||||
}
|
||||
|
||||
const newVersion = await chooseVersion(rootPackage.version)
|
||||
|
||||
console.log(`\nUpdating all packages to ${newVersion}...`)
|
||||
rootPackage.version = newVersion
|
||||
writeJson(rootPackagePath, rootPackage)
|
||||
for (const ws of workspaces) {
|
||||
ws.pkg.version = newVersion
|
||||
writeJson(ws.packagePath, ws.pkg)
|
||||
}
|
||||
|
||||
console.log('Refreshing package-lock.json...')
|
||||
execSync('npm install --package-lock-only --workspaces --include-workspace-root', {
|
||||
stdio: 'inherit',
|
||||
cwd: rootDir
|
||||
})
|
||||
|
||||
console.log('\nDone.')
|
||||
console.log(`\nMake sure to update any relevant documentation and changelogs for version ${newVersion}...`)
|
||||
}
|
||||
|
||||
main().catch(err => {
|
||||
console.error(err)
|
||||
process.exit(1)
|
||||
})
|
||||
Reference in New Issue
Block a user