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,31 @@
# @suaveplan/audio-fx-plugin-limiter - limiter plugin for the @suaveplan/audio-fx-plugin-* family.
limiter plugin for the @suaveplan/audio-fx-plugin-* family.
Member of the `@suaveplan/audio-fx-plugin-*` family. Universal (browser / server / offline / worklet identical). Depends on `@suaveplan/audio-fx-plugin-base` for the `AudioFxPlugin` contract; declares no other `@suaveplan/audio-fx-plugin-*` deps.
<!-- begin:auto-docs -->
## Documentation tree
- [limiter](./src/limiter/limiter.md) — `LimiterPlugin` is the universal brick-wall limiter. Its core contract is unconditional: for any bounded input, no outp…
- [limiter](./src/limiter/limiter.md) — `LimiterPlugin` is the universal brick-wall limiter. Its core contract is unconditional: for any bounded input, no outp…
- [schemas](./src/schemas/schemas.md) — Zod schemas validating the construction-time options of `LimiterPlugin` and the (currently empty) preset payload contra…
- [schemas](./src/schemas/schemas.md) — Zod schemas validating the construction-time options of `LimiterPlugin` and the (currently empty) preset payload contra…
<!-- end:auto-docs -->
## Composition
```ts
import { PluginChain, DefaultBusRegistry } from "@suaveplan/audio-fx-plugin-base/chain";
import { /* Plugin */ } from "@suaveplan/audio-fx-plugin-limiter";
const plugin = new /* Plugin */();
const chain = new PluginChain({
buses: new DefaultBusRegistry(),
plugins: [plugin],
});
```
## Status
Replaces the `./limiter` subpath of the bundled `@suaveplan/audio-plugins-fx`.

View File

@@ -0,0 +1,47 @@
{
"name": "@suaveplan/audio-fx-plugin-limiter",
"version": "0.1.0",
"type": "module",
"sideEffects": false,
"description": "limiter plugin for the @suaveplan/audio-fx-plugin-* family.",
"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"
}
}

View File

@@ -0,0 +1,6 @@
export { LimiterPlugin } from "./limiter/index.js";
export type { LimiterPluginOptions } from "./limiter.types.js";
export {
LimiterPluginOptionsSchema,
LimiterPresetDataSchema,
} from "./schemas/index.js";

View File

@@ -0,0 +1,51 @@
# `limiter.types`
## Purpose
TypeScript-only type definitions for `LimiterPlugin`'s construction options. Co-located with the implementation so the public type surface (the `LimiterPluginOptions` interface) and its Zod validator stay in lockstep. Consumers can import the type for typed form state, dependency-injection wiring, or storybook controls without pulling in the plugin's runtime code.
This module declares no runtime values — it is the canonical "shape" contract the constructor accepts, mirrored by `LimiterPluginOptionsSchema` in `schemas/schemas.ts`.
## Features
- `LimiterPluginOptions` — all fields optional so the default constructor (`new LimiterPlugin()`) is valid.
- `readonly` properties — emphasises that the options object is consumed once at construction and never mutated.
- Numeric ranges are documented here via the Zod schema (not encoded in TS types) so a single source of truth governs validation.
## Usage: Basic Example
```ts
import type { LimiterPluginOptions } from "@suaveplan/audio-fx-plugin-limiter";
const opts: LimiterPluginOptions = { initialCeilingDb: -1 };
```
## Usage: Advanced Example
```ts
import type { LimiterPluginOptions } from "@suaveplan/audio-fx-plugin-limiter";
// Use the options type as a state shape for a UI form.
interface LimiterFormState extends Required<LimiterPluginOptions> {
readonly dirty: boolean;
}
function saveLimiterOptions(state: LimiterFormState): LimiterPluginOptions {
return {
initialCeilingDb: state.initialCeilingDb,
initialReleaseMs: state.initialReleaseMs,
};
}
```
## API Reference
- `LimiterPluginOptions`
- `readonly initialCeilingDb?: number` — initial brick-wall ceiling in dB.
- `readonly initialReleaseMs?: number` — initial release time constant in ms.
## Implementation Notes
The options interface deliberately omits a `gainReductionDb` field. That parameter is a telemetry output, not a configurable input — it is always derived from the per-block worst-case attenuation and would not be meaningful as a construction-time value. Constraining the options surface to inputs prevents callers from creating plugins in an impossible "pre-set" telemetry state.
All fields are `readonly` so structural-equality checks on saved options (e.g., undo/redo stacks) do not need defensive cloning. The constructor never mutates the passed object — it copies relevant values into the parameter list once and forgets the input — but typing the surface as `readonly` lets the type system enforce that intent at every call site, including in long-lived editor stores.

