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
Original file line number Diff line number Diff line change
Expand Up @@ -408,6 +408,179 @@ describe("development runtime artifact snapshots", () => {
expect(acceptableMountLocations.some((location) => existsSync(location))).toBe(true);
});

// https://github.com/vercel/eve/issues/1242 — withEve bare-agent-dir
// expression: the agent root has no package.json or tsconfig.json of its
// own; the parent Next.js-style package owns both, and its tsconfig path
// aliases must keep resolving from dev runtime snapshots exactly like in
// production builds.
it("resolves parent package tsconfig path aliases from dev runtime snapshots", async () => {
const workspaceRoot = await createScratchDirectory("eve-dev-runtime-parent-tsconfig-paths-");
const hostRoot = join(workspaceRoot, "apps", "nextjs");
const appRoot = join(hostRoot, "agents", "app-agent");
const compileDirectoryPath = join(appRoot, ".eve", "compile");

await mkdir(join(appRoot, "tools"), { recursive: true });
await mkdir(join(hostRoot, "lib"), { recursive: true });
await mkdir(compileDirectoryPath, { recursive: true });
await writeFile(join(workspaceRoot, "pnpm-workspace.yaml"), "packages:\n - apps/*\n");
await writeFile(join(workspaceRoot, "package.json"), '{"type":"module"}\n');
await writeFile(join(hostRoot, "package.json"), '{"name":"nextjs-host","type":"module"}\n');
await writeFile(
join(hostRoot, "tsconfig.json"),
`${JSON.stringify(
{
compilerOptions: {
baseUrl: ".",
paths: {
"@/*": ["./*"],
},
},
},
null,
2,
)}\n`,
);
await writeFile(
join(hostRoot, "lib", "utils.ts"),
'export const findFirstOrUndefined = "parent-lib-utils";\n',
);
await writeFile(
join(appRoot, "tools", "query-data.ts"),
[
'import { findFirstOrUndefined } from "@/lib/utils";',
"",
"export const resolved = findFirstOrUndefined;",
"",
].join("\n"),
);
await writeFile(
join(compileDirectoryPath, "compiled-agent-manifest.json"),
`${JSON.stringify({ agentRoot: appRoot, appRoot }, null, 2)}\n`,
);

const snapshot = await stageDevelopmentRuntimeArtifactsSnapshot({
paths: { compileDirectoryPath },
project: { appRoot },
} as CompileAgentResult);
const snapshotHostRoot = join(snapshot.snapshotSourceRoot, "apps", "nextjs");

// The owning package's tsconfig and alias-target sources are part of the
// snapshot.
expect(existsSync(join(snapshotHostRoot, "tsconfig.json"))).toBe(true);
expect(existsSync(join(snapshotHostRoot, "lib", "utils.ts"))).toBe(true);
// No synthetic package.json shadows the owning package boundary that
// production bundling resolves the tsconfig through.
expect(existsSync(join(snapshotHostRoot, "package.json"))).toBe(true);
expect(existsSync(join(snapshot.runtimeAppRoot, "package.json"))).toBe(false);

const moduleNamespace = await loadAuthoredModuleNamespace(
join(snapshot.runtimeAppRoot, "tools", "query-data.ts"),
);

expect(moduleNamespace.resolved).toBe("parent-lib-utils");
});

// https://github.com/vercel/eve/issues/1242 — end-to-end expression: the
// materialized dev generation must hydrate an agent module that imports
// parent application code through the parent tsconfig's path aliases.
it("hydrates parent tsconfig alias imports from materialized dev generations", async () => {
const workspaceRoot = await createScratchDirectory("eve-dev-runtime-parent-alias-hydrate-");
const hostRoot = join(workspaceRoot, "apps", "nextjs");
const appRoot = join(hostRoot, "agents", "app-agent");
const compileDirectoryPath = join(appRoot, ".eve", "compile");
const manifestPath = join(compileDirectoryPath, "compiled-agent-manifest.json");
const moduleMapPath = join(compileDirectoryPath, "module-map.mjs");

await mkdir(appRoot, { recursive: true });
await mkdir(join(hostRoot, "lib"), { recursive: true });
await mkdir(compileDirectoryPath, { recursive: true });
await writeFile(join(workspaceRoot, "pnpm-workspace.yaml"), "packages:\n - apps/*\n");
await writeFile(join(workspaceRoot, "package.json"), '{"type":"module"}\n');
await writeFile(join(hostRoot, "package.json"), '{"name":"nextjs-host","type":"module"}\n');
await writeFile(
join(hostRoot, "tsconfig.json"),
`${JSON.stringify(
{
compilerOptions: {
baseUrl: ".",
paths: {
"@/*": ["./*"],
},
},
},
null,
2,
)}\n`,
);
await writeFile(
join(hostRoot, "lib", "auth.ts"),
'export const eveAuth = "parent-auth-config";\n',
);
await writeFile(
join(appRoot, "agent.ts"),
[
'import { eveAuth } from "@/lib/auth";',
"",
"export const routed = `router:${eveAuth}`;",
"",
].join("\n"),
);

const manifest = createCompiledAgentManifest({
agentRoot: appRoot,
appRoot,
config: {
model: {
id: "openai/gpt-5.4-mini",
routing: {
kind: "gateway",
target: "openai/gpt-5.4-mini",
},
},
name: "Parent Alias Agent",
source: {
logicalPath: "agent.ts",
sourceId: "agent.ts",
sourceKind: "module",
},
},
instructions: {
logicalPath: "instructions.md",
markdown: "Use the routed model.",
name: "instructions",
sourceId: "instructions.md",
sourceKind: "markdown",
},
});

await writeFile(manifestPath, `${JSON.stringify(manifest, null, 2)}\n`);
await writeFile(
moduleMapPath,
createCompiledModuleMapSource({
manifest,
moduleMapPath,
}),
);

await publishDevelopmentGeneration({
paths: { compileDirectoryPath },
project: { appRoot },
} as CompileAgentResult);

await withRuntimeSession(createRuntimeSession("parent-alias-imports-regression"), async () => {
const bundle = await getCompiledRuntimeAgentBundle({
compiledArtifactsSource: resolveNitroCompiledArtifactsSource(
createDevelopmentNitroArtifactsConfig({ appRoot }),
),
});
const agentModule = bundle.moduleMap.nodes[ROOT_COMPILED_AGENT_NODE_ID]?.modules["agent.ts"];

expect(agentModule).toMatchObject({
routed: "router:parent-auth-config",
});
});
});

