everything-dev
Version:
A consolidated product package for building Module Federation apps with oRPC APIs.
315 lines (313 loc) • 12.4 kB
JavaScript
const require_runtime = require('../_virtual/_rolldown/runtime.cjs');
const require_merge = require('../merge.cjs');
const require_config = require('../config.cjs');
const require_infra = require('./infra.cjs');
const require_snapshot = require('./snapshot.cjs');
const require_cli_init = require('./init.cjs');
let node_fs = require("node:fs");
let node_path = require("node:path");
let node_crypto = require("node:crypto");
let glob = require("glob");
//#region src/cli/sync.ts
const FRAMEWORK_OWNED_SYNC_FILES = new Set([
".env.example",
".gitignore",
"AGENTS.md",
"biome.json",
"bos.config.json",
"bunfig.toml",
"CONTRIBUTING.md",
"package.json",
".changeset/config.json",
".changeset/README.md",
".github/renovate.json",
".github/workflows/ci.yml",
".github/workflows/release-sync.yml",
".opencode/skills/everything-dev/SKILL.md",
"ui/package.json",
"ui/postcss.config.mjs",
"ui/rsbuild.config.ts",
"ui/tsconfig.json",
"ui/src/app.ts",
"ui/src/globals.d.ts",
"ui/src/hydrate.tsx",
"ui/src/lib/api.ts",
"ui/src/lib/auth.ts",
"ui/src/router.server.tsx",
"ui/src/router.tsx",
"ui/src/routes/__root.tsx",
"api/package.json",
"api/plugin.dev.ts",
"api/rspack.config.js",
"api/tsconfig.contract.json",
"api/tsconfig.json",
"api/src/lib/auth.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 (0, node_crypto.createHash)("sha256").update(content).digest("hex").substring(0, 16);
} 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 backupDir = (0, node_path.join)(projectDir, ".bos", "sync-backup", (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-"));
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) {
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") {
const mergedScripts = mergeStringMaps(local.scripts, 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;
}
function toDestPath(filePath) {
return require_cli_init.sourcePathToDestinationPath(filePath);
}
function toSourcePath(sourceDir, destPath) {
if ((0, node_fs.existsSync)((0, node_path.join)(sourceDir, destPath))) return 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;
}
return null;
}
function writeSyncedFile(sourceDir, projectDir, filePath) {
const src = (0, node_path.join)(sourceDir, filePath);
const destPath = 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 });
if (filePath.endsWith("bos.config.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");
if (localContent) {
const merged = require_merge.mergeBosConfigWithTemplate(JSON.parse(localContent), JSON.parse(templateContent));
(0, node_fs.writeFileSync)(dest, `${JSON.stringify(merged, null, 2)}\n`);
return;
}
}
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");
if (localContent) {
const merged = mergePackageJson(destPath, JSON.parse(localContent), JSON.parse(templateContent));
(0, node_fs.writeFileSync)(dest, `${JSON.stringify(merged, null, 2)}\n`);
return;
}
}
(0, node_fs.writeFileSync)(dest, (0, node_fs.readFileSync)(src));
}
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;
}
if ((0, node_fs.existsSync)((0, node_path.join)(projectDir, rawEntry.slice(6).trim())) || 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;
}
if ((0, node_fs.existsSync)((0, node_path.join)(projectDir, development.slice(6).trim())) || pluginDirs.has(pluginKey)) selected.push(pluginKey);
}
return selected;
}
async function syncTemplate(projectDir, options) {
const localConfig = JSON.parse((0, node_fs.readFileSync)((0, node_path.join)(projectDir, "bos.config.json"), "utf-8"));
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: [],
error: "No extends field found in bos.config.json — cannot determine parent"
};
const extendsMatch = extendsRef.match(/^bos:\/\/([^/]+)\/(.+)$/);
if (!extendsMatch) return {
status: "error",
updated: [],
skipped: [],
added: [],
error: `Invalid extends reference: ${extendsRef}`
};
const extendsAccount = extendsMatch[1];
const extendsGateway = extendsMatch[2];
const { sourceDir, cleanup } = await require_cli_init.resolveSourceDir({
extendsAccount,
extendsGateway
});
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 filteredFiles = /* @__PURE__ */ new Set();
const destToSource = /* @__PURE__ */ new Map();
for (const destPath of 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;
filteredFiles.add(sourcePath);
destToSource.set(destPath, sourcePath);
}
const updated = [];
const skipped = [];
const added = [];
for (const [destPath, filePath] of destToSource.entries()) {
const localHash = computeLocalHash(projectDir, destPath);
const sourceContent = (0, node_fs.readFileSync)((0, node_path.join)(sourceDir, filePath));
const sourceHash = (0, node_crypto.createHash)("sha256").update(sourceContent).digest("hex").substring(0, 16);
if (localHash === null) {
added.push(destPath);
continue;
}
if (localHash !== sourceHash) updated.push(destPath);
}
if (options.dryRun) return {
status: "dry-run",
updated,
skipped,
added
};
const filesToWrite = [...updated, ...added];
if (filesToWrite.length > 0) {
backupFiles(projectDir, filesToWrite);
for (const destPath of filesToWrite) writeSyncedFile(sourceDir, projectDir, destToSource.get(destPath) ?? destPath);
}
const newSnapshotFiles = {};
for (const filePath of filteredFiles) {
const content = (0, node_fs.readFileSync)((0, node_path.join)(sourceDir, filePath));
newSnapshotFiles[toDestPath(filePath)] = (0, node_crypto.createHash)("sha256").update(content).digest("hex").substring(0, 16);
}
await require_snapshot.writeSnapshot(projectDir, {
parentRef: `bos://${extendsAccount}/${extendsGateway}`,
files: newSnapshotFiles
});
const account = localConfig.account || extendsAccount;
const domain = localConfig.domain || extendsGateway;
const overrides = [];
if (withUi) overrides.push("ui");
if (withApi) overrides.push("api");
if (withHost) overrides.push("host");
if (childPlugins.length > 0) overrides.push("plugins");
await require_cli_init.personalizeConfig(projectDir, {
extendsAccount,
extendsGateway,
account,
domain,
overrides,
plugins: childPlugins,
workspaceOpts: { sourceDir },
mode: "sync",
existingConfig: localConfig
});
const syncedConfig = await require_config.loadConfig({ cwd: projectDir });
if (syncedConfig?.runtime) require_infra.writeGeneratedInfra(projectDir, syncedConfig.runtime);
if (!options.noInstall) {
await require_cli_init.runBunInstall(projectDir);
await require_cli_init.runTypesGen(projectDir);
}
return {
status: "synced",
updated,
skipped,
added
};
} finally {
await cleanup();
}
}
//#endregion
exports.syncTemplate = syncTemplate;
//# sourceMappingURL=sync.cjs.map