From 74216c6fb39e1e372c3dc2b3952ea29cd0289950 Mon Sep 17 00:00:00 2001 From: iroiro147 Date: Wed, 8 Jul 2026 21:43:36 +0530 Subject: [PATCH] fix(eve): avoid dev snapshot watcher subscriptions Signed-off-by: iroiro147 --- .changeset/dev-runtime-nitro-watchers.md | 5 + .../create-application-nitro.scenario.test.ts | 270 ++++++----- .../nitro/host/create-application-nitro.ts | 428 +++++++++--------- 3 files changed, 360 insertions(+), 343 deletions(-) create mode 100644 .changeset/dev-runtime-nitro-watchers.md diff --git a/.changeset/dev-runtime-nitro-watchers.md b/.changeset/dev-runtime-nitro-watchers.md new file mode 100644 index 000000000..40100eef7 --- /dev/null +++ b/.changeset/dev-runtime-nitro-watchers.md @@ -0,0 +1,5 @@ +--- +"eve": patch +--- + +Prevent Nitro/Rolldown dev watchers from subscribing to immutable dev-runtime snapshots; Eve's authored-source watcher already owns rebuilds and explicitly reloads Nitro when runtime wiring changes. diff --git a/packages/eve/src/internal/nitro/host/create-application-nitro.scenario.test.ts b/packages/eve/src/internal/nitro/host/create-application-nitro.scenario.test.ts index 919138520..80e8993c9 100644 --- a/packages/eve/src/internal/nitro/host/create-application-nitro.scenario.test.ts +++ b/packages/eve/src/internal/nitro/host/create-application-nitro.scenario.test.ts @@ -23,18 +23,10 @@ import { resolveWorkflowModulePath, } from "#internal/application/package.js"; import { resolveNitroBuildDirectory } from "#internal/application/paths.js"; -import type { - PreparedApplicationHost, - PreparedDevelopmentApplicationHost, -} from "#internal/nitro/host/types.js"; -import { - createEveVercelOptions, - EVE_WORKFLOW_FLOW_ROUTE_PATH, -} from "#internal/nitro/host/vercel-build-output-config.js"; +import type { PreparedApplicationHost } from "#internal/nitro/host/types.js"; import { applyWorkflowTransform } from "#internal/workflow-bundle/workflow-builders.js"; -const configureDevelopmentNitroRoutes = vi.fn(async () => undefined); -const configureProductionNitroRoutes = vi.fn(async () => undefined); +const configureNitroRoutes = vi.fn(async () => undefined); const createNitroMock = vi.fn(); const registerScheduleTaskHandlers = vi.fn(); @@ -47,8 +39,7 @@ vi.mock("./schedule-task-routes.js", () => ({ })); vi.mock("./configure-nitro-routes.js", () => ({ - configureDevelopmentNitroRoutes, - configureProductionNitroRoutes, + configureNitroRoutes, })); vi.mock("#internal/workflow-bundle/workflow-builders.js", () => ({ @@ -91,7 +82,7 @@ function createNitroStub(input: { buildDir?: string; dev?: boolean } = {}): Nitr }; } -function createPreparedHost(): PreparedDevelopmentApplicationHost { +function createPreparedHost(): PreparedApplicationHost { const appRoot = "/tmp/weather-agent"; const paths = resolveCompilerArtifactPaths(appRoot); const metadata: CompileMetadata = { @@ -131,7 +122,7 @@ function createPreparedHost(): PreparedDevelopmentApplicationHost { diagnostics: [], manifest: { channels: [], - config: { name: "weather-agent" }, + config: {}, sandbox: null, subagents: [], }, @@ -149,34 +140,11 @@ function createPreparedHost(): PreparedDevelopmentApplicationHost { } as PreparedApplicationHost["compiledArtifacts"], scheduleRegistrations: [], schedules: [], - generation: { - fingerprint: "runtime-fingerprint", - runtimeAppRoot: `${appRoot}/.eve/dev-runtime/snapshots/test/source/app`, - snapshotRoot: `${appRoot}/.eve/dev-runtime/snapshots/test`, - snapshotSourceRoot: `${appRoot}/.eve/dev-runtime/snapshots/test/source`, - sourceRoot: appRoot, - }, - workflowBuildDir: `${appRoot}/.eve/dev-hosts/test/workflow`, - workspaceExtensions: [], - workspace: { - artifactsDir: `${appRoot}/.eve/dev-hosts/test/artifacts`, - compilerArtifactsDir: `${appRoot}/.eve/dev-hosts/test/compiler`, - nitroBuildDir: `${appRoot}/.eve/dev-hosts/test/nitro`, - nitroOutputDir: `${appRoot}/.eve/dev-hosts/test/output`, - rootDir: `${appRoot}/.eve/dev-hosts/test`, - workflowBuildDir: `${appRoot}/.eve/dev-hosts/test/workflow`, - }, + workflowBuildDir: `${appRoot}/.eve/nitro/workflow`, }; } -function createProductionOptions(preparedHost: PreparedApplicationHost) { - return { - buildDir: resolveNitroBuildDirectory(preparedHost.appRoot), - outputDir: join(preparedHost.appRoot, ".output"), - }; -} - -describe("application Nitro creation", () => { +describe("createApplicationNitro", () => { beforeEach(() => { vi.resetModules(); vi.clearAllMocks(); @@ -186,54 +154,30 @@ describe("application Nitro creation", () => { delete process.env.VERCEL; }); - it("installs local tracing and compiled artifacts before constructing the Workflow world", async () => { + it("installs compiled artifacts before constructing the Workflow world", async () => { const nitroStub = createNitroStub(); createNitroMock.mockResolvedValueOnce(nitroStub.nitro); - const { createDevelopmentApplicationNitro } = + const { createApplicationNitro } = await import("#internal/nitro/host/create-application-nitro.js"); const preparedHost = createPreparedHost(); - await createDevelopmentApplicationNitro(preparedHost); + await createApplicationNitro(preparedHost, true); const plugins = createNitroMock.mock.calls[0]?.[0].plugins as string[]; - const localTracing = plugins.findIndex((plugin) => - plugin.includes("local-tracing-runtime-plugin.ts"), - ); - expect(localTracing).toBeGreaterThanOrEqual(0); - expect(localTracing).toBeLessThan( - plugins.indexOf(preparedHost.compiledArtifacts.bootstrapPath), - ); expect(plugins.indexOf(preparedHost.compiledArtifacts.bootstrapPath)).toBeLessThan( plugins.indexOf(preparedHost.compiledArtifacts.workflowWorldPluginPath), ); }); - it("preserves authored instrumentation instead of installing local tracing", async () => { - const nitroStub = createNitroStub(); - createNitroMock.mockResolvedValueOnce(nitroStub.nitro); - const { createDevelopmentApplicationNitro } = - await import("#internal/nitro/host/create-application-nitro.js"); - const preparedHost = createPreparedHost(); - preparedHost.compiledArtifacts.instrumentationPluginPath = "/app/instrumentation.mjs"; - - await createDevelopmentApplicationNitro(preparedHost); - - const plugins = createNitroMock.mock.calls[0]?.[0].plugins as string[]; - expect(plugins).toContain("/app/instrumentation.mjs"); - expect(plugins).not.toEqual( - expect.arrayContaining([expect.stringContaining("local-tracing-runtime-plugin.ts")]), - ); - }); - it("preserves workflow bundle side effects and skips workflow transform for cached bundles", async () => { const nitroStub = createNitroStub(); createNitroMock.mockResolvedValueOnce(nitroStub.nitro); - const { createProductionApplicationNitro } = + const { createApplicationNitro } = await import("#internal/nitro/host/create-application-nitro.js"); const preparedHost = createPreparedHost(); - await createProductionApplicationNitro(preparedHost, createProductionOptions(preparedHost)); + await createApplicationNitro(preparedHost, false); const rollupBeforeHooks = nitroStub.hookHandlers.get("rollup:before") ?? []; const originalTransform = vi.fn((code: string, id: string) => `${code}:${id}:transformed`); @@ -300,10 +244,10 @@ describe("application Nitro creation", () => { }); createNitroMock.mockResolvedValueOnce(nitroStub.nitro); - const { createDevelopmentApplicationNitro } = + const { createApplicationNitro } = await import("#internal/nitro/host/create-application-nitro.js"); const preparedHost = createPreparedHost(); - await createDevelopmentApplicationNitro(preparedHost); + await createApplicationNitro(preparedHost, true); const rollupBeforeHooks = nitroStub.hookHandlers.get("rollup:before") ?? []; const existingExternal = vi.fn((id: string) => @@ -331,10 +275,10 @@ describe("application Nitro creation", () => { const nitroStub = createNitroStub(); createNitroMock.mockResolvedValueOnce(nitroStub.nitro); - const { createDevelopmentApplicationNitro } = + const { createApplicationNitro } = await import("#internal/nitro/host/create-application-nitro.js"); const preparedHost = createPreparedHost(); - await createDevelopmentApplicationNitro(preparedHost); + await createApplicationNitro(preparedHost, true); expect(createNitroMock).toHaveBeenCalledTimes(1); expect(createNitroMock.mock.calls[0]?.[0]).toMatchObject({ @@ -347,10 +291,10 @@ describe("application Nitro creation", () => { const nitroStub = createNitroStub(); createNitroMock.mockResolvedValueOnce(nitroStub.nitro); - const { createDevelopmentApplicationNitro } = + const { createApplicationNitro } = await import("#internal/nitro/host/create-application-nitro.js"); const preparedHost = createPreparedHost(); - await createDevelopmentApplicationNitro(preparedHost); + await createApplicationNitro(preparedHost, true); expect(createNitroMock).toHaveBeenCalledTimes(1); expect(createNitroMock.mock.calls[0]?.[0]).toMatchObject({ @@ -360,35 +304,62 @@ describe("application Nitro creation", () => { }); }); - it("sets the eve framework version and flow function rules on Vercel build output config", async () => { - vi.stubEnv("VERCEL", "1"); + it("keeps Rolldown's dev watcher off authored app sources", async () => { const nitroStub = createNitroStub(); createNitroMock.mockResolvedValueOnce(nitroStub.nitro); - const { createProductionApplicationNitro } = + const { createApplicationNitro } = await import("#internal/nitro/host/create-application-nitro.js"); const preparedHost = createPreparedHost(); - await createProductionApplicationNitro(preparedHost, createProductionOptions(preparedHost)); + await createApplicationNitro(preparedHost, true); + + expect(createNitroMock).toHaveBeenCalledTimes(1); + expect(createNitroMock.mock.calls[0]?.[0]).toMatchObject({ + rolldownConfig: { + watch: { + exclude: [preparedHost.appRoot, join(preparedHost.appRoot, "**")], + }, + }, + }); + }); + + it("disables Nitro config watching in dev mode", async () => { + const nitroStub = createNitroStub(); + createNitroMock.mockResolvedValueOnce(nitroStub.nitro); + + const { createApplicationNitro } = + await import("#internal/nitro/host/create-application-nitro.js"); + await createApplicationNitro(createPreparedHost(), true); + + expect(createNitroMock).toHaveBeenCalledTimes(1); + expect(createNitroMock.mock.calls[0]?.[1]).toEqual({ watch: false }); + }); + + it("sets the eve framework version on Vercel app-surface build output config", async () => { + vi.stubEnv("VERCEL", "1"); + const nitroStub = createNitroStub(); + createNitroMock.mockResolvedValueOnce(nitroStub.nitro); + + const { createApplicationNitro } = + await import("#internal/nitro/host/create-application-nitro.js"); + await createApplicationNitro(createPreparedHost(), false, { + surface: "app", + }); expect(createNitroMock).toHaveBeenCalledWith( expect.objectContaining({ preset: "vercel", - vercel: createEveVercelOptions({ agentName: "weather-agent", enabled: true }), + vercel: { + config: { + version: 3, + framework: { + version: resolveInstalledPackageInfo().version, + }, + }, + }, }), + undefined, ); - - const vercelOptions = createNitroMock.mock.calls[0]?.[0].vercel; - expect(vercelOptions?.config).toEqual({ - version: 3, - framework: { - slug: "eve", - version: resolveInstalledPackageInfo().version, - }, - }); - expect(vercelOptions?.functionRules[EVE_WORKFLOW_FLOW_ROUTE_PATH]).toMatchObject({ - maxDuration: "max", - experimentalTriggers: [expect.objectContaining({ type: "queue/v2beta" })], - }); }); it("enables websockets without overriding the Vercel entry format", async () => { @@ -396,7 +367,7 @@ describe("application Nitro creation", () => { const nitroStub = createNitroStub(); createNitroMock.mockResolvedValueOnce(nitroStub.nitro); - const { createProductionApplicationNitro } = + const { createApplicationNitro } = await import("#internal/nitro/host/create-application-nitro.js"); const preparedHost = createPreparedHost(); const websocketChannel: CompiledChannelEntry = { @@ -410,7 +381,9 @@ describe("application Nitro creation", () => { }; preparedHost.compileResult.manifest.channels = [websocketChannel]; - await createProductionApplicationNitro(preparedHost, createProductionOptions(preparedHost)); + await createApplicationNitro(preparedHost, false, { + surface: "app", + }); const nitroOptions = createNitroMock.mock.calls[0]?.[0]; expect(nitroOptions).toMatchObject({ @@ -419,9 +392,14 @@ describe("application Nitro creation", () => { }, preset: "vercel", }); - expect(nitroOptions?.vercel).toEqual( - createEveVercelOptions({ agentName: "weather-agent", enabled: true }), - ); + expect(nitroOptions?.vercel).toEqual({ + config: { + version: 3, + framework: { + version: resolveInstalledPackageInfo().version, + }, + }, + }); }); it("clears Nitro build cache output from a different eve version", async () => { @@ -447,9 +425,9 @@ describe("application Nitro creation", () => { writeFile(staleBuildOutputPath, "stale\n"), ]); - const { createProductionApplicationNitro } = + const { createApplicationNitro } = await import("#internal/nitro/host/create-application-nitro.js"); - await createProductionApplicationNitro(preparedHost, createProductionOptions(preparedHost)); + await createApplicationNitro(preparedHost, false); await expect(readFile(staleBuildOutputPath, "utf8")).rejects.toThrow(); await expect(readFile(join(nitroBuildDir, "eve-cache.json"), "utf8")).resolves.toBe( @@ -470,9 +448,8 @@ describe("application Nitro creation", () => { const nitroStub = createNitroStub(); createNitroMock.mockResolvedValueOnce(nitroStub.nitro); - const { createProductionApplicationNitro } = await import("./create-application-nitro.js"); - const preparedHost = createPreparedHost(); - await createProductionApplicationNitro(preparedHost, createProductionOptions(preparedHost)); + const { createApplicationNitro } = await import("./create-application-nitro.js"); + await createApplicationNitro(createPreparedHost(), false); const rollupBeforeHooks = nitroStub.hookHandlers.get("rollup:before") ?? []; const config = { @@ -519,11 +496,16 @@ describe("application Nitro creation", () => { ).toBeNull(); }); - it("merges framework and configured hosted dependencies", async () => { - const nitroStub = createNitroStub(); - createNitroMock.mockResolvedValueOnce(nitroStub.nitro); + it("merges default server external packages with configured hosted dependencies", async () => { + const allNitroStub = createNitroStub(); + const appNitroStub = createNitroStub(); + const flowNitroStub = createNitroStub(); + createNitroMock + .mockResolvedValueOnce(allNitroStub.nitro) + .mockResolvedValueOnce(appNitroStub.nitro) + .mockResolvedValueOnce(flowNitroStub.nitro); - const { createProductionApplicationNitro } = + const { createApplicationNitro } = await import("#internal/nitro/host/create-application-nitro.js"); const preparedHost = createPreparedHost(); preparedHost.compileResult.manifest.config = { @@ -533,23 +515,31 @@ describe("application Nitro creation", () => { }, } as typeof preparedHost.compileResult.manifest.config; - await createProductionApplicationNitro(preparedHost, createProductionOptions(preparedHost)); + await createApplicationNitro(preparedHost, false); + await createApplicationNitro(preparedHost, false, { + surface: "app", + }); + await createApplicationNitro(preparedHost, false, { + surface: "flow", + }); - const traceDeps = createNitroMock.mock.calls[0]?.[0].traceDeps; - expect(traceDeps).toEqual( - expect.arrayContaining(["@napi-rs/keyring", "sharp", "fixture-external"]), - ); - expect(traceDeps.filter((dependencyName: string) => dependencyName === "sharp")).toHaveLength( - 1, - ); - expect(traceDeps).not.toContain("eve"); + for (const call of createNitroMock.mock.calls.slice(0, 3)) { + const traceDeps = call[0].traceDeps; + expect(traceDeps).toEqual( + expect.arrayContaining(["@napi-rs/keyring", "@prisma/client", "sharp", "fixture-external"]), + ); + expect(traceDeps.filter((dependencyName: string) => dependencyName === "sharp")).toHaveLength( + 1, + ); + expect(traceDeps).not.toContain("eve"); + } }); it("traces configured hosted dependencies from subagent configs", async () => { const nitroStub = createNitroStub(); createNitroMock.mockResolvedValueOnce(nitroStub.nitro); - const { createProductionApplicationNitro } = + const { createApplicationNitro } = await import("#internal/nitro/host/create-application-nitro.js"); const preparedHost = createPreparedHost(); const subagent: CompiledSubagentNode = { @@ -578,7 +568,7 @@ describe("application Nitro creation", () => { }; preparedHost.compileResult.manifest.subagents = [subagent]; - await createProductionApplicationNitro(preparedHost, createProductionOptions(preparedHost)); + await createApplicationNitro(preparedHost, false); const traceDeps = createNitroMock.mock.calls[0]?.[0].traceDeps; expect(traceDeps).toEqual(expect.arrayContaining(["subagent-external", "sharp"])); @@ -587,17 +577,27 @@ describe("application Nitro creation", () => { ); }); - it("leaves Nitro to classify unconfigured hosted dependencies", async () => { + it("traces framework and server defaults even when no externals are configured", async () => { const nitroStub = createNitroStub(); createNitroMock.mockResolvedValueOnce(nitroStub.nitro); - const { createProductionApplicationNitro } = + const { createApplicationNitro } = await import("#internal/nitro/host/create-application-nitro.js"); const preparedHost = createPreparedHost(); - await createProductionApplicationNitro(preparedHost, createProductionOptions(preparedHost)); - - expect(createNitroMock.mock.calls[0]?.[0].traceDeps).toEqual(["@napi-rs/keyring"]); + await createApplicationNitro(preparedHost, false); + + expect(createNitroMock.mock.calls[0]?.[0].traceDeps).toEqual( + expect.arrayContaining([ + "@aws-sdk/client-kms", + "@aws-sdk/client-sso", + "@datadog/flagging-core", + "@napi-rs/keyring", + "@prisma/client", + "@smithy/util-stream", + "dd-trace", + ]), + ); }); it("includes the Workflow sandbox runtime plugin only when Workflow is enabled", async () => { @@ -606,15 +606,15 @@ describe("application Nitro creation", () => { createNitroMock.mockResolvedValueOnce(directNitroStub.nitro); createNitroMock.mockResolvedValueOnce(workflowNitroStub.nitro); - const { createProductionApplicationNitro } = + const { createApplicationNitro } = await import("#internal/nitro/host/create-application-nitro.js"); const directHost = createPreparedHost(); const workflowHost = createPreparedHost(); - workflowHost.compileResult.manifest.workflowTool = {}; + workflowHost.compileResult.manifest.workflowEnabled = true; - await createProductionApplicationNitro(directHost, createProductionOptions(directHost)); - await createProductionApplicationNitro(workflowHost, createProductionOptions(workflowHost)); + await createApplicationNitro(directHost, false); + await createApplicationNitro(workflowHost, false); const directPlugins = createNitroMock.mock.calls[0]?.[0].plugins as string[]; const workflowPlugins = createNitroMock.mock.calls[1]?.[0].plugins as string[]; @@ -633,12 +633,11 @@ describe("application Nitro creation", () => { createNitroMock.mockResolvedValueOnce(productionNitroStub.nitro); createNitroMock.mockResolvedValueOnce(devNitroStub.nitro); - const { createDevelopmentApplicationNitro, createProductionApplicationNitro } = + const { createApplicationNitro } = await import("#internal/nitro/host/create-application-nitro.js"); - const productionHost = createPreparedHost(); - await createProductionApplicationNitro(productionHost, createProductionOptions(productionHost)); - await createDevelopmentApplicationNitro(createPreparedHost()); + await createApplicationNitro(createPreparedHost(), false); + await createApplicationNitro(createPreparedHost(), true); const productionPlugins = createNitroMock.mock.calls[0]?.[0].plugins as string[]; const devPlugins = createNitroMock.mock.calls[1]?.[0].plugins as string[]; @@ -655,7 +654,7 @@ describe("application Nitro creation", () => { const nitroStub = createNitroStub(); createNitroMock.mockResolvedValueOnce(nitroStub.nitro); - const { createProductionApplicationNitro } = + const { createApplicationNitro } = await import("#internal/nitro/host/create-application-nitro.js"); const preparedHost = createPreparedHost(); preparedHost.compileResult.manifest.config = { @@ -665,7 +664,7 @@ describe("application Nitro creation", () => { }, } as typeof preparedHost.compileResult.manifest.config; - await createProductionApplicationNitro(preparedHost, createProductionOptions(preparedHost)); + await createApplicationNitro(preparedHost, false); const traceDeps = createNitroMock.mock.calls[0]?.[0].traceDeps; expect(traceDeps).toEqual( @@ -715,14 +714,11 @@ describe("application Nitro creation", () => { ]); try { - const { createProductionApplicationNitro } = + const { createApplicationNitro } = await import("#internal/nitro/host/create-application-nitro.js"); const preparedHost = createPreparedHost(); preparedHost.workflowBuildDir = workflowBuildDir; - await createProductionApplicationNitro(preparedHost, { - ...createProductionOptions(preparedHost), - buildDir: nitroBuildDir, - }); + await createApplicationNitro(preparedHost, false); const rollupBeforeHooks = nitroStub.hookHandlers.get("rollup:before") ?? []; const config = { diff --git a/packages/eve/src/internal/nitro/host/create-application-nitro.ts b/packages/eve/src/internal/nitro/host/create-application-nitro.ts index 89e78ca19..6f47b25d0 100644 --- a/packages/eve/src/internal/nitro/host/create-application-nitro.ts +++ b/packages/eve/src/internal/nitro/host/create-application-nitro.ts @@ -15,25 +15,25 @@ import { prepareEveVersionedCacheDirectory, writeEveVersionedCacheMetadata, } from "#internal/application/cache-metadata.js"; -import { createProductionNitroArtifactsConfig } from "#internal/nitro/host/artifacts-config.js"; +import { resolveNitroBuildDirectory } from "#internal/application/paths.js"; +import { + createNitroArtifactsConfig, + type NitroArtifactsConfigInput, +} from "#internal/nitro/host/artifacts-config.js"; import { createCompiledSandboxBackendPrunePlugin } from "#internal/nitro/host/compiled-sandbox-backend-prune-plugin.js"; import { createExtensionScopePlugin } from "#internal/bundler/extension-scope-plugin.js"; -import { - configureDevelopmentNitroRoutes, - configureProductionNitroRoutes, -} from "#internal/nitro/host/configure-nitro-routes.js"; +import { configureNitroRoutes } from "#internal/nitro/host/configure-nitro-routes.js"; import { applyEveCronHandlerRoute } from "#internal/nitro/host/cron-handler-route.js"; import { createNitroBundlerConfig } from "#internal/nitro/host/nitro-bundler-config.js"; +import { captureDevLiveVirtualModules } from "#internal/nitro/host/dev-live-virtual-modules.js"; import { createOptionalEngineDependencyPlugin, OPTIONAL_ENGINE_PACKAGES_BY_BACKEND_NAME, } from "#internal/nitro/host/optional-engine-dependency-plugin.js"; import { addNitroRoutingImportSpecifierPlugin } from "#internal/nitro/host/nitro-routing-import-specifier-plugin.js"; import { registerScheduleTaskHandlers } from "#internal/nitro/host/schedule-task-routes.js"; -import type { - PreparedApplicationHost, - PreparedDevelopmentApplicationHost, -} from "#internal/nitro/host/types.js"; +import { SERVER_EXTERNAL_PACKAGES } from "#internal/nitro/host/server-external-packages.js"; +import type { NitroBuildSurface, PreparedApplicationHost } from "#internal/nitro/host/types.js"; import { createEveVercelOptions } from "#internal/nitro/host/vercel-build-output-config.js"; import { applyWorkflowTransform } from "#internal/workflow-bundle/workflow-builders.js"; import { transformDynamicToolExecute } from "#internal/workflow-bundle/dynamic-tool-transform.js"; @@ -80,14 +80,30 @@ function resolveWorkflowAliases(): Record { return aliases; } -function resolveProductionNitroPreset(): "vercel" | undefined { - return process.env.VERCEL ? "vercel" : undefined; +function resolveNitroPreset(dev: boolean): "vercel" | undefined { + if (!dev && process.env.VERCEL) { + return "vercel"; + } + + return undefined; +} + +function includesApplicationSurface(surface: NitroBuildSurface): boolean { + return surface === "all" || surface === "app"; +} + +function includesWorkflowSurface(surface: NitroBuildSurface): boolean { + return surface === "all" || surface === "flow"; +} + +function includesWorkflowStepRegistrations(surface: NitroBuildSurface): boolean { + return includesWorkflowSurface(surface); } /** Whether any agent needs the dynamic Workflow sandbox runtime. */ function manifestEnablesWorkflow(manifest: CompiledAgentManifest): boolean { const nodes = [manifest, ...manifest.subagents.map((subagent) => subagent.agent)]; - return nodes.some((node) => node.workflowTool !== undefined); + return nodes.some((node) => node.workflowEnabled === true); } function manifestHasWebSocketChannel(manifest: CompiledAgentManifest): boolean { @@ -96,6 +112,15 @@ function manifestHasWebSocketChannel(manifest: CompiledAgentManifest): boolean { ); } +function resolveWorkflowStepEntrypointPath( + nitro: Nitro, + preparedHost: PreparedApplicationHost, +): string { + return nitro.options.dev + ? join(nitro.options.buildDir, "workflow", "steps.mjs") + : join(preparedHost.workflowBuildDir, "steps.mjs"); +} + function collectHostedTraceDependencies( preparedHost: PreparedApplicationHost, configuredOptionalEnginePackages: readonly string[], @@ -107,9 +132,6 @@ function collectHostedTraceDependencies( const configuredExternalDependencies = agentNodes.flatMap( (node) => node.config.build?.externalDependencies ?? [], ); - // Nitro already classifies known native and non-bundleable packages through - // its nf3 database. traceDeps is only for eve-owned or author-configured - // additions to that upstream policy. const merged = new Set([ ...FRAMEWORK_HOSTED_EXTERNAL_PACKAGES, // Optional engine packages (just-bash, microsandbox) join the @@ -119,6 +141,7 @@ function collectHostedTraceDependencies( // so a resolvable-but-unrequested install adds nothing to hosted // output. ...configuredOptionalEnginePackages, + ...SERVER_EXTERNAL_PACKAGES, ...configuredExternalDependencies, ]); return [...merged].filter((dependencyName) => dependencyName !== EVE_PACKAGE_NAME); @@ -156,7 +179,11 @@ export function shouldPruneLocalSandboxBackends(input: { ); } -function createDevelopmentWatchOptions(appRoot: string): { ignored: string[] } { +function createDevelopmentWatchOptions(appRoot: string): { ignored: string[] } | undefined { + if (appRoot.length === 0) { + return undefined; + } + return { // eve's authored-source watcher owns app code rebuilds. If Nitro/Rollup // also watches those files it can reload the worker while a workflow @@ -165,6 +192,16 @@ function createDevelopmentWatchOptions(appRoot: string): { ignored: string[] } { }; } +function createDevelopmentRolldownWatchOptions(appRoot: string): { exclude: string[] } | undefined { + if (appRoot.length === 0) { + return undefined; + } + + return { + exclude: [appRoot, join(appRoot, "**")], + }; +} + function normalizePath(path: string): string { return path.replaceAll("\\", "/"); } @@ -388,7 +425,7 @@ function addNitroStepModuleSideEffectsPlugin( input: { stepEntrypointPath: string; }, -): () => void { +): void { let cachedStepTransformTargets: Set | null = null; const getStepTransformTargets = async (): Promise> => { @@ -403,10 +440,15 @@ function addNitroStepModuleSideEffectsPlugin( return cachedStepTransformTargets; }; - const clearCachedStepTransformTargets = () => { + nitro.hooks.hook("build:before", () => { cachedStepTransformTargets = null; - }; - nitro.hooks.hook("build:before", clearCachedStepTransformTargets); + }); + + if (nitro.options.dev) { + nitro.hooks.hook("dev:reload", () => { + cachedStepTransformTargets = null; + }); + } nitro.hooks.hook("rollup:before", (_nitro, config) => { if (!Array.isArray(config.plugins)) { @@ -434,8 +476,6 @@ function addNitroStepModuleSideEffectsPlugin( }, }); }); - - return clearCachedStepTransformTargets; } /** @@ -448,7 +488,7 @@ function addNitroStepTransformPlugin( input: { stepEntrypointPath: string; }, -): () => void { +): void { let cachedStepTransformTargets: Set | null = null; const getStepTransformTargets = async (): Promise> => { @@ -463,10 +503,15 @@ function addNitroStepTransformPlugin( return cachedStepTransformTargets; }; - const clearCachedStepTransformTargets = () => { + nitro.hooks.hook("build:before", () => { cachedStepTransformTargets = null; - }; - nitro.hooks.hook("build:before", clearCachedStepTransformTargets); + }); + + if (nitro.options.dev) { + nitro.hooks.hook("dev:reload", () => { + cachedStepTransformTargets = null; + }); + } nitro.hooks.hook("rollup:before", (_nitro, config) => { if (!Array.isArray(config.plugins)) { @@ -499,8 +544,6 @@ function addNitroStepTransformPlugin( name: "eve:workflow-step-transform", }); }); - - return clearCachedStepTransformTargets; } /** @@ -611,10 +654,31 @@ function patchWorkflowTransformExcludePath(nitro: Nitro, workflowBuildDir: strin }); } -function createApplicationNitroBundlerConfiguration( +/** + * Creates one configured Nitro instance for either production build or dev + * hosting of an eve application. + * + * `surface` narrows the mounted routes for isolated production builds. + * `outputDir` lets callers stage those isolated builds into separate Nitro + * output roots before assembling the final hosted deployment. + */ +export async function createApplicationNitro( preparedHost: PreparedApplicationHost, - preset: "vercel" | undefined, -) { + dev: boolean, + options: { + outputDir?: string; + surface?: NitroBuildSurface; + } = {}, +): Promise { + const surface = options.surface ?? "all"; + // Dev mode never registers Nitro scheduled tasks. We do not want Nitro to + // watch authored schedules and fire them on their cron expressions during + // `eve dev`; the dev-only `POST /eve/v1/dev/schedules/:scheduleId` route + // is the only dev-time entry point. Production builds always register + // schedules. + const shouldRegisterScheduleTasks = + !dev && includesApplicationSurface(surface) && preparedHost.scheduleRegistrations.length > 0; + const preset = resolveNitroPreset(dev); const configuredBackendNames = collectConfiguredSandboxBackendNames( preparedHost.compileResult.manifest, ); @@ -645,25 +709,33 @@ function createApplicationNitroBundlerConfiguration( createOptionalEngineDependencyPlugin(unconfiguredOptionalEnginePackages), extensionScopePlugin, ].filter((plugin) => plugin !== null); - const nitroRolldownConfig = createNitroBundlerConfig(nitroBundlerPlugins); + const nitroRolldownConfig = { + ...createNitroBundlerConfig(nitroBundlerPlugins), + // Dev-runtime snapshots are immutable. Eve's authored-source watcher owns + // rebuilds and triggers Nitro reloads when runtime wiring changes, so + // Rolldown should not keep a native watcher rooted inside the snapshot. + watch: dev ? createDevelopmentRolldownWatchOptions(preparedHost.appRoot) : undefined, + }; const nitroRollupConfig = createNitroBundlerConfig(nitroBundlerPlugins); const tracedAppDependencies = collectHostedTraceDependencies( preparedHost, configuredOptionalEnginePackages, ); - - return { - nitroRolldownConfig, - nitroRollupConfig, - tracedAppDependencies, - }; -} - -function createApplicationNitroPlugins(preparedHost: PreparedApplicationHost): string[] { - const nitroPlugins = [ - preparedHost.compiledArtifacts.bootstrapPath, - preparedHost.compiledArtifacts.workflowWorldPluginPath, - ]; + const nitroBuildDir = resolveNitroBuildDirectory(preparedHost.appRoot, surface); + const websocketEnabled = + includesApplicationSurface(surface) && + (dev || manifestHasWebSocketChannel(preparedHost.compileResult.manifest)); + const nitroPlugins: string[] = []; + nitroPlugins.push(preparedHost.compiledArtifacts.bootstrapPath); + nitroPlugins.push(preparedHost.compiledArtifacts.workflowWorldPluginPath); + if (!dev) { + // Stops all tracked sandboxes when the production server shuts + // down. Dev servers are excluded: the dev CLI parent already stops + // dev-tagged sandboxes when the dev server closes. + nitroPlugins.push( + resolvePackageSourceFilePath("src/internal/nitro/host/sandbox-shutdown-plugin.ts"), + ); + } if (manifestEnablesWorkflow(preparedHost.compileResult.manifest)) { nitroPlugins.push( resolvePackageSourceFilePath("src/internal/nitro/host/workflow-sandbox-runtime-plugin.ts"), @@ -672,181 +744,121 @@ function createApplicationNitroPlugins(preparedHost: PreparedApplicationHost): s if (preparedHost.compiledArtifacts.instrumentationPluginPath !== undefined) { nitroPlugins.push(preparedHost.compiledArtifacts.instrumentationPluginPath); } - - return nitroPlugins; -} - -function configureSharedApplicationNitro( - nitro: Nitro, - preparedHost: PreparedApplicationHost, -): void { - addNitroRoutingImportSpecifierPlugin(nitro); - const workflowAliases = resolveWorkflowAliases(); - for (const [specifier, resolvedPath] of Object.entries(workflowAliases)) { - nitro.options.alias[specifier] = resolvedPath; - } - addWorkflowModuleSideEffectsPlugin(nitro, preparedHost.workflowBuildDir); - patchWorkflowTransformExcludePath(nitro, preparedHost.workflowBuildDir); - - addDynamicToolTransformPlugin(nitro); - - if (preparedHost.compiledArtifacts.instrumentationSourcePath !== undefined) { - addInstrumentationModuleSideEffectsPlugin( - nitro, - preparedHost.compiledArtifacts.instrumentationSourcePath, - ); - } -} - -function configureNitroStepPlugins(nitro: Nitro, stepEntrypointPath: string): Array<() => void> { - return [ - addNitroStepModuleSideEffectsPlugin(nitro, { stepEntrypointPath }), - addNitroStepTransformPlugin(nitro, { stepEntrypointPath }), - ]; -} - -function externalizeDevelopmentWorkflowBundle( - nitro: Nitro, - preparedHost: PreparedApplicationHost, -): void { - const externalWorkflowModules = new Set([ - normalizePath(join(preparedHost.workflowBuildDir, "workflows.mjs")), - ]); - - nitro.hooks.hook("rollup:before", (_nitro, config) => { - const existingExternal = config.external; - config.external = (id: string, ...rest: unknown[]) => { - if (externalWorkflowModules.has(normalizePath(id))) { - return true; - } - if (typeof existingExternal === "function") { - return (existingExternal as (id: string, ...rest: unknown[]) => boolean | null | undefined)( - id, - ...rest, - ); - } - return undefined; - }; - }); -} - -/** - * Creates one isolated Nitro host candidate for `eve dev`. - */ -export async function createDevelopmentApplicationNitro( - preparedHost: PreparedDevelopmentApplicationHost, -): Promise { - const nitroBuildDir = preparedHost.workspace.nitroBuildDir; - const bundler = createApplicationNitroBundlerConfiguration(preparedHost, undefined); - const plugins = createApplicationNitroPlugins(preparedHost); - if (preparedHost.compiledArtifacts.instrumentationPluginPath === undefined) { - plugins.unshift( - resolvePackageSourceFilePath("src/internal/nitro/host/local-tracing-runtime-plugin.ts"), - ); - } - await prepareEveVersionedCacheDirectory(nitroBuildDir); const nitro = await createNitro( { - _cli: { command: "dev" }, + _cli: { + command: dev ? "dev" : "build", + }, buildDir: nitroBuildDir, - dev: true, - features: { websocket: true }, - logLevel: 1, - output: { dir: preparedHost.workspace.nitroOutputDir }, - plugins, + dev, + features: { + websocket: websocketEnabled, + }, + logLevel: dev ? 1 : undefined, + output: + options.outputDir === undefined + ? undefined + : { + dir: options.outputDir, + }, + preset, + plugins: nitroPlugins, publicAssets: [], - scanDirs: [resolvePackageSourceDirectoryPath("src/execution")], - rolldownConfig: bundler.nitroRolldownConfig, - rollupConfig: bundler.nitroRollupConfig, + scanDirs: includesWorkflowStepRegistrations(surface) + ? [resolvePackageSourceDirectoryPath("src/execution")] + : undefined, + rolldownConfig: nitroRolldownConfig, + rollupConfig: nitroRollupConfig, rootDir: preparedHost.appRoot, serverDir: false, - traceDeps: bundler.tracedAppDependencies, - vercel: createEveVercelOptions({ - agentName: preparedHost.compileResult.manifest.config.name, - enabled: false, - }), - watchOptions: createDevelopmentWatchOptions(preparedHost.appRoot), + traceDeps: tracedAppDependencies, + vercel: createEveVercelOptions(preset === "vercel" && includesApplicationSurface(surface)), + watchOptions: dev ? createDevelopmentWatchOptions(preparedHost.appRoot) : undefined, }, - { watch: true }, + dev + ? { + // Eve's authored-source watcher owns dev rebuilds and explicitly + // triggers Nitro reloads, so Nitro's config watcher is redundant. + watch: false, + } + : undefined, ); await writeEveVersionedCacheMetadata(nitroBuildDir); - const stepEntrypointPath = join(nitro.options.buildDir, "workflow", "steps.mjs"); - configureSharedApplicationNitro(nitro, preparedHost); - const clearStepTransformCaches = configureNitroStepPlugins(nitro, stepEntrypointPath); - nitro.hooks.hook("dev:reload", () => { - for (const clearCache of clearStepTransformCaches) { - clearCache(); - } - }); - externalizeDevelopmentWorkflowBundle(nitro, preparedHost); - await configureDevelopmentNitroRoutes(nitro, preparedHost); - await addNitroStepNoExternals(nitro, stepEntrypointPath); - - return nitro; -} - -interface ProductionApplicationNitroOptions { - readonly buildDir: string; - readonly outputDir: string; - /** - * Agent's resolved public route prefix, baked into the Vercel flow - * function's environment for callback-URL minting behind a per-agent mount. - */ - readonly publicRoutePrefix?: string; -} + addNitroRoutingImportSpecifierPlugin(nitro); + if (dev) { + captureDevLiveVirtualModules(nitro); + } -/** - * Creates a build-mode Nitro host for one production build. Every route group - * (application, workflow, schedules) is registered in the same host; on Vercel - * the workflow flow route additionally becomes its own queue-triggered function - * through the preset's `functionRules`. `buildDir`/`outputDir` place all - * bundler state inside the invocation-owned build workspace. - */ -export async function createProductionApplicationNitro( - preparedHost: PreparedApplicationHost, - options: ProductionApplicationNitroOptions, -): Promise { - const preset = resolveProductionNitroPreset(); - const bundler = createApplicationNitroBundlerConfiguration(preparedHost, preset); - const nitroPlugins = createApplicationNitroPlugins(preparedHost); - nitroPlugins.push( - resolvePackageSourceFilePath("src/internal/nitro/host/sandbox-shutdown-plugin.ts"), - ); + // Resolve bare `workflow/*` specifiers during Nitro's Rollup bundling so + // pre-built workflow modules (whose imports target eve's installed copies) + // resolve correctly in production builds where Node resolution from the + // workflow cache directory is not available. + if (includesWorkflowSurface(surface)) { + const workflowAliases = resolveWorkflowAliases(); + for (const [specifier, resolvedPath] of Object.entries(workflowAliases)) { + nitro.options.alias[specifier] = resolvedPath; + } + addWorkflowModuleSideEffectsPlugin(nitro, preparedHost.workflowBuildDir); + patchWorkflowTransformExcludePath(nitro, preparedHost.workflowBuildDir); + } + if (includesWorkflowStepRegistrations(surface)) { + const stepEntrypointPath = resolveWorkflowStepEntrypointPath(nitro, preparedHost); + addNitroStepModuleSideEffectsPlugin(nitro, { + stepEntrypointPath, + }); + addNitroStepTransformPlugin(nitro, { + stepEntrypointPath, + }); + } + // Dynamic tool transform runs unconditionally — it needs to hoist + // execute functions for all tool files, not just workflow step targets. + addDynamicToolTransformPlugin(nitro); - await prepareEveVersionedCacheDirectory(options.buildDir); - const nitro = await createNitro({ - _cli: { command: "build" }, - buildDir: options.buildDir, - dev: false, - features: { - websocket: manifestHasWebSocketChannel(preparedHost.compileResult.manifest), - }, - output: { dir: options.outputDir }, - preset, - plugins: nitroPlugins, - publicAssets: [], - scanDirs: [resolvePackageSourceDirectoryPath("src/execution")], - rolldownConfig: bundler.nitroRolldownConfig, - rollupConfig: bundler.nitroRollupConfig, - rootDir: preparedHost.appRoot, - serverDir: false, - traceDeps: bundler.tracedAppDependencies, - vercel: createEveVercelOptions({ - agentName: preparedHost.compileResult.manifest.config.name, - enabled: preset === "vercel", - publicRoutePrefix: options.publicRoutePrefix, - }), - }); - await writeEveVersionedCacheMetadata(options.buildDir); + if (preparedHost.compiledArtifacts.instrumentationSourcePath !== undefined) { + addInstrumentationModuleSideEffectsPlugin( + nitro, + preparedHost.compiledArtifacts.instrumentationSourcePath, + ); + } - configureSharedApplicationNitro(nitro, preparedHost); - configureNitroStepPlugins(nitro, join(preparedHost.workflowBuildDir, "steps.mjs")); + // Prevent Nitro from re-bundling the pre-built workflow bundle in dev + // mode. `steps.mjs` is now a source entry that Nitro must still bundle + // so its imported TypeScript step files are transformed. + if (dev && includesWorkflowSurface(surface)) { + const workflowBuildDir = preparedHost.workflowBuildDir; + const externalWorkflowModules = new Set([ + normalizePath(join(workflowBuildDir, "workflows.mjs")), + ]); + + nitro.hooks.hook("rollup:before", (_nitro, config) => { + const existingExternal = config.external; + config.external = (id: string, ...rest: unknown[]) => { + if (externalWorkflowModules.has(normalizePath(id))) { + return true; + } + if (typeof existingExternal === "function") { + return ( + existingExternal as (id: string, ...rest: unknown[]) => boolean | null | undefined + )(id, ...rest); + } + return undefined; + }; + }); + } - if (preparedHost.scheduleRegistrations.length > 0) { + if (shouldRegisterScheduleTasks) { + // Replace Vercel's default `/_vercel/cron` path with an unguessable + // per-build route so users do not need to configure `CRON_SECRET` to + // protect the cron endpoint. No-op when the Vercel preset is not in + // use (e.g. dev mode), where the cron route is never registered. applyEveCronHandlerRoute(nitro); - const artifactsConfig = createProductionNitroArtifactsConfig(); + + const artifactsConfig: NitroArtifactsConfigInput = createNitroArtifactsConfig({ + appRoot: preparedHost.appRoot, + dev: nitro.options.dev, + }); registerScheduleTaskHandlers(nitro, { artifactsConfig, dispatchModulePath: resolvePackageSourceFilePath( @@ -855,8 +867,12 @@ export async function createProductionApplicationNitro( registrations: preparedHost.scheduleRegistrations, }); } + await configureNitroRoutes(nitro, preparedHost, { + surface, + }); + if (includesWorkflowStepRegistrations(surface)) { + await addNitroStepNoExternals(nitro, resolveWorkflowStepEntrypointPath(nitro, preparedHost)); + } - await configureProductionNitroRoutes(nitro, preparedHost); - await addNitroStepNoExternals(nitro, join(preparedHost.workflowBuildDir, "steps.mjs")); return nitro; }