mirror of
https://github.com/vercel/workflow.git
synced 2026-09-14 19:59:43 +08:00
25715d4521
* feat(nitro): embed observability dashboard in-process at /_workflow Serve the @workflow/web observability UI inside the Nitro process at a configurable route (default /_workflow) instead of spawning a separate web server and 302-redirecting to it. Enabled in dev, omitted from production builds by default (so prod bundles carry no @workflow/web import). Never mounted on Vercel deploys (use the hosted dashboard). - @workflow/web: add a framework-neutral `@workflow/web/handler` (createWorkflowWebHandler) that serves SSR + static client assets + RPC as one Web Request->Response handler under a runtime basename (asset manifest URLs + publicPath are reprefixed so the dashboard is self-contained under its mount). Add `@workflow/web/registry` for embedded-dashboard discovery; make the RPC/stream client basename-aware. - @workflow/nitro: mount the handler in-process (Nitro v2 h3 + v3 native paths), gated by a new `dashboard` option (default = dev). - @workflow/cli: `workflow web` / `inspect --web` defer to a running embedded dashboard instead of starting a redundant server; pass `--standalone` to force the standalone UI. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * refactor(nitro): normalize dashboard path once, use isNitroV2() helper Address review feedback on the embedded dashboard: - Normalize the dashboard mount path in one place before it feeds both the Nitro route registration (`[path, path + '/**']`) and the handler `basename`. Force a single leading slash, strip trailing slashes, and reject the root mount, so a custom `path` can't make the route and the handler's internal `normalizeBasename` disagree. - Replace the handler-level `!nitro.routing` v2 checks with the existing `isNitroV2()` helper for consistent v2/v3 detection. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com> Co-authored-by: Peter Wielander <mittgfu@gmail.com>
90 lines
3.0 KiB
JavaScript
90 lines
3.0 KiB
JavaScript
/**
|
|
* Best-effort registry of live, embedded observability dashboards.
|
|
*
|
|
* When the dashboard is embedded in another server (e.g. `@workflow/nitro` at
|
|
* `/_workflow`), the running process records its public URL here. The CLI
|
|
* (`workflow web` / `inspect … --web`) reads this so it can point the user at
|
|
* an already-running dashboard instead of spawning a redundant standalone
|
|
* server on a fixed port.
|
|
*
|
|
* This is a coordination hint, never a source of truth: entries may be stale
|
|
* (a SIGKILL'd dev server can't clean up), so consumers MUST health-check a URL
|
|
* before trusting it. A missing or corrupt registry is treated as "none".
|
|
*
|
|
* The file is keyed by the current working directory so multiple projects don't
|
|
* collide, while multiple dev servers for the same project share one file
|
|
* (keyed by pid within it).
|
|
*/
|
|
|
|
import { createHash } from 'node:crypto';
|
|
import { mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
import os from 'node:os';
|
|
import path from 'node:path';
|
|
|
|
/** Absolute path of the registry file for the current working directory. */
|
|
export function dashboardRegistryPath() {
|
|
const key = createHash('sha256')
|
|
.update(process.cwd())
|
|
.digest('hex')
|
|
.slice(0, 16);
|
|
return path.join(os.tmpdir(), 'workflow-observability', `${key}.json`);
|
|
}
|
|
|
|
/** Read the registry entries (always returns an array; never throws). */
|
|
export function readDashboardRegistry() {
|
|
try {
|
|
const parsed = JSON.parse(readFileSync(dashboardRegistryPath(), 'utf8'));
|
|
return Array.isArray(parsed) ? parsed : [];
|
|
} catch {
|
|
return [];
|
|
}
|
|
}
|
|
|
|
function writeEntries(entries) {
|
|
const file = dashboardRegistryPath();
|
|
mkdirSync(path.dirname(file), { recursive: true });
|
|
writeFileSync(file, JSON.stringify(entries));
|
|
}
|
|
|
|
let recorded = false;
|
|
|
|
/**
|
|
* Record this process's embedded dashboard in the registry (idempotent per
|
|
* process). Call once the public origin is known (e.g. from the first request).
|
|
* Entirely best-effort — never throws.
|
|
*
|
|
* @param {{ url: string, basename?: string, world?: string }} entry
|
|
*/
|
|
export function recordDashboard(entry) {
|
|
if (recorded) return;
|
|
recorded = true;
|
|
try {
|
|
const others = readDashboardRegistry().filter(
|
|
(e) => e && e.pid !== process.pid
|
|
);
|
|
others.push({
|
|
url: entry.url,
|
|
basename: entry.basename ?? '',
|
|
world: entry.world ?? '',
|
|
pid: process.pid,
|
|
startedAt: new Date().toISOString(),
|
|
});
|
|
writeEntries(others);
|
|
// Best-effort cleanup on normal exit. Signal-terminated processes (Ctrl+C,
|
|
// SIGKILL) won't run this — consumers prune via health checks instead. We
|
|
// intentionally don't hook SIGINT/SIGTERM so we don't interfere with the
|
|
// host framework's own shutdown handling.
|
|
process.once('exit', () => {
|
|
try {
|
|
writeEntries(
|
|
readDashboardRegistry().filter((e) => e && e.pid !== process.pid)
|
|
);
|
|
} catch {
|
|
// ignore
|
|
}
|
|
});
|
|
} catch {
|
|
// best-effort: a failed write must never break request handling
|
|
}
|
|
}
|