Initial Commit
This commit is contained in:
208
scripts/split-audio-fx-plugin/scaffold.ts
Normal file
208
scripts/split-audio-fx-plugin/scaffold.ts
Normal file
@@ -0,0 +1,208 @@
|
||||
#!/usr/bin/env bun
|
||||
/**
|
||||
* Scaffold a new @suaveplan/audio-fx-plugin-<slug> package by:
|
||||
* 1. Copying every template under ./template/, replacing {PLUGIN_NAME} markers.
|
||||
* 2. git-mv'ing the source folder from packages/audio/audio-plugins-fx/src/plugins/<src-dir>/ → packages/audio-fx/audio-fx-plugin-<slug>/src/.
|
||||
* 3. Rewriting relative imports inside the moved sources to point at @suaveplan/audio-fx-plugin-base/* barrels.
|
||||
* 4. Rewriting @suaveplan/audio-plugins-fx/<slug> doc references to @suaveplan/audio-fx-plugin-<slug>.
|
||||
*
|
||||
* Usage:
|
||||
* bun scripts/split-audio-fx-plugin/scaffold.ts --name compressor
|
||||
* bun scripts/split-audio-fx-plugin/scaffold.ts --name game-audio --source-dir game
|
||||
*
|
||||
* After scaffold:
|
||||
* bunx turbo lint typecheck test --filter=@suaveplan/audio-fx-plugin-<slug>
|
||||
* bunx biome check --write src/ # may need formatting fix
|
||||
*/
|
||||
import { spawnSync } from "node:child_process";
|
||||
import {
|
||||
existsSync,
|
||||
mkdirSync,
|
||||
readdirSync,
|
||||
readFileSync,
|
||||
rmdirSync,
|
||||
statSync,
|
||||
writeFileSync,
|
||||
} from "node:fs";
|
||||
import { dirname, join } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
|
||||
const SLUG_RE = /^[a-z][a-z0-9-]*$/;
|
||||
const SCRIPT_DIR = dirname(fileURLToPath(import.meta.url));
|
||||
const TEMPLATE_DIR = join(SCRIPT_DIR, "template");
|
||||
const REPO_ROOT = join(SCRIPT_DIR, "..", "..");
|
||||
const SOURCE_PARENT = join(
|
||||
REPO_ROOT,
|
||||
"packages",
|
||||
"audio",
|
||||
"audio-plugins-fx",
|
||||
"src",
|
||||
"plugins"
|
||||
);
|
||||
const DEST_PARENT = join(REPO_ROOT, "packages", "audio");
|
||||
|
||||
interface Args {
|
||||
name: string;
|
||||
sourceDir: string;
|
||||
description: string;
|
||||
}
|
||||
|
||||
function parseArgs(): Args {
|
||||
const argv = process.argv.slice(2);
|
||||
let name = "";
|
||||
let sourceDir = "";
|
||||
let description = "";
|
||||
for (let i = 0; i < argv.length; i++) {
|
||||
const flag = argv[i];
|
||||
const next = argv[i + 1];
|
||||
if (!next) continue;
|
||||
if (flag === "--name") {
|
||||
name = next;
|
||||
i++;
|
||||
} else if (flag === "--source-dir") {
|
||||
sourceDir = next;
|
||||
i++;
|
||||
} else if (flag === "--description") {
|
||||
description = next;
|
||||
i++;
|
||||
}
|
||||
}
|
||||
if (!name || !SLUG_RE.test(name)) {
|
||||
throw new Error(
|
||||
`--name must match /^[a-z][a-z0-9-]*$/ (got: ${JSON.stringify(name)})`
|
||||
);
|
||||
}
|
||||
if (!sourceDir) sourceDir = name;
|
||||
if (!description) {
|
||||
description = `${name} plugin for the @suaveplan/audio-fx-plugin-* family.`;
|
||||
}
|
||||
return { name, sourceDir, description };
|
||||
}
|
||||
|
||||
function gitMv(from: string, to: string): void {
|
||||
const result = spawnSync("git", ["mv", from, to], {
|
||||
cwd: REPO_ROOT,
|
||||
stdio: "inherit",
|
||||
});
|
||||
if (result.status !== 0) {
|
||||
throw new Error(
|
||||
`git mv ${from} ${to} failed with status ${result.status}`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function walkFiles(dir: string, out: string[] = []): string[] {
|
||||
for (const entry of readdirSync(dir)) {
|
||||
const full = join(dir, entry);
|
||||
const st = statSync(full);
|
||||
if (st.isDirectory()) walkFiles(full, out);
|
||||
else out.push(full);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
function substitute(content: string, args: Args): string {
|
||||
return content
|
||||
.replaceAll("{PLUGIN_NAME}", args.name)
|
||||
.replaceAll("{PLUGIN_DESCRIPTION}", args.description);
|
||||
}
|
||||
|
||||
function copyTemplates(destPkg: string, args: Args): void {
|
||||
for (const entry of readdirSync(TEMPLATE_DIR)) {
|
||||
const src = join(TEMPLATE_DIR, entry);
|
||||
const destName = entry.endsWith(".template")
|
||||
? entry.slice(0, -".template".length)
|
||||
: entry;
|
||||
const dest = join(destPkg, destName);
|
||||
const content = readFileSync(src, "utf8");
|
||||
writeFileSync(dest, substitute(content, args));
|
||||
}
|
||||
}
|
||||
|
||||
function moveSource(args: Args, destPkg: string): void {
|
||||
const srcDir = join(SOURCE_PARENT, args.sourceDir);
|
||||
if (!existsSync(srcDir)) {
|
||||
throw new Error(`source dir not found: ${srcDir}`);
|
||||
}
|
||||
const destSrc = join(destPkg, "src");
|
||||
for (const child of readdirSync(srcDir)) {
|
||||
gitMv(join(srcDir, child), join(destSrc, child));
|
||||
}
|
||||
try {
|
||||
rmdirSync(srcDir);
|
||||
} catch {
|
||||
// Non-fatal: if non-empty, leave for manual inspection.
|
||||
}
|
||||
}
|
||||
|
||||
const RELATIVE_IMPORT_REWRITES: Array<[RegExp, string]> = [
|
||||
// `base/<anything>.js` → base barrel
|
||||
[
|
||||
/from "\.\.\/\.\.\/base\/[a-z0-9./-]+\.js"/g,
|
||||
'from "@suaveplan/audio-fx-plugin-base/base"',
|
||||
],
|
||||
// `chain/<anything>.js` (including nested context/, event-bus/, etc.) → chain barrel
|
||||
[
|
||||
/from "\.\.\/\.\.\/chain\/[a-z0-9./-]+\.js"/g,
|
||||
'from "@suaveplan/audio-fx-plugin-base/chain"',
|
||||
],
|
||||
// `registry/<anything>.js` → registry barrel
|
||||
[
|
||||
/from "\.\.\/\.\.\/registry\/[a-z0-9./-]+\.js"/g,
|
||||
'from "@suaveplan/audio-fx-plugin-base/registry"',
|
||||
],
|
||||
// root-level error-codes
|
||||
[
|
||||
/from "\.\.\/\.\.\/error-codes\.js"/g,
|
||||
'from "@suaveplan/audio-fx-plugin-base/error-codes"',
|
||||
],
|
||||
];
|
||||
|
||||
const CROSS_PLUGIN_RE = /from "\.\.\/[a-z][a-z0-9-]*\//;
|
||||
|
||||
function rewriteImports(destPkg: string, args: Args): void {
|
||||
const srcDir = join(destPkg, "src");
|
||||
for (const file of walkFiles(srcDir)) {
|
||||
if (!file.endsWith(".ts") && !file.endsWith(".md")) continue;
|
||||
let content = readFileSync(file, "utf8");
|
||||
const before = content;
|
||||
for (const [pattern, replacement] of RELATIVE_IMPORT_REWRITES) {
|
||||
content = content.replace(pattern, replacement);
|
||||
}
|
||||
content = content.replaceAll(
|
||||
`@suaveplan/audio-plugins-fx/${args.name}`,
|
||||
`@suaveplan/audio-fx-plugin-${args.name}`
|
||||
);
|
||||
const lines = content.split("\n");
|
||||
for (const line of lines) {
|
||||
if (
|
||||
CROSS_PLUGIN_RE.test(line) &&
|
||||
!line.includes("audio-fx-plugin-base")
|
||||
) {
|
||||
throw new Error(
|
||||
`Cross-plugin import detected in ${file}: ${line.trim()} — abort. ` +
|
||||
"Fix the moved source manually, then re-run."
|
||||
);
|
||||
}
|
||||
}
|
||||
if (content !== before) writeFileSync(file, content);
|
||||
}
|
||||
}
|
||||
|
||||
function main(): void {
|
||||
const args = parseArgs();
|
||||
const destPkg = join(DEST_PARENT, `audio-fx-plugin-${args.name}`);
|
||||
if (existsSync(destPkg)) {
|
||||
throw new Error(`destination already exists: ${destPkg}`);
|
||||
}
|
||||
mkdirSync(join(destPkg, "src"), { recursive: true });
|
||||
copyTemplates(destPkg, args);
|
||||
moveSource(args, destPkg);
|
||||
rewriteImports(destPkg, args);
|
||||
console.log(
|
||||
`scaffolded @suaveplan/audio-fx-plugin-${args.name} at ${destPkg}\n` +
|
||||
`next: bunx turbo lint typecheck test --filter=@suaveplan/audio-fx-plugin-${args.name}`
|
||||
);
|
||||
}
|
||||
|
||||
main();
|
||||
Reference in New Issue
Block a user