@mesh-tech/mesh-cli
Version:
CLI for Mesh platform development utilities
194 lines (193 loc) • 8.45 kB
JavaScript
import { spawn } from "node:child_process";
import { appendFileSync, existsSync, mkdirSync, mkdtempSync, readFileSync, writeFileSync, } from "node:fs";
import { tmpdir } from "node:os";
import path from "node:path";
import { logError, logInfo, logSuccess, logWarn } from "../utils/log.js";
import { assemblePortal, currentBaseline, currentCommit, diffPublishSets, publishSetAtRef, renderPublishSetSummary, renderVersionJson, renderZudokuConfig, } from "./assemble.js";
import { discoverDocRoots } from "./discover.js";
export const DOCS_APP_DIR = "apps/docs";
const CONTENT_DIR = "content";
const GENERATED_CONFIG = "zudoku.config.ts";
const PUBLISH_MANIFEST = "publish-manifest.json";
const VERSION_JSON_DIR = "public";
const VERSION_JSON = "version.json";
const SITE_TITLE = "Mesh Developer Portal";
const SITE_DESCRIPTION = "Build and run financial-services apps on Mesh Platform with the mesh developer CLI.";
export function runAssemble(args) {
const discovery = discoverDocRoots(args.repoRoot);
const assembled = assemblePortal({
repoRoot: args.repoRoot,
discovery,
outDir: args.outDir,
meshBaseline: currentBaseline(args.repoRoot),
commit: currentCommit(args.repoRoot),
});
const errors = [...discovery.errors];
if (args.writeFiles !== false) {
const manifestPath = args.outDir.includes(tmpdir())
? path.join(args.outDir, PUBLISH_MANIFEST)
: path.join(args.outDir, "..", PUBLISH_MANIFEST);
writeFileSync(manifestPath, `${JSON.stringify(assembled.publishManifest, null, 2)}\n`);
if (args.configOut) {
writeFileSync(args.configOut, renderZudokuConfig({
navigation: assembled.navigation,
title: SITE_TITLE,
description: SITE_DESCRIPTION,
baseline: currentBaseline(args.repoRoot),
commit: currentCommit(args.repoRoot),
}));
const versionDir = path.join(path.dirname(args.configOut), VERSION_JSON_DIR);
mkdirSync(versionDir, { recursive: true });
writeFileSync(path.join(versionDir, VERSION_JSON), renderVersionJson({
baseline: currentBaseline(args.repoRoot),
commit: currentCommit(args.repoRoot),
builtAt: new Date().toISOString(),
}));
}
}
return { assembled, errors };
}
export function reportAssembly(result) {
const { assembled } = result;
logSuccess(`Assembled ${assembled.pages.length} pages from ${new Set(assembled.pages.map((p) => p.sectionTitle)).size} sections → ${assembled.outDir}`);
if (assembled.externalLinks.length > 0) {
const unique = new Set(assembled.externalLinks.map((issue) => `${issue.page} → ${issue.href}`));
logInfo(`${unique.size} link${unique.size === 1 ? "" : "s"} point at repo files outside the portal and were rewritten to blob links`);
}
if (assembled.brokenLinks.length > 0) {
logWarn(`${assembled.brokenLinks.length} link(s) resolve to nothing:`);
for (const issue of assembled.brokenLinks) {
logWarn(` ${issue.page}: ${issue.href}`);
}
}
}
export async function checkPortal(args) {
const scratch = mkdtempSync(path.join(tmpdir(), "mesh-docs-check-"));
const result = runAssemble({ repoRoot: args.repoRoot, outDir: scratch });
const failures = [...result.errors];
for (const violation of result.assembled.reservedViolations) {
failures.push(`assembled output path contains a reserved segment: ${violation}`);
}
for (const issue of result.assembled.brokenLinks) {
failures.push(`broken link on ${issue.page}: ${issue.href}`);
}
const cliReferenceError = await args.checkCliReference();
if (cliReferenceError)
failures.push(cliReferenceError);
reportAssembly(result);
if (args.printManifest) {
process.stdout.write(`${JSON.stringify(result.assembled.publishManifest, null, 2)}\n`);
}
if (args.manifestOut) {
mkdirSync(path.dirname(args.manifestOut), { recursive: true });
writeFileSync(args.manifestOut, `${JSON.stringify(result.assembled.publishManifest, null, 2)}\n`);
}
if (args.diffBase) {
writePublishSetSummary(args.repoRoot, args.diffBase, result.assembled.publishManifest);
}
return failures;
}
function writePublishSetSummary(repoRoot, diffBase, manifest) {
let diff = null;
try {
const base = publishSetAtRef(repoRoot, diffBase);
diff = diffPublishSets(base, manifest.sources);
}
catch (error) {
logWarn(`could not compute the publish-set diff against ${diffBase}: ${error instanceof Error ? error.message : String(error)}`);
}
const summary = renderPublishSetSummary(manifest, diff);
process.stdout.write(`\n${summary}`);
const summaryPath = process.env.GITHUB_STEP_SUMMARY;
if (summaryPath) {
try {
appendFileSync(summaryPath, summary);
}
catch {
logWarn(`GITHUB_STEP_SUMMARY is set but not writable (${summaryPath})`);
}
}
}
function runZudoku(args) {
const cliArgs = args.mode === "dev"
? ["dev", "--port", String(args.port ?? 3000)]
: ["build"];
logInfo(`Running \`zudoku ${cliArgs.join(" ")}\` in ${args.appDir}`);
return new Promise((resolve, reject) => {
const child = spawn("pnpm", ["exec", "zudoku", ...cliArgs], {
cwd: args.appDir,
stdio: "inherit",
env: process.env,
});
child.on("error", reject);
child.on("close", (code) => resolve(code ?? 1));
});
}
export async function runPortalCommand(opts, deps) {
const { repoRoot } = deps;
if (opts.check) {
const failures = await checkPortal({
repoRoot,
diffBase: opts.diffBase,
printManifest: opts.printManifest ?? false,
manifestOut: opts.manifestOut,
checkCliReference: deps.checkCliReference,
});
if (failures.length > 0) {
for (const failure of failures)
logError(failure);
throw new Error(`docs portal check failed with ${failures.length} problem(s)`);
}
logSuccess("Docs portal check passed");
return;
}
const appDir = path.join(repoRoot, DOCS_APP_DIR);
const outDir = opts.out
? path.resolve(repoRoot, opts.out)
: path.join(appDir, CONTENT_DIR);
const result = runAssemble({
repoRoot,
outDir,
configOut: path.join(appDir, GENERATED_CONFIG),
});
reportAssembly(result);
if (result.errors.length > 0) {
for (const error of result.errors)
logError(error);
throw new Error(`docs portal assembly failed with ${result.errors.length} problem(s)`);
}
if (opts.assembleOnly || opts.out) {
logInfo(`Assembled content at ${path.relative(repoRoot, outDir)} — build with: pnpm --filter @mesh-tech/docs build`);
return;
}
const mode = opts.serve ? "dev" : "build";
let port;
if (opts.serve && opts.port) {
port = Number.parseInt(opts.port, 10);
if (!Number.isInteger(port) || port < 0 || port > 65535) {
throw new Error(`Invalid --port: ${opts.port}`);
}
}
const code = await runZudoku({
appDir,
mode,
...(port !== undefined ? { port } : {}),
});
if (code !== 0)
throw new Error(`zudoku ${mode} exited with code ${code}`);
if (mode === "build") {
logSuccess(`Portal built → ${path.join(path.relative(repoRoot, appDir), "dist")}`);
}
}
export async function cliReferenceStaleness(repoRoot) {
const [{ createProgram }, { extractCliReference, renderCliReferenceMarkdown },] = await Promise.all([
import("../program.js"),
import("./cli-reference.js"),
]);
const outPath = path.join(repoRoot, "docs/portal/generated/cli-reference.md");
const markdown = renderCliReferenceMarkdown(extractCliReference(createProgram()));
const existing = existsSync(outPath) ? readFileSync(outPath, "utf-8") : null;
if (existing === markdown)
return null;
return `${existing === null ? "CLI reference is missing" : "CLI reference is stale"}: docs/portal/generated/cli-reference.md — the commander tree changed; regenerate and commit with \`pnpm exec mesh docs cli-reference\``;
}