| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596 |
- import { describe, expect, test } from "bun:test"
- import { mkdtemp, rm } from "node:fs/promises"
- import path from "node:path"
- const root = path.resolve(import.meta.dir, "../../..")
- describe("CLI frontend import boundaries", () => {
- test("does not import Core directly", async () => {
- const glob = new Bun.Glob("{src,test}/**/*.{ts,tsx}")
- const imports: string[] = []
- for await (const file of glob.scan({ cwd: path.join(root, "packages/cli") })) {
- const source = await Bun.file(path.join(root, "packages/cli", file)).text()
- if (/["']@opencode-ai\/core(?:\/[^"']*)?["']/.test(source)) imports.push(file)
- }
- expect(imports).toEqual([])
- })
- test("exposes only the intentional package entrypoints", async () => {
- const run = await import("@opencode-ai/cli/run")
- const mini = await import("@opencode-ai/tui/mini")
- const tool = await import("@opencode-ai/tui/mini/tool")
- const cli = await Bun.file(path.join(root, "packages/cli/package.json")).json()
- expect(Object.keys(run).sort()).toEqual(["runNonInteractive", "runV1Bridge"])
- expect(Object.keys(mini).sort()).toEqual(["runMiniFrontend"])
- expect(Object.keys(tool).sort()).toEqual(["toolInlineInfo", "toolOutputText"])
- expect(Object.keys(cli.exports).filter((key) => key === "./mini" || key.startsWith("./mini/"))).toEqual([])
- })
- test("keeps run and Mini on separate evaluation graphs", async () => {
- const run = await bundleInputs("packages/cli/src/commands/handlers/run.ts")
- expect(run).toContain("packages/cli/src/run/run.ts")
- expect(run).toContain("packages/tui/src/mini/tool.ts")
- expect(run).not.toContain("packages/tui/src/mini/runtime.ts")
- expect(run).not.toContain("packages/tui/src/mini/runtime.lifecycle.ts")
- expect(run).not.toContain("packages/tui/src/mini/footer.ts")
- expect(run).not.toContain("packages/tui/src/mini/scrollback.surface.ts")
- expect(run).not.toContain("packages/tui/src/runtime.tsx")
- const mini = await bundleInputs("packages/cli/src/commands/handlers/mini.ts")
- expect(mini).toContain("packages/cli/src/mini.ts")
- expect(mini).toContain("packages/tui/src/mini/index.ts")
- expect(mini).toContain("packages/tui/src/mini/runtime.ts")
- expect(mini).not.toContain("packages/cli/src/run/run.ts")
- expect(mini).not.toContain("packages/cli/src/run/noninteractive.ts")
- expect(mini).not.toContain("packages/cli/src/run/ui.ts")
- expect(mini).not.toContain("packages/tui/src/runtime.tsx")
- })
- test("keeps TUI Mini independent from Core, Server, and CLI", async () => {
- const glob = new Bun.Glob("**/*.{ts,tsx}")
- const imports: string[] = []
- for await (const file of glob.scan({ cwd: path.join(root, "packages/tui/src/mini") })) {
- const source = await Bun.file(path.join(root, "packages/tui/src/mini", file)).text()
- if (/["']@opencode-ai\/(?:core|server|cli)(?:\/[^"']*)?["']/.test(source)) imports.push(file)
- }
- expect(imports).toEqual([])
- const graph = await bundleInputs("packages/tui/src/mini/index.ts")
- expect(graph.filter((file) => file.startsWith("packages/core/"))).toEqual([])
- expect(graph.filter((file) => file.startsWith("packages/cli/") || file.startsWith("packages/server/"))).toEqual([])
- })
- })
- async function bundleInputs(entrypoint: string) {
- const temporary = await mkdtemp(path.join(import.meta.dir, ".import-boundary-"))
- const metafile = path.join(temporary, "meta.json")
- try {
- const child = Bun.spawn(
- [
- process.execPath,
- "build",
- entrypoint,
- "--target=bun",
- "--format=esm",
- "--packages=bundle",
- "--external=@opentui/core-*",
- `--metafile=${metafile}`,
- `--outdir=${path.join(temporary, "out")}`,
- ],
- { cwd: root, stdout: "pipe", stderr: "pipe" },
- )
- const [exitCode, stdout, stderr] = await Promise.all([
- child.exited,
- new Response(child.stdout).text(),
- new Response(child.stderr).text(),
- ])
- if (exitCode !== 0) throw new Error(stdout + stderr)
- const metadata = await Bun.file(metafile).json()
- return Object.keys(metadata.inputs).map((input) =>
- path.relative(root, path.resolve(root, input)).replaceAll(path.sep, "/"),
- )
- } finally {
- await rm(temporary, { recursive: true, force: true })
- }
- }
|