// The project file, /.mosaic/project.json. It lives in the project's // repository and changes by reviewed commits there. The stack only reads it. import { isAbsolute, join, normalize } from "node:path"; import { refuse } from "./errors.mjs"; import { checkVars } from "./vars.mjs"; import { requireObject, rejectUnknownKeys, requireId, readJsonFile, deepFreeze } from "./util.mjs"; export function projectFilePath(root) { return join(root, ".mosaic", "project.json"); } export function validateProjectDocument(document, file) { requireObject(document, "project file"); rejectUnknownKeys(document, ["projectVersion", "id", "vars", "roles"], "project file"); if (document.projectVersion !== 1) refuse(`project file "projectVersion" must be 1 (${file})`); requireId(document.id, "project id"); const vars = checkVars(document.vars ?? {}, "project", `${file} vars`); const roles = {}; if (document.roles !== undefined) { requireObject(document.roles, `${file} roles`); for (const [instance, entry] of Object.entries(document.roles)) { requireId(instance, `${file} role instance`); requireObject(entry, `${file} roles.${instance}`); rejectUnknownKeys(entry, ["vars"], `${file} roles.${instance}`); roles[instance] = { vars: checkVars(entry.vars ?? {}, "project", `${file} roles.${instance}.vars`) }; } } return deepFreeze({ projectVersion: 1, id: document.id, file, vars, roles }); } export function loadProject(root) { if (typeof root !== "string" || !isAbsolute(root) || normalize(root) !== root) refuse(`project root must be a normalized absolute path (got ${JSON.stringify(root)})`); const file = projectFilePath(root); return validateProjectDocument(readJsonFile(file, "project file"), file); }