UNPKG

@lunora/cli

Version:

The Lunora CLI: init, dev, deploy, codegen, run, reset, and migrate commands

122 lines (119 loc) 5.44 kB
import { LunoraError } from '@lunora/errors'; const NEWLINE_PRESENT = /[\r\n]/u; const VALID_ENV_NAME = /^[A-Za-z_]\w*$/u; const VALID_ITEM_NAME = /^[A-Za-z0-9][\w-]*$/u; const VALID_MODULE_PATH = /^[\w.-]+(?:\/[\w.-]+)*$/u; const parseManifest = (raw, itemName) => { if (typeof raw !== "object" || raw === null) { throw new LunoraError("INTERNAL", `registry.json for "${itemName}" is not an object`); } const record = raw; const { name } = record; if (typeof name !== "string" || name.length === 0) { throw new LunoraError("INTERNAL", `registry.json for "${itemName}" is missing a string "name"`); } if (!VALID_ITEM_NAME.test(name)) { throw new LunoraError( "INTERNAL", `registry.json for "${itemName}": name "${name}" must match ${VALID_ITEM_NAME.source} (letters, digits, "-", "_"; no path separators, "..", or code)` ); } const filesRaw = record.files; if (!Array.isArray(filesRaw)) { throw new TypeError(`registry.json for "${itemName}" is missing a "files" array`); } const files = filesRaw.map((entry, index) => { if (typeof entry !== "object" || entry === null) { throw new LunoraError("INTERNAL", `registry.json "${itemName}": files[${String(index)}] is not an object`); } const fileRecord = entry; const { from } = fileRecord; const { to } = fileRecord; const { merge } = fileRecord; if (typeof from !== "string" || typeof to !== "string") { throw new TypeError(`registry.json "${itemName}": files[${String(index)}] needs string "from" and "to"`); } if (merge !== "create-or-skip" && merge !== "schema-extension") { throw new LunoraError("INTERNAL", `registry.json "${itemName}": files[${String(index)}].merge must be "create-or-skip" or "schema-extension"`); } for (const [field, value] of [ ["from", from], ["to", to] ]) { if (value.includes("..") || value.startsWith("/")) { throw new LunoraError( "INTERNAL", `registry.json "${itemName}": files[${String(index)}].${field} "${value}" must be a relative path without ".."` ); } } return { from, merge, to }; }); const asStringMap = (value) => typeof value === "object" && value !== null ? value : void 0; const deps = asStringMap(record.deps); const devDependencies = asStringMap(record.devDependencies); const requires = Array.isArray(record.requires) ? record.requires.filter((value) => typeof value === "string") : void 0; const bindings = Array.isArray(record.bindings) ? record.bindings.filter((value) => { if (typeof value !== "object" || value === null) { return false; } const bindingRecord = value; return Array.isArray(bindingRecord.path) && bindingRecord.path.every((segment) => typeof segment === "string"); }) : void 0; const entrypointReexports = Array.isArray(record.entrypointReexports) ? record.entrypointReexports.filter( (value) => typeof value === "object" && value !== null && typeof value.module === "string" ).map((entry) => { if (entry.module.includes("..") || entry.module.startsWith("/") || !VALID_MODULE_PATH.test(entry.module)) { throw new LunoraError( "INTERNAL", `registry.json "${itemName}": entrypointReexports[].module "${entry.module}" must be a safe relative module path without path traversal or unsafe characters` ); } const reexport = { module: entry.module }; if (typeof entry.comment === "string") { if (NEWLINE_PRESENT.test(entry.comment)) { throw new LunoraError("INTERNAL", `registry.json "${itemName}": entrypointReexports[].comment must not contain a newline`); } reexport.comment = entry.comment; } return reexport; }) : void 0; const envVariables = Array.isArray(record.envVars) ? record.envVars.filter( (value) => typeof value === "object" && value !== null && typeof value.name === "string" ).map((entry) => { const hasValue = typeof entry.value === "string"; if (!VALID_ENV_NAME.test(entry.name)) { throw new LunoraError( "INTERNAL", `registry.json "${itemName}": envVars["${entry.name}"].name must match ${VALID_ENV_NAME.source} (letters, digits, underscore; no "=" or newline)` ); } if (hasValue && NEWLINE_PRESENT.test(entry.value)) { throw new LunoraError("INTERNAL", `registry.json "${itemName}": envVars["${entry.name}"].value must not contain a newline`); } if (typeof entry.description === "string" && NEWLINE_PRESENT.test(entry.description)) { throw new LunoraError("INTERNAL", `registry.json "${itemName}": envVars["${entry.name}"].description must not contain a newline`); } return { ...typeof entry.description === "string" ? { description: entry.description } : {}, name: entry.name, // Default to secret unless a concrete value is provided. secret: typeof entry.secret === "boolean" ? entry.secret : !hasValue, ...hasValue ? { value: entry.value } : {} }; }) : void 0; return { bindings, deps, description: typeof record.description === "string" ? record.description : void 0, devDependencies, docs: typeof record.docs === "string" ? record.docs : void 0, entrypointReexports, envVars: envVariables, files, name, requires, title: typeof record.title === "string" ? record.title : void 0 }; }; export { parseManifest as default };