diff --git a/turbo/generators/lib/core-package-utils.test.ts b/turbo/generators/lib/core-package-utils.test.ts new file mode 100644 index 0000000..a2eb0b2 --- /dev/null +++ b/turbo/generators/lib/core-package-utils.test.ts @@ -0,0 +1,64 @@ +import { describe, it, expect } from "vitest"; +import { + existsSync, + mkdtempSync, + mkdirSync, + writeFileSync, + readFileSync, +} from "node:fs"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import { + assertOptionalPackageNotPresent, + addToTranspilePackages, +} from "./core-package-utils"; + +describe("assertOptionalPackageNotPresent", () => { + it("throws if packages// exists in cwd", () => { + const tmp = mkdtempSync(join(tmpdir(), "core-pkg-")); + mkdirSync(join(tmp, "packages", "core-foo"), { recursive: true }); + expect(() => assertOptionalPackageNotPresent("core-foo", tmp)).toThrow(/already exists/); + }); + + it("returns silently if packages// is absent", () => { + const tmp = mkdtempSync(join(tmpdir(), "core-pkg-")); + expect(() => assertOptionalPackageNotPresent("core-foo", tmp)).not.toThrow(); + }); +}); + +describe("addToTranspilePackages", () => { + it("inserts package name alphabetically into transpilePackages array", () => { + const tmp = mkdtempSync(join(tmpdir(), "core-pkg-")); + const cfgPath = join(tmp, "next.config.mjs"); + writeFileSync( + cfgPath, + `const nextConfig = { + transpilePackages: [ + "@repo/core-cms", + "@repo/core-shared", + ], +}; +`, + ); + addToTranspilePackages(cfgPath, "@repo/core-realtime"); + const result = readFileSync(cfgPath, "utf8"); + // Order should be: core-cms, core-realtime, core-shared + const order = result.match(/@repo\/core-\w+/g) ?? []; + expect(order).toEqual(["@repo/core-cms", "@repo/core-realtime", "@repo/core-shared"]); + }); + + it("is idempotent — duplicate insertion is a no-op", () => { + const tmp = mkdtempSync(join(tmpdir(), "core-pkg-")); + const cfgPath = join(tmp, "next.config.mjs"); + writeFileSync( + cfgPath, + `const nextConfig = { + transpilePackages: ["@repo/core-realtime"], +}; +`, + ); + addToTranspilePackages(cfgPath, "@repo/core-realtime"); + const result = readFileSync(cfgPath, "utf8"); + expect(result.match(/@repo\/core-realtime/g)?.length).toBe(1); + }); +}); diff --git a/turbo/generators/lib/core-package-utils.ts b/turbo/generators/lib/core-package-utils.ts new file mode 100644 index 0000000..bbca5ae --- /dev/null +++ b/turbo/generators/lib/core-package-utils.ts @@ -0,0 +1,44 @@ +import { existsSync, readFileSync, writeFileSync } from "node:fs"; +import { join } from "node:path"; + +/** + * Throws if a core package directory already exists. Used as the first action + * in every per-package generator so re-running is safe. + */ +export function assertOptionalPackageNotPresent( + name: string, + cwd: string = process.cwd(), +): void { + const pkgRoot = join(cwd, "packages", name); + if (existsSync(pkgRoot)) { + throw new Error( + `packages/${name}/ already exists — refusing to scaffold (delete it first if intentional)`, + ); + } +} + +/** + * Inserts a package name into the transpilePackages array of a Next.js config + * file, preserving alphabetical order. Idempotent. + */ +export function addToTranspilePackages( + nextConfigPath: string, + pkgName: string, +): void { + const source = readFileSync(nextConfigPath, "utf8"); + if (source.includes(`"${pkgName}"`)) return; + const updated = source.replace( + /(transpilePackages:\s*\[\s*)([\s\S]*?)(\s*\])/, + (_match, open: string, body: string, close: string) => { + const entries = body + .split(",") + .map((e) => e.trim()) + .filter(Boolean); + entries.push(`"${pkgName}"`); + entries.sort(); + const formatted = entries.map((e) => ` ${e}`).join(",\n"); + return `${open}\n${formatted},${close}`; + }, + ); + writeFileSync(nextConfigPath, updated); +}