mirror of
https://github.com/hanzoai/gui.git
synced 2026-10-25 02:31:41 +00:00
workspace:, link: and file: resolve only inside the monorepo that declares them, so any of the three in a published manifest is a consumer install that dies. The rewrite handled workspace: alone, and it trusted the manifest it had just written — but npm pack runs prepack and prepare, which are free to rewrite package.json again. Cover all three protocols and read the manifest back out of the archive before pushing it. A version is immutable once it lands: @hanzogui/[email protected] still carries eleven of these and no publish can repair it. Co-authored-by: Hanzo Dev <[email protected]>
187 lines
6.3 KiB
JavaScript
187 lines
6.3 KiB
JavaScript
// Publish exactly the gui surface — every `@hanzogui/*` package + the
|
|
// `hanzogui` umbrella + the `@hanzo/gui` alias — at the current on-disk
|
|
// version, with workspace:* specs rewritten to concrete versions.
|
|
//
|
|
// Unlike `release.ts --force-publish-all`, this does NOT touch unrelated
|
|
// workspace packages (fonts, demos…) and uses
|
|
// `npm pack --json` to publish the produced tarball directly (no fragile
|
|
// rename step). Idempotent: skips any name@version already on npm.
|
|
//
|
|
// Auth: relies on the npm registry config already in place (CI writes
|
|
// ~/.npmrc with the token). Run from the repo root.
|
|
|
|
import fs from 'node:fs'
|
|
import path from 'node:path'
|
|
import { execSync, execFileSync } from 'node:child_process'
|
|
|
|
const ROOT = process.cwd()
|
|
const PKGS = path.join(ROOT, 'pkgs')
|
|
const DEST = '/tmp/gui-surface'
|
|
fs.rmSync(DEST, { recursive: true, force: true })
|
|
fs.mkdirSync(DEST, { recursive: true })
|
|
|
|
// 1. Collect every workspace package.json (skip node_modules/dist).
|
|
const pkgDirs = []
|
|
function walk(d) {
|
|
const base = path.basename(d)
|
|
if (base === 'node_modules' || base === 'dist') return
|
|
if (fs.existsSync(path.join(d, 'package.json'))) pkgDirs.push(d)
|
|
for (const e of fs.readdirSync(d, { withFileTypes: true })) {
|
|
if (e.isDirectory()) walk(path.join(d, e.name))
|
|
}
|
|
}
|
|
walk(PKGS)
|
|
|
|
const versionMap = {}
|
|
const meta = {}
|
|
for (const d of pkgDirs) {
|
|
try {
|
|
const j = JSON.parse(fs.readFileSync(path.join(d, 'package.json')))
|
|
if (j.name && j.version) {
|
|
versionMap[j.name] = j.version
|
|
meta[j.name] = { dir: d, json: j }
|
|
}
|
|
} catch {}
|
|
}
|
|
|
|
// 2. Targets: the gui surface only, excluding private packages.
|
|
const targets = Object.keys(meta)
|
|
.filter(
|
|
(n) => (n === 'hanzogui' || n.startsWith('@hanzogui/')) && !meta[n].json.private
|
|
)
|
|
.sort()
|
|
|
|
console.log(
|
|
`Publishing ${targets.length} gui packages (+ @hanzo/gui alias) at their on-disk versions\n`
|
|
)
|
|
|
|
// `workspace:`, `link:` and `file:` all resolve only inside the monorepo that
|
|
// declares them. Any of the three in a published manifest is a consumer install
|
|
// that dies with ERR_PNPM_WORKSPACE_PKG_NOT_FOUND / EUNSUPPORTEDPROTOCOL, and
|
|
// the version is immutable once it lands — @hanzogui/[email protected] still carries 11
|
|
// of them and nothing can be published to repair it.
|
|
const LOCAL_PROTOCOLS = ['workspace:', 'link:', 'file:']
|
|
const localProtocol = (spec) =>
|
|
typeof spec === 'string' && LOCAL_PROTOCOLS.find((p) => spec.startsWith(p))
|
|
|
|
const DEP_FIELDS = [
|
|
'dependencies',
|
|
'devDependencies',
|
|
'peerDependencies',
|
|
'optionalDependencies',
|
|
]
|
|
|
|
function rewriteWorkspaceSpecs(deps, name) {
|
|
if (!deps) return
|
|
for (const k of Object.keys(deps)) {
|
|
const proto = localProtocol(deps[k])
|
|
if (!proto) continue
|
|
const v = versionMap[k]
|
|
if (!v)
|
|
throw new Error(
|
|
`${name}: dependency ${k} is ${proto}* but has no resolvable version`
|
|
)
|
|
deps[k] = v
|
|
}
|
|
}
|
|
|
|
// Read back what npm actually put in the tarball. The rewrite above works on
|
|
// the manifest we wrote, but `npm pack` runs lifecycle scripts (prepack,
|
|
// prepare) that are free to rewrite package.json again — so the only manifest
|
|
// worth asserting on is the one inside the archive we are about to push.
|
|
function assertTarballResolvable(tgz, cwd, name) {
|
|
const raw = execFileSync('tar', ['-xOzf', tgz, 'package/package.json'], {
|
|
cwd,
|
|
maxBuffer: 32 * 1024 * 1024,
|
|
}).toString()
|
|
const j = JSON.parse(raw)
|
|
const offences = []
|
|
for (const f of DEP_FIELDS) {
|
|
for (const [k, spec] of Object.entries(j[f] || {})) {
|
|
const proto = localProtocol(spec)
|
|
if (proto) offences.push(`${f}.${k} = ${spec}`)
|
|
}
|
|
}
|
|
if (offences.length)
|
|
throw new Error(
|
|
`${name}: tarball carries unresolvable specifiers — ${offences.join(', ')}`
|
|
)
|
|
}
|
|
|
|
const published = []
|
|
const skipped = []
|
|
const failed = []
|
|
|
|
function alreadyOnNpm(name, version) {
|
|
try {
|
|
execFileSync('npm', ['view', `${name}@${version}`, 'version'], { stdio: 'pipe' })
|
|
return true
|
|
} catch {
|
|
return false
|
|
}
|
|
}
|
|
|
|
function publishPackage(srcDir, baseJson, overrideName) {
|
|
const name = overrideName || baseJson.name
|
|
const version = baseJson.version
|
|
if (alreadyOnNpm(name, version)) {
|
|
skipped.push(`${name}@${version}`)
|
|
console.log(`= skip ${name}@${version} (already on npm)`)
|
|
return
|
|
}
|
|
try {
|
|
const tmp = fs.mkdtempSync(path.join(DEST, 'p-'))
|
|
execSync(`cp -R ${JSON.stringify(srcDir)}/. ${JSON.stringify(tmp)}/`)
|
|
fs.rmSync(path.join(tmp, 'node_modules'), { recursive: true, force: true })
|
|
|
|
const j = JSON.parse(fs.readFileSync(path.join(tmp, 'package.json')))
|
|
j.name = name
|
|
for (const f of DEP_FIELDS) {
|
|
rewriteWorkspaceSpecs(j[f], name)
|
|
}
|
|
fs.writeFileSync(path.join(tmp, 'package.json'), JSON.stringify(j, null, 2) + '\n')
|
|
|
|
const packOut = execFileSync('npm', ['pack', '--json'], { cwd: tmp }).toString()
|
|
const tgz = JSON.parse(packOut)[0].filename
|
|
assertTarballResolvable(tgz, tmp, name)
|
|
execFileSync('npm', ['publish', path.join(tmp, tgz), '--access', 'public'], {
|
|
cwd: tmp,
|
|
stdio: 'inherit',
|
|
})
|
|
published.push(`${name}@${version}`)
|
|
console.log(`+ published ${name}@${version}`)
|
|
} catch (e) {
|
|
const msg = (e && e.message ? e.message : String(e)).split('\n')[0]
|
|
failed.push(`${name}@${version}: ${msg}`)
|
|
console.error(`x FAIL ${name}@${version}: ${msg}`)
|
|
}
|
|
}
|
|
|
|
for (const name of targets) {
|
|
publishPackage(meta[name].dir, meta[name].json)
|
|
}
|
|
|
|
// The umbrella. It is NAMED `@hanzo/gui` in the tree, so it matches neither the
|
|
// `@hanzogui/*` target filter above nor the `hanzogui` lookup this used to do —
|
|
// and it therefore published nothing. Every primitive went to the registry at
|
|
// 8.3.1 while the one package consumers actually depend on stayed at 8.3.0, so a
|
|
// `@hanzo/[email protected]` pin resolved to nothing at all.
|
|
//
|
|
// Take it by name, whichever of the two it wears.
|
|
const umbrella = meta['@hanzo/gui'] || meta['hanzogui']
|
|
if (umbrella) {
|
|
publishPackage(umbrella.dir, umbrella.json, '@hanzo/gui')
|
|
} else {
|
|
console.error('! no umbrella package found — @hanzo/gui was NOT published')
|
|
failed.push('@hanzo/gui (package not found in the tree)')
|
|
}
|
|
|
|
console.log(`\n--- summary ---`)
|
|
console.log(`published: ${published.length}`)
|
|
console.log(`skipped: ${skipped.length}`)
|
|
console.log(`failed: ${failed.length}`)
|
|
if (failed.length) {
|
|
failed.forEach((f) => console.error(' - ' + f))
|
|
process.exit(1)
|
|
}
|