Something went wrong. Try again.
Monorepo for the nibling.run platform
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153import { adopt, define, sheet } from "@nibling/web/internals";import type { ProcessInfo } from "@nibling/kernel";import { commandLine, localUrl, processTree, projectScripts, type ProcessNode } from "../process-table.ts";import { runInTerminal } from "../run-in-terminal.ts";import type { PanelId } from "../shell/layout.ts";import type { Sandbox } from "../sandbox.ts";import type { Unsubscribe, Workbench } from "../shell/workbench.ts";import { openBrowser } from "./browser.ts";
const processesSheet = sheet(` :host { display: flex; flex-direction: column; overflow: auto; container: nib-processes / inline-size; font: var(--nib-font-ui); } [part="scripts"] { display: flex; flex-wrap: wrap; align-items: center; gap: var(--nib-space-xs); padding: var(--nib-space-xs) var(--nib-space-sm); border-bottom: var(--nib-size-border) solid var(--nib-color-border); } [part="scripts"] h2 { margin: 0; font: inherit; color: var(--nib-color-text-muted); } [part="status"] { margin: 0; padding: var(--nib-space-sm); color: var(--nib-color-text-muted); } ul { margin: 0; padding: 0; list-style: none; } ul ul { padding-inline-start: var(--nib-size-indent); } [part="row"] { display: flex; align-items: center; gap: var(--nib-space-sm); min-height: var(--nib-size-row); padding-inline: var(--nib-space-sm); } [part="row"]:hover { background: var(--nib-color-hover); } [part="command"] { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font: var(--nib-font-code); } [part="pid"] { color: var(--nib-color-text-muted); font-variant-numeric: tabular-nums; } button { border: var(--nib-size-border) solid var(--nib-color-border); border-radius: var(--nib-radius-sm); background: var(--nib-color-surface); color: var(--nib-color-text); font: inherit; padding: var(--nib-space-none) var(--nib-space-xs); } button:hover { background: var(--nib-color-hover); } button[part="port"] { font: var(--nib-font-code); color: var(--nib-color-accent); } button:focus-visible { outline: var(--nib-size-focus-ring) solid var(--nib-color-focus-ring); outline-offset: calc(-1 * var(--nib-size-focus-ring)); } code { font: var(--nib-font-code); } @container nib-processes (max-width: 240px) { [part="pid"] { display: none; } }`);
/** * The sandbox's process table, nested by parent, with each process's ports and a Stop button, and * the project's npm scripts to run. Scripts run in a terminal, so their output and Ctrl+C are there. */export class ProcessesElement extends HTMLElement { readonly #scripts = document.createElement("div"); readonly #status = document.createElement("p"); readonly #list = document.createElement("ul");
constructor() { super(); const root = this.attachShadow({ mode: "open" }); adopt(root, processesSheet); this.#scripts.part = "scripts"; this.#scripts.role = "group"; this.#scripts.ariaLabel = "Scripts"; this.#status.part = "status"; this.#status.role = "status"; this.#list.ariaLabel = "Processes"; root.append(this.#scripts, this.#status, this.#list); }
connect(sandbox: Sandbox, workbench: Workbench, self: PanelId): Unsubscribe { const drawScripts = () => { const names = projectScripts(sandbox.files); this.#scripts.hidden = names.length === 0; const heading = Object.assign(document.createElement("h2"), { textContent: "Scripts" }); this.#scripts.replaceChildren(heading, ...names.map((name) => { const button = Object.assign(document.createElement("button"), { textContent: `Run ${name}` }); button.addEventListener("click", () => runInTerminal(sandbox, workbench, `npm run ${name}`, self)); return button; })); }; drawScripts(); const unwatch = sandbox.files.watch((path) => { if (path === "package.json") drawScripts(); });
this.#status.textContent = "Starting sandbox…"; let stopped = false; let unsubscribe = () => {}; sandbox.kernel.then( (kernel) => { if (stopped) return; unsubscribe = kernel.onProcesses((table) => this.#draw(table, (pid) => kernel.kill(pid), (port) => openBrowser(workbench, localUrl(port)))); }, (error: unknown) => { this.#status.textContent = `The sandbox did not start: ${error instanceof Error ? error.message : String(error)}`; }, ); return () => { stopped = true; unwatch(); unsubscribe(); }; }
#draw(table: readonly ProcessInfo[], stop: (pid: number) => void, open: (port: number) => void): void { // Rows are rebuilt on every change, so focus goes back to the same control by its key. const focused = (this.shadowRoot!.activeElement as HTMLElement | null)?.dataset.key; this.#status.hidden = table.length > 0; this.#status.replaceChildren("No processes. Run a script, or ", Object.assign(document.createElement("code"), { textContent: "npm run dev" }), " in the terminal."); const item = ({ process, children }: ProcessNode): HTMLLIElement => { const command = commandLine(process.argv); const li = document.createElement("li"); const line = document.createElement("div"); line.part = "row"; const label = Object.assign(document.createElement("span"), { textContent: command, title: process.argv.join(" ") }); label.part = "command"; const pid = Object.assign(document.createElement("span"), { textContent: `pid ${process.pid}` }); pid.part = "pid"; const ports = process.ports.map((port) => { const button = Object.assign(document.createElement("button"), { textContent: `localhost:${port}`, ariaLabel: `Open localhost:${port}` }); button.part = "port"; button.dataset.key = `port:${port}`; button.addEventListener("click", () => open(port)); return button; }); const stopButton = Object.assign(document.createElement("button"), { textContent: "Stop", ariaLabel: `Stop ${command} (pid ${process.pid})` }); stopButton.dataset.key = `stop:${process.pid}`; stopButton.addEventListener("click", () => stop(process.pid)); line.append(label, pid, ...ports, stopButton); li.append(line); if (children.length) { const nested = Object.assign(document.createElement("ul"), { ariaLabel: `Started by ${command}` }); nested.append(...children.map(item)); li.append(nested); } return li; }; this.#list.replaceChildren(...processTree(table).map(item)); if (focused) this.shadowRoot!.querySelector<HTMLElement>(`[data-key="${focused}"]`)?.focus(); }}
declare global { interface HTMLElementTagNameMap { "nib-processes": ProcessesElement; }}
define("nib-processes", ProcessesElement);