diff --git a/test/cli/init/init.test.ts b/test/cli/init/init.test.ts index 6862cb9936af..7e9dc82772c8 100644 --- a/test/cli/init/init.test.ts +++ b/test/cli/init/init.test.ts @@ -1,32 +1,133 @@ -import { beforeAll, describe, expect, test } from "bun:test"; +import { afterAll, beforeAll, describe, expect, test } from "bun:test"; import fs, { readdirSync } from "fs"; -import { bunEnv, bunExe, isWindows, nodeExe, tempDir, tempDirWithFiles } from "harness"; +import { bunEnv, bunExe, nodeExe, tempDir, tempDirWithFiles } from "harness"; import path from "path"; +// The `bun install` that `bun init` runs reads this global bunfig. The +// tailwind and shadcn templates depend on bun-plugin-tailwind, whose `bun` peer +// dependency pulls in the npm `bun` package, and that package's postinstall +// costs several seconds per install on some CI machines (it shells out to +// `npm install` on musl and to PowerShell on Windows x64). Nothing in this file +// asserts on lifecycle scripts. +const globalConfigDir = tempDirWithFiles("bun-init-global-config", { + ".bunfig.toml": "[install]\nignoreScripts = true\n", +}); + // Whether `bun init` emits CLAUDE.md depends on a `claude` binary being on // PATH, which varies by CI machine — disable the detection so the directory // snapshots are stable everywhere. -const initEnv = { ...bunEnv, BUN_AGENT_RULE_DISABLED: "1" }; - -(isWindows ? describe : describe.concurrent)("bun init", () => { - // Every test's `bun init` runs a real `bun install`. bun dedupes downloads - // within a process but not across them, so on a cold CI cache the concurrent - // inits each re-fetch the same tarballs. Prime the shared install cache once, - // serially: `--react=shadcn`'s lockfile is a superset of the other react - // templates', and `-y` covers the blank template (typescript + @types/bun). - beforeAll(async () => { - for (const flag of ["-y", "--react=shadcn"]) { - const temp = tempDirWithFiles("bun-init-cache-prime", {}); - await using proc = Bun.spawn({ - cmd: [bunExe(), "init", flag], - cwd: temp, - stdio: ["ignore", "ignore", "ignore"], - env: initEnv, - }); - await proc.exited; +const initEnv = { ...bunEnv, BUN_AGENT_RULE_DISABLED: "1", XDG_CONFIG_HOME: globalConfigDir }; + +// `src/cli` is a symlink to `src/runtime/cli`, which Windows checkouts may +// materialize as a plain file, so go through the real path. +const templateSourceDir = path.join(import.meta.dir, "../../../src/runtime/cli/init"); + +interface InitResult { + dir: string; + stdout: string; + stderr: string; + exitCode: number; + /** Everything `bun init` left in `dir`, captured before any test builds into it. */ + files: string[]; +} + +// Relative paths of everything under `dir`, sorted, with node_modules listed +// as a single entry: the installed packages are asserted on separately. +function listFiles(dir: string, prefix = ""): string[] { + const files: string[] = []; + for (const entry of readdirSync(path.join(dir, prefix), { withFileTypes: true })) { + const rel = prefix ? `${prefix}/${entry.name}` : entry.name; + if (entry.isDirectory() && entry.name !== "node_modules") { + files.push(...listFiles(dir, rel)); + } else { + files.push(rel); + } + } + return files.sort(); +} + +describe.concurrent("bun init", () => { + // `bun init