100 lines
4.8 KiB
JavaScript
100 lines
4.8 KiB
JavaScript
import { expect, test } from "bun:test";
|
|
import { spawnSync } from "node:child_process";
|
|
import { mkdtempSync, readFileSync, rmSync } from "node:fs";
|
|
import { tmpdir } from "node:os";
|
|
import { resolve } from "node:path";
|
|
|
|
// Metafile imports reference output keys, not paths relative to each chunk.
|
|
// Follow the entire static graph: checking index.js alone misses eager shared chunks.
|
|
function reachableOutputs(outputs, start, includeDynamic = false) {
|
|
const visited = new Set();
|
|
const pending = [start];
|
|
while (pending.length) {
|
|
const path = pending.pop();
|
|
if (visited.has(path)) continue;
|
|
const output = outputs[path];
|
|
if (!output) throw new Error(`Missing build output: ${path}`);
|
|
visited.add(path);
|
|
for (const dependency of output.imports) {
|
|
if (!dependency.external && (includeDynamic || dependency.kind !== "dynamic-import")) {
|
|
pending.push(dependency.path);
|
|
}
|
|
}
|
|
}
|
|
return visited;
|
|
}
|
|
|
|
test("startup graph follows transitive shared chunks and cycles, but not dynamic or external imports", () => {
|
|
const outputs = {
|
|
entry: { imports: [{ path: "shared", kind: "import-statement" }, { path: "lazy", kind: "dynamic-import" }] },
|
|
shared: { imports: [{ path: "runtime", kind: "import-statement" }, { path: "external", kind: "import-statement", external: true }] },
|
|
runtime: { imports: [{ path: "shared", kind: "import-statement" }] },
|
|
lazy: { imports: [] },
|
|
};
|
|
expect([...reachableOutputs(outputs, "entry")].sort()).toEqual(["entry", "runtime", "shared"]);
|
|
expect([...reachableOutputs(outputs, "entry", true)].sort()).toEqual(["entry", "lazy", "runtime", "shared"]);
|
|
expect(() => reachableOutputs(outputs, "missing")).toThrow("Missing build output");
|
|
});
|
|
|
|
test("a lazy facade may share its implementation with another lazy entry without loading it at startup", () => {
|
|
const outputs = {
|
|
entry: { imports: [{ path: "ai", kind: "dynamic-import" }, { path: "agent", kind: "dynamic-import" }] },
|
|
ai: { imports: [{ path: "runtime", kind: "import-statement" }] },
|
|
agent: { imports: [{ path: "runtime", kind: "import-statement" }] },
|
|
runtime: { imports: [] },
|
|
};
|
|
expect([...reachableOutputs(outputs, "entry")]).toEqual(["entry"]);
|
|
expect(reachableOutputs(outputs, "ai").has("runtime")).toBe(true);
|
|
expect(reachableOutputs(outputs, "agent").has("runtime")).toBe(true);
|
|
});
|
|
|
|
test("keeps model runtimes and SDKs out of the entire Cloudflare Worker build", () => {
|
|
// Keep esbuild's service isolated from Bun's test process, as in the CLI build.
|
|
const directory = mkdtempSync(resolve(tmpdir(), "edgeever-worker-graph-"));
|
|
const metadataPath = resolve(directory, "metadata.json");
|
|
let build;
|
|
try {
|
|
const result = spawnSync("bun", ["--eval", `
|
|
import { writeFileSync } from "node:fs";
|
|
import { buildCloudflareWorker } from ${JSON.stringify(new URL("./build-cloudflare-worker.mjs", import.meta.url).href)};
|
|
const { entry, metafile } = await buildCloudflareWorker();
|
|
writeFileSync(${JSON.stringify(metadataPath)}, JSON.stringify({ entry, metafile: { outputs: metafile.outputs } }));
|
|
`], {
|
|
cwd: resolve(import.meta.dir, ".."),
|
|
encoding: "utf8",
|
|
env: {
|
|
...process.env,
|
|
EDGE_EVER_DEPLOYMENT_TRIGGER: "github_release",
|
|
EDGE_EVER_DEPLOYMENT_METHOD: "github_actions",
|
|
},
|
|
});
|
|
expect(result.error).toBeUndefined();
|
|
expect(result.status, result.stderr).toBe(0);
|
|
build = JSON.parse(readFileSync(metadataPath, "utf8"));
|
|
} finally {
|
|
rmSync(directory, { recursive: true, force: true });
|
|
}
|
|
const { entry, metafile } = build;
|
|
const startup = reachableOutputs(metafile.outputs, entry.path);
|
|
const all = reachableOutputs(metafile.outputs, entry.path, true);
|
|
const startupSource = [...startup].map(path => readFileSync(resolve(import.meta.dir, "..", path), "utf8")).join("\n");
|
|
expect(startupSource).toContain("github_release");
|
|
expect(startupSource).toContain("github_actions");
|
|
const inputsFor = paths => [...new Set([...paths].flatMap(path =>
|
|
Object.entries(metafile.outputs[path].inputs)
|
|
.filter(([, input]) => input.bytesInOutput > 0)
|
|
.map(([input]) => input.replaceAll("\\", "/")),
|
|
))];
|
|
const runtimeSources = ["ai-runtime", "companion-runtime", "companion-discovery-runtime"]
|
|
.map(name => `apps/api/src/${name}.ts`);
|
|
const isAiInput = path => runtimeSources.includes(path)
|
|
|| /(?:^|\/)node_modules\/(?:ai|@ai-sdk\/[^/]+)\//.test(path);
|
|
expect(inputsFor(startup).filter(isAiInput)).toEqual([]);
|
|
// Ensure a passing startup check cannot be explained by dropping AI from the build.
|
|
const allInputs = inputsFor(all);
|
|
for (const source of runtimeSources) expect(allInputs).not.toContain(source);
|
|
for (const sdk of ["ai", "@ai-sdk/anthropic", "@ai-sdk/google", "@ai-sdk/openai-compatible"]) {
|
|
expect(allInputs.some(path => `/${path}`.includes(`/node_modules/${sdk}/`))).toBe(false);
|
|
}
|
|
});
|