209 lines
6.7 KiB
TypeScript
209 lines
6.7 KiB
TypeScript
#!/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();
|