diff --git a/.changeset/nitro-dashboard-route.md b/.changeset/nitro-dashboard-route.md new file mode 100644 index 0000000000..1d8ef64a01 --- /dev/null +++ b/.changeset/nitro-dashboard-route.md @@ -0,0 +1,5 @@ +--- +'@workflow/nitro': minor +--- + +Add `/_workflow` route in dev mode that opens the workflow observability dashboard. diff --git a/packages/nitro/package.json b/packages/nitro/package.json index ed9782b491..fa9190425c 100644 --- a/packages/nitro/package.json +++ b/packages/nitro/package.json @@ -32,6 +32,7 @@ "@workflow/rollup": "workspace:*", "@workflow/swc-plugin": "workspace:*", "@workflow/vite": "workspace:*", + "@workflow/web": "workspace:*", "exsolve": "1.0.8", "pathe": "2.0.3" }, diff --git a/packages/nitro/src/index.ts b/packages/nitro/src/index.ts index b010247451..3d2989bf46 100644 --- a/packages/nitro/src/index.ts +++ b/packages/nitro/src/index.ts @@ -1,5 +1,6 @@ +import { createRequire } from 'node:module'; import { mkdirSync, readFileSync, writeFileSync } from 'node:fs'; -import { fileURLToPath } from 'node:url'; +import { fileURLToPath, pathToFileURL } from 'node:url'; import { WORKFLOW_QUEUE_TRIGGER } from '@workflow/builders'; import { workflowTransformPlugin } from '@workflow/rollup'; import type { Nitro, NitroModule, RollupConfig } from 'nitro/types'; @@ -114,7 +115,7 @@ export default { // so we don't intercept the rest of the resolution chain. const isWorkflowPkg = /^@?workflow(\/|$)/.test(source) || - /[\\/]packages[\\/](workflow|core|serde|errors|utils|builders|rollup|ai|world|world-local|world-vercel|world-postgres|world-testing|cli|next|nitro|nuxt|vite|vitest|web|web-shared|astro|sveltekit|nest)[\\/]/.test( + /[\\/]packages[\\/](workflow|core|serde|errors|utils|builders|rollup|ai|world|world-local|world-vercel|world-postgres|world-testing|cli|next|nitro|nuxt|vite|vitest|astro|sveltekit|nest)[\\/]/.test( source ); if (!isWorkflowPkg) return null; @@ -209,6 +210,10 @@ export default { }); } + if (nitro.options.dev) { + addDashboardHandler(nitro); + } + addVirtualHandler( nitro, '/.well-known/workflow/v1/webhook/:token', @@ -287,6 +292,74 @@ export default { }, } satisfies NitroModule; +const DASHBOARD_VIRTUAL_ID = '#workflow/dashboard-handler'; + +function addDashboardHandler(nitro: Nitro) { + const route = '/_workflow'; + nitro.options.handlers.push({ route, handler: DASHBOARD_VIRTUAL_ID }); + + // Resolve `@workflow/web/server` relative to this module so consumers don't + // need a direct dependency on `@workflow/web`. The path is inlined into the + // virtual handler as a file:// URL so Node can `import()` it at runtime + // regardless of where the generated Nitro bundle ends up. + const require_ = createRequire(import.meta.url); + let webServerUrl: string; + try { + webServerUrl = pathToFileURL(require_.resolve('@workflow/web/server')).href; + } catch { + webServerUrl = '@workflow/web/server'; + } + + const handlerSource = /* js */ ` + const __workflowWebServerUrl = ${JSON.stringify(webServerUrl)}; + let serverPromise = null; + async function getDashboardUrl() { + if (!serverPromise) { + serverPromise = (async () => { + const { startServer } = await import(/* @vite-ignore */ /* webpackIgnore: true */ __workflowWebServerUrl); + const server = await startServer(0); + const address = server.address(); + const port = typeof address === 'object' && address ? address.port : 3456; + return 'http://localhost:' + port; + })().catch((error) => { + serverPromise = null; + throw error; + }); + } + return serverPromise; + } + `; + + if (!nitro.routing) { + nitro.options.virtual[DASHBOARD_VIRTUAL_ID] = /* js */ ` + import { fromWebHandler } from "h3"; + ${handlerSource} + export default fromWebHandler(async () => { + try { + const url = await getDashboardUrl(); + return Response.redirect(url, 302); + } catch (error) { + console.error('Failed to start workflow dashboard:', error); + return new Response('Failed to start workflow dashboard: ' + error.message, { status: 500 }); + } + }); + `; + } else { + nitro.options.virtual[DASHBOARD_VIRTUAL_ID] = /* js */ ` + ${handlerSource} + export default async () => { + try { + const url = await getDashboardUrl(); + return Response.redirect(url, 302); + } catch (error) { + console.error('Failed to start workflow dashboard:', error); + return new Response('Failed to start workflow dashboard: ' + error.message, { status: 500 }); + } + }; + `; + } +} + function addVirtualHandler(nitro: Nitro, route: string, buildPath: string) { nitro.options.handlers.push({ route, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index a32d75e3a8..1d2ca2743a 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -797,6 +797,9 @@ importers: '@workflow/vite': specifier: workspace:* version: link:../vite + '@workflow/web': + specifier: workspace:* + version: link:../web exsolve: specifier: 1.0.8 version: 1.0.8