const crypto = require("crypto"); const fs = require("fs"); const path = require("path"); function isPlainObject(value) { return value !== null && typeof value === "object" && !Array.isArray(value); } function overlaySettingsValue(base, overlay) { if (!isPlainObject(base) || !isPlainObject(overlay)) return overlay; const result = { ...base }; for (const [key, value] of Object.entries(overlay)) { result[key] = key in result ? overlaySettingsValue(result[key], value) : value; } return result; } function applySettingOverlay(settings, projectRoot, overlayPath) { const resolvedPath = path.isAbsolute(overlayPath) ? overlayPath : path.resolve(projectRoot, overlayPath); const overlay = JSON.parse(fs.readFileSync(resolvedPath, "utf8")); if (!isPlainObject(overlay)) throw new Error(`Setting overlay must contain an object: ${overlayPath}`); for (const [name, value] of Object.entries(overlay)) { settings.set(name, settings.has(name) ? overlaySettingsValue(settings.get(name), value) : value); } } function loadSettings(projectRoot, settingOverlays = []) { const settingsRoot = path.join(projectRoot, "settings"); if (!fs.existsSync(settingsRoot)) throw new Error(`No settings folder found in ${projectRoot}`); const settings = new Map(); for (const fileName of fs.readdirSync(settingsRoot)) { if (path.extname(fileName).toLowerCase() !== ".json") continue; const name = path.basename(fileName, path.extname(fileName)); const filePath = path.join(settingsRoot, fileName); settings.set(name, JSON.parse(fs.readFileSync(filePath, "utf8"))); } for (const overlayPath of settingOverlays) applySettingOverlay(settings, projectRoot, overlayPath); return settings; } function hashPayload(value) { const json = JSON.stringify(value); return crypto.createHash("sha256").update(json === undefined ? "undefined" : json).digest("hex"); } function getSourceHash(sources) { const sourceHash = {}; for (const [name, source] of Object.entries(sources)) { if (!source || typeof source !== "object" || !("hash" in source)) { throw new Error(`Source "${name}" must be a node with a hash`); } sourceHash[name] = source.hash; } return sourceHash; } function buildNode(payload, sources = {}) { return { sourceHash: getSourceHash(sources), hash: hashPayload(payload), payload, }; } function buildBinaryNode(filePath, sources = {}) { const bytes = fs.readFileSync(filePath); return { sourceHash: getSourceHash(sources), hash: crypto.createHash("sha256").update(bytes).digest("hex"), payloadBin: filePath, }; } function initialize(projectRoot = process.cwd(), options = {}) { const settingOverlays = options.settingOverlays || []; if (!Array.isArray(settingOverlays)) throw new Error("options.settingOverlays must be an array"); const settings = loadSettings(projectRoot, settingOverlays); const registry = new Map(); let artifactOutcome; const utils = { loadModule(name) { const entry = registry.get(name); if (!entry) throw new Error(`Unknown module: ${name}`); if (entry.module) return entry.module; if (entry.initializing) throw new Error(`Circular module initialization involving: ${name}`); entry.initializing = true; try { const initialized = entry.init(utils); if (!initialized || (typeof initialized !== "object" && typeof initialized !== "function")) { throw new Error(`Module "${name}" initializer must return a module object or function`); } entry.module = initialized; return entry.module; } finally { entry.initializing = false; } }, getSettings(name) { if (!settings.has(name)) throw new Error(`Unknown settings: ${name}`); return settings.get(name); }, hashPayload, buildNode, buildBinaryNode, setArtifactOutcome(value) { artifactOutcome = `${value ?? ""}`.trim() || undefined; }, }; const modules = utils.getSettings("modules"); if (!modules || typeof modules !== "object" || Array.isArray(modules)) throw new Error("settings/modules.json must contain an object"); for (const [name, modulePath] of Object.entries(modules)) { const resolvedPath = path.resolve(projectRoot, modulePath); const init = require(resolvedPath); if (typeof init !== "function") throw new Error(`Module "${name}" must export an initializer function: ${modulePath}`); registry.set(name, { init, module: null, initializing: false }); } for (const name of registry.keys()) utils.loadModule(name); function buildArtifact(name) { const artifacts = utils.getSettings("artifacts"); const artifact = artifacts[name]; if (!artifact) throw new Error(`Unknown artifact: ${name}`); if (!artifact.module) throw new Error(`Artifact "${name}" is missing module`); if (!artifact.builder) throw new Error(`Artifact "${name}" is missing builder`); const module = utils.loadModule(artifact.module); const builder = module[artifact.builder]; if (typeof builder !== "function") { throw new Error(`Artifact "${name}" builder not found: ${artifact.module}.${artifact.builder}`); } const params = artifact.params === undefined ? [] : artifact.params; if (!Array.isArray(params)) throw new Error(`Artifact "${name}" params must be an array`); artifactOutcome = undefined; console.log(`Ensuring artifact: ${name}`); try { const result = builder(...params); if (result && typeof result.then === "function") { return result.then(value => { console.log(` ${artifactOutcome ?? "completed — success"}`); artifactOutcome = undefined; return value; }, error => { console.error(" failed"); artifactOutcome = undefined; throw error; }); } console.log(` ${artifactOutcome ?? "completed — success"}`); artifactOutcome = undefined; return result; } catch (error) { console.error(" failed"); artifactOutcome = undefined; throw error; } } async function buildAll() { const artifacts = utils.getSettings("artifacts"); for (const name of Object.keys(artifacts)) await buildArtifact(name); } return { utils, loadModule: utils.loadModule, getSettings: utils.getSettings, hashPayload, buildNode, buildBinaryNode, buildArtifact, buildAll, modules: [...registry.keys()], }; } module.exports = { initialize, hashPayload, buildNode, buildBinaryNode };