Skip to content

Commit 531087d

Browse files
committed
manual npm package publisher script.
1 parent 4484d29 commit 531087d

3 files changed

Lines changed: 131 additions & 0 deletions

File tree

‎CONTRIBUTING.md‎

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,25 @@ enforce branch protection. See GitHub's [ruleset documentation](https://docs.git
5353
Dependabot opens weekly pull requests for GitHub Actions and dependencies. Review
5454
them through the same CI and pull request process as other changes.
5555

56+
To publish one package manually, log in to npm once and run the command from the
57+
repository root:
58+
59+
```sh
60+
npm login --registry=https://registry.npmjs.org
61+
yarn publish-package ts-images
62+
```
63+
64+
Both short names (`ts-images`) and full names (`@purestack/ts-images`, `purestack`)
65+
are accepted. The command rebuilds and verifies all distributable bundles, packs
66+
only the selected workspace with Yarn to resolve its `workspace:` dependencies,
67+
then publishes its current version publicly to npm with the `latest` tag. npm's
68+
authentication prompts remain interactive. It does not bump versions or publish
69+
dependencies; the package's dependency versions must already be available on npm.
70+
71+
Use `yarn publish-package ts-images --dry-run` to build, pack, and inspect npm's
72+
publish preview without uploading anything. An existing name/version cannot be
73+
published again; update the version through the normal release process first.
74+
5675
The existing **Publish new npm packages** workflow remains a manual operation.
5776
It only publishes packages missing from npm; it is not a version-release workflow.
5877
Use its default dry run first and review the registry results before publishing.

‎package.json‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@
5252
"bump-version": "yarn tsx --no-warnings scripts/bump-version.ts",
5353
"bundle": "yarn tsx --no-warnings scripts/bundle.ts",
5454
"cleanup": "yarn tsx --no-warnings scripts/cleanup.ts",
55+
"publish-package": "yarn tsx --no-warnings scripts/publish-package.ts",
5556
"publish-new-packages": "yarn tsx --no-warnings scripts/publish-new-packages.ts",
5657
"test": "vitest",
5758
"test:ci": "vitest run",

‎scripts/publish-package.ts‎

Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
import { spawn } from 'node:child_process'
2+
import { globSync, readFileSync } from 'node:fs'
3+
import path from 'node:path'
4+
import { fileURLToPath } from 'node:url'
5+
6+
const projectRoot = fileURLToPath(new URL('..', import.meta.url))
7+
8+
interface PackageJson {
9+
name: string
10+
version?: string
11+
private?: boolean
12+
exports?: Record<string, unknown>
13+
}
14+
15+
function run(command: string, args: string[]) {
16+
return new Promise<void>((resolve, reject) => {
17+
const child = spawn(command, args, {
18+
cwd: projectRoot,
19+
stdio: 'inherit',
20+
})
21+
child.once('error', reject)
22+
child.once('exit', (code, signal) => {
23+
if (code === 0) resolve()
24+
else reject(new Error(`${command} failed (${signal ?? `exit ${code}`}).`))
25+
})
26+
})
27+
}
28+
29+
async function main() {
30+
const args = process.argv.slice(2)
31+
if (args.includes('--help') || args.includes('-h')) {
32+
console.log(`Build, pack, and publish one PureStack package publicly with the latest tag.
33+
34+
Usage:
35+
yarn publish-package ts-images
36+
yarn publish-package @purestack/ts-images --dry-run
37+
38+
Log in once with npm login before publishing. --dry-run builds and packs the
39+
package, then runs npm publish --dry-run without uploading it.
40+
`)
41+
return
42+
}
43+
44+
const dryRun = args.includes('--dry-run')
45+
const names = args.filter((arg) => arg !== '--dry-run')
46+
if (names.length !== 1 || names[0].startsWith('-'))
47+
throw new Error('Usage: yarn publish-package <package-name> [--dry-run]')
48+
49+
const requested = names[0]
50+
const packageNames = new Set([requested, `@purestack/${requested}`])
51+
const packages = globSync('packages/*/package.json', {
52+
cwd: projectRoot,
53+
}).map((file) => ({
54+
directory: path.dirname(path.join(projectRoot, file)),
55+
manifest: JSON.parse(
56+
readFileSync(path.join(projectRoot, file), 'utf8'),
57+
) as PackageJson,
58+
}))
59+
const selected = packages.find((pkg) => packageNames.has(pkg.manifest.name))
60+
if (!selected) throw new Error(`Unknown workspace package: ${requested}`)
61+
const { directory, manifest } = selected
62+
if (manifest.private || !manifest.exports || !manifest.version)
63+
throw new Error(`${manifest.name} is not a versioned, publishable package.`)
64+
65+
// Yarn's npm_execpath can point to a shell shim. Use the Corepack entry directly.
66+
const corepackRoot = process.env.COREPACK_ROOT
67+
if (!corepackRoot)
68+
throw new Error('Enable Corepack, then run yarn publish-package.')
69+
const yarnCli = path.join(corepackRoot, 'dist', 'yarn.js')
70+
const yarn = (commandArgs: string[]) =>
71+
run(process.execPath, [yarnCli, ...commandArgs])
72+
const tarball = path.join(directory, 'package.tgz')
73+
console.log(
74+
`${dryRun ? 'Preparing dry run for' : 'Preparing'} ${manifest.name}@${manifest.version}`,
75+
)
76+
77+
// Reuse the repository's bundling and verification before packing fresh output.
78+
await yarn(['bundle'])
79+
await yarn(['workspace', manifest.name, 'pack', '--out', tarball])
80+
81+
const publishArgs = [
82+
'publish',
83+
tarball,
84+
'--access',
85+
'public',
86+
'--tag',
87+
'latest',
88+
'--registry',
89+
'https://registry.npmjs.org',
90+
]
91+
if (dryRun) publishArgs.push('--dry-run')
92+
93+
// Inherit the terminal so npm's browser authentication and 2FA stay interactive.
94+
if (process.platform === 'win32') {
95+
const npmCli = path.join(
96+
path.dirname(process.execPath),
97+
'node_modules',
98+
'npm',
99+
'bin',
100+
'npm-cli.js',
101+
)
102+
await run(process.execPath, [npmCli, ...publishArgs])
103+
} else {
104+
await run('npm', publishArgs)
105+
}
106+
}
107+
108+
main().catch((error) => {
109+
console.error(error instanceof Error ? error.message : error)
110+
process.exitCode = 1
111+
})

0 commit comments

Comments
 (0)