Initial Commit

This commit is contained in:
2026-05-27 23:47:53 +01:00
parent fbc70f96e6
commit 9b28db724f
18103 changed files with 1600554 additions and 0 deletions

View File

@@ -0,0 +1,160 @@
import { existsSync, readdirSync, readFileSync, statSync } from "node:fs";
import { join, resolve } from "node:path";
import { describe, expect, it } from "@suaveplan/testing/runner";
const PKG_ROOT = resolve(__dirname, "..");
const PKG_JSON = JSON.parse(
readFileSync(join(PKG_ROOT, "package.json"), "utf8")
) as { exports: Record<string, unknown> };
const REQUIRED_HOOKS = [
"use-engine-state",
"use-audio-engine",
"use-timeline",
"use-transport",
"use-transport-time",
"use-track",
"use-plugin-chain",
"use-chain-plugin",
"use-audio-fx-plugin",
"use-parameter",
"use-preset",
"use-chain-snapshot",
"use-bus",
"use-midi-controller",
"use-analyser",
"use-playlist",
];
const REQUIRED_COMPONENTS = [
"plugin-slot",
"plugin-chain-view",
"plugin-parameter-knob",
"plugin-parameter-slider",
"preset-manager",
"midi-controller-setup",
"transport-controls",
"track-list",
"track-row",
"master-meter",
"mixer",
"audio-visualizer",
"audio-player",
"basic-player",
"premium-player",
"microphone-controls",
"glados-controls",
"plugin-loader",
];
const REQUIRED_SURFACES = [
".",
"./provider",
"./hooks",
...REQUIRED_HOOKS.map((h) => `./hooks/${h}`),
"./components",
...REQUIRED_COMPONENTS.map((c) => `./components/${c}`),
"./stores",
"./a11y",
"./error-codes",
];
describe("subpath exports", () => {
it("every Phase-1 surface has a subpath export entry", {
timeout: 5000,
}, () => {
for (const surface of REQUIRED_SURFACES) {
expect(PKG_JSON.exports[surface]).toBeDefined();
}
});
it("every component has a subpath export entry", {
timeout: 5000,
}, () => {
for (const c of REQUIRED_COMPONENTS) {
expect(PKG_JSON.exports[`./components/${c}`]).toBeDefined();
}
expect(PKG_JSON.exports["./components"]).toBeDefined();
});
it("includes exactly 16 hook subpaths (13 foundation + 1 Phase 3 + 2 player-tier)", {
timeout: 5000,
}, () => {
const hookKeys = Object.keys(PKG_JSON.exports).filter((k) =>
k.startsWith("./hooks/")
);
expect(hookKeys.length).toBe(16);
});
});
const STORY_NAME_REGEX = /^(Default|Interactive|Edge.*)$/;
function readStoryNames(filePath: string): ReadonlyArray<string> {
if (!existsSync(filePath)) return [];
const text = readFileSync(filePath, "utf8");
const names: string[] = [];
const re = /export\s+const\s+(\w+)\s*:\s*Story\s*=/g;
let m: RegExpExecArray | null = re.exec(text);
while (m !== null) {
names.push(m[1] as string);
m = re.exec(text);
}
return names;
}
interface ExportedComponentFile {
readonly path: string;
readonly storyPath: string;
}
function listExportedComponentFiles(): ExportedComponentFile[] {
const files: ExportedComponentFile[] = [];
const componentsDir = join(PKG_ROOT, "src", "components");
for (const name of readdirSync(componentsDir)) {
const dir = join(componentsDir, name);
if (!statSync(dir).isDirectory()) continue;
if (name === "internal") continue;
files.push({
path: join(dir, `${name}.tsx`),
storyPath: join(dir, `${name}.stories.tsx`),
});
}
files.push({
path: join(PKG_ROOT, "src", "provider", "audio-provider.tsx"),
storyPath: join(
PKG_ROOT,
"src",
"provider",
"audio-provider.stories.tsx"
),
});
return files;
}
describe("Storybook coverage gate", () => {
it("every component has default, interactive, and edge-case Storybook stories", {
timeout: 5000,
}, () => {
const componentFiles = listExportedComponentFiles();
const missing: string[] = [];
for (const file of componentFiles) {
if (!existsSync(file.storyPath)) {
missing.push(`${file.path}: no sibling ${file.storyPath}`);
continue;
}
const names = readStoryNames(file.storyPath);
const matched = names.filter((n) => STORY_NAME_REGEX.test(n));
const hasDefault = matched.includes("Default");
const hasInteractive = matched.includes("Interactive");
const hasEdge = matched.some((n) => n.startsWith("Edge"));
if (!hasDefault || !hasInteractive || !hasEdge) {
missing.push(
`${file.path}: stories=[${names.join(
", "
)}]; require Default + Interactive + Edge*`
);
}
}
expect(missing).toEqual([]);
});
});