View File

@@ -0,0 +1,5 @@
/** Construction options for `LimiterPlugin`. */
export interface LimiterPluginOptions {
readonly initialCeilingDb?: number;
readonly initialReleaseMs?: number;
}

View File

@@ -0,0 +1 @@
export { LimiterPlugin } from "./limiter.js";

View File

@@ -0,0 +1,70 @@
# `limiter`
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
`LimiterPlugin` is the universal brick-wall limiter. Its core contract is unconditional: for any bounded input, no output sample's absolute value exceeds the linear conversion of the `ceiling` parameter. The plugin pairs an instantaneous (zero-lookahead) attack with a one-pole exponential release, so transients are clamped on the sample they occur — no lookahead buffer, no group delay, no latency to report.
The plugin extends `AbstractAudioFxPlugin`, so it runs identically across browser / server / worklet renderers. State is a single linked gain value plus a frame-peak reduction metric for telemetry. `reset()` snaps the gain back to unity in place.
## Features
- Three parameters: `ceiling` (dB, -60..0), `release` (ms, 1..1000), and `gainReductionDb` (telemetry).
- Linked-stereo brick-wall: gain is shared across all input channels per sample, so a transient on one channel attenuates every channel identically — the stereo image stays phase-coherent.
- Zero-lookahead attack: when `|peak| > ceilingLin` at sample `i`, the gain at sample `i` is set to `ceilingLin / |peak|` *before* writing the output, so the ceiling is a hard contract verifiable by audit.
- One-pole release: `gain += releaseCoef · (1 − gain)` per sample with `releaseCoef = 1 − exp(-1/(τ · sr))`.
- `gainReductionDb` is updated once per block with the worst-case in-block reduction so meters get a useful peak reading without per-sample state.
- `getGainReductionDb()` accessor mirrors the parameter for low-friction metering.
## Usage: Basic Example
```ts
import { LimiterPlugin } from "@suaveplan/audio-fx-plugin-limiter";
const limiter = new LimiterPlugin({
initialCeilingDb: -1,
initialReleaseMs: 50,
});
chain.add(limiter);
```
## Usage: Advanced Example
```ts
import { LimiterPlugin } from "@suaveplan/audio-fx-plugin-limiter";
// Master-bus limiter feeding a UI meter from the live gain reduction.
const limiter = new LimiterPlugin({
initialCeilingDb: -0.3,
initialReleaseMs: 80,
});
await limiter.setup(
{ sampleRate: 48_000, channels: 2, blockSize: 512 },
ctx
);
setInterval(() => {
const gr = limiter.getGainReductionDb();
if (gr > 6) {
console.warn(`Master limiter pulling ${gr.toFixed(2)} dB`);
}
}, 100);
```
## API Reference
- `LimiterPlugin` (class extends `AbstractAudioFxPlugin`)
- `new LimiterPlugin(options?: LimiterPluginOptions)` — throws `AUDIO_FX.INVALID_OPTIONS` on malformed options.
- `getGainReductionDb(): number` — last block's peak gain reduction (positive number, in dB).
- Inherited parameter accessors for `ceiling`, `release`, `gainReductionDb`.
## Implementation Notes
The limiter is intentionally a true brick-wall, not a "soft" or "transparent" limiter. We do not interpolate around the threshold or apply a knee. The reason: many use sites layer this plugin downstream of an output-stage compressor that already provides knee shaping, and rely on the limiter for guaranteed bound semantics. Adding a knee would weaken the contract that the test in `output peak never exceeds the configured ceiling` enforces.
There is no lookahead buffer. That keeps the plugin latency-free (`latencySamples: 0`, inherited default) and makes the bound testable by static inspection of the inner loop: at sample `i`, if `peak > ceilingLin` then `gain := ceilingLin / peak` *before* `output[ch][i] = input[ch][i] * gain`. The multiplication therefore can produce at most `|input[ch][i]| * (ceilingLin / |peak|) ≤ ceilingLin`. The downside is that an unusually fast transient (single-sample spike at a very low source sample rate) can produce visible distortion on its leading edge — for those cases a future `LookaheadLimiterPlugin` can sit on a separate subpath without breaking this contract.
`gainReductionDb` is reported as a per-block peak rather than a running average. Meters typically expect peak; if a future surface wants RMS, it can compute that from the gain-reduction stream by subscribing to a parameter automation channel. The release coefficient is recomputed every block from the current `release` parameter so automation works without introducing zipper noise — the one-pole step is small per sample even when the parameter changes mid-block.

