UNPKG

everything-dev

Version:

A consolidated product package for building Module Federation apps with oRPC APIs.

340 lines (338 loc) • 12.5 kB
const require_runtime = require('../_virtual/_rolldown/runtime.cjs'); const require_theme = require('../utils/theme.cjs'); const require_dev_log_pipeline = require('../dev-log-pipeline.cjs'); const require_linkify = require('../utils/linkify.cjs'); let chalk = require("chalk"); chalk = require_runtime.__toESM(chalk, 1); //#region src/components/dev-render.ts const PLUGIN_PREFIX = "plugin:"; const VIEW_WIDTH = 52; const getDisplayName = (name) => name.startsWith(PLUGIN_PREFIX) ? name.slice(7).toUpperCase() : name.toUpperCase(); const isPlugin = (name) => name.startsWith(PLUGIN_PREFIX); const CORE_SERVICES = /* @__PURE__ */ new Set([ "host", "api", "ui" ]); const UI_PREFIX = "plugin-ui:"; const STATUS_RANK = { ready: 0, pending: 1, starting: 2, error: 3 }; const worstStatus = (a, b) => STATUS_RANK[a] >= STATUS_RANK[b] ? a : b; /** * One row per plugin: `plugin-ui:<id>` companions merge into their parent * (`plugin:<id>`, or the `auth` app slot for the auth mirror) as an inline * ui-port annotation. Orphan ui rows (no parent process) render as-is. * Folder-form plugin UIs never get a companion service — their UI build is a * child of the parent's dev process — so their `uiPort` synthesizes the same * annotation. */ const mergePluginUiRows = (processes) => { const byName = new Map(processes.map((p) => [p.name, p])); const consumed = /* @__PURE__ */ new Set(); const merged = []; for (const proc of processes) { if (proc.name.startsWith(UI_PREFIX)) continue; const row = { ...proc }; const pluginId = isPlugin(proc.name) ? proc.name.slice(7) : proc.name; const ui = byName.get(`${UI_PREFIX}${pluginId}`); if (ui) { row.ui = ui; row.status = worstStatus(proc.status, ui.status); consumed.add(ui.name); } else if (proc.uiPort && proc.uiPort > 0) row.ui = { ...proc, port: proc.uiPort }; merged.push(row); } for (const proc of processes) if (!consumed.has(proc.name) && proc.name.startsWith(UI_PREFIX)) merged.push({ ...proc }); return merged; }; const getServiceColor = (name) => { if (name === "host") return require_theme.colors.cyan; if (name === "ui") return require_theme.colors.magenta; if (CORE_SERVICES.has(name)) return require_theme.colors.blue; return require_theme.colors.orange; }; const getStatusText = (proc) => { switch (proc.status) { case "pending": return "waiting"; case "starting": return "starting"; case "ready": return proc.source === "remote" && proc.name !== "host" ? "loaded" : "running"; case "error": return "failed"; } }; const getStatusIcon = (status) => { switch (status) { case "pending": return require_theme.colors.gray(require_theme.icons.pending); case "starting": return require_theme.colors.cyan(require_theme.icons.scan); case "ready": return require_theme.colors.green(require_theme.icons.ok); case "error": return require_theme.colors.error(require_theme.icons.err); } }; const sectionProcesses = (processes) => { const plugins = processes.filter((p) => !CORE_SERVICES.has(p.name)); const services = processes.filter((p) => CORE_SERVICES.has(p.name)); const sections = []; if (plugins.length > 0) sections.push({ key: "plugins", title: "PLUGINS", processes: plugins }); if (services.length > 0) sections.push({ key: "services", title: "SERVICES", processes: services }); return sections; }; const getColumnWidths = (processes) => { return { name: Math.max(6, ...processes.map((p) => getDisplayName(p.name).length)), source: Math.max(10, ...processes.map((p) => p.source ? ` (${p.source})`.length : 0)) }; }; const renderProcessRow = (proc, nameWidth, sourceWidth) => { const color = getServiceColor(proc.name); const isRemote = proc.source === "remote"; const isHost = proc.name === "host"; const showPort = proc.port > 0 && (isHost || !isRemote); const portStr = showPort ? `:${proc.port}` : ""; const sourceLabel = proc.source ? ` (${proc.source})` : ""; const statusColor = proc.status === "ready" ? require_theme.colors.green : require_theme.colors.gray; const uiAnnotation = proc.ui && proc.ui.port > 0 ? require_theme.colors.gray(` · ui :${proc.ui.port}`) : ""; return ` ${getStatusIcon(proc.status)} ${chalk.default.bold(color(getDisplayName(proc.name).padEnd(nameWidth)))}${require_theme.colors.gray(sourceLabel.padEnd(sourceWidth))} ${statusColor(getStatusText(proc))}${showPort ? require_theme.colors.cyan(` ${portStr}`) : ""}${uiAnnotation}`; }; const renderLogLine = (entry) => { return `${getServiceColor(entry.source)(`[${entry.source}]`)} ${entry.isError ? require_theme.colors.error(require_linkify.linkify(entry.line)) : require_linkify.linkify(entry.line)}`; }; const truncateUrl = (url, maxLen) => { if (url.length <= maxLen) return url; try { const host = new URL(url).host; if (host.length > maxLen - 10) return `${host.slice(0, maxLen - 13)}…`; return `${host}…`; } catch { return `${url.slice(0, maxLen - 3)}…`; } }; const renderReadyBlock = (hostPort) => [` ${require_theme.colors.green(require_theme.icons.app)} APP READY`, ` ${chalk.default.bold(require_theme.colors.green(require_theme.icons.arrow))} http://localhost:${hostPort}`]; const renderProxyLine = (proxyTarget) => ` ${require_theme.colors.orange(require_theme.icons.arrow)} API PROXY → ${truncateUrl(proxyTarget, 38)}`; const renderBanner = (description) => [ require_theme.colors.cyan(require_theme.frames.top(VIEW_WIDTH)), ` ${require_theme.icons.run} ${require_theme.gradients.cyber(description.toUpperCase())}`, require_theme.colors.cyan(require_theme.frames.bottom(VIEW_WIDTH)) ]; const getHostPort = (processes) => processes.find((p) => p.name === "host")?.port || 3e3; function renderDevLines(state) { const rows = mergePluginUiRows(state.processes); const lines = renderBanner(state.description); const allReady = rows.length > 0 && rows.every((p) => p.status === "ready"); const hostPort = getHostPort(state.processes); if (allReady) { lines.push(""); lines.push(...renderReadyBlock(hostPort)); } if (state.proxyTarget) { lines.push(""); lines.push(renderProxyLine(state.proxyTarget)); } lines.push(""); lines.push(require_theme.colors.dim(require_theme.divider(VIEW_WIDTH))); const widths = getColumnWidths(rows); for (const section of sectionProcesses(rows)) { lines.push(` ${chalk.default.bold(require_theme.colors.cyan(section.title))}`); for (const proc of section.processes) lines.push(renderProcessRow(proc, widths.name, widths.source)); } lines.push(require_theme.colors.dim(require_theme.divider(VIEW_WIDTH))); lines.push(` ${allReady ? require_theme.colors.green(require_theme.icons.ok) : require_theme.colors.cyan(require_theme.icons.scan)} ${allReady ? `All ${rows.length} services running` : `${rows.filter((p) => p.status === "ready").length}/${rows.length} ready`}${require_theme.colors.gray(` ${require_theme.icons.dot} q quit ${require_theme.icons.dot} l logs`)}`); if (state.logs.length > 0) { lines.push(require_theme.colors.dim(require_theme.divider(VIEW_WIDTH))); for (const entry of state.logs.slice(-12)) lines.push(renderLogLine(entry)); } return lines; } function createDevRenderer(initialProcesses, description, env, onExit, onExportLogs, options) { const output = options?.output ?? process.stdout; const stdinSource = options?.stdin ?? process.stdin; const stdoutSource = output; const isInteractive = stdinSource.isTTY === true && stdoutSource.isTTY === true && options?.interactive !== false; const state = { description, proxyTarget: env.API_PROXY, processes: initialProcesses.map((p) => ({ ...p })), logs: [] }; let nextLogSeq = 0; let lastLogKey = null; const listeners = []; let mounted = true; const stopListeners = () => { mounted = false; listeners.length = 0; }; const setState = (mutate) => { if (!mounted) return; mutate(); for (const listener of listeners) listener(); }; const handle = { updateProcess: (name, status, message) => setState(() => { state.processes = state.processes.map((p) => p.name === name ? { ...p, status, message } : p); }), addLog: (source, line, isError = false) => setState(() => { const nextKey = `${source}:${isError ? "1" : "0"}:${line}`; if (nextKey === lastLogKey) return; lastLogKey = nextKey; state.logs = [...state.logs, { source, line, isError, seq: nextLogSeq++ }]; if (state.logs.length > 100) state.logs = state.logs.slice(-100); }), unmount: () => { stopListeners(); } }; if (!isInteractive) { let readyPrinted = false; let printedLogSeq = -1; const printedRows = /* @__PURE__ */ new Map(); const rowKey = (row) => `${row.status}:${row.ui?.port ?? ""}`; const printHeader = () => { const rows = mergePluginUiRows(state.processes); const widths = getColumnWidths(rows); const lines = ["", ...renderBanner(description)]; if (state.proxyTarget) lines.push(renderProxyLine(state.proxyTarget)); lines.push(""); for (const section of sectionProcesses(rows)) { lines.push(` ${chalk.default.bold(require_theme.colors.cyan(section.title))}`); for (const proc of section.processes) { lines.push(renderProcessRow(proc, widths.name, widths.source)); printedRows.set(proc.name, rowKey(proc)); } } lines.push(""); output.write(`${lines.join("\n")}\n`); }; const printChanges = () => { const rows = mergePluginUiRows(state.processes); const widths = getColumnWidths(rows); for (const row of rows) if (printedRows.get(row.name) !== rowKey(row)) { printedRows.set(row.name, rowKey(row)); output.write(`${renderProcessRow(row, widths.name, widths.source)}\n`); } if (rows.length > 0 && rows.every((p) => p.status === "ready") && !readyPrinted) { readyPrinted = true; output.write(`\n${renderReadyBlock(getHostPort(state.processes)).join("\n")}\n\n`); } for (const log of state.logs) { if (log.seq <= printedLogSeq) continue; output.write(`${renderLogLine(log)}\n`); printedLogSeq = log.seq; } }; printHeader(); listeners.push(printChanges); return handle; } output.write("\x1B[?1049h\x1B[?25l"); const ttyOutput = output; const viewportRows = () => Math.max(4, ttyOutput.rows ?? 24); const viewportCols = () => Math.max(20, ttyOutput.columns ?? 80); const clipLine = (line, cols) => { if (require_dev_log_pipeline.stripAnsi(line).length <= cols) return line; let consumed = 0; let cut = line.length; let i = 0; while (i < line.length) { if (line[i] === "\x1B") { const next = line[i + 1]; if (next === "[") { i += 2; while (i < line.length && !(line.charCodeAt(i) >= 64 && line.charCodeAt(i) <= 126)) i++; i++; continue; } if (next === "]") { const bel = line.indexOf("\x07", i + 2); i = bel === -1 ? line.length : bel + 1; continue; } i += 2; continue; } consumed++; if (consumed > cols - 1) { cut = i; break; } i++; } return `${line.slice(0, cut)}…`; }; const repaint = () => { const maxRows = viewportRows() - 1; const lines = renderDevLines(state).slice(0, maxRows); const cols = viewportCols(); const frame = lines.map((line) => `${clipLine(line, cols)}\x1b[K`).join("\n"); output.write(`\x1b[H${frame}\x1b[J`); }; repaint(); listeners.push(repaint); const onResize = () => repaint(); ttyOutput.on?.("resize", onResize); const rawCapable = stdinSource; let quitCount = 0; const onKey = (...args) => { const key = args[0].toString(); if (key === "l") { Promise.resolve(onExportLogs?.()); return; } if (key === "q" || key === "") { quitCount++; if (quitCount > 1) { Promise.resolve(options?.onForceExit?.()); return; } Promise.resolve(onExit?.()); } }; try { rawCapable.setRawMode?.(true); rawCapable.resume?.(); } catch {} rawCapable.on("data", onKey); let restored = false; handle.unmount = () => { stopListeners(); if (restored) return; restored = true; rawCapable.removeListener("data", onKey); ttyOutput.removeListener?.("resize", onResize); rawCapable.setRawMode?.(false); rawCapable.pause?.(); output.write("\x1B[?25h\x1B[?1049l"); }; return handle; } //#endregion exports.createDevRenderer = createDevRenderer; exports.getDisplayName = getDisplayName; exports.mergePluginUiRows = mergePluginUiRows; exports.renderDevLines = renderDevLines; exports.renderLogLine = renderLogLine; exports.renderProcessRow = renderProcessRow; exports.sectionProcesses = sectionProcesses; //# sourceMappingURL=dev-render.cjs.map