View File

@@ -0,0 +1,131 @@
/**
* Real `MidiAccessLike` + `MidiAccessAdapter` fixtures — mirrors the
* `tests/fake-midi.ts` shipped by `@suaveplan/audio-controllers`. Lives
* outside `src/` so it does not appear in coverage reports yet remains
* shared by every Phase-3 unit / story / integration test.
*
* Re-implements the audio-controllers fixture locally because that
* fixture is not published through the package exports. The shape is
* intentionally identical so the controller plumbing is unchanged.
*/
import type {
MidiAccessAdapter,
MidiAccessLike,
MidiInputLike,
MidiOutputLike,
} from "@suaveplan/audio-controllers";
export interface FakeMidiInput extends MidiInputLike {
emit(data: ReadonlyArray<number>, timeStamp?: number): void;
}
export interface FakeMidiOutput extends MidiOutputLike {
readonly sent: number[][];
disconnect(): void;
}
export interface FakeMidiAccess extends MidiAccessLike {
readonly mutableInputs: Map<string, FakeMidiInput>;
readonly mutableOutputs: Map<string, FakeMidiOutput>;
disconnectPort(port: FakeMidiInput | FakeMidiOutput): void;
reconnectPort(port: FakeMidiInput | FakeMidiOutput): void;
}
function createInput(id: string, name: string): FakeMidiInput {
const port: FakeMidiInput = {
id,
name,
state: "connected",
type: "input",
onmidimessage: null,
emit(data: ReadonlyArray<number>, timeStamp = 0) {
port.onmidimessage?.({
data: new Uint8Array(data),
timeStamp,
});
},
} as FakeMidiInput;
return port;
}
function createOutput(id: string, name: string): FakeMidiOutput {
const sent: number[][] = [];
let connectedState: "connected" | "disconnected" = "connected";
const port = {
id,
name,
get state(): "connected" | "disconnected" {
return connectedState;
},
type: "output" as const,
sent,
send(data: ReadonlyArray<number> | Uint8Array) {
sent.push(Array.isArray(data) ? data.slice() : Array.from(data));
},
disconnect() {
connectedState = "disconnected";
},
} satisfies FakeMidiOutput;
return port;
}
export function createFakeAccess(
options: { readonly inputName?: string; readonly outputName?: string } = {}
): FakeMidiAccess {
const inputName = options.inputName ?? "Demo Input";
const outputName = options.outputName ?? "Demo Output";
const input = createInput("in-1", inputName);
const output = createOutput("out-1", outputName);
const mutableInputs = new Map<string, FakeMidiInput>([[input.id, input]]);
const mutableOutputs = new Map<string, FakeMidiOutput>([
[output.id, output],
]);
const access: FakeMidiAccess = {
get inputs(): ReadonlyMap<string, MidiInputLike> {
return mutableInputs;
},
get outputs(): ReadonlyMap<string, MidiOutputLike> {
return mutableOutputs;
},
mutableInputs,
mutableOutputs,
onstatechange: null,
disconnectPort(port) {
if (port.type === "input") mutableInputs.delete(port.id);
else {
port.disconnect();
mutableOutputs.delete(port.id);
}
access.onstatechange?.({
port: { ...port, state: "disconnected" },
});
},
reconnectPort(port) {
if (port.type === "input") mutableInputs.set(port.id, port);
else mutableOutputs.set(port.id, port);
access.onstatechange?.({
port: { ...port, state: "connected" },
});
},
};
return access;
}
export function createFakeAdapter(
access: FakeMidiAccess,
options: { readonly reject?: Error } = {}
): MidiAccessAdapter {
return {
async requestAccess() {
if (options.reject) throw options.reject;
return access;
},
};
}
export function getFirstInput(access: FakeMidiAccess): FakeMidiInput {
const first = access.mutableInputs.values().next().value;
if (!first) throw new Error("no input");
return first;
}

