everything-dev
Version:
A consolidated product package for building Module Federation apps with oRPC APIs.
136 lines (134 loc) • 6.31 kB
JavaScript
import { CATALOG_PINNED_OVERRIDE_KEYS, PNPM_PACKAGE_MANAGER, frameworkReleaseAgeExcludes } from "../../../workspace/catalog-overrides.mjs";
import { listFiles, readText, removePath, writeText } from "../fs.mjs";
import { V2_CATALOG } from "./v2-catalog.mjs";
import { Effect } from "effect";
//#region src/services/scaffold/upgrade-audit/steps/migrate-pnpm.ts
const YAML_PLAIN_VALUE_RE = /^[A-Za-z0-9^~][A-Za-z0-9.^~*+\-/]*$/;
const yamlScalar = (value) => YAML_PLAIN_VALUE_RE.test(value) ? value : JSON.stringify(value);
const yamlMap = (entries, indent) => entries.map(([key, value]) => /^[A-Za-z0-9_.-]+$/.test(key) ? `${indent}${key}: ${yamlScalar(value)}` : `${indent}${JSON.stringify(key)}: ${yamlScalar(value)}`).join("\n");
const renderWorkspaceYaml = (input) => {
const catalogEntries = Object.entries(input.catalog).map(([key, value]) => [key, value]);
const catalog = Object.fromEntries(catalogEntries);
const overrideEntries = CATALOG_PINNED_OVERRIDE_KEYS.filter((key) => catalog[key]).map((key) => [key, catalog[key]]);
const releaseAgeExcludes = frameworkReleaseAgeExcludes(catalog);
return [
"packages:",
...input.packages.map((pkg) => ` - ${yamlScalar(pkg)}`),
"",
"allowBuilds:",
" esbuild: true",
" sharp: true",
" protobufjs: true",
" '@openapitools/openapi-generator-cli': true",
"",
...overrideEntries.length > 0 ? [
"overrides:",
...overrideEntries.map(([key, value]) => ` ${yamlMap([[key, value]], "").trimStart()}`),
""
] : [],
"catalog:",
yamlMap(catalogEntries, " "),
"",
...releaseAgeExcludes.length > 0 ? [
"minimumReleaseAgeExcludePrune: true",
"minimumReleaseAgeExclude:",
...releaseAgeExcludes.map((entry) => ` - ${yamlScalar(entry)}`),
""
] : []
].join("\n");
};
const PLUGIN_TYPECHECK_LOOP = "for dir in plugins/*; do if [ -f \"$dir/package.json\" ]; then pnpm --dir \"$dir\" run typecheck || exit 1; fi; done";
const PLUGIN_TEST_LOOP = "for d in plugins/*; do if [ -f \"$d/package.json\" ]; then pnpm --dir \"$d\" --if-present run test || exit 1; fi; done";
const hardenedTypecheck = (dirs) => [
"pnpm run types:gen",
...dirs.map((dir) => `if [ -d ${dir} ]; then pnpm --dir ${dir} run typecheck || exit 1; fi`),
PLUGIN_TYPECHECK_LOOP
].join(" && ");
const hardenedTest = (dirs) => [...dirs.map((dir) => `pnpm --dir ${dir} --if-present run test || exit 1`), `[ -d plugins ] && ${PLUGIN_TEST_LOOP} || true`].join(" && ");
/** Mechanical bun→pnpm rewrites, safe for any workspace's scripts. */
const mechanicalScriptRewrite = (value) => value.replaceAll("bun run --if-present --cwd ", "pnpm --dir ").replaceAll("bun run --cwd ", "pnpm --dir ").replace(/bun run --cwd (\S+) (.+)/, "pnpm --dir $1 exec $2").replaceAll("bun run ", "pnpm run ").replace(/node node_modules\/\.bin\/bos /, "bos ").replace("for d in plugins/*; do [ -f \"$d/package.json\" ] && bun run --if-present --cwd \"$d\" test; done", "for d in plugins/*; do if [ -f \"$d/package.json\" ]; then pnpm --dir \"$d\" --if-present run test || exit 1; fi; done").replace("for dir in plugins/*; do if [ -f \"$dir/package.json\" ]; then bun run --cwd \"$dir\" typecheck; fi; done", "for dir in plugins/*; do if [ -f \"$dir/package.json\" ]; then pnpm --dir \"$dir\" run typecheck || exit 1; fi; done");
const rewriteScripts = (scripts) => {
const next = {};
for (const [key, value] of Object.entries(scripts)) {
if (key === "typecheck") {
next[key] = hardenedTypecheck(["ui", "api"]);
continue;
}
if (key === "test") {
next[key] = hardenedTest(["api", "ui"]);
continue;
}
next[key] = mechanicalScriptRewrite(value);
}
return next;
};
const WORKSPACE_PKG_GLOB = "{ui/package.json,api/package.json,plugins/*/package.json}";
/**
* Workspace package.json scripts still speak bun (`bun run build:client`,
* `bun run --cwd api typecheck`). Rewrite every bun invocation to the pnpm
* form so the pnpm-only tree (packageManager pin, no bun in CI) works.
* Unlike the root pass, workspace typecheck/test scripts keep their own
* bodies — only the bun invocations are mechanically rewritten.
*/
const rewriteWorkspaceScripts = (projectDir, dryRun) => Effect.gen(function* () {
const pkgPaths = yield* listFiles(projectDir, WORKSPACE_PKG_GLOB);
const changed = [];
for (const pkgPath of pkgPaths) {
const source = yield* readText(projectDir, pkgPath);
if (!source.includes("bun ")) continue;
const pkg = JSON.parse(source);
if (!pkg.scripts) continue;
const scripts = Object.fromEntries(Object.entries(pkg.scripts).map(([key, value]) => [key, mechanicalScriptRewrite(value)]));
if (JSON.stringify(scripts) === JSON.stringify(pkg.scripts)) continue;
if (!dryRun) yield* writeText(projectDir, pkgPath, `${JSON.stringify({
...pkg,
scripts
}, null, 2)}\n`);
changed.push(pkgPath);
}
return changed;
});
const BUN_ARTIFACTS = ["bun.lock", "bunfig.toml"];
const RETIRED_FILES = ["railway.toml", "Dockerfile"];
const migratePnpm = (projectDir, dryRun) => Effect.gen(function* () {
const pkg = JSON.parse(yield* readText(projectDir, "package.json"));
const catalog = {
...pkg.workspaces?.catalog,
...V2_CATALOG
};
const packages = pkg.workspaces?.packages ?? [
"api",
"ui",
"plugins/*"
];
const workspaceYaml = renderWorkspaceYaml({
packages,
catalog
});
const nextPkg = {
...pkg,
packageManager: PNPM_PACKAGE_MANAGER,
scripts: rewriteScripts(pkg.scripts ?? {}),
workspaces: void 0
};
const writes = ["pnpm-workspace.yaml", "package.json"];
const deletes = [...BUN_ARTIFACTS, ...RETIRED_FILES];
const notes = ["catalog now lives in pnpm-workspace.yaml; bos upgrade syncs framework versions from the parent", "Dockerfile + railway.toml retired — deploys use ci.image (the upstream runtime image)"];
const workspaceScripts = yield* rewriteWorkspaceScripts(projectDir, dryRun);
if (dryRun) return {
writes: [...writes, ...workspaceScripts],
deletes,
notes
};
yield* writeText(projectDir, "pnpm-workspace.yaml", workspaceYaml);
yield* writeText(projectDir, "package.json", `${JSON.stringify(nextPkg, null, 2)}\n`);
for (const path of [...BUN_ARTIFACTS, ...RETIRED_FILES]) yield* removePath(projectDir, path);
return {
writes: [...writes, ...workspaceScripts],
deletes,
notes
};
});
//#endregion
export { migratePnpm };
//# sourceMappingURL=migrate-pnpm.mjs.map