UNPKG

everything-dev

Version:

A consolidated product package for building Module Federation apps with oRPC APIs.

147 lines (145 loc) • 6.12 kB
import { compareSemver } from "../../utils/semver.mjs"; import { existsSync, readFileSync, writeFileSync } from "node:fs"; import { join } from "node:path"; import { parse, stringify } from "yaml"; //#region src/services/workspace/catalog-overrides.ts /** The pnpm pin stamped into every generated and migrated child workspace. * Drift-guarded against the parent's own `packageManager` pin by * tests/unit/pnpm-pin.test.ts. */ const PNPM_PACKAGE_MANAGER = "pnpm@12.16.1"; /** * Workspace keys whose `overrides:` row must equal the catalog row. An * override that re-pins a version literal drifts the moment the catalog * moves (#659/#682): the next build resolves the old version while the * upgrade reports success, and the #546 skew gate fails with a message * pointing at the wrong fix. Reconciliation (init, sync, upgrade) rewrites * these rows from the catalog on every run; keys absent from the catalog are * left unpinned. */ const CATALOG_PINNED_OVERRIDE_KEYS = [ "@better-fetch/fetch", "@rspack/core", "@tanstack/react-router", "@tanstack/router-core", "better-auth", "react", "react-dom" ]; /** * Framework packages children install from the registry. A freshly released * version is younger than pnpm's `minimumReleaseAge` holdback (1440 default), * so each current version is excluded by exact `name@version` — derived from * the catalog on every write, never frozen. `minimumReleaseAgeExcludePrune` * makes pnpm drop entries the lockfile no longer resolves, so the list * replaces instead of accumulating (#659's append trap). */ const FRAMEWORK_EXCLUDE_PACKAGES = [ "better-near-auth", "every-plugin", "everything-dev" ]; const EXACT_VERSION_RE = /^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?$/; /** * pnpm's minimumReleaseAgeExclude accepts exact versions only — a range like * `everything-dev@^2.0.0-rc.27` is a hard config error ("Use exact versions * only") that fails every install. The catalog's framework rows are ranges; * strip the operator. A specifier that doesn't reduce to an exact version is * skipped rather than emitted invalid. */ function exactReleaseAgeExclude(name, specifier) { const exact = specifier.trim().replace(/^[~^=]+/, "").trim(); return EXACT_VERSION_RE.test(exact) ? `${name}@${exact}` : void 0; } function frameworkReleaseAgeExcludes(catalog) { return FRAMEWORK_EXCLUDE_PACKAGES.flatMap((name) => { const specifier = catalog[name]; if (!specifier) return []; const entry = exactReleaseAgeExclude(name, specifier); return entry ? [entry] : []; }); } /** * Mutates the parsed workspace doc: overrides for the catalog-pinned keys * follow the catalog rows, framework release-age excludes are regenerated * from the catalog's current framework versions (non-framework entries the * child authored are preserved), and the prune flag is set. Returns whether * anything changed. */ function reconcileWorkspacePosture(doc) { let changed = false; const catalog = doc.catalog ?? {}; const overrides = { ...doc.overrides }; let overridesChanged = false; for (const key of CATALOG_PINNED_OVERRIDE_KEYS) if (catalog[key] && overrides[key] !== catalog[key]) { overrides[key] = catalog[key]; overridesChanged = true; } if (overridesChanged) { doc.overrides = Object.fromEntries(Object.entries(overrides).sort(([a], [b]) => a.localeCompare(b))); changed = true; } const excludes = frameworkReleaseAgeExcludes(catalog); if (excludes.length > 0) { const retained = (doc.minimumReleaseAgeExclude ?? []).filter((entry) => !FRAMEWORK_EXCLUDE_PACKAGES.some((name) => entry.startsWith(`${name}@`))); const merged = [.../* @__PURE__ */ new Set([...retained, ...excludes])].sort(); if (JSON.stringify(merged) !== JSON.stringify(doc.minimumReleaseAgeExclude ?? [])) { doc.minimumReleaseAgeExclude = merged; changed = true; } if (doc.minimumReleaseAgeExcludePrune !== true) { doc.minimumReleaseAgeExcludePrune = true; changed = true; } } return changed; } /** * File-level reconciliation for an existing child workspace: reads * pnpm-workspace.yaml, applies `reconcileWorkspacePosture`, writes back only * on change. Catalog-sync callers run this after writing new catalog rows so * the child's overrides and release-age excludes land with the versions they * must agree with. */ function reconcileWorkspaceYamlFile(root) { const yamlPath = join(root, "pnpm-workspace.yaml"); if (!existsSync(yamlPath)) return false; let doc; try { const parsed = parse(readFileSync(yamlPath, "utf8")); if (!parsed || typeof parsed !== "object") return false; doc = parsed; } catch { return false; } if (!doc.catalog) return false; if (!reconcileWorkspacePosture(doc)) return false; writeFileSync(yamlPath, `${stringify(doc, { lineWidth: 0 }).trimEnd()}\n`); return true; } /** * Forward-only pnpm pin propagation for existing children: `migrate-pnpm` is * a one-time v1→v2 step and `bos init` only writes new projects, so without * this a parent toolchain bump never reaches already-v2 workspaces. Never * downgrades (a child pinning newer than the parent keeps its pin) and never * touches non-pnpm managers (the bun cutover is migrate-pnpm's job). */ function reconcilePackageManagerPin(pkgPath) { if (!existsSync(pkgPath)) return false; let pkg; try { pkg = JSON.parse(readFileSync(pkgPath, "utf8")); } catch { return false; } const current = typeof pkg.packageManager === "string" ? pkg.packageManager : void 0; if (current !== void 0 && !current.startsWith("pnpm@")) return false; const currentVersion = current?.slice(5).split("+")[0]; const targetVersion = PNPM_PACKAGE_MANAGER.slice(5).split("+")[0]; if (currentVersion !== void 0 && compareSemver(currentVersion, targetVersion) >= 0) return false; pkg.packageManager = PNPM_PACKAGE_MANAGER; writeFileSync(pkgPath, `${JSON.stringify(pkg, null, 2)}\n`); return true; } //#endregion export { CATALOG_PINNED_OVERRIDE_KEYS, FRAMEWORK_EXCLUDE_PACKAGES, PNPM_PACKAGE_MANAGER, exactReleaseAgeExclude, frameworkReleaseAgeExcludes, reconcilePackageManagerPin, reconcileWorkspacePosture, reconcileWorkspaceYamlFile }; //# sourceMappingURL=catalog-overrides.mjs.map