192 lines
7.0 KiB
JavaScript
192 lines
7.0 KiB
JavaScript
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 };
|