View File

@@ -0,0 +1,120 @@
/**
* Real `TransportLike` implementation backed by an in-memory event log.
* Mirrors the fixture shipped by `@suaveplan/audio-controllers/tests/
* fake-transport.ts`. Lives outside `src/` so it is excluded from
* coverage but available to every test in this package.
*/
import type {
BarsBeatsSixteenths,
SignalLike,
TransportCallback,
TransportLike,
TransportTime,
} from "@suaveplan/audio-transport";
interface ScheduledRepeat {
readonly id: number;
readonly callback: TransportCallback;
readonly interval: TransportTime;
}
interface ScheduledOnce {
readonly id: number;
readonly callback: TransportCallback;
readonly time: number;
}
const NOOP_BBS: BarsBeatsSixteenths = "0:0:0";
function makeFakeSignal(): SignalLike {
let v = 120;
const points: { time: number; value: number; type: "set" }[] = [];
const sig: SignalLike = {
get points() {
return points.slice();
},
get initialValue() {
return 120;
},
get value() {
return v;
},
setValueAtTime(value: number, time: number) {
v = value;
points.push({ time, value, type: "set" });
},
linearRampToValueAtTime(value: number) {
v = value;
},
exponentialRampToValueAtTime(value: number) {
v = value;
},
getValueAtTime() {
return v;
},
cancelScheduledValues() {
points.length = 0;
},
};
return sig;
}
export class FakeTransport implements TransportLike {
public bpm: SignalLike = makeFakeSignal();
public readonly ppq = 192;
public readonly timeSignature = 4;
public swing = 0;
public swingSubdivision: TransportTime = "8n";
public loop = false;
public loopStart: TransportTime = 0;
public loopEnd: TransportTime = 0;
public state: "stopped" | "playing" | "paused" = "stopped";
public position: BarsBeatsSixteenths = NOOP_BBS;
public ticks = 0;
public seconds = 0;
public readonly repeats = new Map<number, ScheduledRepeat>();
public readonly onces = new Map<number, ScheduledOnce>();
private nextId = 1;
start(): void {
this.state = "playing";
}
stop(): void {
this.state = "stopped";
}
pause(): void {
this.state = "paused";
}
schedule(callback: TransportCallback, time: TransportTime): number {
return this.scheduleOnce(callback, time);
}
scheduleOnce(callback: TransportCallback, time: TransportTime): number {
const id = this.nextId++;
const num = typeof time === "number" ? time : 0;
this.onces.set(id, { id, callback, time: num });
return id;
}
scheduleRepeat(
callback: TransportCallback,
interval: TransportTime
): number {
const id = this.nextId++;
this.repeats.set(id, { id, callback, interval });
return id;
}
cancel(eventId: number): void {
this.repeats.delete(eventId);
this.onces.delete(eventId);
}
cancelAfter(_time: TransportTime): void {
// unused
}
toSeconds(time: TransportTime): number {
return typeof time === "number" ? time / this.ppq : 0;
}
toTicks(time: TransportTime): number {
return typeof time === "number" ? time * this.ppq : 0;
}
}

View File

