validate

tcmts.cli.manifests.validate · #bsk2hms6 · term · runs anywhere

import type { Manifest } from "#codebase/tcmts/cli/manifests";
import { ROLES } from "#codebase/tcmts/cli/roles";
import type { Role } from "#codebase/tcmts/cli/roles";

export function validate(
  at: string,
  v: Record<string, unknown>,
  doc: string | undefined,
): Manifest {
  const fail = (what: string) => {
    throw new Error(`${at}: ${what}`);
  };
  if (typeof v.name !== "string" || !/^(@[a-z0-9][\w.-]*\/)?[a-z0-9][\w.-]*$/.test(v.name))
    fail("`name` must be the npm package's name (lowercase, like `greeter` or `@me/greeter`)");
  const strings = (x: unknown, what: string): string[] => {
    const xs = typeof x === "string" ? [x] : x;
    if (!Array.isArray(xs) || xs.some((s) => typeof s !== "string"))
      fail(`${what} must be a name or a list of names`);
    return xs as string[];
  };
  const exports: Record<string, string[]> = {};
  const given = v.exports ?? { ".": "" };
  if (typeof given === "string" || Array.isArray(given)) exports["."] = strings(given, "`exports`");
  else if (typeof given === "object" && given) {
    for (const [sub, t] of Object.entries(given)) {
      if (sub !== "." && !/^\.\/[\w./-]+$/.test(sub))
        fail(`\`exports\` subpaths are "." or "./something", not '${sub}'`);
      exports[sub] = strings(t, `\`exports["${sub}"]\``);
    }
  } else fail("`exports` must map subpaths to names");
  const bin: Record<string, string> = {};
  if (v.bin !== undefined) {
    if (typeof v.bin !== "object" || !v.bin) fail("`bin` must map commands to definitions");
    for (const [cmd, t] of Object.entries(v.bin as object)) {
      if (typeof t !== "string") fail(`\`bin.${cmd}\` must be a definition's name`);
      bin[cmd] = t as string;
    }
  }
  const suites: Record<string, string[]> = {};
  if (v.suites !== undefined) {
    if (typeof v.suites !== "object" || !v.suites || Array.isArray(v.suites))
      fail("`suites` must map commands to test files");
    for (const [cmd, files] of Object.entries(v.suites as object)) {
      if (!(cmd in bin)) fail(`\`suites.${cmd}\` is for a command \`bin\` doesn't have`);
      suites[cmd] = strings(files, `\`suites.${cmd}\``);
    }
  }
  const apps: Record<string, string> = {};
  if (v.apps !== undefined) {
    if (typeof v.apps !== "object" || !v.apps || Array.isArray(v.apps))
      fail("`apps` must map names to apps (definitions)");
    for (const [name, t] of Object.entries(v.apps as object)) {
      if (!/^[a-z][\w-]*$/i.test(name))
        fail(`\`apps\`: '${name}' isn't a name (letters, digits, - and _)`);
      if (typeof t !== "string") fail(`\`apps.${name}\` must be a definition's name`);
      apps[name] = t as string;
    }
  }
  const list = (k: "with" | "files" | "libraries") =>
    v[k] === undefined ? [] : strings(v[k], `\`${k}\``);
  const description =
    typeof v.description === "string"
      ? v.description
      : doc
          ?.split(/\n\s*\n/)[0]!
          .replace(/\s+/g, " ")
          .trim() || undefined;
  if (v.packageJson !== undefined && typeof v.packageJson !== "string")
    fail("`packageJson` must name a JSON file of the codebase");
  if (v.npm !== undefined && v.npm !== false)
    fail("`npm` is false for a tcm library (shared by tcm lib add, not npm), or left out");
  if (v.host !== undefined && !(Number.isInteger(v.host) && (v.host as number) > 0))
    fail("`host` is the version of tcm's host protocol its code speaks (1, say)");
  const roles: Record<string, Role> = {};
  if (v.roles !== undefined) {
    if (typeof v.roles !== "object" || !v.roles) fail("`roles` must map names to roles");
    for (const [name, role] of Object.entries(v.roles as object)) {
      if (typeof role !== "string" || !Object.hasOwn(ROLES, role))
        fail(`\`roles.${name}\` must be one of tcm's roles (${Object.keys(ROLES).join(", ")})`);
      roles[name] = role as Role;
    }
  }
  return {
    name: v.name as string,
    description,
    exports,
    bin,
    with: list("with"),
    files: list("files"),
    suites,
    apps,
    libraries: list("libraries"),
    ...(typeof v.packageJson === "string" ? { packageJson: v.packageJson } : {}),
    ...(v.npm === false ? { npm: false as const } : {}),
    ...(typeof v.host === "number" ? { host: v.host } : {}),
    ...(Object.keys(roles).length ? { roles } : {}),
  };
}

uses

used by