import { readFileSync, writeFileSync, existsSync } from "node:fs"; import { join } from "node:path"; const MANIFEST_FILE = ".release-please-manifest.json"; const CONFIG_FILE = "release-please-config.json"; const INITIAL_VERSION = "0.1.0"; /** * Register a new feature package in release-please tracking (ADR-021). * * Mutates two files at the repo root: * - `.release-please-manifest.json` — adds `"packages/": "0.1.0"` * - `release-please-config.json` — adds the per-package config block * * Idempotent: if the package is already tracked, both writes are no-ops. * * Throws if either file is missing — release-please MUST be set up before * a feature generator can register against it (ADR-021 land-down sequence). */ export function registerFeatureInReleasePlease( repoRoot: string, featureKebab: string, ): { manifestChanged: boolean; configChanged: boolean } { const manifestPath = join(repoRoot, MANIFEST_FILE); const configPath = join(repoRoot, CONFIG_FILE); if (!existsSync(manifestPath)) { throw new Error( `Cannot register feature in release-please: ${MANIFEST_FILE} is missing. ` + `release-please must be set up before scaffolding features that participate in tracking.`, ); } if (!existsSync(configPath)) { throw new Error( `Cannot register feature in release-please: ${CONFIG_FILE} is missing.`, ); } const packagePath = `packages/${featureKebab}`; const manifestChanged = addToManifest(manifestPath, packagePath); const configChanged = addToConfig(configPath, packagePath, featureKebab); return { manifestChanged, configChanged }; } function addToManifest(manifestPath: string, packagePath: string): boolean { const text = readFileSync(manifestPath, "utf8"); const manifest = JSON.parse(text) as Record; if (manifest[packagePath]) return false; // already tracked manifest[packagePath] = INITIAL_VERSION; // Sort keys: root (".") stays first if present; the rest alphabetically by // path so insertions are deterministic + future diffs stay minimal. const sorted: Record = {}; if (manifest["."] !== undefined) sorted["."] = manifest["."]; for (const key of Object.keys(manifest).sort()) { if (key === ".") continue; sorted[key] = manifest[key]; } writeFileSync(manifestPath, JSON.stringify(sorted, null, 2) + "\n"); return true; } function addToConfig( configPath: string, packagePath: string, featureKebab: string, ): boolean { const text = readFileSync(configPath, "utf8"); const config = JSON.parse(text) as { packages?: Record< string, { "package-name": string; component: string; "changelog-path"?: string; } >; [k: string]: unknown; }; if (!config.packages) { throw new Error( `release-please-config.json has no "packages" key — config is malformed.`, ); } if (config.packages[packagePath]) return false; // already tracked config.packages[packagePath] = { "package-name": `@repo/${featureKebab}`, component: featureKebab, "changelog-path": "CHANGELOG.md", }; // Sort packages the same way as the manifest: root first, rest alphabetical. const sortedPackages: typeof config.packages = {}; if (config.packages["."]) sortedPackages["."] = config.packages["."]; for (const key of Object.keys(config.packages).sort()) { if (key === ".") continue; sortedPackages[key] = config.packages[key]; } config.packages = sortedPackages; writeFileSync(configPath, JSON.stringify(config, null, 2) + "\n"); return true; }