mirror of
https://github.com/vercel/eve.git
synced 2026-09-20 05:35:39 +08:00
fix(eve): start - resolve prewarm modules after app relocation (#3093)
Signed-off-by: Andrew Barba <barba@hey.com>
This commit is contained in:
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"eve": patch
|
||||
---
|
||||
|
||||
Fix `eve start` failing when an app built on another machine is deployed to a different directory. Sandbox prewarming now resolves authored modules from the deployed app while preserving TypeScript aliases and workspace package resolution.
|
||||
@@ -203,6 +203,8 @@ eve start [--host <host>] [--port <port>]
|
||||
|
||||
Serves the previously built output. Prints the listening URL.
|
||||
|
||||
For self-hosted deployments, copy the app source, `.output/`, and installed dependencies together. The deployment directory can differ from the build directory. Preserve the relative layout of any workspace packages used by the app; startup resolves sandbox prewarm modules from the deployed source.
|
||||
|
||||
## `eve dev`
|
||||
|
||||
```bash
|
||||
|
||||
@@ -206,6 +206,7 @@ export async function prewarmBuiltAppSandboxes(input: {
|
||||
compiledArtifactsSource: builtArtifactsSource,
|
||||
}),
|
||||
loadCompiledModuleMapFromAuthoredSource({
|
||||
authoredAppRoot: input.appRoot,
|
||||
compiledArtifactsSource: builtArtifactsSource,
|
||||
}),
|
||||
]);
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { join } from "node:path";
|
||||
import { join, relative, resolve } from "node:path";
|
||||
import { pathToFileURL } from "node:url";
|
||||
|
||||
import type {
|
||||
@@ -23,9 +23,11 @@ import { formatValidationError } from "#runtime/validation.js";
|
||||
|
||||
const EXT_CONFIG_SCOPE = Symbol.for("eve.ext-config-scope");
|
||||
|
||||
/** Hydrates the compiled module map from the exact physical bindings in the manifest. */
|
||||
/** Hydrates the compiled module map from the manifest’s authored bindings. */
|
||||
export async function loadCompiledModuleMapFromAuthoredSource(input: {
|
||||
readonly compiledArtifactsSource: RuntimeDiskCompiledArtifactsSource;
|
||||
/** Current location of a built application deployed with its source and dependencies. */
|
||||
readonly authoredAppRoot?: string;
|
||||
}): Promise<CompiledModuleMap> {
|
||||
const manifest = await loadCompiledManifest({
|
||||
compiledArtifactsSource: input.compiledArtifactsSource,
|
||||
@@ -33,12 +35,14 @@ export async function loadCompiledModuleMapFromAuthoredSource(input: {
|
||||
return await hydrateCompiledModuleMapFromManifest(
|
||||
manifest,
|
||||
input.compiledArtifactsSource.appRoot,
|
||||
input.authoredAppRoot,
|
||||
);
|
||||
}
|
||||
|
||||
async function hydrateCompiledModuleMapFromManifest(
|
||||
manifest: CompiledAgentManifest,
|
||||
runtimeAppRoot: string,
|
||||
authoredAppRoot: string = manifest.appRoot,
|
||||
): Promise<CompiledModuleMap> {
|
||||
const materializedIndex = await readMaterializedAuthoredModuleIndex(runtimeAppRoot);
|
||||
if (materializedIndex !== undefined) {
|
||||
@@ -60,7 +64,9 @@ async function hydrateCompiledModuleMapFromManifest(
|
||||
];
|
||||
for (const node of nodeManifests) {
|
||||
nodes[node.nodeId] = {
|
||||
modules: await hydrateCompiledNodeScope(node.manifest),
|
||||
modules: await hydrateCompiledNodeScope(node.manifest, (sourcePath) =>
|
||||
resolve(authoredAppRoot, relative(manifest.appRoot, sourcePath)),
|
||||
),
|
||||
};
|
||||
}
|
||||
return { nodes };
|
||||
@@ -68,6 +74,7 @@ async function hydrateCompiledModuleMapFromManifest(
|
||||
|
||||
async function hydrateCompiledNodeScope(
|
||||
manifest: CompiledAgentNodeManifest | CompiledAgentResources,
|
||||
resolveSourcePath: (sourcePath: string) => string,
|
||||
): Promise<CompiledModuleMap["nodes"][string]["modules"]> {
|
||||
const mountScopes = new Map(
|
||||
manifest.extensionMounts.map((mount) => [mount.mountSourceId, mount.packageNamespace]),
|
||||
@@ -89,7 +96,7 @@ async function hydrateCompiledNodeScope(
|
||||
),
|
||||
)
|
||||
: memoizeModuleNamespaceFactories(
|
||||
await loadAuthoredModuleNamespace(binding.backing.sourcePath, {
|
||||
await loadAuthoredModuleNamespace(resolveSourcePath(binding.backing.sourcePath), {
|
||||
externalDependencies: binding.backing.externalDependencies,
|
||||
extensionScopeNamespace: resolveCompiledModuleExtensionScopeNamespace(binding),
|
||||
}),
|
||||
|
||||
@@ -0,0 +1,113 @@
|
||||
import { access, mkdir, realpath, rename } from "node:fs/promises";
|
||||
import { join } from "node:path";
|
||||
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
import { prewarmBuiltAppSandboxes } from "#execution/sandbox/prewarm.js";
|
||||
import { useScenarioApp } from "#internal/testing/scenario-app.js";
|
||||
import { buildApplication } from "./build-application.js";
|
||||
import { startProductionServer } from "./start-production-server.js";
|
||||
|
||||
describe("relocated production applications", () => {
|
||||
const scenarioApp = useScenarioApp();
|
||||
|
||||
it("prewarms and boots a moved workspace with app and extension TypeScript aliases", async () => {
|
||||
const workspace = await scenarioApp({
|
||||
name: "relocated-production",
|
||||
installDependencies: true,
|
||||
dependencies: { "just-bash": "3.1.0" },
|
||||
files: {
|
||||
"pnpm-workspace.yaml": "packages:\n - apps/*\n - packages/*\n",
|
||||
"apps/service/package.json": JSON.stringify({
|
||||
name: "relocated-service",
|
||||
type: "module",
|
||||
dependencies: { "@acme/relocated": "workspace:*" },
|
||||
}),
|
||||
"apps/service/tsconfig.json": JSON.stringify({
|
||||
compilerOptions: { baseUrl: ".", paths: { "@/*": ["./lib/*"] } },
|
||||
}),
|
||||
"apps/service/lib/marker.ts": 'export const marker = "app-bootstrap";',
|
||||
"apps/service/agent/agent.ts": 'export default { model: "openai/gpt-5.4" };',
|
||||
"apps/service/agent/instructions.md": "Use the available tools.",
|
||||
"apps/service/agent/skills/probe.md":
|
||||
"---\ndescription: Probe the sandbox.\n---\nProbe content.",
|
||||
"apps/service/agent/sandbox/sandbox.ts": [
|
||||
'import { justbash } from "eve/sandbox/just-bash";',
|
||||
'import { marker } from "@/marker";',
|
||||
"export default {",
|
||||
" backend: justbash(),",
|
||||
' revalidationKey: () => "relocated-v1",',
|
||||
" async bootstrap({ use }) {",
|
||||
" const sandbox = await use();",
|
||||
" const result = await sandbox.run({ command: `echo ${marker}` });",
|
||||
' if (result.stdout.trim() !== "app-bootstrap") throw new Error("Wrong app alias");',
|
||||
" },",
|
||||
"};",
|
||||
].join("\n"),
|
||||
"apps/service/agent/extensions/acme.ts":
|
||||
'import extension from "@acme/relocated"; export default extension();',
|
||||
"packages/extension/package.json": JSON.stringify({
|
||||
name: "@acme/relocated",
|
||||
type: "module",
|
||||
exports: "./extension/extension.ts",
|
||||
eve: { extension: { source: "source", dist: "extension" } },
|
||||
}),
|
||||
"packages/extension/tsconfig.json": JSON.stringify({
|
||||
compilerOptions: { baseUrl: ".", paths: { "@/*": ["./lib/*"] } },
|
||||
}),
|
||||
"packages/extension/lib/marker.ts": 'export const marker = "extension-tool";',
|
||||
"packages/extension/extension/extension.ts":
|
||||
'import { defineExtension } from "eve/extension"; export default defineExtension();',
|
||||
"packages/extension/extension/tools/probe.ts": [
|
||||
'import { marker } from "@/marker";',
|
||||
'if (marker !== "extension-tool") throw new Error("Wrong extension alias");',
|
||||
'export default { description: "Probe the extension.", execute: () => marker };',
|
||||
].join("\n"),
|
||||
"packages/extension/extension/_manifest.json": JSON.stringify({
|
||||
kind: "eve-extension",
|
||||
formatVersion: 1,
|
||||
builtWithEve: "0.0.0-test",
|
||||
requires: { extension: 1, tool: 1 },
|
||||
}),
|
||||
},
|
||||
});
|
||||
const root = await realpath(workspace.appRoot);
|
||||
const buildRoot = join(root, "build-machine");
|
||||
const runtimeRoot = join(root, "runtime-machine");
|
||||
await mkdir(buildRoot);
|
||||
for (const entry of [
|
||||
"apps",
|
||||
"packages",
|
||||
"node_modules",
|
||||
"pnpm-workspace.yaml",
|
||||
"package.json",
|
||||
]) {
|
||||
await rename(join(root, entry), join(buildRoot, entry));
|
||||
}
|
||||
const appRoot = join(buildRoot, "apps", "service");
|
||||
await buildApplication(appRoot, { skipVercelSandboxPrewarm: false });
|
||||
await rename(buildRoot, runtimeRoot);
|
||||
await expect(access(buildRoot)).rejects.toMatchObject({ code: "ENOENT" });
|
||||
|
||||
const runtimeAppRoot = join(runtimeRoot, "apps", "service");
|
||||
const prewarmedRoots: string[] = [];
|
||||
const seededPaths: string[] = [];
|
||||
await prewarmBuiltAppSandboxes({
|
||||
appRoot: runtimeAppRoot,
|
||||
dispatch: async ({ backend, input }) => {
|
||||
prewarmedRoots.push(input.runtimeContext.appRoot);
|
||||
seededPaths.push(...(input.seedFiles ?? []).map((file) => file.path));
|
||||
return await backend.prewarm(input);
|
||||
},
|
||||
});
|
||||
expect(prewarmedRoots).toEqual([runtimeAppRoot]);
|
||||
expect(seededPaths).toContain("$HOME/.agents/skills/probe/SKILL.md");
|
||||
|
||||
const server = await startProductionServer(runtimeAppRoot, { host: "127.0.0.1", port: 0 });
|
||||
try {
|
||||
expect((await fetch(new URL("/eve/v1/health", server.url))).status).toBe(200);
|
||||
} finally {
|
||||
await server.close();
|
||||
}
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user