UNPKG

everything-dev

Version:

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

515 lines (513 loc) • 22.4 kB
const require_merge = require('../resolution/merge.cjs'); const require_load = require('../resolution/load.cjs'); const require_catalog = require('../workspace/catalog.cjs'); const require_init_source = require('../resolution/init-source.cjs'); const require_services_resolution_session = require('../resolution/session.cjs'); const require_workspace = require('../workspace/workspace.cjs'); const require_shared_deps = require('../workspace/shared-deps.cjs'); const require_ownership = require('./ownership.cjs'); const require_manifest = require('../platform/manifest.cjs'); const require_materializer = require('../../infra/materializer.cjs'); const require_snapshot_hash = require('../../utils/snapshot-hash.cjs'); const require_snapshot = require('./snapshot.cjs'); const require_init = require('./init.cjs'); let node_fs = require("node:fs"); let node_path = require("node:path"); let yaml = require("yaml"); let glob = require("glob"); //#region src/services/scaffold/sync.ts function computeLocalHash(projectDir, filePath) { const fullPath = (0, node_path.join)(projectDir, filePath); if (!(0, node_fs.existsSync)(fullPath)) return null; try { const content = (0, node_fs.readFileSync)(fullPath); return require_snapshot_hash.computeSnapshotHash(content); } catch { return null; } } function backupFiles(projectDir, filePaths) { const filesToBackup = filePaths.filter((f) => (0, node_fs.existsSync)((0, node_path.join)(projectDir, f))); if (filesToBackup.length === 0) return null; const timestamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-"); const backupDir = (0, node_path.join)(projectDir, ".bos", "sync-backup", timestamp); for (const filePath of filesToBackup) { const src = (0, node_path.join)(projectDir, filePath); const dest = (0, node_path.join)(backupDir, filePath); (0, node_fs.mkdirSync)((0, node_path.dirname)(dest), { recursive: true }); (0, node_fs.copyFileSync)(src, dest); } return backupDir; } function mergeStringMaps(local, template) { if (!local && !template) return void 0; const merged = { ...local }; for (const [name, value] of Object.entries(template ?? {})) merged[name] = value; return Object.keys(merged).length > 0 ? merged : void 0; } function mergeWorkspacePackages(local, template) { const localPackages = Array.isArray(local) ? local : []; const templatePackages = Array.isArray(template) ? template : []; if (localPackages.length === 0 && templatePackages.length === 0) return void 0; const ordered = /* @__PURE__ */ new Set(); for (const entry of templatePackages) if (typeof entry === "string" && entry.length > 0) ordered.add(entry); for (const entry of localPackages) if (typeof entry === "string" && entry.length > 0) ordered.add(entry); if ([...ordered].some((e) => e.startsWith("plugins/") && e !== "plugins/*")) { for (const entry of ordered) if (entry.startsWith("plugins/") && entry !== "plugins/*") ordered.delete(entry); ordered.add("plugins/*"); } return ordered.size > 0 ? [...ordered] : void 0; } function mergePackageJson(filePath, local, template, childScripts) { const merged = { ...local, ...template }; if (filePath === "package.json") { for (const key of [ "name", "private", "version" ]) if (key in local) merged[key] = local[key]; } else if ("version" in local) merged.version = local.version; for (const depField of [ "dependencies", "devDependencies", "peerDependencies", "overrides" ]) { const localDeps = local[depField]; const templateDeps = template[depField]; const mergedDeps = mergeStringMaps(localDeps, templateDeps); if (mergedDeps) merged[depField] = mergedDeps; else delete merged[depField]; } if (local.scripts && typeof local.scripts === "object" || template.scripts && typeof template.scripts === "object" || childScripts) { const mergedScripts = mergeStringMaps(local.scripts, filePath === "package.json" && childScripts ? childScripts : template.scripts); if (mergedScripts) merged.scripts = mergedScripts; else delete merged.scripts; } if (local.workspaces && typeof local.workspaces === "object" || template.workspaces && typeof template.workspaces === "object") { const localWorkspaces = local.workspaces ?? {}; const templateWorkspaces = template.workspaces ?? {}; const mergedWorkspaces = { ...localWorkspaces, ...templateWorkspaces }; const mergedPackages = mergeWorkspacePackages(localWorkspaces.packages, templateWorkspaces.packages); if (mergedPackages) mergedWorkspaces.packages = mergedPackages; else delete mergedWorkspaces.packages; const mergedCatalog = mergeStringMaps(localWorkspaces.catalog, templateWorkspaces.catalog); if (mergedCatalog) mergedWorkspaces.catalog = mergedCatalog; else delete mergedWorkspaces.catalog; if (Object.keys(mergedWorkspaces).length > 0) merged.workspaces = mergedWorkspaces; else delete merged.workspaces; } return merged; } /** * Child-shape normalization for every manifest sync writes (#521): the parent * repo's own root manifest uses `workspace:^` specifiers, which only resolve * where the framework packages are vendored workspace members. A child has no * such members — any `workspace:` specifier that lands in a child manifest * breaks the next `pnpm install --frozen-lockfile`. Runs on the merged (or * raw-template) object before the bytes are written, so both the `updated` * and `conflicted` outcomes land in child shape. A `workspace:` specifier * without a catalog entry is left alone — there is no version to pin. */ function normalizeChildManifest(pkg, catalog) { let modified = false; for (const fieldName of [ "dependencies", "devDependencies", "peerDependencies" ]) { const field = pkg[fieldName]; if (!field) continue; for (const [name, specifier] of Object.entries(field)) if (specifier.startsWith("workspace:") && catalog[name]) { field[name] = "catalog:"; modified = true; } } const dependencies = pkg.dependencies; const devDependencies = pkg.devDependencies; if (dependencies && devDependencies) { for (const name of Object.keys(devDependencies)) if (name in dependencies) { delete devDependencies[name]; modified = true; } } return modified; } function toSourcePath(sourceDir, destPath) { if (destPath.startsWith(".github/")) { const templatePath = destPath.replace(/^\.github\//, ".github/templates/"); if ((0, node_fs.existsSync)((0, node_path.join)(sourceDir, templatePath))) return templatePath; } const directPath = (0, node_path.join)(sourceDir, destPath); if ((0, node_fs.existsSync)(directPath)) return destPath; return null; } /** * Release-scoped workflow refs (ADR 0034 pt 4): when a platform manifest * resolves, the child's *framework-owned* reusable-workflow wrappers pin the * framework's version tag instead of `@main`. Only refs into the framework * repo's own `.github/workflows/` repoint — third-party actions tracking * `@main` are nobody's release contract. Applied at content-build time so the * three-way hash comparison and the written bytes stay consistent. */ function applyReleaseRefs(content, releaseTag) { return content.replace(/(uses:\s*\S*everything-dev\/\.github\/workflows\/[^\s@]+@)main\b/g, `$1${releaseTag}`); } /** * The workflow pin derives from what the release tarball actually resolved * from: the canonical `v<version>` tag when it resolved, otherwise the * resolved commit sha. No resolved ref/sha → the release tag itself. */ function resolveWorkflowReleasePin(releaseTag, resolvedRef, resolvedSha) { if (resolvedRef?.startsWith("v")) return resolvedRef; return resolvedSha ?? releaseTag; } function buildSyncedFileContent(sourceDir, projectDir, filePath, explicitDestPath, childScripts, release) { const src = (0, node_path.join)(sourceDir, filePath); const destPath = explicitDestPath ?? (filePath.startsWith(".github/templates/") ? filePath.replace(/^\.github\/templates\//, ".github/") : filePath); const dest = (0, node_path.join)(projectDir, destPath); if (filePath.endsWith("package.json")) { const localContent = (0, node_fs.existsSync)(dest) ? (0, node_fs.readFileSync)(dest, "utf-8") : null; const templateContent = (0, node_fs.readFileSync)(src, "utf-8"); const catalog = require_catalog.readWorkspaceCatalog(require_workspace.findWorkspaceRoot(projectDir)?.dir ?? projectDir); if (localContent) { const merged = mergePackageJson(destPath, JSON.parse(localContent), JSON.parse(templateContent), childScripts); normalizeChildManifest(merged, catalog); return `${JSON.stringify(merged, null, 2)}\n`; } const template = JSON.parse(templateContent); normalizeChildManifest(template, catalog); return `${JSON.stringify(template, null, 2)}\n`; } const raw = (0, node_fs.readFileSync)(src); const template = raw.toString("utf-8"); if (template.includes("// child-owned-start") && filePath.endsWith("plugin.dev.ts")) { const destPathLocal = (0, node_path.join)(projectDir, destPath); if ((0, node_fs.existsSync)(destPathLocal)) { const childBlock = require_ownership.extractChildOwnedBlock((0, node_fs.readFileSync)(destPathLocal, "utf-8")); if (childBlock) return require_ownership.spliceChildOwnedBlock(template, childBlock); } return template; } if (release && filePath.startsWith(".github/templates/workflows/")) return applyReleaseRefs(raw.toString("utf-8"), release); return raw; } function writeSyncedFile(sourceDir, projectDir, filePath, explicitDestPath, childScripts, release) { const destPath = explicitDestPath ?? (filePath.startsWith(".github/templates/") ? filePath.replace(/^\.github\/templates\//, ".github/") : filePath); const dest = (0, node_path.join)(projectDir, destPath); (0, node_fs.mkdirSync)((0, node_path.dirname)(dest), { recursive: true }); (0, node_fs.writeFileSync)(dest, buildSyncedFileContent(sourceDir, projectDir, filePath, destPath, childScripts, release)); } /** * The child AGENTS.md the parent template's skills block personalizes to — * shared by the dry-run preview and the write path so both agree on when * AGENTS.md counts as drifted. */ function expectedChildAgentsMd(sourceDir, overrides, childPlugins) { const agentsMdSourcePath = toSourcePath(sourceDir, "AGENTS.md"); if (!agentsMdSourcePath) return null; const skillsBlock = require_init.extractSkillsBlock((0, node_fs.readFileSync)((0, node_path.join)(sourceDir, agentsMdSourcePath), "utf-8")); if (!skillsBlock) return null; const content = require_init.buildChildAgentsMd(skillsBlock, { overrides, plugins: childPlugins }); return { content, hash: require_snapshot_hash.computeSnapshotHash(content) }; } async function getSelectedChildPlugins(projectDir, localConfig) { if (!localConfig.plugins || typeof localConfig.plugins !== "object") return []; const pluginDirs = new Set((await (0, glob.glob)("plugins/*/package.json", { cwd: projectDir, nodir: true, dot: false, absolute: false })).map((file) => file.match(/^plugins\/([^/]+)\/package\.json$/)?.[1]).filter((key) => Boolean(key))); const selected = []; for (const [pluginKey, rawEntry] of Object.entries(localConfig.plugins)) { if (typeof rawEntry === "string") { if (!rawEntry.startsWith("local:")) { selected.push(pluginKey); continue; } const localPath = (0, node_path.join)(projectDir, rawEntry.slice(6).trim()); if ((0, node_fs.existsSync)(localPath) || pluginDirs.has(pluginKey)) selected.push(pluginKey); continue; } if (!rawEntry || typeof rawEntry !== "object") { selected.push(pluginKey); continue; } const entry = rawEntry; const development = typeof entry.development === "string" ? entry.development : void 0; if (!development?.startsWith("local:")) { selected.push(pluginKey); continue; } const localPath = (0, node_path.join)(projectDir, development.slice(6).trim()); if ((0, node_fs.existsSync)(localPath) || pluginDirs.has(pluginKey)) selected.push(pluginKey); } return selected; } function hasPluginsWorkspace(projectDir) { const root = require_workspace.findWorkspaceRoot(projectDir); if (!root) return false; const rel = (0, node_path.relative)(root.dir, projectDir); const baseDir = rel.startsWith("..") || rel === "" ? root.dir : projectDir; for (const manifest of ["pnpm-workspace.yaml", "package.json"]) { const manifestPath = (0, node_path.join)(baseDir, manifest); if (!(0, node_fs.existsSync)(manifestPath)) continue; try { const content = (0, node_fs.readFileSync)(manifestPath, "utf-8"); const packages = manifest === "pnpm-workspace.yaml" ? (0, yaml.parse)(content)?.packages : JSON.parse(content).workspaces; if ((Array.isArray(packages) ? packages : Array.isArray(packages?.packages) ? packages.packages ?? [] : []).includes("plugins/*")) return true; } catch { return false; } } return false; } async function syncTemplate(projectDir, options = {}) { const configPath = (0, node_path.join)(projectDir, "bos.app.ts"); if (!(0, node_fs.existsSync)(configPath)) return { status: "error", updated: [], skipped: [], added: [], conflicted: [], error: "No bos.app.ts found — template sync only applies to TS-form children" }; const localConfig = await require_load.loadInProcessDescriptor((0, node_fs.readFileSync)(configPath, "utf-8"), configPath); let extendsRef; if (typeof localConfig.extends === "string") extendsRef = localConfig.extends; else if (require_merge.isPlainObject(localConfig.extends)) extendsRef = require_merge.resolveExtendsRef(localConfig.extends, "production"); if (!extendsRef?.startsWith("bos://")) return { status: "error", updated: [], skipped: [], added: [], conflicted: [], error: "No extends field found in bos.app.ts — cannot determine parent" }; const extendsMatch = extendsRef.match(/^bos:\/\/([^/]+)\/(.+)$/); if (!extendsMatch) return { status: "error", updated: [], skipped: [], added: [], conflicted: [], error: `Invalid extends reference: ${extendsRef}` }; const extendsAccount = extendsMatch[1]; const extendsGateway = extendsMatch[2]; const platform = await require_manifest.resolvePlatformManifest({ configDir: projectDir, localConfig }); if (!platform) return { status: "error", updated: [], skipped: [], added: [], conflicted: [], error: `No platform release manifest resolved (platform pin missing in ${(0, node_path.join)(projectDir, "bos.app.ts")} and its extends chain) — run \`bos upgrade\` to pin a release, or \`bos status\` to inspect the chain` }; const releaseTag = require_init_source.releaseRefCandidates(platform.manifest.frameworkVersion); const { sourceDir, cleanup, resolvedRef, resolvedSha } = await require_init_source.resolveSourceDir({ extendsAccount, extendsGateway, ref: releaseTag }); const workflowPin = resolveWorkflowReleasePin(releaseTag, resolvedRef, resolvedSha); try { const childPlugins = await getSelectedChildPlugins(projectDir, localConfig); const withUi = (0, node_fs.existsSync)((0, node_path.join)(projectDir, "ui", "package.json")); const withApi = (0, node_fs.existsSync)((0, node_path.join)(projectDir, "api", "package.json")); const withHost = (0, node_fs.existsSync)((0, node_path.join)(projectDir, "host", "package.json")); const withPlugins = childPlugins.length > 0 || hasPluginsWorkspace(projectDir); const childScripts = require_init.buildChildRootScripts({ ui: withUi, api: withApi, host: withHost, plugins: withPlugins }); const destToSource = /* @__PURE__ */ new Map(); for (const destPath of require_ownership.FRAMEWORK_OWNED_SYNC_FILES) { if (destPath.startsWith("ui/") && !withUi) continue; if (destPath.startsWith("api/") && !withApi) continue; if (destPath.startsWith("host/") && !withHost) continue; const sourcePath = toSourcePath(sourceDir, destPath); if (!sourcePath) continue; destToSource.set(destPath, sourcePath); } for (const destPath of await (0, glob.glob)(require_ownership.AGENT_SYNC_GLOBS, { cwd: sourceDir, nodir: true, dot: true, absolute: false, ignore: [ "**/node_modules/**", "**/.git/**", "**/dist/**", "**/.bos/**" ] })) destToSource.set(destPath, destPath); for (const pluginKey of childPlugins) { if (!(0, node_fs.existsSync)((0, node_path.join)(projectDir, "plugins", pluginKey, "src"))) continue; const sourceFile = "api/src/global.d.ts"; if (!(0, node_fs.existsSync)((0, node_path.join)(sourceDir, sourceFile))) continue; destToSource.set(`plugins/${pluginKey}/src/global.d.ts`, sourceFile); } for (const pluginKey of childPlugins) { if (!(0, node_fs.existsSync)((0, node_path.join)(projectDir, "plugins", pluginKey))) continue; const sourceFile = "plugins/_template/tests/types.d.ts"; if (!(0, node_fs.existsSync)((0, node_path.join)(sourceDir, sourceFile))) continue; destToSource.set(`plugins/${pluginKey}/tests/types.d.ts`, sourceFile); } for (const pluginKey of childPlugins) { if (!(0, node_fs.existsSync)((0, node_path.join)(projectDir, "plugins", pluginKey))) continue; const sourceFile = "plugins/_template/rspack.config.js"; if (!(0, node_fs.existsSync)((0, node_path.join)(sourceDir, sourceFile))) continue; destToSource.set(`plugins/${pluginKey}/rspack.config.js`, sourceFile); } for (const pluginKey of childPlugins) { if (!(0, node_fs.existsSync)((0, node_path.join)(projectDir, "plugins", pluginKey, "src", "db"))) continue; const sourceFile = "api/drizzle.config.ts"; if (!(0, node_fs.existsSync)((0, node_path.join)(sourceDir, sourceFile))) continue; destToSource.set(`plugins/${pluginKey}/drizzle.config.ts`, sourceFile); } for (const pluginKey of childPlugins) { if (!(0, node_fs.existsSync)((0, node_path.join)(projectDir, "plugins", pluginKey))) continue; const sourceFile = "plugins/_template/tsconfig.json"; if (!(0, node_fs.existsSync)((0, node_path.join)(sourceDir, sourceFile))) continue; destToSource.set(`plugins/${pluginKey}/tsconfig.json`, sourceFile); } const updated = []; const skipped = []; const added = []; const conflicted = []; const snapshotFiles = (await require_snapshot.readSnapshot(projectDir))?.files ?? {}; for (const [destPath, filePath] of destToSource.entries()) { if (!(0, node_fs.existsSync)((0, node_path.join)(sourceDir, filePath))) { skipped.push(destPath); continue; } const localHash = computeLocalHash(projectDir, destPath); const sourceHash = require_snapshot_hash.computeSnapshotHash(buildSyncedFileContent(sourceDir, projectDir, filePath, void 0, childScripts, workflowPin)); if (localHash === null) { added.push(destPath); continue; } if (localHash === sourceHash) { skipped.push(destPath); continue; } const snapshotHash = snapshotFiles[destPath]; if (!snapshotHash) { updated.push(destPath); continue; } if (localHash === snapshotHash) { updated.push(destPath); continue; } if (sourceHash === snapshotHash) { skipped.push(destPath); continue; } conflicted.push(destPath); } const overrides = []; if (withUi) overrides.push("ui"); if (withApi) overrides.push("api"); if (withHost) overrides.push("host"); if (withPlugins) overrides.push("plugins"); if (conflicted.length > 0 && !options.force && !options.dryRun) return { status: "conflicted", updated: [], skipped, added: [], conflicted, error: `Local edits conflict with the upstream template (run \`bos upgrade --force\` to overwrite — backups land in .bos/sync-backup/): ${conflicted.join(", ")}` }; if (options.dryRun) { const agentsMd = expectedChildAgentsMd(sourceDir, overrides, childPlugins); if (agentsMd && computeLocalHash(projectDir, "AGENTS.md") !== agentsMd.hash) updated.push("AGENTS.md"); return { status: "dry-run", updated, skipped, added, conflicted }; } const filesToWrite = [ ...updated, ...added, ...conflicted ]; let backupDir; if (filesToWrite.length > 0) { backupDir = backupFiles(projectDir, filesToWrite) ?? void 0; for (const destPath of filesToWrite) writeSyncedFile(sourceDir, projectDir, destToSource.get(destPath) ?? destPath, destPath, childScripts, workflowPin); } await require_shared_deps.syncResolvedSharedDeps({ configDir: projectDir, hostMode: "local" }); const syncedConfig = await require_services_resolution_session.openResolution({ cwd: projectDir }); if (syncedConfig?.runtime) await require_materializer.materializeViaLayer(projectDir, syncedConfig.runtime); const newSnapshotFiles = {}; for (const destPath of destToSource.keys()) { const hash = computeLocalHash(projectDir, destPath); if (hash) newSnapshotFiles[destPath] = hash; } const agentsMd = expectedChildAgentsMd(sourceDir, overrides, childPlugins); if (agentsMd) { if (computeLocalHash(projectDir, "AGENTS.md") !== agentsMd.hash) { (0, node_fs.writeFileSync)((0, node_path.join)(projectDir, "AGENTS.md"), agentsMd.content); updated.push("AGENTS.md"); } newSnapshotFiles["AGENTS.md"] = agentsMd.hash; } await require_snapshot.writeSnapshot(projectDir, { files: newSnapshotFiles }); if (!options.noInstall) { await require_init.runPnpmInstall(require_workspace.findWorkspaceRoot(projectDir)?.dir ?? projectDir); await require_init.runTypesGen(projectDir); } return { status: "synced", updated, skipped, added, conflicted, backupDir }; } finally { await cleanup(); } } //#endregion exports.AGENT_SYNC_GLOBS = require_ownership.AGENT_SYNC_GLOBS; exports.CHILD_OWNED_BLOCK_END = require_ownership.CHILD_OWNED_BLOCK_END; exports.CHILD_OWNED_BLOCK_START = require_ownership.CHILD_OWNED_BLOCK_START; exports.FRAMEWORK_OWNED_HEADER = require_ownership.FRAMEWORK_OWNED_HEADER; exports.FRAMEWORK_OWNED_SYNC_FILES = require_ownership.FRAMEWORK_OWNED_SYNC_FILES; exports.applyReleaseRefs = applyReleaseRefs; exports.extractChildOwnedBlock = require_ownership.extractChildOwnedBlock; exports.frameworkOwnedHeaderComment = require_ownership.frameworkOwnedHeaderComment; exports.isFrameworkOwnedSyncFile = require_ownership.isFrameworkOwnedSyncFile; exports.mergePackageJson = mergePackageJson; exports.normalizeChildManifest = normalizeChildManifest; exports.resolveWorkflowReleasePin = resolveWorkflowReleasePin; exports.spliceChildOwnedBlock = require_ownership.spliceChildOwnedBlock; exports.syncTemplate = syncTemplate; //# sourceMappingURL=sync.cjs.map