testNoCircularImports
No circular imports: definitions that refer to each other are stored together, as one module, so the modules code runs from import each other without ever going round in a loop (no import order to get wrong, nothing half-loaded)
import { okIn, tempDir } from "#codebase/tcmts/cli/test/e2e/harness"; import assert from "node:assert/strict"; import fs from "node:fs"; import path from "node:path"; export const testNoCircularImports = () => { const dir = tempDir("no circular imports"); const tcm = (...args: string[]) => okIn(dir, ...args); tcm("init"); fs.writeFileSync( path.join(dir, "scratch.ts"), `// tcm: namespace lib export const isEven = (n: number): boolean => (n === 0 ? true : isOdd(n - 1)); export const isOdd = (n: number): boolean => (n === 0 ? false : isEven(n - 1)); export const describe = (n: number): string => (isEven(n) ? "even" : "odd") + " " + label(n); export const label = (n: number): string => (n > 9 ? "big: " + describe(n % 10) : "small"); export const main = (n: string) => console.log(describe(Number(n))); `, ); tcm("add"); assert.equal(tcm("run", "lib.main", "17").trim(), "odd big: odd small"); // the modules it ran from, and what each imports const cache = path.join(dir, ".tcm", "cache"); const modules = new Map<string, string[]>(); for (const f of fs.readdirSync(cache).filter((f) => /^[a-z2-7]{52}\.tsx?$/.test(f))) { const text = fs.readFileSync(path.join(cache, f), "utf8"); modules.set( f.replace(/\.tsx?$/, ""), [...text.matchAll(/from "\.\/([a-z2-7]{52})\.tsx?"/g)].map((m) => m[1]!), ); } // isEven and isOdd, which call each other, are one module; and so are describe and label const holding = (name: string) => [...modules.keys()].filter((h) => new RegExp(`\\b(const|function) ${name}\\b`).test( fs.readFileSync(path.join(cache, `${h}.ts`), "utf8"), ), ); for (const [a, b] of [ ["isEven", "isOdd"], ["describe", "label"], ] as const) { assert.equal(holding(a).length, 1, `${a} is in one module`); assert.deepEqual(holding(a), holding(b)); } // and no module imports itself, directly or round a loop const visiting = new Set<string>(); const done = new Set<string>(); const visit = (h: string) => { if (done.has(h)) return; assert.ok(!visiting.has(h), `an import loop through ${h}`); visiting.add(h); for (const d of modules.get(h) ?? []) visit(d); visiting.delete(h); done.add(h); }; for (const h of modules.keys()) visit(h); assert.ok(modules.size >= 3, `${modules.size} modules`); }