Initial commit
This commit is contained in:
176
turbo/generators/lib/core-package-utils.ts
Normal file
176
turbo/generators/lib/core-package-utils.ts
Normal file
@@ -0,0 +1,176 @@
|
||||
import {
|
||||
existsSync,
|
||||
readFileSync,
|
||||
writeFileSync,
|
||||
readdirSync,
|
||||
statSync,
|
||||
} from "node:fs";
|
||||
import { join, relative } from "node:path";
|
||||
import type { PlopTypes } from "@turbo/gen";
|
||||
|
||||
/**
|
||||
* 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);
|
||||
}
|
||||
|
||||
/**
|
||||
* Inserts a code block immediately after a `// <gen:NAME>` anchor in a file.
|
||||
* Idempotent: refuses to insert if the exact block already follows the anchor.
|
||||
*/
|
||||
export function splicePluginRulesAt(
|
||||
filePath: string,
|
||||
anchorName: string,
|
||||
block: string,
|
||||
): void {
|
||||
const source = readFileSync(filePath, "utf8");
|
||||
const anchor = `// <gen:${anchorName}>`;
|
||||
const idx = source.indexOf(anchor);
|
||||
if (idx === -1) {
|
||||
throw new Error(`Anchor ${anchor} not found in ${filePath}`);
|
||||
}
|
||||
const after = source.slice(idx + anchor.length);
|
||||
if (after.trimStart().startsWith(block.trim())) return; // idempotent
|
||||
const updated = source.slice(0, idx + anchor.length) + "\n" + block + after;
|
||||
writeFileSync(filePath, updated);
|
||||
}
|
||||
|
||||
/**
|
||||
* Inserts an import line immediately after the matching anchor. Idempotent.
|
||||
*/
|
||||
export function splicePluginImportsAt(
|
||||
filePath: string,
|
||||
anchorName: string,
|
||||
importLine: string,
|
||||
): void {
|
||||
splicePluginRulesAt(filePath, anchorName, importLine);
|
||||
}
|
||||
|
||||
/**
|
||||
* Inserts a `{ type, pattern, ...opts }` entry into the boundaries/elements
|
||||
* array of `core-eslint/base.js`, placed immediately BEFORE the
|
||||
* `packages/core-*` wildcard so its more-specific match wins. Idempotent.
|
||||
*/
|
||||
export function addBoundariesEntry(
|
||||
baseJsPath: string,
|
||||
packagePath: string,
|
||||
opts: { mode?: "folder" } = {},
|
||||
): void {
|
||||
const source = readFileSync(baseJsPath, "utf8");
|
||||
if (source.includes(`pattern: "${packagePath}"`)) return; // idempotent
|
||||
const wildcardLine = source.match(
|
||||
/(\s*\{\s*type:\s*"core",\s*pattern:\s*"packages\/core-\*"[^}]*\},)/,
|
||||
);
|
||||
if (!wildcardLine) {
|
||||
throw new Error(`packages/core-* wildcard not found in ${baseJsPath}`);
|
||||
}
|
||||
const modeFragment = opts.mode ? `, mode: "${opts.mode}"` : "";
|
||||
const newEntry = ` { type: "core", pattern: "${packagePath}"${modeFragment} },\n`;
|
||||
const updated = source.replace(
|
||||
wildcardLine[0],
|
||||
`\n${newEntry}${wildcardLine[1]}`,
|
||||
);
|
||||
writeFileSync(baseJsPath, updated);
|
||||
}
|
||||
|
||||
/**
|
||||
* Walks turbo/generators/templates/<srcPrefix>/ recursively. For each .hbs
|
||||
* file, returns a plop `add` action that emits the file (without .hbs
|
||||
* extension) at <destPrefix>/<relative-path>. The actions are sorted so
|
||||
* directory creation is deterministic.
|
||||
*
|
||||
* Set `opts.force` to overwrite existing files (idempotent re-runs).
|
||||
*/
|
||||
export function emitTemplateTree(
|
||||
srcPrefix: string,
|
||||
destPrefix: string,
|
||||
opts: { templatesRoot?: string; force?: boolean } = {},
|
||||
): PlopTypes.AddActionConfig[] {
|
||||
// The templates directory is resolved in priority order:
|
||||
// 1. opts.templatesRoot — test injection (temp directory)
|
||||
// 2. cwd/turbo/generators/templates — turbo gen context (cwd = repo root)
|
||||
// 3. cwd/templates — vitest context (cwd = turbo/generators)
|
||||
let root: string;
|
||||
if (opts.templatesRoot) {
|
||||
root = opts.templatesRoot;
|
||||
} else {
|
||||
const fromRepoRoot = join(
|
||||
process.cwd(),
|
||||
"turbo",
|
||||
"generators",
|
||||
"templates",
|
||||
);
|
||||
const fromGeneratorsDir = join(process.cwd(), "templates");
|
||||
root = existsSync(fromRepoRoot) ? fromRepoRoot : fromGeneratorsDir;
|
||||
}
|
||||
const srcRoot = join(root, srcPrefix);
|
||||
const out: PlopTypes.AddActionConfig[] = [];
|
||||
walkHbs(srcRoot, srcRoot, srcPrefix, destPrefix, out, opts.force ?? false);
|
||||
out.sort((a, b) => (a.path ?? "").localeCompare(b.path ?? ""));
|
||||
return out;
|
||||
}
|
||||
|
||||
function walkHbs(
|
||||
topRoot: string,
|
||||
dir: string,
|
||||
srcPrefix: string,
|
||||
destPrefix: string,
|
||||
out: PlopTypes.AddActionConfig[],
|
||||
force: boolean,
|
||||
): void {
|
||||
for (const name of readdirSync(dir)) {
|
||||
const full = join(dir, name);
|
||||
if (statSync(full).isDirectory()) {
|
||||
walkHbs(topRoot, full, srcPrefix, destPrefix, out, force);
|
||||
continue;
|
||||
}
|
||||
if (!name.endsWith(".hbs")) continue;
|
||||
const rel = relative(topRoot, full).replace(/\.hbs$/, "");
|
||||
out.push({
|
||||
type: "add",
|
||||
path: join(destPrefix, rel).replace(/\\/g, "/"),
|
||||
templateFile: join(
|
||||
"templates",
|
||||
srcPrefix,
|
||||
relative(topRoot, full),
|
||||
).replace(/\\/g, "/"),
|
||||
force,
|
||||
} as PlopTypes.AddActionConfig);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user