Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/quiet-markers-build.md
Original file line number Diff line number Diff line change
@@ -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.
4 changes: 4 additions & 0 deletions docs/reference/cli.md
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,10 @@ eve build [--profile <path>] [--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 <path>` | string | off | Best-effort versioned JSON report with build-phase timings and final output-size measurements |
Expand Down
23 changes: 23 additions & 0 deletions packages/eve/src/internal/authored-module-loader.scenario.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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: {
Expand Down
4 changes: 4 additions & 0 deletions packages/eve/src/internal/authored-module-loader.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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?$/;
Expand Down Expand Up @@ -202,6 +203,7 @@ export async function bundleExtensionDistributionGraph(input: {
}): Promise<ReadonlyMap<string, string>> {
const plugins = [
createAuthoredDirectiveGuardPlugin(),
createPseudoPackagePlugin(),
createAuthoredRelativeExtensionResolverPlugin({ extensions: RESOLVE_EXTENSIONS }),
createAuthoredAssetImportPlugin(),
createAuthoredPackageTsConfigPathsPlugin({
Expand Down Expand Up @@ -280,6 +282,7 @@ export async function bundleAuthoredModuleMapForGeneration(input: {
}),
createAuthoredDirectiveGuardPlugin(),
extensionScopePlugin,
createPseudoPackagePlugin(),
createAuthoredRelativeExtensionResolverPlugin({ extensions: RESOLVE_EXTENSIONS }),
createAuthoredAssetImportPlugin(),
createAuthoredPackageTsConfigPathsPlugin({
Expand Down Expand Up @@ -390,6 +393,7 @@ async function buildAuthoredModuleBundle(
const plugins = [
channelIdentityPlugin,
...configuration.plugins,
createPseudoPackagePlugin(),
options.extensionScopeNamespace === undefined
? null
: createFixedNamespaceScopePlugin(options.extensionScopeNamespace),
Expand Down
31 changes: 31 additions & 0 deletions packages/eve/src/internal/bundler/pseudo-package-plugin.test.ts
Original file line number Diff line number Diff line change
@@ -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();
});
});
51 changes: 51 additions & 0 deletions packages/eve/src/internal/bundler/pseudo-package-plugin.ts
Original file line number Diff line number Diff line change
@@ -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<string>(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",
};
},
};
}
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -641,6 +642,7 @@ function createApplicationNitroBundlerConfiguration(
})),
);
const nitroBundlerPlugins = [
createPseudoPackagePlugin(),
compiledSandboxBackendPrunePlugin,
createOptionalEngineDependencyPlugin(unconfiguredOptionalEnginePackages),
extensionScopePlugin,
Expand Down
32 changes: 2 additions & 30 deletions packages/eve/src/internal/workflow-bundle/builder-support.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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}`),
Expand Down Expand Up @@ -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",
Expand Down Expand Up @@ -330,7 +302,7 @@ export async function bundleWorkflowStepRegistrations(input: {
platform: "node",
plugins: [
createWorkflowVirtualEntryPlugin(virtualEntrySource),
createWorkflowPseudoPackagePlugin(),
createPseudoPackagePlugin(),
createWorkflowRuntimeAliasPlugin(),
createEvePackageImportsPlugin(input.workingDir),
createWorkflowTransformPlugin({
Expand Down
4 changes: 2 additions & 2 deletions packages/eve/src/internal/workflow-bundle/builder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,6 @@ import {
createEvePackageImportsPlugin,
createWorkflowImport,
createWorkflowNodeBuiltinGuardPlugin,
createWorkflowPseudoPackagePlugin,
createWorkflowTransformPlugin,
createWorkflowVirtualEntryPlugin,
WORKFLOW_VIRTUAL_ENTRY_ID,
Expand All @@ -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,
Expand Down Expand Up @@ -272,7 +272,7 @@ export class WorkflowBundleBuilder {
platform: "neutral",
plugins: [
createWorkflowVirtualEntryPlugin(virtualEntrySource),
createWorkflowPseudoPackagePlugin(),
createPseudoPackagePlugin(),
createEvePackageImportsPlugin(this.config.workingDir, { workflowCondition: true }),
createWorkflowTransformPlugin({
manifest: workflowManifest,
Expand Down
33 changes: 33 additions & 0 deletions packages/eve/test/scenarios/bin-build-output.scenario.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -162,6 +162,39 @@ async function runEveBuild(appRoot: string): Promise<ProcessResult> {
}

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-",
Expand Down