Skip to content

Commit 05bea5c

Browse files
committed
feat: executable examples of package usage
1 parent 94e8352 commit 05bea5c

File tree

13 files changed

+263
-15
lines changed

13 files changed

+263
-15
lines changed

.github/workflows/ci.yml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,12 @@ jobs:
4343
- name: Build package
4444
run: pnpm run build
4545

46+
- name: Run examples
47+
run: |
48+
pnpm run example:wav-to-mp3
49+
pnpm run example:mp3-to-wav
50+
pnpm run example:stream
51+
4652
postinstall:
4753
name: LAME install on ${{ matrix.os }}
4854
runs-on: ${{ matrix.os }}

.gitignore

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,4 +39,6 @@ dist/
3939
# Test
4040
coverage/
4141
coverage-unit.json
42-
test/encoded.mp3
42+
43+
# Examples
44+
examples/audios/example.*.*
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,13 @@ You operate as a senior Node.js and TypeScript backend engineer focused on maint
6363
- Keep `normalizeCliMessage` translating warnings and errors into `lame:` prefixed messages to signal user-visible issues consistently.
6464
- When adding new parsing rules, guard them with unit tests to prevent regressions on partial output lines or malformed data.
6565

66+
## Examples
67+
- Place runnable demos under `examples/` as TypeScript ESM files and invoke them through `pnpm example:*` scripts that rely on `tsx`; update `package.json` scripts whenever a new example is added.
68+
- Keep shared logic (dynamic `node-lame` resolution, temp-file cleanup, etc.) inside `examples/helpers/` and reuse those utilities to avoid duplicating `ENOENT` guards or import fallbacks.
69+
- Build all paths via `new URL("./audios/<file>", import.meta.url)` (wrapped with `fileURLToPath`/`resolve`) instead of `__dirname` so the code mirrors production ESM usage.
70+
- Examples must log encoder/decoder progress through the emitter APIs and ensure they clean up/overwrite destination files before running to keep reruns deterministic.
71+
- Whenever a new recipe is added, update `package.json` and `.github/workflows/ci.yml` so the corresponding `pnpm example:*` script runs during CI; the pipeline is the canonical source of truth for example coverage.
72+
6673
## Error Handling
6774
- Throw descriptive `Error` instances; avoid silent failures or rejected promises without context.
6875
- Exit code `255` triggers a tailored error message about unexpected termination—preserve this behaviour for backwards compatibility.

examples/audios/example.mp3

405 KB
Binary file not shown.

examples/audios/example.wav

2.18 MB
Binary file not shown.

examples/convert-mp3-to-wav.ts

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
import { resolve } from "node:path";
2+
import { fileURLToPath } from "node:url";
3+
4+
import { Lame } from "./helpers/load-node-lame.js";
5+
import { removeIfExists } from "./helpers/remove-if-exists.js";
6+
7+
const inputPath = resolve(
8+
fileURLToPath(new URL("./audios/example.mp3", import.meta.url)),
9+
);
10+
const outputPath = resolve(
11+
fileURLToPath(new URL("./audios/example.from-mp3.wav", import.meta.url)),
12+
);
13+
14+
async function main(): Promise<void> {
15+
await removeIfExists(outputPath);
16+
17+
const decoder = new Lame({
18+
output: outputPath,
19+
}).setFile(inputPath);
20+
21+
decoder.getEmitter().on("progress", ([progress, eta]) => {
22+
process.stdout.write(
23+
`Decoding progress: ${progress}%${eta ? ` – ETA ${eta}` : ""}\r`,
24+
);
25+
});
26+
27+
await decoder.decode();
28+
29+
console.log(`Created WAV at ${outputPath}`);
30+
}
31+
32+
main().catch((error) => {
33+
console.error(error);
34+
process.exitCode = 1;
35+
});

examples/convert-stream-to-mp3.ts

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
import { createReadStream, createWriteStream } from "node:fs";
2+
import { resolve } from "node:path";
3+
import { pipeline } from "node:stream/promises";
4+
import { fileURLToPath } from "node:url";
5+
6+
import { createLameEncoderStream } from "./helpers/load-node-lame.js";
7+
import { removeIfExists } from "./helpers/remove-if-exists.js";
8+
9+
const inputPath = resolve(
10+
fileURLToPath(new URL("./audios/example.wav", import.meta.url)),
11+
);
12+
const outputPath = resolve(
13+
fileURLToPath(new URL("./audios/example.stream.mp3", import.meta.url)),
14+
);
15+
16+
async function main(): Promise<void> {
17+
await removeIfExists(outputPath);
18+
19+
const encoderStream = createLameEncoderStream({
20+
bitrate: 192,
21+
});
22+
23+
encoderStream.getEmitter().on("progress", ([progress, eta]) => {
24+
process.stdout.write(
25+
`Streaming progress: ${progress}%${eta ? ` – ETA ${eta}` : ""}\r`,
26+
);
27+
});
28+
29+
await pipeline(
30+
createReadStream(inputPath),
31+
encoderStream,
32+
createWriteStream(outputPath),
33+
);
34+
35+
console.log(`Streamed MP3 written to ${outputPath}`);
36+
}
37+
38+
main().catch((error) => {
39+
console.error(error);
40+
process.exitCode = 1;
41+
});