View File

@@ -0,0 +1,193 @@
import type { BlockContext, ProcessorConfig } from "@suaveplan/audio-core";
import type {
ChainContext,
ChainHandle,
} from "@suaveplan/audio-fx-plugin-base/base";
import { defaultAmbientState } from "@suaveplan/audio-fx-plugin-base/chain";
import {
AUDIO_FX_CODES,
type AudioFxError,
} from "@suaveplan/audio-fx-plugin-base/error-codes";
import { describe, expect, it } from "@suaveplan/testing/runner";
import { LimiterPlugin } from "./limiter.js";
const SAMPLE_RATE = 48_000;
function blockCtx(): BlockContext {
return {
blockOffsetSamples: 0,
sampleRate: SAMPLE_RATE,
channels: 1,
opts: {},
};
}
function buffers(
size: number,
channels = 1
): { input: Float32Array[]; output: Float32Array[] } {
const input: Float32Array[] = [];
const output: Float32Array[] = [];
for (let c = 0; c < channels; c++) {
input.push(new Float32Array(size));
output.push(new Float32Array(size));
}
return { input, output };
}
function newConfig(blockSize: number, channels = 1): ProcessorConfig {
return { sampleRate: SAMPLE_RATE, channels, blockSize };
}
function newCtx(channels = 1): ChainContext {
const chain: ChainHandle = {
id: "limiter-test",
name: "limiter-test",
channels,
sampleRate: SAMPLE_RATE,
blockSize: 256,
slotCount: 1,
};
return {
chain,
position: { index: 0, total: 1 },
predecessors: [],
successors: [],
ambient: defaultAmbientState(),
buses: { get: () => undefined, list: () => [] },
};
}
describe("LimiterPlugin: contract surface", () => {
it("declares the AudioFxPlugin contract", { timeout: 5_000 }, () => {
const l = new LimiterPlugin();
expect(l.metadata.id).toBe("limiter");
expect(l.metadata.category).toBe("dynamics");
expect(l.parameters.map((p) => p.id)).toEqual([
"ceiling",
"release",
"gainReductionDb",
]);
});
it("rejects malformed options with INVALID_OPTIONS", {
timeout: 5_000,
}, () => {
try {
new LimiterPlugin({ initialCeilingDb: 99 });
} catch (error) {
expect((error as AudioFxError).code).toBe(
AUDIO_FX_CODES.INVALID_OPTIONS
);
return;
}
throw new Error("expected INVALID_OPTIONS");
});
});
describe("LimiterPlugin: output peak never exceeds the configured ceiling", () => {
it("brick-wall: every output sample's |x| <= ceilingLin + 1e-6", {
timeout: 5_000,
}, async () => {
const l = new LimiterPlugin({
initialCeilingDb: -6,
initialReleaseMs: 50,
});
await l.setup(newConfig(SAMPLE_RATE, 1), newCtx(1));
const { input, output } = buffers(SAMPLE_RATE, 1);
// Loud signal: hot sine + occasional transients that exceed ceiling.
for (let i = 0; i < SAMPLE_RATE; i++) {
input[0]![i] =
0.95 * Math.sin((2 * Math.PI * 1_000 * i) / SAMPLE_RATE);
}
// Inject transients well above ceiling.
for (let i = 1_000; i < SAMPLE_RATE; i += 5_000) {
input[0]![i] = 4;
input[0]![i + 1] = -4;
}
l.process(input, output, blockCtx());
const ceilingLin = 10 ** (-6 / 20);
let maxObserved = 0;
for (let i = 0; i < SAMPLE_RATE; i++) {
const a = Math.abs(output[0]![i]!);
if (a > maxObserved) {
maxObserved = a;
}
}
expect(maxObserved).toBeLessThanOrEqual(ceilingLin + 1e-6);
expect(l.getGainReductionDb()).toBeGreaterThan(0);
});
});
describe("LimiterPlugin: signal below ceiling passes through unchanged", () => {
it("|input| < ceiling => output ≈ input (gain == 1)", {
timeout: 5_000,
}, async () => {
const l = new LimiterPlugin({
initialCeilingDb: -1,
initialReleaseMs: 10,
});
await l.setup(newConfig(2048, 1), newCtx(1));
const { input, output } = buffers(2048, 1);
for (let i = 0; i < 2048; i++) {
input[0]![i] =
0.5 * Math.sin((2 * Math.PI * 1_000 * i) / SAMPLE_RATE);
}
l.process(input, output, blockCtx());
for (let i = 0; i < 2048; i++) {
expect(output[0]![i]).toBeCloseTo(input[0]![i]!, 6);
}
expect(l.getGainReductionDb()).toBe(0);
});
});
describe("LimiterPlugin: linked stereo gain", () => {
it("transient on left channel attenuates right channel by same gain", {
timeout: 5_000,
}, async () => {
const l = new LimiterPlugin({ initialCeilingDb: -6 });
await l.setup(newConfig(512, 2), newCtx(2));
const { input, output } = buffers(512, 2);
// Left has a big transient at sample 100; right is steady-mid.
input[0]![100] = 4;
for (let i = 0; i < 512; i++) {
input[1]![i] = 0.5;
}
l.process(input, output, blockCtx());
// At sample 100, gain << 1 due to left transient — right output
// must be reduced by the same gain.
expect(Math.abs(output[1]![100]!)).toBeLessThan(0.5);
expect(Math.abs(output[0]![100]!)).toBeLessThanOrEqual(
10 ** (-6 / 20) + 1e-6
);
});
});
describe("LimiterPlugin: reset clears state", () => {
it("after a hot block + reset, gain returns to 1 and grDb to 0", {
timeout: 5_000,
}, async () => {
const l = new LimiterPlugin({ initialCeilingDb: -6 });
await l.setup(newConfig(256, 1), newCtx(1));
const { input, output } = buffers(256, 1);
for (let i = 0; i < 256; i++) {
input[0]![i] = 4;
}
l.process(input, output, blockCtx());
expect(l.getGainReductionDb()).toBeGreaterThan(0);
l.reset();
expect(l.getGainReductionDb()).toBe(0);
});
});
describe("LimiterPlugin: preset round-trip", () => {
it("getPreset / applyPreset round-trips ceiling parameter", {
timeout: 5_000,
}, () => {
const a = new LimiterPlugin({ initialCeilingDb: -3 });
const snap = a.getPreset();
const b = new LimiterPlugin();
b.applyPreset(snap);
expect(b.getParameter("ceiling")).toBe(-3);
});
});