@@ -0,0 +1,104 @@
/**
* Main-branch component families captured at authoring time of the
* enterprise-react-audio-components-extras change. Read live via:
*
* ls /mnt/s/web/suaveplan/packages/react/react-audio/src/components/
*
* Updated 2026-05-19. Each family maps to one or more genesis surfaces.
* The parity test asserts that every entry's `genesisSubpaths` resolves
* to a real exports entry in `@suaveplan/react-audio`.
*/
export interface ParityEntry {
/** Main-branch family directory name. */
readonly mainFamily: string;
/** Genesis subpath(s) covering the family. */
readonly genesisSubpaths: ReadonlyArray<string>;
/**
* Human-readable rationale when the genesis equivalent is not a
* direct rename (e.g. plugin-controls is folded into PluginSlot).
*/
readonly notes?: string;
}
export const MAIN_BRANCH_COMPONENT_FAMILIES: ReadonlyArray<ParityEntry> = [
{
mainFamily: "audio-dashboard",
genesisSubpaths: [
"./components/mixer",
"./components/transport-controls",
],
notes: "audio-dashboard upstream is a composition over mixer + transport + visualiser; genesis composes the same surface from <Mixer>, <TransportControls>, <AudioVisualizer>.",
},
{
mainFamily: "audio-engine-controls",
genesisSubpaths: ["./components/transport-controls"],
notes: "audio-engine-controls upstream is the transport-style play/stop/seek/tempo bar.",
},
{
mainFamily: "audio-player",
genesisSubpaths: ["./components/audio-player"],
},
{
mainFamily: "audio-provider",
genesisSubpaths: ["./provider"],
notes: "audio-provider upstream is the React context provider; genesis exports <AudioProvider> from the ./provider subpath.",
},
{
mainFamily: "audio-visualizer",
genesisSubpaths: ["./components/audio-visualizer"],
},
{
mainFamily: "basic-player",
genesisSubpaths: ["./components/basic-player"],
},
{
mainFamily: "glados-controls",
genesisSubpaths: ["./components/glados-controls"],
},
{
mainFamily: "microphone-controls",
genesisSubpaths: ["./components/microphone-controls"],
},
{
mainFamily: "mixer-controls",
genesisSubpaths: ["./components/mixer"],
notes: "Renamed mixer-controls → mixer in genesis.",
},
{
mainFamily: "players",
genesisSubpaths: [
"./components/audio-player",
"./components/basic-player",
"./components/premium-player",
],
notes: "Upstream `players/` directory holds premium-player; genesis splits into three sibling player components.",
},
{
mainFamily: "plugin-chain",
genesisSubpaths: ["./components/plugin-chain-view"],
notes: "Renamed plugin-chain → plugin-chain-view in genesis.",
},
{
mainFamily: "plugin-controls",
genesisSubpaths: [
"./components/plugin-slot",
"./components/plugin-parameter-knob",
"./components/plugin-parameter-slider",
],
notes: "Upstream plugin-controls contains the EQ/Compressor/Delay/Reverb specialised slot renderers; genesis exposes the same surface via <PluginSlot> + the `slotRenderers` provider option, plus the primitive knob/slider components.",
},
{
mainFamily: "plugin-loader",
genesisSubpaths: ["./components/plugin-loader"],
},
{
mainFamily: "plugins",
genesisSubpaths: ["./components/audio-player"],
notes: "Upstream `plugins/` is the audio-player's internal subcomponents (library-panel, queue-panel, progress-bar, track-info, volume-control). Genesis folds them into <AudioPlayer>'s control surface.",
},
{
mainFamily: "preset-manager",
genesisSubpaths: ["./components/preset-manager"],
},
];

View File

