import { readdir, readFile, stat } from "node:fs/promises"; import path from "node:path"; import process from "node:process"; import { resolveRealWorkspaceRoot } from "./load-env.mjs"; const rootDir = resolveRealWorkspaceRoot(); const workspaceDirs = ["apps", "packages", "tooling"]; const sourceFileExtensions = new Set([ ".ts", ".tsx", ".mts", ".cts", ".js", ".jsx", ".mjs", ".cjs", ]); const resolutionExtensions = [ "", ".ts", ".tsx", ".mts", ".cts", ".js", ".jsx", ".mjs", ".cjs", ]; const violations = []; const importPattern = /(?:import|export)\s+(?:[^"'`]*?\s+from\s+)?["'`](\.{1,2}\/[^"'`]+)["'`]|import\s*\(\s*["'`](\.{1,2}\/[^"'`]+)["'`]\s*\)/g; async function pathExists(targetPath) { try { await stat(targetPath); return true; } catch { return false; } } async function collectSourceFiles(directoryPath, result) { const entries = await readdir(directoryPath, { withFileTypes: true }); for (const entry of entries) { const absolutePath = path.join(directoryPath, entry.name); if (entry.isDirectory()) { if ( entry.name === "node_modules" || entry.name.startsWith(".next") || entry.name === ".turbo" || entry.name === "dist" || entry.name === "coverage" || entry.name === "playwright-report" || entry.name === "test-results" ) { continue; } await collectSourceFiles(absolutePath, result); continue; } if (!entry.isFile()) { continue; } const extension = path.extname(entry.name); if (sourceFileExtensions.has(extension)) { result.push(absolutePath); } } } function getExtensionlessCandidates(basePath) { return resolutionExtensions.flatMap((extension) => [ `${basePath}${extension}`, path.join(basePath, `index${extension}`), ]); } function getExplicitExtensionCandidates(basePath, importExtension) { const baseWithoutExtension = basePath.slice(0, -importExtension.length); const siblingExtensions = importExtension === ".js" || importExtension === ".mjs" || importExtension === ".cjs" ? [importExtension, ".ts", ".tsx", ".mts", ".cts"] : [importExtension]; return siblingExtensions.flatMap((extension) => [ `${baseWithoutExtension}${extension}`, path.join(baseWithoutExtension, `index${extension}`), ]); } async function resolvesImport(fromFilePath, importPath) { const resolvedBasePath = path.resolve(path.dirname(fromFilePath), importPath); const rawExtension = path.extname(importPath); const importExtension = resolutionExtensions.includes(rawExtension) ? rawExtension : ""; const candidates = importExtension ? getExplicitExtensionCandidates(resolvedBasePath, importExtension) : getExtensionlessCandidates(resolvedBasePath); for (const candidate of candidates) { if (await pathExists(candidate)) { return true; } } return false; } const sourceFiles = []; for (const workspaceDir of workspaceDirs) { const absoluteWorkspaceDir = path.join(rootDir, workspaceDir); if (!(await pathExists(absoluteWorkspaceDir))) { continue; } await collectSourceFiles(absoluteWorkspaceDir, sourceFiles); } for (const sourceFile of sourceFiles) { const content = await readFile(sourceFile, "utf8"); const relativeSourceFile = path.relative(rootDir, sourceFile); const imports = new Set(); for (const match of content.matchAll(importPattern)) { const importPath = match[1] ?? match[2]; if (importPath) { imports.add(importPath.trim()); } } for (const importPath of imports) { if (!(await resolvesImport(sourceFile, importPath))) { violations.push(`${relativeSourceFile}: unresolved relative import ${importPath}`); } } } if (violations.length > 0) { console.error("Workspace import check failed:"); for (const violation of violations) { console.error(`- ${violation}`); } process.exit(1); } console.log("Workspace imports passed.");