View File

@@ -0,0 +1,143 @@
import type { BlockContext, ProcessorConfig } from "@suaveplan/audio-core";
import type { ChainContext } from "@suaveplan/audio-fx-plugin-base/base";
import { AbstractAudioFxPlugin } from "@suaveplan/audio-fx-plugin-base/base";
import {
AUDIO_FX_CODES,
AudioFxError,
} from "@suaveplan/audio-fx-plugin-base/error-codes";
import type { LimiterPluginOptions } from "../limiter.types.js";
import {
LimiterPluginOptionsSchema,
LimiterPresetDataSchema,
} from "../schemas/index.js";
/**
* Brick-wall limiter. Per-sample peak detector across all input
* channels chooses the minimum gain such that the linked output peak
* never exceeds `ceiling`. Attack is instantaneous (zero-lookahead) so
* the ceiling is a hard contract: no transient slips through. Release
* is a one-pole exponential ramp back toward unity. `gainReductionDb`
* is exposed as a parameter for metering.
*/
export class LimiterPlugin extends AbstractAudioFxPlugin {
private currentGain = 1;
private currentGrDb = 0;
public constructor(options: LimiterPluginOptions = {}) {
const parsed = LimiterPluginOptionsSchema.safeParse(options);
if (!parsed.success) {
throw new AudioFxError(
AUDIO_FX_CODES.INVALID_OPTIONS,
"audio-fx.limiter-invalid-options",
{ context: { issues: parsed.error.issues } }
);
}
const opts = parsed.data;
super({
metadata: {
id: "limiter",
name: "Limiter",
version: "1.0.0",
category: "dynamics",
},
schema: LimiterPresetDataSchema,
io: {
inputs: [{ count: "preserve" }],
outputs: [{ count: "preserve" }],
},
parameters: [
{
id: "ceiling",
name: "Ceiling",
value: opts.initialCeilingDb ?? -1,
min: -60,
max: 0,
default: -1,
unit: "db",
automatable: true,
},
{
id: "release",
name: "Release",
value: opts.initialReleaseMs ?? 50,
min: 1,
max: 1_000,
default: 50,
unit: "ms",
automatable: true,
},
{
id: "gainReductionDb",
name: "Gain Reduction",
value: 0,
min: 0,
max: 60,
default: 0,
unit: "db",
automatable: false,
},
],
supportedSchemaVersion: 1,
});
}
public getGainReductionDb(): number {
return this.currentGrDb;
}
protected override setupState(
_config: ProcessorConfig,
_chain: ChainContext
): void {
this.currentGain = 1;
this.currentGrDb = 0;
}
protected override resetState(): void {
this.currentGain = 1;
this.currentGrDb = 0;
}
protected processBlock(
input: Float32Array[],
output: Float32Array[],
_ctx: BlockContext
): void {
const ceilingDb = this.parameterValue("ceiling");
const releaseMs = this.parameterValue("release");
const sr = this.config!.sampleRate;
const ceilingLin = 10 ** (ceilingDb / 20);
const releaseCoef = 1 - Math.exp(-1 / ((releaseMs / 1000) * sr));
const blockSize = input[0]!.length;
let gain = this.currentGain;
let maxGrDb = 0;
for (let i = 0; i < blockSize; i++) {
let peak = 0;
for (let ch = 0; ch < input.length; ch++) {
const v = input[ch]![i]!;
const a = v < 0 ? -v : v;
if (a > peak) {
peak = a;
}
}
const required = peak > ceilingLin ? ceilingLin / peak : 1;
gain += releaseCoef * (1 - gain);
if (gain > required) {
gain = required;
}
for (let ch = 0; ch < input.length; ch++) {
output[ch]![i] = input[ch]![i]! * gain;
}
const grDb = gain >= 1 ? 0 : -20 * Math.log10(gain);
if (grDb > maxGrDb) {
maxGrDb = grDb;
}
}
this.currentGain = gain;
this.currentGrDb = maxGrDb;
this.setParameter("gainReductionDb", maxGrDb);
}
}