@@ -0,0 +1,53 @@
import { readFileSync } from "node:fs";
import { join, resolve } from "node:path";
import { describe, expect, it } from "@suaveplan/testing/runner";
import { MAIN_BRANCH_COMPONENT_FAMILIES } from "./parity-fixture.js";
const PKG_ROOT = resolve(__dirname, "..");
const PKG_JSON = JSON.parse(
readFileSync(join(PKG_ROOT, "package.json"), "utf8")
) as { exports: Record<string, unknown> };
describe("main-branch parity", () => {
it("every main-branch component family has a genesis equivalent", {
timeout: 5000,
}, () => {
const missing: string[] = [];
for (const entry of MAIN_BRANCH_COMPONENT_FAMILIES) {
for (const subpath of entry.genesisSubpaths) {
if (PKG_JSON.exports[subpath] === undefined) {
missing.push(
`${entry.mainFamily} -> ${subpath} (not exported)`
);
}
}
}
expect(missing).toEqual([]);
});
it("fixture lists every required parity family", {
timeout: 5000,
}, () => {
const expected = [
"audio-dashboard",
"audio-engine-controls",
"audio-player",
"audio-provider",
"audio-visualizer",
"basic-player",
"glados-controls",
"microphone-controls",
"mixer-controls",
"players",
"plugin-chain",
"plugin-controls",
"plugin-loader",
"plugins",
"preset-manager",
];
const got = MAIN_BRANCH_COMPONENT_FAMILIES.map((e) => e.mainFamily);
for (const name of expected) {
expect(got.includes(name)).toBe(true);
}
});
});

View File

@@ -0,0 +1,77 @@
import { existsSync, readdirSync, readFileSync, statSync } from "node:fs";
import { join, resolve } from "node:path";
import { describe, expect, it } from "@suaveplan/testing/runner";
const PKG_ROOT = resolve(__dirname, "..");
const DIST = join(PKG_ROOT, "dist");
const ALLOWED_RUNTIME_DEPS = new Set([
"@suaveplan/audio-controllers",
"@suaveplan/audio-engine-browser",
"@suaveplan/audio-fx-plugin-audio-player",
"@suaveplan/audio-fx-plugin-base",
"@suaveplan/audio-fx-plugin-glados",
"@suaveplan/audio-fx-plugin-microphone",
"@suaveplan/audio-fx-plugin-mixer",
"@suaveplan/audio-timeline",
"@suaveplan/audio-transport",
"@suaveplan/error",
"@suaveplan/events",
"@suaveplan/opentelemetry",
"@suaveplan/types",
"tslib",
"zod",
"zustand",
]);
function walk(dir: string, out: string[] = []): string[] {
if (!existsSync(dir)) return out;
for (const name of readdirSync(dir)) {
const p = join(dir, name);
if (statSync(p).isDirectory()) walk(p, out);
else if (/\.(js|mjs|cjs)$/.test(name)) out.push(p);
}
return out;
}
describe("react-audio purity gates", () => {
it("runtime dependency set matches the allowlist", {
timeout: 5000,
}, () => {
const pkg = JSON.parse(
readFileSync(join(PKG_ROOT, "package.json"), "utf8")
) as { dependencies?: Record<string, string> };
const deps = Object.keys(pkg.dependencies ?? {});
for (const dep of deps) {
expect(ALLOWED_RUNTIME_DEPS.has(dep)).toBe(true);
}
for (const allowed of ALLOWED_RUNTIME_DEPS) {
expect(deps.includes(allowed)).toBe(true);
}
});
it("dist references no audio context or Node globals", {
timeout: 30000,
}, () => {
if (!existsSync(DIST)) {
// Skip pre-build — `bunx turbo build` will rerun.
return;
}
const files = walk(DIST);
const forbidden = [
/\bnew\s+AudioContext\b/,
/\bnew\s+OfflineAudioContext\b/,
/\binstanceof\s+AudioNode\b/,
/['"]node:fs['"]/,
/['"]node:path['"]/,
/\bprocess\.env\b/,
/\bBuffer\.from\b/,
];
for (const file of files) {
const text = readFileSync(file, "utf8");
for (const rx of forbidden) {
expect(rx.test(text)).toBe(false);
}
}
});
});