#!/usr/bin/env node /** * check-circular-deps.mjs — detect circular imports across the SF codebase. * * Usage: * npm run check:circular # scan src/ + packages/ * npm run check:circular -- --ext # scan extension source only * node scripts/check-circular-deps.mjs [--ext] [--json] * * Exit 0 = no cycles found. Exit 1 = cycles detected (or scan error). */ import { dirname, resolve } from "node:path"; import { fileURLToPath } from "node:url"; import madge from "madge"; const __dirname = dirname(fileURLToPath(import.meta.url)); const root = resolve(__dirname, ".."); const args = process.argv.slice(2); const extOnly = args.includes("--ext"); const jsonOut = args.includes("--json"); const entries = extOnly ? [resolve(root, "src/resources/extensions/sf")] : [resolve(root, "src"), resolve(root, "packages")]; console.error( `Scanning: ${entries.map((e) => e.replace(root + "/", "")).join(", ")}`, ); let result; try { result = await madge(entries, { fileExtensions: ["js", "mjs", "ts"], excludeRegExp: [ /node_modules/, /\.test\.(js|mjs|ts)$/, /\/dist\//, /\/tests?\//, ], detectiveOptions: { es6: { mixedImports: true, skipAsyncImports: true }, }, }); } catch (err) { console.error(`Scan failed: ${err.message}`); process.exit(1); } const cycles = result.circular(); if (jsonOut) { console.log(JSON.stringify({ cycles, count: cycles.length }, null, 2)); } else if (cycles.length === 0) { console.log("✅ No circular dependencies found."); } else { console.log(`❌ ${cycles.length} circular dependency chain(s) found:\n`); for (const [i, chain] of cycles.entries()) { console.log(` ${i + 1}. ${chain.join(" → ")} → ${chain[0]}`); } } process.exit(cycles.length > 0 ? 1 : 0);