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();
|
||||
22
scripts/split-audio-fx-plugin/template/README.md.template
Normal file
22
scripts/split-audio-fx-plugin/template/README.md.template
Normal file
@@ -0,0 +1,22 @@
|
||||
# @suaveplan/audio-fx-plugin-{PLUGIN_NAME}
|
||||
|
||||
{PLUGIN_DESCRIPTION}
|
||||
|
||||
Member of the `@suaveplan/audio-fx-plugin-*` family. Universal-tier (browser / server / offline / worklet identical). Depends on `@suaveplan/audio-fx-plugin-base` for the `AudioFxPlugin` contract; declares no other `@suaveplan/audio-fx-plugin-*` deps.
|
||||
|
||||
## Composition
|
||||
|
||||
```ts
|
||||
import { PluginChain, DefaultBusRegistry } from "@suaveplan/audio-fx-plugin-base/chain";
|
||||
import { /* Plugin */ } from "@suaveplan/audio-fx-plugin-{PLUGIN_NAME}";
|
||||
|
||||
const plugin = new /* Plugin */();
|
||||
const chain = new PluginChain({
|
||||
buses: new DefaultBusRegistry(),
|
||||
plugins: [plugin],
|
||||
});
|
||||
```
|
||||
|
||||
## Status
|
||||
|
||||
Replaces the `./{PLUGIN_NAME}` subpath of the bundled `@suaveplan/audio-plugins-fx`. See `openspec/archive/2026-05-19-split-audio-plugins-fx/` for migration history.
|
||||
47
scripts/split-audio-fx-plugin/template/package.json.template
Normal file
47
scripts/split-audio-fx-plugin/template/package.json.template
Normal file
@@ -0,0 +1,47 @@
|
||||
{
|
||||
"name": "@suaveplan/audio-fx-plugin-{PLUGIN_NAME}",
|
||||
"version": "0.1.0",
|
||||
"type": "module",
|
||||
"sideEffects": false,
|
||||
"description": "{PLUGIN_DESCRIPTION}",
|
||||
"license": "MIT",
|
||||
"author": "Suaveplan Team",
|
||||
"main": "./dist/index.js",
|
||||
"module": "./dist/index.js",
|
||||
"types": "./dist/index.d.ts",
|
||||
"files": ["dist", "README.md"],
|
||||
"exports": {
|
||||
"./package.json": "./package.json",
|
||||
".": {
|
||||
"import": {
|
||||
"types": "./dist/index.d.ts",
|
||||
"default": "./dist/index.js"
|
||||
}
|
||||
}
|
||||
},
|
||||
"scripts": {
|
||||
"build": "vite build",
|
||||
"lint": "biome check --max-diagnostics 500 src/",
|
||||
"typecheck": "tsc --noEmit -p tsconfig.lib.json",
|
||||
"test": "vitest run --passWithNoTests",
|
||||
"test:coverage": "vitest run --coverage --passWithNoTests",
|
||||
"docs:check": "bun run ../../../scripts/docs-check.ts ."
|
||||
},
|
||||
"dependencies": {
|
||||
"@suaveplan/audio-fx-plugin-base": "workspace:^",
|
||||
"@suaveplan/error": "workspace:^",
|
||||
"@suaveplan/types": "workspace:^",
|
||||
"tslib": "catalog:build",
|
||||
"zod": "catalog:forms"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@suaveplan/audio-core": "workspace:^",
|
||||
"@suaveplan/testing": "workspace:^",
|
||||
"@suaveplan/typescript-config": "workspace:^",
|
||||
"@vitest/coverage-istanbul": "catalog:testing",
|
||||
"typescript": "catalog:",
|
||||
"vite": "catalog:build",
|
||||
"vite-plugin-dts": "catalog:build",
|
||||
"vitest": "catalog:testing"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/tsconfig",
|
||||
"extends": "../../../tsconfig.base.json",
|
||||
"compilerOptions": {
|
||||
"rootDir": ".",
|
||||
"outDir": "./dist",
|
||||
"lib": ["ES2022", "DOM"],
|
||||
"types": ["node"]
|
||||
},
|
||||
"include": ["src/**/*.ts"],
|
||||
"exclude": ["dist", "node_modules"]
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/tsconfig",
|
||||
"extends": "../../../tsconfig.base.json",
|
||||
"compilerOptions": {
|
||||
"rootDir": "./src",
|
||||
"outDir": "./dist",
|
||||
"lib": ["ES2022", "DOM"],
|
||||
"types": ["node"]
|
||||
},
|
||||
"include": ["src/**/*.ts"],
|
||||
"exclude": [
|
||||
"src/**/*.test.ts",
|
||||
"src/**/*.bench.ts",
|
||||
"tests/**",
|
||||
"dist",
|
||||
"node_modules"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
import { defineConfig } from "vite";
|
||||
import dts from "vite-plugin-dts";
|
||||
|
||||
export default defineConfig({
|
||||
root: __dirname,
|
||||
cacheDir: "../../../node_modules/.vite/packages/audio-fx-plugin-{PLUGIN_NAME}",
|
||||
plugins: [
|
||||
dts({
|
||||
entryRoot: "src",
|
||||
tsconfigPath: "./tsconfig.lib.json",
|
||||
insertTypesEntry: true,
|
||||
}),
|
||||
],
|
||||
build: {
|
||||
outDir: "./dist",
|
||||
emptyOutDir: true,
|
||||
reportCompressedSize: true,
|
||||
sourcemap: true,
|
||||
lib: {
|
||||
entry: { index: "src/index.ts" },
|
||||
formats: ["es"],
|
||||
},
|
||||
rollupOptions: {
|
||||
treeshake: true,
|
||||
output: {
|
||||
preserveModules: true,
|
||||
preserveModulesRoot: "src",
|
||||
entryFileNames: "[name].js",
|
||||
},
|
||||
external: [
|
||||
/^@suaveplan\//,
|
||||
/^@opentelemetry\//,
|
||||
"tslib",
|
||||
"zod",
|
||||
],
|
||||
},
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,3 @@
|
||||
import { createTestConfig } from "@suaveplan/testing/config";
|
||||
|
||||
export default createTestConfig({ testTimeout: 30_000 });
|
||||
Reference in New Issue
Block a user