#!/usr/bin/env bun/** * Publishes the public @executor-js/* workspace packages to npm. * * Walks a hard-coded list of publishable package directories, determines the * dist-tag from the version string (anything containing `-` is treated as beta), * and packs + publishes each package whose current version is not already on npm. * * Invoked from `.github/workflows/release.yml` via the `publish:` input on * changesets/action after the Version Packages PR has been merged, and locally * via `bun run release:publish:packages` (or `--dry-run`). */import { $ } from "bun";import { existsSync } from "node:fs";import { readdir, readFile, rm, writeFile } from "node:fs/promises";import { dirname, join, resolve } from "node:path";import { fileURLToPath } from "node:url"; type Channel = "latest" | "beta"; const repoRoot = resolve(dirname(fileURLToPath(import.meta.url)), ".."); const PACKAGE_SCOPE = "@executor-js"; /** * Workspace-relative paths of the public packages. Kept explicit so a new * directory under `packages/plugins/` does not accidentally ship to npm. */const PUBLIC_PACKAGE_DIRS = [ "packages/core/fumadb", "packages/kernel/core", "packages/kernel/runtime-quickjs", "packages/core/sdk", "packages/core/config", "packages/core/execution", "packages/core/cli", "packages/plugins/example", "packages/plugins/file-secrets", "packages/plugins/graphql", "packages/plugins/keychain", "packages/plugins/mcp", "packages/plugins/onepassword", "packages/plugins/openapi",] as const; const parseArgs = (argv: ReadonlyArray<string>): { dryRun: boolean; prepareOnly: boolean } => { let dryRun = false; let prepareOnly = false; for (const arg of argv) { if (arg === "--dry-run") { dryRun = true; continue; } if (arg === "--prepare-only") { prepareOnly = true; continue; } throw new Error(`Unknown argument: ${arg}`); } return { dryRun, prepareOnly };}; const resolveChannel = (version: string): Channel => (version.includes("-") ? "beta" : "latest"); const readPackageMeta = async (pkgDir: string) => { const pkgJsonPath = join(pkgDir, "package.json"); const pkg = (await Bun.file(pkgJsonPath).json()) as { name?: string; version?: string; private?: boolean; }; if (!pkg.name || !pkg.version) { throw new Error(`Missing name/version in ${pkgJsonPath}`); } if (pkg.private === true) { throw new Error(`${pkg.name} is marked private and cannot be published`); } return { name: pkg.name, version: pkg.version };}; const packageAlreadyPublished = async (name: string, version: string): Promise<boolean> => { const proc = Bun.spawn(["npm", "view", `${name}@${version}`, "version"], { stdio: ["ignore", "ignore", "ignore"], }); return (await proc.exited) === 0;}; type DependencyBlock = Record<string, string>;type PeerDependenciesMeta = Record<string, { optional?: boolean }>;type MutablePackageJson = { name?: string; dependencies?: DependencyBlock; devDependencies?: DependencyBlock; peerDependencies?: DependencyBlock; peerDependenciesMeta?: PeerDependenciesMeta; optionalDependencies?: DependencyBlock; [key: string]: unknown;}; /** * Resolves `workspace:*` dependencies between public packages to concrete * versions before packing. Returns a restore function that reverts package.json. * * Workspace-only `@executor-js/*` peer deps (e.g. `@executor-js/api`, * `@executor-js/react`) that aren't in `publishable` are stripped from * `peerDependencies` (and `peerDependenciesMeta`) entirely — they don't * exist on npm, so leaving them in the packed manifest would emit * install-time warnings for unresolvable packages. */const applyWorkspaceVersions = async ( pkgDir: string, publishable: ReadonlySet<string>, publishableVersions: ReadonlyMap<string, string>,): Promise<() => Promise<void>> => { const isInternalScope = (key: string): boolean => key.startsWith(`${PACKAGE_SCOPE}/`); const renameDepBlock = (block: DependencyBlock | undefined): DependencyBlock | undefined => { if (!block) return block; const next: DependencyBlock = {}; let mutated = false; for (const [key, value] of Object.entries(block)) { if (publishable.has(key) && value.startsWith("workspace:")) { next[key] = publishableVersions.get(key) ?? value; mutated = true; } else if (isInternalScope(key) && !publishable.has(key)) { // Workspace-only `@executor-js/*` regular dep that we don't // publish (e.g. `@executor-js/api`). Strip it: it's not in the // shipped runtime entries (those imports live in // `src/api/*` / `src/react/*` which don't make it into the // packed dist), and leaving it in would 404 at install time. mutated = true; } else { next[key] = value; } } return mutated ? next : block; }; /** * Peer-deps variant of `renameDepBlock`: resolve workspace specifiers for * publishable peers, but DROP non-publishable `@executor-js/*` peers. * They reference workspace-only packages (`@executor-js/api`, * `@executor-js/react`) that don't exist on npm, so leaving them in * the packed manifest emits install-time warnings for unresolvable * packages. Non-`@executor-js` peers (`react`, `@tanstack/*`, * `@effect-atom/*`, etc.) are real npm packages and pass through * unchanged. */ const renamePeerDepBlock = (block: DependencyBlock | undefined): DependencyBlock | undefined => { if (!block) return block; const next: DependencyBlock = {}; let mutated = false; for (const [key, value] of Object.entries(block)) { if (publishable.has(key)) { next[key] = value.startsWith("workspace:") ? (publishableVersions.get(key) ?? value) : value; if (next[key] !== value) mutated = true; } else if (isInternalScope(key)) { // Workspace-only `@executor-js/*` peer that we don't publish — // strip it so the packed tarball doesn't reference an // npm package that doesn't exist. mutated = true; } else { next[key] = value; } } return mutated ? next : block; }; /** * Strips `peerDependenciesMeta` entries that target an * `@executor-js/*` peer we don't publish, mirroring `renamePeerDepBlock` * so the meta block can't drift out of sync with the deps block. */ const renamePeerMetaBlock = ( block: PeerDependenciesMeta | undefined, ): PeerDependenciesMeta | undefined => { if (!block) return block; const next: PeerDependenciesMeta = {}; let mutated = false; for (const [key, value] of Object.entries(block)) { if (publishable.has(key)) { next[key] = value; } else if (isInternalScope(key)) { mutated = true; } else { next[key] = value; } } return mutated ? next : block; }; const pkgJsonPath = join(pkgDir, "package.json"); const original = await readFile(pkgJsonPath, "utf8"); const pkg = JSON.parse(original) as MutablePackageJson; pkg.dependencies = renameDepBlock(pkg.dependencies); pkg.devDependencies = renameDepBlock(pkg.devDependencies); pkg.peerDependencies = renamePeerDepBlock(pkg.peerDependencies); pkg.peerDependenciesMeta = renamePeerMetaBlock(pkg.peerDependenciesMeta); pkg.optionalDependencies = renameDepBlock(pkg.optionalDependencies); const pkgNext = `${JSON.stringify(pkg, null, 2)}\n`; if (pkgNext !== original) { await writeFile(pkgJsonPath, pkgNext); return async () => { await writeFile(pkgJsonPath, original); }; } return async () => {};}; /** * Applies `publishConfig` field overrides to package.json in place, returning a * function that restores the original file. `bun pm pack` does not substitute * `publishConfig.exports` / `publishConfig.main` etc at pack time (npm does, * but only for a subset of fields and only for `npm pack`), so we rewrite the * file ourselves so the packed tarball has the correct `exports` pointing at * `dist/` instead of the dev-time `src/index.ts`. */const applyPublishConfig = async (pkgDir: string): Promise<() => Promise<void>> => { const pkgJsonPath = join(pkgDir, "package.json"); const original = await readFile(pkgJsonPath, "utf8"); const parsed = JSON.parse(original) as { publishConfig?: Record<string, unknown>; [key: string]: unknown; }; const publishConfig = parsed.publishConfig; if (!publishConfig || typeof publishConfig !== "object") { return async () => {}; } // Fields we allow publishConfig to override. `access`/`tag`/`registry` are // real npm publish-time config keys — they must NOT be hoisted into the // top-level manifest. const overridable = new Set([ "exports", "main", "module", "types", "typings", "bin", "browser", "files", ]); const nextPublishConfig: Record<string, unknown> = {}; let mutated = false; for (const [key, value] of Object.entries(publishConfig)) { if (overridable.has(key)) { parsed[key] = value; mutated = true; } else { nextPublishConfig[key] = value; } } if (!mutated) { return async () => {}; } if (Object.keys(nextPublishConfig).length === 0) { delete parsed.publishConfig; } else { parsed.publishConfig = nextPublishConfig; } await writeFile(pkgJsonPath, `${JSON.stringify(parsed, null, 2)}\n`); return async () => { await writeFile(pkgJsonPath, original); };}; const publishPackage = async ( pkgDir: string, dryRun: boolean, publishable: ReadonlySet<string>, publishableVersions: ReadonlyMap<string, string>,) => { const { name, version } = await readPackageMeta(pkgDir); const channel = resolveChannel(version); if (!existsSync(join(pkgDir, "dist"))) { throw new Error(`Missing dist/ in ${pkgDir}. Did you run 'bun run build:packages'?`); } console.log(`[pack] ${name}@${version} (${channel})${dryRun ? " [dry-run]" : ""}`); // Clean any stale tarballs from previous runs so our readdir finds exactly // the archive produced by the pack below. const stale = (await readdir(pkgDir)).filter((entry) => entry.endsWith(".tgz")); for (const entry of stale) { await rm(join(pkgDir, entry), { force: true }); } const restoreWorkspaceVersions = await applyWorkspaceVersions( pkgDir, publishable, publishableVersions, ); const restorePublishConfig = await applyPublishConfig(pkgDir); try { await $`bun pm pack`.cwd(pkgDir); } finally { await restorePublishConfig(); await restoreWorkspaceVersions(); } const produced = (await readdir(pkgDir)).filter((entry) => entry.endsWith(".tgz")); if (produced.length !== 1) { throw new Error( `Expected exactly 1 .tgz in ${pkgDir}, found ${produced.length}: ${produced.join(", ")}`, ); } const tarball = produced[0]!; if (dryRun) { return; } // Skip publishing already-shipped versions. The pack still ran above so // smoke tests / pkg-pr-new previews always have a fresh tarball. if (await packageAlreadyPublished(name, version)) { console.log(`[skip] ${name}@${version} already on npm`); return; } console.log(`[publish] ${name}@${version} (${channel})`); const args = ["publish", tarball, "--access", "public", "--tag", channel]; if (process.env.GITHUB_ACTIONS === "true") { args.push("--provenance"); } await $`npm ${args}`.cwd(pkgDir);}; /** * Rewrite-in-place mode for the pkg.pr.new preview workflow. pkg-pr-new runs * `bun pm pack` against each workspace package directly, which can't see our * `publishConfig.exports` overrides or resolve `workspace:*` references. This * walks every public package, applies the same rewrites `publishPackage` * does, and intentionally leaves them mutated — the CI job is ephemeral and * the workflow tears down right after pkg-pr-new finishes. */const prepareOnePackage = async ( pkgDir: string, publishable: ReadonlySet<string>, publishableVersions: ReadonlyMap<string, string>,) => { const { name, version } = await readPackageMeta(pkgDir); if (!existsSync(join(pkgDir, "dist"))) { throw new Error(`Missing dist/ in ${pkgDir}. Did you run 'bun run build:packages'?`); } console.log(`[prepare] ${name}@${version}`); await applyWorkspaceVersions(pkgDir, publishable, publishableVersions); await applyPublishConfig(pkgDir);}; const main = async () => { const { dryRun, prepareOnly } = parseArgs(process.argv.slice(2)); // Each package's own version determines its dist-tag (pre-release versions // with `-` publish to `beta`, everything else to `latest`). Packages are // only skipped when their current version is already on npm. const mode = prepareOnly ? " [prepare-only]" : dryRun ? " [dry-run]" : ""; console.log(`Publishing ${PACKAGE_SCOPE} packages${mode}`); await $`bun run build:packages`.cwd(repoRoot); // Snapshot the public package names and versions up front so public // workspace dependencies can be written as exact versions in packed tarballs. const publishable = new Set<string>(); const publishableVersions = new Map<string, string>(); for (const relDir of PUBLIC_PACKAGE_DIRS) { const pkg = await readPackageMeta(join(repoRoot, relDir)); publishable.add(pkg.name); publishableVersions.set(pkg.name, pkg.version); } if (prepareOnly) { for (const relDir of PUBLIC_PACKAGE_DIRS) { await prepareOnePackage(join(repoRoot, relDir), publishable, publishableVersions); } return; } for (const relDir of PUBLIC_PACKAGE_DIRS) { await publishPackage(join(repoRoot, relDir), dryRun, publishable, publishableVersions); }}; await main();
#!/usr/bin/env bun
/**
* Publishes the public @executor-js/* workspace packages to npm.
*
* Walks a hard-coded list of publishable package directories, determines the
* dist-tag from the version string (anything containing `-` is treated as beta),
* and packs + publishes each package whose current version is not already on npm.
*
* Invoked from `.github/workflows/release.yml` via the `publish:` input on
* changesets/action after the Version Packages PR has been merged, and locally
* via `bun run release:publish:packages` (or `--dry-run`).
*/
import { $ } from "bun";
import { existsSync } from "node:fs";
import { readdir, readFile, rm, writeFile } from "node:fs/promises";
import { dirname, join, resolve } from "node:path";
import { fileURLToPath } from "node:url";
type Channel = "latest" | "beta";
const repoRoot = resolve(dirname(fileURLToPath(import.meta.url)), "..");
const PACKAGE_SCOPE = "@executor-js";
/**
* Workspace-relative paths of the public packages. Kept explicit so a new
* directory under `packages/plugins/` does not accidentally ship to npm.
*/
const PUBLIC_PACKAGE_DIRS = [
"packages/core/fumadb",
"packages/kernel/core",
"packages/kernel/runtime-quickjs",
"packages/core/sdk",
"packages/core/config",
"packages/core/execution",
"packages/core/cli",
"packages/plugins/example",
"packages/plugins/file-secrets",
"packages/plugins/graphql",
"packages/plugins/keychain",
"packages/plugins/mcp",
"packages/plugins/onepassword",
"packages/plugins/openapi",
] as const;
const parseArgs = (argv: ReadonlyArray<string>): { dryRun: boolean; prepareOnly: boolean } => {
let dryRun = false;
let prepareOnly = false;
for (const arg of argv) {
if (arg === "--dry-run") {
dryRun = true;
continue;
}
if (arg === "--prepare-only") {
prepareOnly = true;
continue;
}
throw new Error(`Unknown argument: ${arg}`);
}
return { dryRun, prepareOnly };
};
const resolveChannel = (version: string): Channel => (version.includes("-") ? "beta" : "latest");
const readPackageMeta = async (pkgDir: string) => {
const pkgJsonPath = join(pkgDir, "package.json");
const pkg = (await Bun.file(pkgJsonPath).json()) as {
name?: string;
version?: string;
private?: boolean;
};
if (!pkg.name || !pkg.version) {
throw new Error(`Missing name/version in ${pkgJsonPath}`);
}
if (pkg.private === true) {
throw new Error(`${pkg.name} is marked private and cannot be published`);
}
return { name: pkg.name, version: pkg.version };
};
const packageAlreadyPublished = async (name: string, version: string): Promise<boolean> => {
const proc = Bun.spawn(["npm", "view", `${name}@${version}`, "version"], {
stdio: ["ignore", "ignore", "ignore"],
});
return (await proc.exited) === 0;
};
type DependencyBlock = Record<string, string>;
type PeerDependenciesMeta = Record<string, { optional?: boolean }>;
type MutablePackageJson = {
name?: string;
dependencies?: DependencyBlock;
devDependencies?: DependencyBlock;
peerDependencies?: DependencyBlock;
peerDependenciesMeta?: PeerDependenciesMeta;
optionalDependencies?: DependencyBlock;
[key: string]: unknown;
};
/**
* Resolves `workspace:*` dependencies between public packages to concrete
* versions before packing. Returns a restore function that reverts package.json.
*
* Workspace-only `@executor-js/*` peer deps (e.g. `@executor-js/api`,
* `@executor-js/react`) that aren't in `publishable` are stripped from
* `peerDependencies` (and `peerDependenciesMeta`) entirely — they don't
* exist on npm, so leaving them in the packed manifest would emit
* install-time warnings for unresolvable packages.
*/
const applyWorkspaceVersions = async (
pkgDir: string,
publishable: ReadonlySet<string>,
publishableVersions: ReadonlyMap<string, string>,
): Promise<() => Promise<void>> => {
const isInternalScope = (key: string): boolean => key.startsWith(`${PACKAGE_SCOPE}/`);
const renameDepBlock = (block: DependencyBlock | undefined): DependencyBlock | undefined => {
if (!block) return block;
const next: DependencyBlock = {};
let mutated = false;
for (const [key, value] of Object.entries(block)) {
if (publishable.has(key) && value.startsWith("workspace:")) {
next[key] = publishableVersions.get(key) ?? value;
mutated = true;
} else if (isInternalScope(key) && !publishable.has(key)) {
// Workspace-only `@executor-js/*` regular dep that we don't
// publish (e.g. `@executor-js/api`). Strip it: it's not in the
// shipped runtime entries (those imports live in
// `src/api/*` / `src/react/*` which don't make it into the
// packed dist), and leaving it in would 404 at install time.
mutated = true;
} else {
next[key] = value;
}
}
return mutated ? next : block;
};
/**
* Peer-deps variant of `renameDepBlock`: resolve workspace specifiers for
* publishable peers, but DROP non-publishable `@executor-js/*` peers.
* They reference workspace-only packages (`@executor-js/api`,
* `@executor-js/react`) that don't exist on npm, so leaving them in
* the packed manifest emits install-time warnings for unresolvable
* packages. Non-`@executor-js` peers (`react`, `@tanstack/*`,
* `@effect-atom/*`, etc.) are real npm packages and pass through
* unchanged.
*/
const renamePeerDepBlock = (block: DependencyBlock | undefined): DependencyBlock | undefined => {
if (!block) return block;
const next: DependencyBlock = {};
let mutated = false;
for (const [key, value] of Object.entries(block)) {
if (publishable.has(key)) {
next[key] = value.startsWith("workspace:")
? (publishableVersions.get(key) ?? value)
: value;
if (next[key] !== value) mutated = true;
} else if (isInternalScope(key)) {
// Workspace-only `@executor-js/*` peer that we don't publish —
// strip it so the packed tarball doesn't reference an
// npm package that doesn't exist.
mutated = true;
} else {
next[key] = value;
}
}
return mutated ? next : block;
};
/**
* Strips `peerDependenciesMeta` entries that target an
* `@executor-js/*` peer we don't publish, mirroring `renamePeerDepBlock`
* so the meta block can't drift out of sync with the deps block.
*/
const renamePeerMetaBlock = (
block: PeerDependenciesMeta | undefined,
): PeerDependenciesMeta | undefined => {
if (!block) return block;
const next: PeerDependenciesMeta = {};
let mutated = false;
for (const [key, value] of Object.entries(block)) {
if (publishable.has(key)) {
next[key] = value;
} else if (isInternalScope(key)) {
mutated = true;
} else {
next[key] = value;
}
}
return mutated ? next : block;
};
const pkgJsonPath = join(pkgDir, "package.json");
const original = await readFile(pkgJsonPath, "utf8");
const pkg = JSON.parse(original) as MutablePackageJson;
pkg.dependencies = renameDepBlock(pkg.dependencies);
pkg.devDependencies = renameDepBlock(pkg.devDependencies);
pkg.peerDependencies = renamePeerDepBlock(pkg.peerDependencies);
pkg.peerDependenciesMeta = renamePeerMetaBlock(pkg.peerDependenciesMeta);
pkg.optionalDependencies = renameDepBlock(pkg.optionalDependencies);
const pkgNext = `${JSON.stringify(pkg, null, 2)}\n`;
if (pkgNext !== original) {
await writeFile(pkgJsonPath, pkgNext);
return async () => {
await writeFile(pkgJsonPath, original);
};
}
return async () => {};
};
/**
* Applies `publishConfig` field overrides to package.json in place, returning a
* function that restores the original file. `bun pm pack` does not substitute
* `publishConfig.exports` / `publishConfig.main` etc at pack time (npm does,
* but only for a subset of fields and only for `npm pack`), so we rewrite the
* file ourselves so the packed tarball has the correct `exports` pointing at
* `dist/` instead of the dev-time `src/index.ts`.
*/
const applyPublishConfig = async (pkgDir: string): Promise<() => Promise<void>> => {
const pkgJsonPath = join(pkgDir, "package.json");
const original = await readFile(pkgJsonPath, "utf8");
const parsed = JSON.parse(original) as {
publishConfig?: Record<string, unknown>;
[key: string]: unknown;
};
const publishConfig = parsed.publishConfig;
if (!publishConfig || typeof publishConfig !== "object") {
return async () => {};
}
// Fields we allow publishConfig to override. `access`/`tag`/`registry` are
// real npm publish-time config keys — they must NOT be hoisted into the
// top-level manifest.
const overridable = new Set([
"exports",
"main",
"module",
"types",
"typings",
"bin",
"browser",
"files",
]);
const nextPublishConfig: Record<string, unknown> = {};
let mutated = false;
for (const [key, value] of Object.entries(publishConfig)) {
if (overridable.has(key)) {
parsed[key] = value;
mutated = true;
} else {
nextPublishConfig[key] = value;
}
}
if (!mutated) {
return async () => {};
}
if (Object.keys(nextPublishConfig).length === 0) {
delete parsed.publishConfig;
} else {
parsed.publishConfig = nextPublishConfig;
}
await writeFile(pkgJsonPath, `${JSON.stringify(parsed, null, 2)}\n`);
return async () => {
await writeFile(pkgJsonPath, original);
};
};
const publishPackage = async (
pkgDir: string,
dryRun: boolean,
publishable: ReadonlySet<string>,
publishableVersions: ReadonlyMap<string, string>,
) => {
const { name, version } = await readPackageMeta(pkgDir);
const channel = resolveChannel(version);
if (!existsSync(join(pkgDir, "dist"))) {
throw new Error(`Missing dist/ in ${pkgDir}. Did you run 'bun run build:packages'?`);
}
console.log(`[pack] ${name}@${version} (${channel})${dryRun ? " [dry-run]" : ""}`);
// Clean any stale tarballs from previous runs so our readdir finds exactly
// the archive produced by the pack below.
const stale = (await readdir(pkgDir)).filter((entry) => entry.endsWith(".tgz"));
for (const entry of stale) {
await rm(join(pkgDir, entry), { force: true });
}
const restoreWorkspaceVersions = await applyWorkspaceVersions(
pkgDir,
publishable,
publishableVersions,
);
const restorePublishConfig = await applyPublishConfig(pkgDir);
try {
await $`bun pm pack`.cwd(pkgDir);
} finally {
await restorePublishConfig();
await restoreWorkspaceVersions();
}
const produced = (await readdir(pkgDir)).filter((entry) => entry.endsWith(".tgz"));
if (produced.length !== 1) {
throw new Error(
`Expected exactly 1 .tgz in ${pkgDir}, found ${produced.length}: ${produced.join(", ")}`,
);
}
const tarball = produced[0]!;
if (dryRun) {
return;
}
// Skip publishing already-shipped versions. The pack still ran above so
// smoke tests / pkg-pr-new previews always have a fresh tarball.
if (await packageAlreadyPublished(name, version)) {
console.log(`[skip] ${name}@${version} already on npm`);
return;
}
console.log(`[publish] ${name}@${version} (${channel})`);
const args = ["publish", tarball, "--access", "public", "--tag", channel];
if (process.env.GITHUB_ACTIONS === "true") {
args.push("--provenance");
}
await $`npm ${args}`.cwd(pkgDir);
};
/**
* Rewrite-in-place mode for the pkg.pr.new preview workflow. pkg-pr-new runs
* `bun pm pack` against each workspace package directly, which can't see our
* `publishConfig.exports` overrides or resolve `workspace:*` references. This
* walks every public package, applies the same rewrites `publishPackage`
* does, and intentionally leaves them mutated — the CI job is ephemeral and
* the workflow tears down right after pkg-pr-new finishes.
*/
const prepareOnePackage = async (
pkgDir: string,
publishable: ReadonlySet<string>,
publishableVersions: ReadonlyMap<string, string>,
) => {
const { name, version } = await readPackageMeta(pkgDir);
if (!existsSync(join(pkgDir, "dist"))) {
throw new Error(`Missing dist/ in ${pkgDir}. Did you run 'bun run build:packages'?`);
}
console.log(`[prepare] ${name}@${version}`);
await applyWorkspaceVersions(pkgDir, publishable, publishableVersions);
await applyPublishConfig(pkgDir);
};
const main = async () => {
const { dryRun, prepareOnly } = parseArgs(process.argv.slice(2));
// Each package's own version determines its dist-tag (pre-release versions
// with `-` publish to `beta`, everything else to `latest`). Packages are
// only skipped when their current version is already on npm.
const mode = prepareOnly ? " [prepare-only]" : dryRun ? " [dry-run]" : "";
console.log(`Publishing ${PACKAGE_SCOPE} packages${mode}`);
await $`bun run build:packages`.cwd(repoRoot);
// Snapshot the public package names and versions up front so public
// workspace dependencies can be written as exact versions in packed tarballs.
const publishable = new Set<string>();
const publishableVersions = new Map<string, string>();
for (const relDir of PUBLIC_PACKAGE_DIRS) {
const pkg = await readPackageMeta(join(repoRoot, relDir));
publishable.add(pkg.name);
publishableVersions.set(pkg.name, pkg.version);
}
if (prepareOnly) {
for (const relDir of PUBLIC_PACKAGE_DIRS) {
await prepareOnePackage(join(repoRoot, relDir), publishable, publishableVersions);
}
return;
}
for (const relDir of PUBLIC_PACKAGE_DIRS) {
await publishPackage(join(repoRoot, relDir), dryRun, publishable, publishableVersions);
}
};
await main();