import-boundaries.test.ts 4.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596
  1. import { describe, expect, test } from "bun:test"
  2. import { mkdtemp, rm } from "node:fs/promises"
  3. import path from "node:path"
  4. const root = path.resolve(import.meta.dir, "../../..")
  5. describe("CLI frontend import boundaries", () => {
  6. test("does not import Core directly", async () => {
  7. const glob = new Bun.Glob("{src,test}/**/*.{ts,tsx}")
  8. const imports: string[] = []
  9. for await (const file of glob.scan({ cwd: path.join(root, "packages/cli") })) {
  10. const source = await Bun.file(path.join(root, "packages/cli", file)).text()
  11. if (/["']@opencode-ai\/core(?:\/[^"']*)?["']/.test(source)) imports.push(file)
  12. }
  13. expect(imports).toEqual([])
  14. })
  15. test("exposes only the intentional package entrypoints", async () => {
  16. const run = await import("@opencode-ai/cli/run")
  17. const mini = await import("@opencode-ai/tui/mini")
  18. const tool = await import("@opencode-ai/tui/mini/tool")
  19. const cli = await Bun.file(path.join(root, "packages/cli/package.json")).json()
  20. expect(Object.keys(run).sort()).toEqual(["runNonInteractive", "runV1Bridge"])
  21. expect(Object.keys(mini).sort()).toEqual(["runMiniFrontend"])
  22. expect(Object.keys(tool).sort()).toEqual(["toolInlineInfo", "toolOutputText"])
  23. expect(Object.keys(cli.exports).filter((key) => key === "./mini" || key.startsWith("./mini/"))).toEqual([])
  24. })
  25. test("keeps run and Mini on separate evaluation graphs", async () => {
  26. const run = await bundleInputs("packages/cli/src/commands/handlers/run.ts")
  27. expect(run).toContain("packages/cli/src/run/run.ts")
  28. expect(run).toContain("packages/tui/src/mini/tool.ts")
  29. expect(run).not.toContain("packages/tui/src/mini/runtime.ts")
  30. expect(run).not.toContain("packages/tui/src/mini/runtime.lifecycle.ts")
  31. expect(run).not.toContain("packages/tui/src/mini/footer.ts")
  32. expect(run).not.toContain("packages/tui/src/mini/scrollback.surface.ts")
  33. expect(run).not.toContain("packages/tui/src/runtime.tsx")
  34. const mini = await bundleInputs("packages/cli/src/commands/handlers/mini.ts")
  35. expect(mini).toContain("packages/cli/src/mini.ts")
  36. expect(mini).toContain("packages/tui/src/mini/index.ts")
  37. expect(mini).toContain("packages/tui/src/mini/runtime.ts")
  38. expect(mini).not.toContain("packages/cli/src/run/run.ts")
  39. expect(mini).not.toContain("packages/cli/src/run/noninteractive.ts")
  40. expect(mini).not.toContain("packages/cli/src/run/ui.ts")
  41. expect(mini).not.toContain("packages/tui/src/runtime.tsx")
  42. })
  43. test("keeps TUI Mini independent from Core, Server, and CLI", async () => {
  44. const glob = new Bun.Glob("**/*.{ts,tsx}")
  45. const imports: string[] = []
  46. for await (const file of glob.scan({ cwd: path.join(root, "packages/tui/src/mini") })) {
  47. const source = await Bun.file(path.join(root, "packages/tui/src/mini", file)).text()
  48. if (/["']@opencode-ai\/(?:core|server|cli)(?:\/[^"']*)?["']/.test(source)) imports.push(file)
  49. }
  50. expect(imports).toEqual([])
  51. const graph = await bundleInputs("packages/tui/src/mini/index.ts")
  52. expect(graph.filter((file) => file.startsWith("packages/core/"))).toEqual([])
  53. expect(graph.filter((file) => file.startsWith("packages/cli/") || file.startsWith("packages/server/"))).toEqual([])
  54. })
  55. })
  56. async function bundleInputs(entrypoint: string) {
  57. const temporary = await mkdtemp(path.join(import.meta.dir, ".import-boundary-"))
  58. const metafile = path.join(temporary, "meta.json")
  59. try {
  60. const child = Bun.spawn(
  61. [
  62. process.execPath,
  63. "build",
  64. entrypoint,
  65. "--target=bun",
  66. "--format=esm",
  67. "--packages=bundle",
  68. "--external=@opentui/core-*",
  69. `--metafile=${metafile}`,
  70. `--outdir=${path.join(temporary, "out")}`,
  71. ],
  72. { cwd: root, stdout: "pipe", stderr: "pipe" },
  73. )
  74. const [exitCode, stdout, stderr] = await Promise.all([
  75. child.exited,
  76. new Response(child.stdout).text(),
  77. new Response(child.stderr).text(),
  78. ])
  79. if (exitCode !== 0) throw new Error(stdout + stderr)
  80. const metadata = await Bun.file(metafile).json()
  81. return Object.keys(metadata.inputs).map((input) =>
  82. path.relative(root, path.resolve(root, input)).replaceAll(path.sep, "/"),
  83. )
  84. } finally {
  85. await rm(temporary, { recursive: true, force: true })
  86. }
  87. }