@lunora/cli
Version:
The Lunora CLI: init, dev, deploy, codegen, run, reset, and migrate commands
122 lines (119 loc) • 5.44 kB
JavaScript
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 };