UNPKG

@mesh-tech/mesh-cli

Version:

CLI for Mesh platform development utilities

194 lines (193 loc) 8.45 kB
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\``; }