everything-dev
Version:
A consolidated product package for building Module Federation apps with oRPC APIs.
515 lines (513 loc) • 22.4 kB
JavaScript
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