From 0928dcb2eb5ff86b19849d8330a50d754c566ee8 Mon Sep 17 00:00:00 2001 From: Lucas Gaillard Date: Tue, 28 Jul 2026 08:56:49 +0200 Subject: [PATCH] fix(eve): ignore framework marker packages Signed-off-by: Lucas Gaillard --- .changeset/quiet-markers-build.md | 5 ++ docs/reference/cli.md | 4 ++ .../authored-module-loader.scenario.test.ts | 23 +++++++++ .../src/internal/authored-module-loader.ts | 4 ++ .../bundler/pseudo-package-plugin.test.ts | 31 +++++++++++ .../internal/bundler/pseudo-package-plugin.ts | 51 +++++++++++++++++++ .../create-application-nitro.scenario.test.ts | 19 +++++++ .../nitro/host/create-application-nitro.ts | 2 + .../workflow-bundle/builder-support.ts | 32 +----------- .../src/internal/workflow-bundle/builder.ts | 4 +- .../bin-build-output.scenario.test.ts | 33 ++++++++++++ 11 files changed, 176 insertions(+), 32 deletions(-) create mode 100644 .changeset/quiet-markers-build.md create mode 100644 packages/eve/src/internal/bundler/pseudo-package-plugin.test.ts create mode 100644 packages/eve/src/internal/bundler/pseudo-package-plugin.ts diff --git a/.changeset/quiet-markers-build.md b/.changeset/quiet-markers-build.md new file mode 100644 index 000000000..8d7ec05f1 --- /dev/null +++ b/.changeset/quiet-markers-build.md @@ -0,0 +1,5 @@ +--- +"eve": patch +--- + +Treat `server-only`, `client-only`, and their Next.js compiled variants as no-op markers across authored, Workflow, and hosted application bundles. diff --git a/docs/reference/cli.md b/docs/reference/cli.md index e6a00e9a0..d7af1568d 100644 --- a/docs/reference/cli.md +++ b/docs/reference/cli.md @@ -115,6 +115,10 @@ eve build [--profile ] [--skip-sandbox-prewarm] Compiles and bundles in an invocation-owned directory under `.eve/builds/`, then publishes the completed host output and prints its path. Scratch workspaces are removed after success or failure. +Because eve's authored, Workflow, and host bundles all execute on the server, build-only +`server-only` and `client-only` marker imports (including Next.js's compiled variants) are +treated as no-ops. + | Flag | Type | Default | Description | | ------------------------ | ------ | ------- | --------------------------------------------------------------------------------------------- | | `--profile ` | string | off | Best-effort versioned JSON report with build-phase timings and final output-size measurements | diff --git a/packages/eve/src/internal/authored-module-loader.scenario.test.ts b/packages/eve/src/internal/authored-module-loader.scenario.test.ts index ad3993797..f4bf1c28a 100644 --- a/packages/eve/src/internal/authored-module-loader.scenario.test.ts +++ b/packages/eve/src/internal/authored-module-loader.scenario.test.ts @@ -15,6 +15,29 @@ import { useScenarioApp } from "#internal/testing/scenario-app.js"; describe("loadAuthoredModuleNamespace", () => { const scenarioApp = useScenarioApp(); + it("treats framework-only marker packages as no-ops", async () => { + const app = await scenarioApp({ + files: { + "agent/tools/uses-markers.ts": [ + 'import "server-only";', + 'import "client-only";', + 'import "next/dist/compiled/server-only";', + 'import "next/dist/compiled/client-only";', + "", + 'export const result = "loaded";', + "", + ].join("\n"), + }, + name: "framework-only-marker-packages", + }); + + const moduleNamespace = await loadAuthoredModuleNamespace( + join(app.appRoot, "agent", "tools", "uses-markers.ts"), + ); + + expect(moduleNamespace.result).toBe("loaded"); + }); + it("preserves cached channel identity for relative channel imports", async () => { const app = await scenarioApp({ files: { diff --git a/packages/eve/src/internal/authored-module-loader.ts b/packages/eve/src/internal/authored-module-loader.ts index 7bd85af55..b02b8d938 100644 --- a/packages/eve/src/internal/authored-module-loader.ts +++ b/packages/eve/src/internal/authored-module-loader.ts @@ -29,6 +29,7 @@ import { buildSingleRolldownChunk, buildWithNitroRolldown, } from "#internal/bundler/nitro-rolldown.js"; +import { createPseudoPackagePlugin } from "#internal/bundler/pseudo-package-plugin.js"; import { createNodeEsmCompatBannerPlugin } from "#internal/node-esm-compat-banner.js"; const AUTHORED_BUNDLED_MODULE_EXTENSION = /\.[cm]?[jt]sx?$/; @@ -202,6 +203,7 @@ export async function bundleExtensionDistributionGraph(input: { }): Promise> { const plugins = [ createAuthoredDirectiveGuardPlugin(), + createPseudoPackagePlugin(), createAuthoredRelativeExtensionResolverPlugin({ extensions: RESOLVE_EXTENSIONS }), createAuthoredAssetImportPlugin(), createAuthoredPackageTsConfigPathsPlugin({ @@ -280,6 +282,7 @@ export async function bundleAuthoredModuleMapForGeneration(input: { }), createAuthoredDirectiveGuardPlugin(), extensionScopePlugin, + createPseudoPackagePlugin(), createAuthoredRelativeExtensionResolverPlugin({ extensions: RESOLVE_EXTENSIONS }), createAuthoredAssetImportPlugin(), createAuthoredPackageTsConfigPathsPlugin({ @@ -390,6 +393,7 @@ async function buildAuthoredModuleBundle( const plugins = [ channelIdentityPlugin, ...configuration.plugins, + createPseudoPackagePlugin(), options.extensionScopeNamespace === undefined ? null : createFixedNamespaceScopePlugin(options.extensionScopeNamespace), diff --git a/packages/eve/src/internal/bundler/pseudo-package-plugin.test.ts b/packages/eve/src/internal/bundler/pseudo-package-plugin.test.ts new file mode 100644 index 000000000..7e703b4c0 --- /dev/null +++ b/packages/eve/src/internal/bundler/pseudo-package-plugin.test.ts @@ -0,0 +1,31 @@ +import { describe, expect, it } from "vitest"; + +import { + PSEUDO_PACKAGE_SPECIFIERS, + createPseudoPackagePlugin, +} from "#internal/bundler/pseudo-package-plugin.js"; + +describe("createPseudoPackagePlugin", () => { + it("resolves framework-only marker packages to empty virtual modules", () => { + const plugin = createPseudoPackagePlugin(); + + for (const specifier of PSEUDO_PACKAGE_SPECIFIERS) { + const resolved = plugin.resolveId(specifier); + + expect(resolved).toEqual({ + id: `\0eve-pseudo-package:${specifier}`, + }); + expect(plugin.load(resolved?.id ?? "")).toEqual({ + code: "", + moduleType: "js", + }); + } + }); + + it("leaves ordinary packages and unrelated virtual modules untouched", () => { + const plugin = createPseudoPackagePlugin(); + + expect(plugin.resolveId("zod")).toBeUndefined(); + expect(plugin.load("\0another-plugin:server-only")).toBeUndefined(); + }); +}); diff --git a/packages/eve/src/internal/bundler/pseudo-package-plugin.ts b/packages/eve/src/internal/bundler/pseudo-package-plugin.ts new file mode 100644 index 000000000..4173f5b48 --- /dev/null +++ b/packages/eve/src/internal/bundler/pseudo-package-plugin.ts @@ -0,0 +1,51 @@ +/** + * Framework marker packages that exist only to assert a module's intended + * environment. eve bundles authored and hosted application code exclusively + * for the server, so these markers have no runtime behavior in an eve bundle. + */ +export const PSEUDO_PACKAGE_SPECIFIERS = [ + "server-only", + "client-only", + "next/dist/compiled/server-only", + "next/dist/compiled/client-only", +] as const; + +const PSEUDO_PACKAGE_SPECIFIER_SET = new Set(PSEUDO_PACKAGE_SPECIFIERS); +const VIRTUAL_PREFIX = "\0eve-pseudo-package:"; + +/** The shared subset of the Rolldown/Rollup plugin shape used by eve bundlers. */ +export interface PseudoPackageBundlerPlugin { + readonly name: string; + resolveId(source: string): { id: string } | undefined; + load(id: string): { code: string; moduleType: "js" } | undefined; +} + +/** + * Resolves framework-only marker packages to empty virtual modules. + * + * This plugin belongs in every eve-owned server bundling path. Keeping the + * marker handling here prevents discovery, immutable generation, Workflow, + * and Nitro host bundles from drifting into different behavior. + */ +export function createPseudoPackagePlugin(): PseudoPackageBundlerPlugin { + return { + name: "eve-pseudo-packages", + resolveId(source: string) { + if (!PSEUDO_PACKAGE_SPECIFIER_SET.has(source)) { + return undefined; + } + + return { id: `${VIRTUAL_PREFIX}${source}` }; + }, + load(id: string) { + if (!id.startsWith(VIRTUAL_PREFIX)) { + return undefined; + } + + return { + code: "", + moduleType: "js", + }; + }, + }; +} 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 0aaaec38d..a412a4bb1 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 @@ -202,6 +202,25 @@ describe("application Nitro creation", () => { ); }); + it("installs the shared pseudo-package plugin in both hosted bundlers", async () => { + const nitroStub = createNitroStub(); + createNitroMock.mockResolvedValueOnce(nitroStub.nitro); + + const { createProductionApplicationNitro } = + await import("#internal/nitro/host/create-application-nitro.js"); + const preparedHost = createPreparedHost(); + await createProductionApplicationNitro(preparedHost, createProductionOptions(preparedHost)); + + const nitroOptions = createNitroMock.mock.calls[0]?.[0] as { + rolldownConfig: { plugins: Array<{ name?: string }> }; + rollupConfig: { plugins: Array<{ name?: string }> }; + }; + + for (const config of [nitroOptions.rolldownConfig, nitroOptions.rollupConfig]) { + expect(config.plugins.map((plugin) => plugin.name)).toContain("eve-pseudo-packages"); + } + }); + it("preserves workflow bundle side effects and skips workflow transform for cached bundles", async () => { const nitroStub = createNitroStub(); createNitroMock.mockResolvedValueOnce(nitroStub.nitro); 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 08cae87b3..631bd5523 100644 --- a/packages/eve/src/internal/nitro/host/create-application-nitro.ts +++ b/packages/eve/src/internal/nitro/host/create-application-nitro.ts @@ -18,6 +18,7 @@ import { import { createProductionNitroArtifactsConfig } 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 { createPseudoPackagePlugin } from "#internal/bundler/pseudo-package-plugin.js"; import { configureDevelopmentNitroRoutes, configureProductionNitroRoutes, @@ -641,6 +642,7 @@ function createApplicationNitroBundlerConfiguration( })), ); const nitroBundlerPlugins = [ + createPseudoPackagePlugin(), compiledSandboxBackendPrunePlugin, createOptionalEngineDependencyPlugin(unconfiguredOptionalEnginePackages), extensionScopePlugin, diff --git a/packages/eve/src/internal/workflow-bundle/builder-support.ts b/packages/eve/src/internal/workflow-bundle/builder-support.ts index ec1cefd2b..d805313ae 100644 --- a/packages/eve/src/internal/workflow-bundle/builder-support.ts +++ b/packages/eve/src/internal/workflow-bundle/builder-support.ts @@ -6,6 +6,7 @@ import { dirname, join, relative, resolve } from "node:path"; import { atomicWriteFile } from "#shared/atomic-write-file.js"; import { buildSingleRolldownChunk } from "#internal/bundler/nitro-rolldown.js"; +import { createPseudoPackagePlugin } from "#internal/bundler/pseudo-package-plugin.js"; import { resolveWorkflowModulePath } from "#internal/application/package.js"; import { applyWorkflowTransform, @@ -26,12 +27,6 @@ export interface WorkflowBundleBuilderOptions { /** Test-harness-only: also scans `src/internal/testing/`. */ includeTestFixtures?: boolean; } -const PSEUDO_PACKAGES = new Set([ - "server-only", - "client-only", - "next/dist/compiled/server-only", - "next/dist/compiled/client-only", -]); const NODE_BUILTIN_MODULES = new Set([ ...builtinModules, ...builtinModules.map((moduleName) => `node:${moduleName}`), @@ -183,29 +178,6 @@ export function createWorkflowVirtualEntryPlugin(source: string): WorkflowRolldo }; } -export function createWorkflowPseudoPackagePlugin(): WorkflowRolldownPlugin { - return { - name: "eve-workflow-pseudo-packages", - resolveId(source: string) { - if (!PSEUDO_PACKAGES.has(source)) { - return undefined; - } - - return { id: `\0eve-workflow-pseudo-package:${source}` }; - }, - load(id: string) { - if (!id.startsWith("\0eve-workflow-pseudo-package:")) { - return undefined; - } - - return { - code: "", - moduleType: "js", - }; - }, - }; -} - export function createWorkflowRuntimeAliasPlugin(): WorkflowRolldownPlugin { return { name: "eve-workflow-runtime-aliases", @@ -330,7 +302,7 @@ export async function bundleWorkflowStepRegistrations(input: { platform: "node", plugins: [ createWorkflowVirtualEntryPlugin(virtualEntrySource), - createWorkflowPseudoPackagePlugin(), + createPseudoPackagePlugin(), createWorkflowRuntimeAliasPlugin(), createEvePackageImportsPlugin(input.workingDir), createWorkflowTransformPlugin({ diff --git a/packages/eve/src/internal/workflow-bundle/builder.ts b/packages/eve/src/internal/workflow-bundle/builder.ts index 0547510ff..83f094fc8 100644 --- a/packages/eve/src/internal/workflow-bundle/builder.ts +++ b/packages/eve/src/internal/workflow-bundle/builder.ts @@ -22,7 +22,6 @@ import { createEvePackageImportsPlugin, createWorkflowImport, createWorkflowNodeBuiltinGuardPlugin, - createWorkflowPseudoPackagePlugin, createWorkflowTransformPlugin, createWorkflowVirtualEntryPlugin, WORKFLOW_VIRTUAL_ENTRY_ID, @@ -33,6 +32,7 @@ import { type WorkflowBundleDiscoveredEntries, } from "#internal/workflow-bundle/builder-support.js"; import { buildSingleRolldownChunk } from "#internal/bundler/nitro-rolldown.js"; +import { createPseudoPackagePlugin } from "#internal/bundler/pseudo-package-plugin.js"; import { writeNitroStepEntrypoint } from "#internal/workflow-bundle/nitro-step-entry.js"; import { WORKFLOW_BUILDER_DEFERRED_PACKAGES, @@ -272,7 +272,7 @@ export class WorkflowBundleBuilder { platform: "neutral", plugins: [ createWorkflowVirtualEntryPlugin(virtualEntrySource), - createWorkflowPseudoPackagePlugin(), + createPseudoPackagePlugin(), createEvePackageImportsPlugin(this.config.workingDir, { workflowCondition: true }), createWorkflowTransformPlugin({ manifest: workflowManifest, diff --git a/packages/eve/test/scenarios/bin-build-output.scenario.test.ts b/packages/eve/test/scenarios/bin-build-output.scenario.test.ts index e4741d0cd..03bf38a16 100644 --- a/packages/eve/test/scenarios/bin-build-output.scenario.test.ts +++ b/packages/eve/test/scenarios/bin-build-output.scenario.test.ts @@ -162,6 +162,39 @@ async function runEveBuild(appRoot: string): Promise { } describe("eve build process output", () => { + it("builds authored modules that import framework-only marker packages", async () => { + const appRoot = await createTemporaryAppRoot({ + prefix: "eve-bin-build-output-marker-packages-", + }); + + await mkdir(join(appRoot, "agent", "tools"), { + recursive: true, + }); + await writeFile( + join(appRoot, "agent", "tools", "uses-markers.ts"), + [ + 'import "server-only";', + 'import "client-only";', + 'import "next/dist/compiled/server-only";', + 'import "next/dist/compiled/client-only";', + "", + "export default {", + ' description: "Framework-only marker package test.",', + ' inputSchema: { type: "object", properties: {}, required: [] },', + ' execute: async () => "loaded",', + "};", + "", + ].join("\n"), + ); + + const result = await runEveBuild(appRoot); + + expect(result.code).toBe(0); + expect(result.signal).toBeNull(); + expect(removeRolldownPluginTimingWarningBlock(result.stderr)).toBe(""); + expect(result.stdout).toContain("[BUILD] built output at"); + }, 120_000); + it("prints successful build output to stdout", async () => { const appRoot = await createTemporaryAppRoot({ prefix: "eve-bin-build-output-success-",