Files
Kirill Dubovitskiy 802aece4f2 feat: migrate from yarn 1 to pnpm 10 (#1033)
* feat: migrate from yarn 1 to pnpm 10

Yarn install was taking ~82s (warm cache, no scripts). pnpm achieves
~7.5s for the same operation — an 11x speedup thanks to its
content-addressable store and hard-linking.

Key changes:
- Add pnpm-workspace.yaml, .npmrc (shamefully-hoist for RN compat)
- Replace yarn.lock with pnpm-lock.yaml (imported via pnpm import)
- Use workspace:* protocol for @slopus/happy-wire cross-references
- Move happy-cli resolutions to root pnpm.overrides
- Add pnpm.onlyBuiltDependencies allowlist for native build scripts
- Update all 3 Dockerfiles to use corepack + pnpm
- Update CI workflows to use pnpm/action-setup@v4
- Update all scripts, tests, and docs referencing yarn

Generated with [Claude Code](https://claude.ai/code)
via [Happy](https://happy.engineering)

Co-Authored-By: Claude <noreply@anthropic.com>
Co-Authored-By: Happy <yesreply@happy.engineering>

* chore: bump changelog to version 8 for pnpm migration OTA

Generated with [Claude Code](https://claude.ai/code)
via [Happy](https://happy.engineering)

Co-Authored-By: Claude <noreply@anthropic.com>
Co-Authored-By: Happy <yesreply@happy.engineering>

* fix(ci): pnpm pack outputs to repo root, use --pack-destination

pnpm pack puts the tarball in the current directory (repo root) unlike
yarn which puts it in the package directory. Use --pack-destination to
match the expected path in the smoke test workflow.

Generated with [Claude Code](https://claude.ai/code)
via [Happy](https://happy.engineering)

Co-Authored-By: Claude <noreply@anthropic.com>
Co-Authored-By: Happy <yesreply@happy.engineering>

---------

Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: Happy <yesreply@happy.engineering>
2026-04-12 02:52:14 -07:00

194 lines
5.0 KiB
JavaScript

#!/usr/bin/env node
const fs = require("fs");
const path = require("path");
const { spawnSync } = require("child_process");
const repoRoot = path.resolve(__dirname, "..");
function readJson(filePath) {
return JSON.parse(fs.readFileSync(filePath, "utf8"));
}
function getReleaseTargets() {
const rootPackageJson = readJson(path.join(repoRoot, "package.json"));
const workspacePaths = rootPackageJson.workspaces?.packages;
if (!Array.isArray(workspacePaths)) {
throw new Error("No workspace packages found in root package.json");
}
const targets = [];
for (const workspacePath of workspacePaths) {
if (workspacePath.includes("*")) {
continue;
}
const packageJsonPath = path.join(repoRoot, workspacePath, "package.json");
if (!fs.existsSync(packageJsonPath)) {
continue;
}
const workspacePackageJson = readJson(packageJsonPath);
if (!workspacePackageJson.scripts || !workspacePackageJson.scripts.release) {
continue;
}
const releaseScript = workspacePackageJson.scripts.release;
targets.push({
id: path.basename(workspacePath),
workspaceName: workspacePackageJson.name,
workspacePath,
releaseScript,
requiresReleaseIt: /\brelease-it\b/.test(releaseScript),
});
}
return targets;
}
function findTarget(targets, input) {
return targets.find(
(target) =>
input === target.id ||
input === target.workspaceName ||
input === target.workspacePath
);
}
function getReleaseItPath(baseDir) {
const binName = process.platform === "win32" ? "release-it.cmd" : "release-it";
return path.join(baseDir, "node_modules", ".bin", binName);
}
function ensureReleaseToolingInstalled(target) {
if (!target.requiresReleaseIt) {
return;
}
const rootReleaseIt = getReleaseItPath(repoRoot);
const workspaceReleaseIt = getReleaseItPath(path.join(repoRoot, target.workspacePath));
if (fs.existsSync(rootReleaseIt) || fs.existsSync(workspaceReleaseIt)) {
return;
}
console.error(
"Missing release tooling: `release-it` is not installed in this checkout."
);
console.error("Run `pnpm install` from repository root, then run `pnpm release` again.");
process.exit(1);
}
function runRelease(target, releaseArgs = []) {
ensureReleaseToolingInstalled(target);
console.log(
`Running release pipeline for ${target.id} (workspace: ${target.workspaceName})`
);
const workspaceBin = path.join(
repoRoot,
target.workspacePath,
"node_modules",
".bin"
);
const rootBin = path.join(repoRoot, "node_modules", ".bin");
const existingPath = process.env.PATH || "";
const releasePath = [workspaceBin, rootBin, existingPath]
.filter(Boolean)
.join(path.delimiter);
const commandArgs = ["--filter", target.workspaceName, "run", "release"];
if (releaseArgs.length > 0) {
commandArgs.push(...releaseArgs);
}
const result = spawnSync("pnpm", commandArgs, {
cwd: repoRoot,
stdio: "inherit",
env: {
...process.env,
PATH: releasePath,
},
});
if (result.error) {
throw result.error;
}
process.exit(result.status ?? 1);
}
async function promptForTarget(targets) {
if (!process.stdin.isTTY || !process.stdout.isTTY) {
console.error("Interactive release selection requires a TTY.");
console.error("Available targets:");
for (const target of targets) {
console.error(`- ${target.id} (workspace: ${target.workspaceName})`);
}
console.error("Run `pnpm release -- <target> [args...]` in non-interactive mode.");
process.exit(1);
}
let select;
try {
({ select } = await import("@inquirer/prompts"));
} catch (error) {
console.error("Missing interactive prompt dependency: `@inquirer/prompts`.");
console.error("Run `pnpm install` from repository root, then run `pnpm release` again.");
process.exit(1);
}
const targetId = await select({
message: "Select workspace to release:",
pageSize: 10,
choices: targets.map((target) => ({
name: `${target.id} (${target.workspaceName})`,
value: target.id,
description: target.workspacePath,
})),
});
const target = findTarget(targets, targetId);
if (!target) {
console.error("Invalid selection.");
process.exit(1);
}
runRelease(target);
}
async function main() {
const targets = getReleaseTargets();
if (targets.length === 0) {
console.error("No releasable workspace packages found.");
process.exit(1);
}
const input = process.argv[2];
const releaseArgs = process.argv.slice(3);
if (input) {
const target = findTarget(targets, input);
if (!target) {
console.error(`Unknown release target: ${input}`);
console.error("Available targets:");
for (const candidate of targets) {
console.error(
`- ${candidate.id} (workspace: ${candidate.workspaceName}, path: ${candidate.workspacePath})`
);
}
process.exit(1);
}
runRelease(target, releaseArgs);
return;
}
await promptForTarget(targets);
}
main().catch((error) => {
console.error(error.message || String(error));
process.exit(1);
});