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