fix(eve): workflow builds - resolve scoped application imports (#3088)

Signed-off-by: Andrew Barba <barba@hey.com>
This commit is contained in:
Andrew Barba
2026-09-07 11:11:52 -04:00
committed by GitHub
parent f881bc7a9d
commit 9034901068
17 changed files with 230 additions and 256 deletions
+5
View File
@@ -0,0 +1,5 @@
---
"eve": patch
---
Resolve workflow path aliases from the application config and bundle only workflows reachable from the agent. Unresolved workflow imports now fail the build instead of producing a bundle that crashes every durable session.
+6
View File
@@ -93,6 +93,12 @@ tool object, a channel handler, or a schedule handler fails the build. To start
channel or schedule, use the [channel operations](/docs/channels/custom#channel-operations-and-session-handles)
or [schedule handler](/docs/schedules#handler-form-run) APIs.
Workflow imports resolve `paths` aliases from your application's `tsconfig.json` or
`jsconfig.json`, including when the application is a workspace package. eve includes only
workflow and step modules reachable from the agent's runtime modules. Unrelated workflows
in the host application stay outside the agent bundle. An unresolved workflow import fails
the build with the missing import in the error.
### Migrate an existing workflow tool
Replace `defineTool` with `defineWorkflowTool`, keep the executor's `"use workflow"` directive,
@@ -1,7 +1,7 @@
import type { WorkflowToolContext } from "eve/tools";
import { sleep } from "workflow";
import { describePlan, hashPlan } from "./plan.ts";
import { describePlan, hashPlan } from "@/agent/lib/plan.ts";
export async function deployService({ service }: { service: string }, ctx: WorkflowToolContext) {
"use workflow";
@@ -12,7 +12,10 @@
"declaration": false,
"noEmit": true,
"types": ["node", "eve/workflow-modules"],
"allowImportingTsExtensions": true
"allowImportingTsExtensions": true,
"paths": {
"@/*": ["./*"]
}
},
"include": ["agent/**/*.ts", "evals/**/*.ts"]
}
@@ -1,6 +1,7 @@
import { copyFile, mkdir, writeFile } from "node:fs/promises";
import { join } from "node:path";
import type { AuthoredWorkflowModules } from "#internal/workflow-bundle/builder-support.js";
import type { CompileMetadata } from "#compiler/artifacts.js";
import type { CompileAgentResult } from "#compiler/compile-agent.js";
import { stringifyEsmImportSpecifier } from "#internal/application/import-specifier.js";
@@ -41,6 +42,7 @@ type InstrumentationPluginLayout =
* vendored workflow bundles for one application.
*/
export interface GeneratedCompiledArtifactsFiles {
readonly authoredWorkflowModules?: AuthoredWorkflowModules;
/**
* Shared bundled-artifacts bootstrap installed by Nitro and vendored
* workflow handlers.
@@ -105,6 +107,7 @@ export async function writeCompiledArtifactsFiles(input: {
);
const generatedArtifacts: GeneratedCompiledArtifactsFiles = {
authoredWorkflowModules: prepared.authoredWorkflowModules,
bootstrapPath,
workflowWorldPluginPath,
};
@@ -167,6 +170,7 @@ function instrumentationSourcePathsOf(layout: InstrumentationLayout): readonly s
// bootstrap references no authored module, and the instrumentation bundle is
// copied out of the generation into the stable host directory.
export async function writeDevelopmentCompiledArtifactsFiles(input: {
readonly authoredWorkflowModules?: AuthoredWorkflowModules;
readonly compileResult: CompileAgentResult;
readonly outDir: string;
readonly runtimeAppRoot: string;
@@ -194,6 +198,7 @@ export async function writeDevelopmentCompiledArtifactsFiles(input: {
);
const generatedArtifacts: GeneratedCompiledArtifactsFiles = {
authoredWorkflowModules: input.authoredWorkflowModules,
bootstrapPath,
workflowWorldPluginPath,
};
@@ -31,6 +31,7 @@ import {
buildWithNitroRolldown,
} from "#internal/bundler/nitro-rolldown.js";
import { createNodeEsmCompatBannerPlugin } from "#internal/node-esm-compat-banner.js";
import type { AuthoredWorkflowModules } from "#internal/workflow-bundle/builder-support.js";
import { prepareAuthoredWorkflowDirectives } from "#internal/workflow-bundle/authored-workflow-directives.js";
import { createDynamicCapabilityTransformPlugin } from "#internal/workflow-bundle/dynamic-capability-transform-plugin.js";
import {
@@ -263,6 +264,7 @@ export async function bundleExtensionDistributionGraph(input: {
* entry.
*/
export interface AuthoredModuleMapBundle {
readonly authoredWorkflowModules: AuthoredWorkflowModules;
readonly code: string;
/** Fingerprint of the sources that also feed the driver and step registry; a change rebuilds the host. */
readonly workflowSourceFingerprint: string | undefined;
@@ -340,6 +342,7 @@ export async function bundleAuthoredModuleMapForGeneration(input: {
},
});
return {
authoredWorkflowModules: workflowSources.modules(),
code: removeRolldownModuleRegionComments(chunk.code),
workflowSourceFingerprint: workflowSources.fingerprint(),
};
@@ -399,6 +402,13 @@ class AuthoredWorkflowSourceRecorder {
};
}
modules(): AuthoredWorkflowModules {
return {
directiveModules: [...this.#directiveModules].sort(),
workflowModules: [...this.#workflowFunctions.keys()].sort(),
};
}
fingerprint(): string | undefined {
if (this.#directiveModules.size === 0) return undefined;
@@ -637,7 +647,7 @@ function createInFlightModuleLoadKey(
return `${modulePath}\0${externalDependencies.join("\0")}\0${options.extensionScopeNamespace ?? ""}`;
}
function resolveAuthoredTsConfigPath(packageRoot: string): string | false {
export function resolveAuthoredTsConfigPath(packageRoot: string): string | false {
for (const fileName of ["tsconfig.json", "jsconfig.json"]) {
const path = join(packageRoot, fileName);
if (existsSync(path)) {
@@ -1,3 +1,4 @@
import type { AuthoredWorkflowModules } from "#internal/workflow-bundle/builder-support.js";
import type { CompiledAgentManifest } from "#compiler/manifest.js";
import {
bundleAuthoredModuleForGeneration,
@@ -11,6 +12,7 @@ interface PreparedAuthoredRuntimeInstrumentation {
}
export interface PreparedAuthoredRuntimeModules {
readonly authoredWorkflowModules: AuthoredWorkflowModules;
readonly instrumentation?: PreparedAuthoredRuntimeInstrumentation;
readonly moduleMapCode: string;
/** Identity of authored sources shared by the workflow driver and step registrations. */
@@ -22,8 +24,11 @@ export async function prepareAuthoredRuntimeModules(input: {
readonly manifest: CompiledAgentManifest;
readonly moduleMapPath: string;
}): Promise<PreparedAuthoredRuntimeModules> {
const { code: moduleMapCode, workflowSourceFingerprint } =
await bundleAuthoredModuleMapForGeneration(input);
const {
authoredWorkflowModules,
code: moduleMapCode,
workflowSourceFingerprint,
} = await bundleAuthoredModuleMapForGeneration(input);
const providersEnabled = input.manifest.config.experimental?.instrumentationProviders ?? false;
const layout = providersEnabled
? resolveInstrumentationLayout({ agentRoot: input.manifest.agentRoot, providersEnabled: true })
@@ -42,6 +47,6 @@ export async function prepareAuthoredRuntimeModules(input: {
}
return instrumentation === undefined
? { moduleMapCode, workflowSourceFingerprint }
: { instrumentation, moduleMapCode, workflowSourceFingerprint };
? { authoredWorkflowModules, moduleMapCode, workflowSourceFingerprint }
: { authoredWorkflowModules, instrumentation, moduleMapCode, workflowSourceFingerprint };
}
@@ -1,5 +1,6 @@
import { rm } from "node:fs/promises";
import type { AuthoredWorkflowModules } from "#internal/workflow-bundle/builder-support.js";
import type { CompileAgentResult } from "#compiler/compile-agent.js";
import { prepareAuthoredRuntimeModules } from "#internal/authored-runtime-modules.js";
import { writeMaterializedAuthoredModules } from "#internal/materialized-authored-modules.js";
@@ -12,6 +13,7 @@ import {
} from "#internal/nitro/dev-runtime-artifacts.js";
export interface DevelopmentGeneration extends DevelopmentRuntimeArtifactsSnapshot {
readonly authoredWorkflowModules?: AuthoredWorkflowModules;
readonly fingerprint: string;
/** Identity of the authored sources the workflow driver and step registrations are built from. */
readonly workflowSourceFingerprint?: string;
@@ -40,9 +42,14 @@ export async function stageDevelopmentGeneration(
});
return prepared.workflowSourceFingerprint === undefined
? { ...snapshot, fingerprint: materialized.fingerprint }
? {
...snapshot,
authoredWorkflowModules: prepared.authoredWorkflowModules,
fingerprint: materialized.fingerprint,
}
: {
...snapshot,
authoredWorkflowModules: prepared.authoredWorkflowModules,
fingerprint: materialized.fingerprint,
workflowSourceFingerprint: prepared.workflowSourceFingerprint,
};
@@ -0,0 +1,126 @@
import { readFile, realpath } from "node:fs/promises";
import { join } from "node:path";
import { describe, expect, it } from "vitest";
import {
createApplicationBuildWorkspace,
removeApplicationBuildWorkspace,
} from "#internal/application/build-workspace.js";
import { resolvePackageRoot } from "#internal/application/package.js";
import { useScenarioApp } from "#internal/testing/scenario-app.js";
import { WorkflowBundleBuilder } from "#internal/workflow-bundle/builder.js";
import { buildApplication } from "./build-application.js";
import { startProductionServer } from "./start-production-server.js";
import { prepareProductionApplicationHost } from "./prepare-application-host.js";
const ALIAS_MARKER = "workflow-alias-resolved";
function workflowCode(source: string): string {
const match = source.match(
/Buffer\.from\((\[[\s\S]*?\])\.join\(""\), "base64"\)\.toString\("utf8"\)/,
);
return Buffer.from((JSON.parse(match?.[1] ?? "[]") as string[]).join(""), "base64").toString(
"utf8",
);
}
describe("authored workflow scope", () => {
const scenarioApp = useScenarioApp();
it.each(["tsconfig.json", "jsconfig.json"])(
"resolves workflow aliases from the application's %s",
async (configFile) => {
const app = await scenarioApp({
name: "workflow-app-alias",
installDependencies: true,
files: {
[configFile]: JSON.stringify({
compilerOptions: { baseUrl: ".", paths: { "@/*": ["./*"] } },
}),
"agent/agent.ts": 'export default { model: "openai/gpt-5.4" };',
"agent/instructions.md": "Call the probe tool.",
"agent/tools/probe.ts":
'import { defineWorkflowTool } from "eve/tools"; import { describe } from "@/lib/describe"; export default defineWorkflowTool({ description: "Probe", inputSchema: {}, async execute() { "use workflow"; return describe(); } });',
"lib/describe.ts": `export function describe() { return ${JSON.stringify(ALIAS_MARKER)}; }`,
},
});
const appRoot = await realpath(app.appRoot);
const workspace = await createApplicationBuildWorkspace(appRoot);
try {
const host = await prepareProductionApplicationHost(workspace);
const builder = new WorkflowBundleBuilder({
agentName: host.compileResult.manifest.config.name,
appRoot,
compiledArtifactsBootstrapPath: host.compiledArtifacts.bootstrapPath,
outDir: workspace.workflow.buildDir,
rootDir: resolvePackageRoot(),
watch: false,
authoredWorkflowModules: host.compiledArtifacts.authoredWorkflowModules,
});
await builder.build();
const code = workflowCode(
await readFile(join(workspace.workflow.buildDir, "workflows.mjs"), "utf8"),
);
expect(code).toContain(ALIAS_MARKER);
expect(code).not.toContain('require("@/lib/describe")');
await buildApplication(appRoot, { skipVercelSandboxPrewarm: false });
const server = await startProductionServer(appRoot, { port: 0, host: "127.0.0.1" });
try {
expect((await fetch(new URL("/eve/v1/health", server.url))).status).toBe(200);
} finally {
await server.close();
}
} finally {
await removeApplicationBuildWorkspace(workspace);
}
},
);
it("excludes host workflows while retaining reachable workflow step helpers", async () => {
const app = await scenarioApp({
name: "workflow-agent-scope",
installDependencies: true,
files: {
"agent/agent.ts": 'export default { model: "openai/gpt-5.4" };',
"agent/instructions.md": "Call the probe tool.",
"agent/tools/probe.ts":
'import { defineWorkflowTool } from "eve/tools"; import { run } from "../../lib/run"; export default defineWorkflowTool({ description: "Probe", inputSchema: {}, execute: run });',
"lib/run.ts":
'import { readMarker } from "./step"; export async function run() { "use workflow"; return readMarker(); }',
"lib/step.ts":
'import { hostname } from "node:os"; export async function readMarker() { "use step"; return hostname(); }',
"components/layout.js":
"export default function Layout() { return <div>Host application</div>; }",
"workflows/host.ts":
'import { readFileSync } from "node:fs"; export async function unrelatedHostWorkflow() { "use workflow"; return readFileSync("host.txt", "utf8"); }',
},
});
const appRoot = await realpath(app.appRoot);
const workspace = await createApplicationBuildWorkspace(appRoot);
try {
const host = await prepareProductionApplicationHost(workspace);
const builder = new WorkflowBundleBuilder({
agentName: host.compileResult.manifest.config.name,
appRoot,
compiledArtifactsBootstrapPath: host.compiledArtifacts.bootstrapPath,
outDir: workspace.workflow.buildDir,
rootDir: resolvePackageRoot(),
watch: false,
authoredWorkflowModules: host.compiledArtifacts.authoredWorkflowModules,
});
await builder.build();
const steps = await readFile(join(workspace.workflow.buildDir, "steps.mjs"), "utf8");
const code = workflowCode(
await readFile(join(workspace.workflow.buildDir, "workflows.mjs"), "utf8"),
);
expect(steps).toContain("lib/step.ts");
expect(code).toContain("workflow//./lib/run//run");
expect(code).toContain("step//./lib/step//readMarker");
expect(code).not.toContain("unrelatedHostWorkflow");
expect(steps).not.toContain("workflows/host.ts");
} finally {
await removeApplicationBuildWorkspace(workspace);
}
});
});
@@ -344,6 +344,7 @@ export async function configureDevelopmentNitroRoutes(
): Promise<void> {
const workflowBuildDirectory = resolveNitroWorkflowBuildDirectory(nitro);
const builder = new WorkflowBundleBuilder({
authoredWorkflowModules: preparedHost.compiledArtifacts.authoredWorkflowModules,
agentName: preparedHost.compileResult.manifest.config.name,
appRoot: preparedHost.appRoot,
compiledArtifactsBootstrapPath: preparedHost.compiledArtifacts.bootstrapPath,
@@ -398,6 +399,7 @@ export async function configureProductionNitroRoutes(
preparedHost: PreparedApplicationHost,
): Promise<void> {
const builder = new WorkflowBundleBuilder({
authoredWorkflowModules: preparedHost.compiledArtifacts.authoredWorkflowModules,
agentName: preparedHost.compileResult.manifest.config.name,
appRoot: preparedHost.appRoot,
compiledArtifactsBootstrapPath: preparedHost.compiledArtifacts.bootstrapPath,
@@ -2,6 +2,7 @@ import { readFile } from "node:fs/promises";
import { dirname, isAbsolute, join, relative, resolve } from "node:path";
import { fileURLToPath } from "node:url";
import { resolveAuthoredTsConfigPath } from "#internal/authored-module-loader.js";
import { createNitro } from "nitro/builder";
import type { Nitro } from "nitro/types";
import { EVE_PACKAGE_NAME } from "#internal/package-name.js";
@@ -637,7 +638,10 @@ function createApplicationNitroBundlerConfiguration(
createExtensionExternalDependencyPlugin(extensionMounts),
extensionScopePlugin,
].filter((plugin) => plugin !== null);
const nitroRolldownConfig = createNitroBundlerConfig(nitroBundlerPlugins);
const nitroRolldownConfig = {
...createNitroBundlerConfig(nitroBundlerPlugins),
tsconfig: resolveAuthoredTsConfigPath(preparedHost.appRoot),
};
const nitroRollupConfig = createNitroBundlerConfig(nitroBundlerPlugins);
const tracedAppDependencies = collectHostedTraceDependencies(
preparedHost,
@@ -63,6 +63,7 @@ export async function prepareDevelopmentApplicationHost(
const schedules = await resolveSchedules({ manifest: compileResult.manifest });
generation = await stageDevelopmentGeneration(compileResult);
const compiledArtifacts = await writeDevelopmentCompiledArtifactsFiles({
authoredWorkflowModules: generation.authoredWorkflowModules,
compileResult,
outDir: workspace.artifactsDir,
runtimeAppRoot: generation.runtimeAppRoot,
@@ -1,149 +0,0 @@
import { mkdir, mkdtemp, rm, writeFile } from "node:fs/promises";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { afterEach, beforeEach, describe, expect, it } from "vitest";
import { discoverAuthoredWorkflowModules } from "./authored-workflow-modules.js";
let appRoot: string;
async function write(relativePath: string, source: string): Promise<string> {
const filePath = join(appRoot, relativePath);
await mkdir(join(filePath, ".."), { recursive: true });
await writeFile(filePath, source);
return filePath;
}
beforeEach(async () => {
appRoot = await mkdtemp(join(tmpdir(), "eve-authored-workflows-"));
await write("package.json", JSON.stringify({ name: "fixture-app", type: "module" }));
});
afterEach(async () => {
await rm(appRoot, { force: true, recursive: true });
});
describe("discoverAuthoredWorkflowModules", () => {
it("separates workflow modules from step-only modules and skips the rest", async () => {
const tool = await write(
"agent/tools/deploy.ts",
`import { defineWorkflowTool } from "eve/tools";
import { plan } from "../lib/plan.ts";
export default defineWorkflowTool({
description: "d",
inputSchema: {},
async execute(input) {
"use workflow";
return plan(input);
},
});`,
);
const helper = await write(
"agent/lib/plan.ts",
`export async function plan(input) {
"use step";
return input;
}`,
);
await write(
"agent/tools/plain.ts",
`export default { description: "p", async execute() { return 1; } };`,
);
await write(
"agent/lib/quoted.ts",
`export const text = '"use workflow" is only a directive as a statement';`,
);
await write("node_modules/dep/index.js", `export async function f() { "use workflow"; }`);
await write(
".output.eve-backup-crashed/server/_libs/eve+zod.mjs",
`const step = async function () { "use step"; };\nexport { step };`,
);
await write(
".eve/builds/x/output/server/index.mjs",
`export async function f() { "use step"; }`,
);
await write(
"src/components/banner.js",
`// "use step" helpers live in ../lib\nexport function Banner() { return <b>hi</b>; }`,
);
await write(
".well-known/workflow/v1/flow.js",
`export async function generated() {\n "use workflow";\n}`,
);
await write("dist/out.js", `export async function f() { "use step"; }`);
await write("agent/README.md", `"use workflow"`);
await expect(discoverAuthoredWorkflowModules(appRoot)).resolves.toEqual({
directiveModules: [helper, tool],
workflowModules: [tool],
});
});
it("leaves modules without a directive unparsed, so JSX in plain .js never fails the scan", async () => {
await write(
"src/app/layout.js",
`export default function RootLayout({ children }) {
return <html><body><code>defineWorkflowTool is only text here</code>{children}</body></html>;
}`,
);
const tool = await write(
"agent/tools/deploy.ts",
`import { defineWorkflowTool } from "eve/tools";
export default defineWorkflowTool({
description: "d",
async execute() {
"use workflow";
return 1;
},
});`,
);
await expect(discoverAuthoredWorkflowModules(appRoot)).resolves.toEqual({
directiveModules: [tool],
workflowModules: [tool],
});
});
it("finds nothing without an application package.json", async () => {
await rm(join(appRoot, "package.json"));
await write("agent/tools/deploy.ts", `export async function run() { "use workflow"; }`);
await expect(discoverAuthoredWorkflowModules(appRoot)).resolves.toEqual({
directiveModules: [],
workflowModules: [],
});
});
it("discovers directives on the same line as their function declaration", async () => {
const tool = await write(
"agent/tools/inline.ts",
`import { defineWorkflowTool } from "eve/tools";
export default defineWorkflowTool({ async execute() { "use workflow"; return plan(); } });`,
);
const helper = await write(
"agent/lib/plan.ts",
`export async function plan() { "use step"; return 1; }`,
);
await expect(discoverAuthoredWorkflowModules(appRoot)).resolves.toEqual({
directiveModules: [helper, tool],
workflowModules: [tool],
});
});
it("reports an invalid directive placement as a build error", async () => {
await write(
"agent/tools/bad.ts",
`export default {
execute() {
const inner = async () => {
"use workflow";
};
return inner();
},
};`,
);
await expect(discoverAuthoredWorkflowModules(appRoot)).rejects.toThrow(/use workflow/);
});
});
@@ -1,93 +0,0 @@
import type { Dirent } from "node:fs";
import { readdir, readFile } from "node:fs/promises";
import { join } from "node:path";
import { isGeneratedWorkflowFile } from "#compiled/@workflow/builders/index.js";
import { prepareAuthoredWorkflowDirectives } from "./authored-workflow-directives.js";
import { isWorkflowSourceFile } from "./builder-support.js";
import { isAuthoredApplicationModule, isAuthoredApplicationRoot } from "./workflow-builders.js";
// The SDK's own ignore list (`BaseBuilder.getInputFiles`) plus eve's generated locations.
const IGNORED_DIRECTORIES = new Set([
".cache",
".eve",
".git",
".next",
".nitro",
".nuxt",
".output",
".pnpm-store",
".svelte-kit",
".swc",
".turbo",
".vercel",
".workflow-data",
".workflow-vitest",
".yarn",
"coverage",
"dist",
"node_modules",
]);
function isIgnoredDirectory(name: string): boolean {
return IGNORED_DIRECTORIES.has(name) || name.startsWith(".output.");
}
export interface AuthoredWorkflowModules {
readonly directiveModules: readonly string[];
readonly workflowModules: readonly string[];
}
/**
* Scans the whole application root, as the SDK's bundler integrations do, so a
* step helper can live wherever the tool imports it from.
*/
export async function discoverAuthoredWorkflowModules(
appRoot: string,
): Promise<AuthoredWorkflowModules> {
const directiveModules: string[] = [];
const workflowModules: string[] = [];
if (!isAuthoredApplicationRoot(appRoot)) return { directiveModules, workflowModules };
const files = await collectSourceFiles(appRoot);
for (const filePath of files.sort()) {
if (!isAuthoredApplicationModule(filePath, appRoot) || isGeneratedWorkflowFile(filePath))
continue;
const source = await readFile(filePath, "utf8");
if (!source.includes("use workflow") && !source.includes("use step")) continue;
const prepared = await prepareAuthoredWorkflowDirectives({ filePath, source });
if (!prepared.hasDirectives) continue;
directiveModules.push(filePath);
if (prepared.hasWorkflowDirective) workflowModules.push(filePath);
}
return { directiveModules, workflowModules };
}
async function collectSourceFiles(root: string): Promise<string[]> {
const files: string[] = [];
async function visit(directory: string): Promise<void> {
let entries: Dirent[];
try {
entries = await readdir(directory, { withFileTypes: true });
} catch (error) {
if (error instanceof Error && "code" in error && error.code === "ENOENT") return;
throw error;
}
for (const entry of entries) {
const entryPath = join(directory, entry.name);
if (entry.isDirectory()) {
if (!isIgnoredDirectory(entry.name)) await visit(entryPath);
continue;
}
if (!entry.isFile()) continue;
if (isWorkflowSourceFile(entry.name)) files.push(entryPath);
}
}
await visit(root);
return files;
}
@@ -18,7 +18,13 @@ import { WORKFLOW_STEP_EXTERNAL_PACKAGES } from "#internal/workflow-bundle/verce
export const WORKFLOW_VIRTUAL_ENTRY_ID = "\0eve-workflow-entry";
export interface AuthoredWorkflowModules {
readonly directiveModules: readonly string[];
readonly workflowModules: readonly string[];
}
export interface WorkflowBundleBuilderOptions {
readonly authoredWorkflowModules?: AuthoredWorkflowModules;
agentName: string;
appRoot: string;
compiledArtifactsBootstrapPath: string;
@@ -834,6 +834,10 @@ describe("WorkflowBundleBuilder", () => {
const builder = new FixtureWorkflowBundleBuilder(
{
authoredWorkflowModules: {
directiveModules: [toolPath, stepsPath, join(appRoot, "agent", "lib", "run.ts")],
workflowModules: [toolPath, join(appRoot, "agent", "lib", "run.ts")],
},
agentName: "test-agent",
appRoot,
compiledArtifactsBootstrapPath,
@@ -868,4 +872,28 @@ describe("WorkflowBundleBuilder", () => {
await rm(tempRoot, { force: true, recursive: true });
}
});
it("rejects unresolved workflow imports before emitting a VM bundle", async () => {
const root = await mkdtemp(join(tmpdir(), "eve-workflow-missing-import-"));
const flow = join(root, "flow.ts");
try {
await writeFile(
flow,
'import { value } from "missing-workflow-package"; export async function flow() { "use workflow"; return value; }',
);
const builder = new FixtureWorkflowBundleBuilder(
{
agentName: "missing-import",
appRoot: root,
rootDir: root,
outDir: join(root, "out"),
compiledArtifactsBootstrapPath: join(root, "bootstrap.mjs"),
watch: false,
},
[flow],
);
await expect(builder.build()).rejects.toThrow("missing-workflow-package");
} finally {
await rm(root, { recursive: true, force: true });
}
});
});
@@ -14,9 +14,6 @@ import { createAuthoredPackageTsConfigPathsPlugin } from "#internal/authored-pac
import { createAuthoredRelativeExtensionResolverPlugin } from "#internal/authored-relative-extension-resolver.js";
import {
type AuthoredWorkflowModules,
discoverAuthoredWorkflowModules,
} from "#internal/workflow-bundle/authored-workflow-modules.js";
import {
bundleFinalWorkflowOutput,
collectWorkflowInputFiles,
composeWorkflowDriverCode,
@@ -54,6 +51,7 @@ import {
import { deriveEveWorkflowQueueNamespace } from "#internal/workflow/queue-namespace.js";
export class WorkflowBundleBuilder {
readonly #authoredWorkflowModules: AuthoredWorkflowModules;
readonly #compiledArtifactsBootstrapPath: string;
readonly #outDir: string;
readonly #queueNamespace: string;
@@ -78,6 +76,10 @@ export class WorkflowBundleBuilder {
workingDir: options.rootDir,
};
this.#authoredWorkflowModules = options.authoredWorkflowModules ?? {
directiveModules: [],
workflowModules: [],
};
this.#compiledArtifactsBootstrapPath = options.compiledArtifactsBootstrapPath;
this.#outDir = options.outDir;
this.#queueNamespace = deriveEveWorkflowQueueNamespace(options.agentName);
@@ -107,7 +109,7 @@ export class WorkflowBundleBuilder {
await mkdir(this.#outDir, { recursive: true });
const frameworkEntries = await this.discoverEntries(frameworkInputFiles);
const appEntries = await discoverAuthoredWorkflowModules(this.transformProjectRoot);
const appEntries = this.#authoredWorkflowModules;
const stepEntries = mergeStepEntries(frameworkEntries, appEntries);
const stepsOutfile = join(this.#outDir, "steps.mjs");
@@ -172,7 +174,7 @@ export class WorkflowBundleBuilder {
}
protected async findTsConfigPath(): Promise<string | undefined> {
let current = this.config.workingDir;
let current = this.transformProjectRoot;
while (true) {
for (const filename of ["tsconfig.json", "jsconfig.json"]) {
@@ -290,6 +292,12 @@ export class WorkflowBundleBuilder {
].join("\n");
const interimBundle = await buildSingleRolldownChunk(`${options.label} workflow driver chunk`, {
cwd: this.config.workingDir,
onwarn(warning: { code: string; message: string }, warn: (warning: unknown) => void) {
if (warning.code === "UNRESOLVED_IMPORT") {
throw new Error(`Cannot build workflow bundle: ${warning.message}`);
}
warn(warning);
},
input: WORKFLOW_VIRTUAL_ENTRY_ID,
platform: "neutral",
plugins: [