feat(generators): core-package-utils (assertNotPresent + addToTranspilePackages)

Two helpers for use by per-package generator actions:
- assertOptionalPackageNotPresent — guards against double-scaffolding
- addToTranspilePackages — idempotent alphabetical splice into next.config.mjs

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-05-09 13:22:52 +02:00
parent c59f5552af
commit d5313f95ea
2 changed files with 108 additions and 0 deletions

View File

@@ -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/<name>/ 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/<name>/ 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);
});
});

View File

@@ -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);
}