92 lines
2.9 KiB
TypeScript
92 lines
2.9 KiB
TypeScript
import { readdir, readFile } from "node:fs/promises";
|
|
import { join } from "node:path";
|
|
import { describe, expect, it } from "@suaveplan/testing/runner";
|
|
|
|
const PKG_ROOT = join(__dirname, "..");
|
|
const SRC = join(PKG_ROOT, "src");
|
|
|
|
// Match a *use* of the global as a free identifier (assignment, property access,
|
|
// or function call), not occurrences inside compound identifiers like `AudioDocument`.
|
|
const BROWSER_GLOBALS = [
|
|
"window",
|
|
"document",
|
|
"navigator",
|
|
"AudioContext",
|
|
"AudioBuffer",
|
|
];
|
|
|
|
function stripCommentsAndStrings(src: string): string {
|
|
// Drop /* ... */, // ..., and "..." / '...' content so we only see code.
|
|
return src
|
|
.replace(/\/\*[\s\S]*?\*\//g, " ")
|
|
.replace(/\/\/[^\n]*/g, " ")
|
|
.replace(/"(?:[^"\\]|\\.)*"/g, '""')
|
|
.replace(/'(?:[^'\\]|\\.)*'/g, "''");
|
|
}
|
|
|
|
function refersToGlobal(src: string, name: string): boolean {
|
|
const code = stripCommentsAndStrings(src);
|
|
// Token must start after a non-identifier char and be followed by a
|
|
// property access (.), call site (() or [), assignment (= but not ==),
|
|
// semicolon, comma, or close paren.
|
|
const re = new RegExp(
|
|
`(?:^|[^A-Za-z0-9_.])${name}(?=[.(\\[]|\\s*=[^=])`,
|
|
"m"
|
|
);
|
|
return re.test(code);
|
|
}
|
|
|
|
async function* walk(dir: string): AsyncGenerator<string> {
|
|
const entries = await readdir(dir, { withFileTypes: true });
|
|
for (const e of entries) {
|
|
const full = join(dir, e.name);
|
|
if (e.isDirectory()) yield* walk(full);
|
|
else if (
|
|
e.isFile() &&
|
|
e.name.endsWith(".ts") &&
|
|
!e.name.endsWith(".test.ts")
|
|
)
|
|
yield full;
|
|
}
|
|
}
|
|
|
|
describe("audio-cli purity", () => {
|
|
it("source references no browser-only globals", {
|
|
timeout: 5000,
|
|
}, async () => {
|
|
for await (const path of walk(SRC)) {
|
|
const src = await readFile(path, "utf8");
|
|
for (const global of BROWSER_GLOBALS) {
|
|
expect(
|
|
refersToGlobal(src, global),
|
|
`${path} references ${global}`
|
|
).toBe(false);
|
|
}
|
|
}
|
|
});
|
|
|
|
it("declared dependency set is limited to the spec list", {
|
|
timeout: 5000,
|
|
}, async () => {
|
|
const raw = await readFile(join(PKG_ROOT, "package.json"), "utf8");
|
|
const pkg = JSON.parse(raw) as {
|
|
dependencies?: Record<string, string>;
|
|
};
|
|
const allowed = new Set([
|
|
"@suaveplan/audio-codecs",
|
|
"@suaveplan/audio-core",
|
|
"@suaveplan/audio-engine-server",
|
|
"@suaveplan/error",
|
|
"@suaveplan/types",
|
|
"@opentelemetry/api",
|
|
"tslib",
|
|
"zod",
|
|
]);
|
|
for (const dep of Object.keys(pkg.dependencies ?? {})) {
|
|
expect(allowed.has(dep), `unexpected runtime dep ${dep}`).toBe(
|
|
true
|
|
);
|
|
}
|
|
});
|
|
});
|