1import { existsSync, readFileSync, readdirSync } from "node:fs";
2import { fileURLToPath } from "node:url";
3import { dirname, join, resolve } from "node:path";
4import { describe, expect, it } from "vitest";
5
6
19describe("@factory/shared/dto browser safety (no Node builtins in the web bundle)", () => {
20 const dtoDir = dirname(fileURLToPath(import.meta.url));
21
22
23 const importSpecifiersOf = (file: string): string[] => {
24 const src = readFileSync(file, "utf8");
25
26 const code = src.replace(/\/\*[\s\S]*?\*\
27 const specs = new Set<string>();
28 for (const m of code.matchAll(/\b(?:import|export)\b[\s\S]*?\bfrom\s*["']([^"']+)["']/g)) {
29 const spec = m[1];
30 if (spec) specs.add(spec);
31 }
32 for (const m of code.matchAll(/^\s*import\s*["']([^"']+)["']/gm)) {
33 const spec = m[1];
34 if (spec) specs.add(spec);
35 }
36 return [...specs];
37 };
38
39 it("contains no file that imports a node: builtin (directly or transitively)", () => {
40 const seen = new Set<string>();
41 const queue = readdirSync(dtoDir)
42 .filter((f) => f.endsWith(".ts") && !f.endsWith(".test.ts"))
43 .map((f) => join(dtoDir, f));
44 const offenders: string[] = [];
45 while (queue.length) {
46 const file = queue.pop()!;
47 if (seen.has(file)) continue;
48 seen.add(file);
49 for (const spec of importSpecifiersOf(file)) {
50 if (spec.startsWith("node:") || /^(crypto|fs|path|os|stream|buffer)$/.test(spec)) {
51 offenders.push(`${file} → "${spec}"`);
52 continue;
53 }
54 if (spec.startsWith(".")) {
55 const target = resolve(dirname(file), spec.replace(/\.js$/, ".ts"));
56 if (!existsSync(target)) {
57 offenders.push(`${file} → "${spec}" (unresolvable relative import)`);
58 continue;
59 }
60 queue.push(target);
61 }
62 }
63 }
64 expect(offenders).toEqual([]);
65 });
66});
67