it("stops copying at nested git repository boundaries", async () => {
const appRoot = await createScratchDirectory("eve-dev-runtime-nested-git-");
const agentRoot = join(appRoot, "agent");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,20 @@ async function copySnapshotDirectory(input: {
continue;
}

// `cp` refuses to copy a directory that contains its own destination,
// even when the filter excludes that subtree. A copy root above the app
// root (an owning package with tsconfig path aliases, vercel/eve#1242)
// contains the snapshot inside the app root's `.eve` directory, so
// recurse manually until the skipped `.eve` subtree is filtered out.
if (entry.isDirectory() && isPathInsideOrEqual(input.plan.snapshotRoot, sourcePath)) {
await copySnapshotDirectory({
plan: input.plan,
sourcePath,
targetPath: join(input.targetPath, entry.name),
});
continue;
}

await cp(sourcePath, join(input.targetPath, entry.name), {
filter: (source) => !shouldSkipSnapshotSource(input.plan, source),
mode: SNAPSHOT_COPY_MODE,
Expand Down Expand Up @@ -310,12 +324,18 @@ async function createSnapshotDependencyMounts(plan: DevelopmentSourceSnapshotPla
}

async function ensureRuntimePackageJson(plan: DevelopmentSourceSnapshotPlan): Promise<void> {
const runtimePackageJsonPath = join(plan.runtimeAppRoot, "package.json");

if (existsSync(runtimePackageJsonPath)) {
// A synthetic package.json below an owning package that made it into the

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bare-agent dev-runtime snapshots move the package boundary from the owning package to the workspace root when the owning package's tsconfig has no path aliases, diverging from production module resolution.

Fix on Vercel

// snapshot would move the package boundary that authored-module bundling
// (`resolveAuthoredPackageRoot`) and Node resolution walk up to, hiding the
// owning package's tsconfig path aliases (vercel/eve#1242). Only generate
// one when the snapshot carries no owning package.json at or above the
// runtime app root.
if (findNearestSnapshotPackageJsonPath(plan) !== undefined) {
return;
}

const runtimePackageJsonPath = join(plan.runtimeAppRoot, "package.json");

await mkdir(plan.runtimeAppRoot, { recursive: true });
await writeFile(
runtimePackageJsonPath,
Expand All @@ -333,11 +353,9 @@ async function ensureRuntimePackageJson(plan: DevelopmentSourceSnapshotPlan): Pr
async function validateDevelopmentSourceSnapshot(
plan: DevelopmentSourceSnapshotPlan,
): Promise<void> {
const runtimePackageJsonPath = join(plan.runtimeAppRoot, "package.json");

if (!existsSync(runtimePackageJsonPath)) {
if (findNearestSnapshotPackageJsonPath(plan) === undefined) {
throw new DevelopmentRuntimeSourceSnapshotError(
`Development runtime source snapshot is missing the runtime app package.json at "${runtimePackageJsonPath}".`,
`Development runtime source snapshot is missing a package.json at or above the runtime app root "${plan.runtimeAppRoot}".`,
);
}

Expand Down Expand Up @@ -378,6 +396,35 @@ async function validateSnapshotTsConfigExtends(configPath: string): Promise<void
}
}

/**
* Finds the package.json that owns the runtime app root inside the snapshot
* source tree, walking up like `resolveAuthoredPackageRoot` does at bundle
* time but bounded by the snapshot source root.
*/
function findNearestSnapshotPackageJsonPath(
plan: DevelopmentSourceSnapshotPlan,
): string | undefined {
let currentDirectory = resolve(plan.runtimeAppRoot);

while (isPathInsideOrEqual(currentDirectory, plan.snapshotSourceRoot)) {
const packageJsonPath = join(currentDirectory, "package.json");

if (existsSync(packageJsonPath)) {
return packageJsonPath;
}

const parentDirectory = dirname(currentDirectory);

if (parentDirectory === currentDirectory) {
return undefined;
}

currentDirectory = parentDirectory;
}

return undefined;
}

function toSnapshotPathForPlan(plan: DevelopmentSourceSnapshotPlan, sourcePath: string): string {
return toDevelopmentSourceSnapshotPath({
snapshotSourceRoot: plan.snapshotSourceRoot,
Expand Down
Loading