View File

@@ -0,0 +1,4 @@
export {
LimiterPluginOptionsSchema,
LimiterPresetDataSchema,
} from "./schemas.js";

View File

@@ -0,0 +1,48 @@
# `limiter/schemas`
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
Zod schemas validating the construction-time options of `LimiterPlugin` and the (currently empty) preset payload contract. These run on the public boundary between user code and the plugin: any malformed object hitting the constructor or `applyPreset` is rejected with `AUDIO_FX.INVALID_OPTIONS` / `AUDIO_FX.INVALID_PRESET` before it can corrupt runtime state.
Separating schemas from the plugin module lets `index.ts` re-export them so callers can introspect the option surface (for forms, editors, or RPC validation) without importing the audio implementation.
## Features
- `LimiterPluginOptionsSchema` — validates `initialCeilingDb` ∈ [-60, 0] and `initialReleaseMs` ∈ [1, 1000].
- `LimiterPresetDataSchema` — empty strict object: the limiter has no plugin-specific preset payload, so any extra keys are rejected by `.strict()`.
- Both schemas are `z.object` / `z.number().finite()` based so `NaN` and `Infinity` cannot sneak through.
## Usage: Basic Example
```ts
import { LimiterPluginOptionsSchema } from "@suaveplan/audio-fx-plugin-limiter";
const parsed = LimiterPluginOptionsSchema.parse({ initialCeilingDb: -3 });
```
## Usage: Advanced Example
```ts
import { LimiterPluginOptionsSchema } from "@suaveplan/audio-fx-plugin-limiter";
// Drive an editor form from the schema's introspected shape.
const fields = LimiterPluginOptionsSchema.keyof().options;
for (const field of fields) {
form.addRow(field);
}
```
## API Reference
- `LimiterPluginOptionsSchema: z.ZodObject<{ initialCeilingDb?, initialReleaseMs? }>`
- `LimiterPresetDataSchema: z.ZodObject<{}, "strict">`
## Implementation Notes
The constructor in `limiter.ts` calls `LimiterPluginOptionsSchema.safeParse(options)` and throws `AudioFxError(INVALID_OPTIONS, ...)` on failure with the Zod `issues` array attached as `context`. This keeps the surface uniform with the other plugins (eq/compressor/reverb/filter/delay/gate) and makes log triage easy: a single grep for `audio-fx.limiter-invalid-options` finds every malformed-options site.
`LimiterPresetDataSchema` is `.strict()` on purpose. The limiter has no preset-specific payload today, but `applyPreset` from `AbstractAudioFxPlugin` will still call the schema's `parse`. Using `.strict()` rejects accidentally-mixed payloads from sibling plugins (e.g., a `CompressorPresetData` blob applied to a `LimiterPlugin`), surfacing the mistake at the boundary rather than silently no-op'ing parameter restoration.

View File

@@ -0,0 +1,9 @@
import z from "zod";
export const LimiterPluginOptionsSchema = z.object({
initialCeilingDb: z.number().finite().min(-60).max(0).optional(),
initialReleaseMs: z.number().finite().min(1).max(1_000).optional(),
});
/** No plugin-specific preset payload — all parameters are universal. */
export const LimiterPresetDataSchema = z.object({}).strict();

View File

@@ -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"]
}

View File

@@ -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"
]
}

View File

@@ -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-limiter",
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",
],
},
},
});

View File

@@ -0,0 +1,3 @@
import { createTestConfig } from "@suaveplan/testing/config";
export default createTestConfig({ testTimeout: 30_000 });