everything-dev
Version:
A consolidated product package for building Module Federation apps with oRPC APIs.
340 lines (338 loc) • 12.5 kB
JavaScript
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