examples/convert-wav-to-mp3.ts

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
import { resolve } from "node:path";
2+
import { fileURLToPath } from "node:url";
3+
4+
import { Lame } from "./helpers/load-node-lame.js";
5+
import { removeIfExists } from "./helpers/remove-if-exists.js";
6+
7+
const inputPath = resolve(
8+
fileURLToPath(new URL("./audios/example.wav", import.meta.url)),
9+
);
10+
const outputPath = resolve(
11+
fileURLToPath(new URL("./audios/example.from-wav.mp3", import.meta.url)),
12+
);
13+
14+
async function main(): Promise<void> {
15+
await removeIfExists(outputPath);
16+
17+
const encoder = new Lame({
18+
output: outputPath,
19+
bitrate: 192,
20+
});
21+
22+
encoder.setFile(inputPath);
23+
24+
encoder.getEmitter().on("progress", ([progress, eta]) => {
25+
process.stdout.write(
26+
`Encoding progress: ${progress}%${eta ? ` – ETA ${eta}` : ""}\r`,
27+
);
28+
});
29+
30+
await encoder.encode();
31+
32+
console.log(`Created MP3 at ${outputPath}`);
33+
}
34+
35+
main().catch((error) => {
36+
console.error(error);
37+
process.exitCode = 1;
38+
});

examples/helpers/load-node-lame.ts

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
type NodeLameModule = typeof import("../../src/index");
2+
3+
const MODULE_NOT_FOUND_CODE = "ERR_MODULE_NOT_FOUND";
4+
const LOCAL_FALLBACKS = ["../../dist/index.cjs", "../../src/index.ts"] as const;
5+
6+
type ErrorWithCode = {
7+
code?: string;
8+
};
9+
10+
const isModuleNotFoundError = (
11+
error: unknown,
12+
): error is ErrorWithCode => {
13+
if (typeof error !== "object" || error === null) {
14+
return false;
15+
}
16+
17+
return (
18+
"code" in error &&
19+
(error as ErrorWithCode).code === MODULE_NOT_FOUND_CODE
20+
);
21+
};
22+
23+
const resolveLocalModule = async (): Promise<NodeLameModule> => {
24+
for (const relativePath of LOCAL_FALLBACKS) {
25+
try {
26+
const moduleUrl = new URL(relativePath, import.meta.url);
27+
return (await import(moduleUrl.href)) as unknown as NodeLameModule;
28+
} catch (error) {
29+
if (isModuleNotFoundError(error)) {
30+
continue;
31+
}
32+
33+
throw error;
34+
}
35+
}
36+
37+
throw new Error(
38+
'node-lame: Unable to locate local sources. Run "pnpm build" or ensure src/ is available.',
39+
);
40+
};
41+
42+
const loadNodeLame = async (): Promise<NodeLameModule> => {
43+
try {
44+
return (await import("node-lame")) as unknown as NodeLameModule;
45+
} catch (error) {
46+
if (isModuleNotFoundError(error)) {
47+
return resolveLocalModule();
48+
}
49+
50+
throw error;
51+
}
52+
};
53+
54+
const nodeLame = await loadNodeLame();
55+
56+
export const { Lame, createLameDecoderStream, createLameEncoderStream } =
57+
nodeLame;
58+
export default nodeLame;
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
import { unlink } from "node:fs/promises";
2+
3+
type ErrorWithCode = {
4+
code?: string;
5+
};
6+
7+
const isEnoentError = (error: unknown): error is ErrorWithCode => {
8+
if (typeof error !== "object" || error === null) {
9+
return false;
10+
}
11+
12+
return "code" in error && (error as ErrorWithCode).code === "ENOENT";
13+
};
14+
15+
const removeIfExists = async (filePath: string): Promise<void> => {
16+
try {
17+
await unlink(filePath);
18+
} catch (error) {
19+
if (isEnoentError(error)) {
20+
return;
21+
}
22+
23+
throw error;
24+
}
25+
};
26+
27+
export { removeIfExists };

0 commit comments

Comments
 (0)