import { readFileSync } from "node:fs"; import { dirname, isAbsolute, join, relative, sep } from "node:path"; import { fileURLToPath } from "node:url"; import ts from "typescript"; import { findShortestStableCycle } from "../graph/find-shortest-stable-cycle"; import { getModuleReferences, getModuleSpecifiers, } from "./module-import-syntax-test-support"; import { formatStaticModuleReference, resolveStaticModuleReferenceFileName, type StaticModuleReference, } from "./module-reference-resolution-test-support"; import { getBoundaryViolation, getProductionLayer, getProductionLayerMatches, isContributionPhaseModulePath, isTestSupportModulePath, publicModuleAllowedTargetRuntimePaths, schemaOnlyContributionMaterializers, } from "./module-layer-boundary-test-support"; export { getBoundaryViolation, getModuleSpecifiers, getProductionLayer, getProductionLayerMatches, isContributionPhaseModulePath, publicModuleAllowedTargetRuntimePaths, schemaOnlyContributionMaterializers, }; const modulesDirectory = dirname(dirname(fileURLToPath(import.meta.url))); const runtimeSourceDirectory = dirname(modulesDirectory); const runtimePackageDirectory = dirname(runtimeSourceDirectory); type RuntimeCompilerConfiguration = Readonly<{ fileNames: readonly string[]; options: ts.CompilerOptions; }>; function loadRuntimeCompilerConfiguration(): RuntimeCompilerConfiguration { const configPath = join(runtimePackageDirectory, "tsconfig.json"); const config = ts.readConfigFile(configPath, ts.sys.readFile); if (config.error !== undefined) { throw new Error( ts.flattenDiagnosticMessageText(config.error.messageText, "\n"), ); } const parsed = ts.parseJsonConfigFileContent( config.config, ts.sys, runtimePackageDirectory, undefined, configPath, ); if (parsed.errors.length > 0) { throw new Error( parsed.errors .map((error) => ts.flattenDiagnosticMessageText(error.messageText, "\n"), ) .join("\n"), ); } return Object.freeze({ fileNames: Object.freeze([...parsed.fileNames]), options: parsed.options, }); } const runtimeCompilerConfiguration = loadRuntimeCompilerConfiguration(); function toPosixPath(filePath: string): string { return filePath.split(sep).join("/"); } function toRuntimeSourcePath(filePath: string): string | undefined { const runtimePath = toPosixPath(relative(runtimeSourceDirectory, filePath)); return runtimePath === ".." || runtimePath.startsWith("../") || isAbsolute(runtimePath) ? undefined : runtimePath; } type StaticReferenceResolution = Readonly<{ targetRuntimePath?: string; violation?: string; }>; function resolveStaticReference( importerModulePath: string, importerRuntimePath: string, reference: StaticModuleReference, compilerOptions: ts.CompilerOptions, ): StaticReferenceResolution { const importerFilePath = join(runtimeSourceDirectory, importerRuntimePath); const resolvedFileName = resolveStaticModuleReferenceFileName( reference, importerFilePath, compilerOptions, ); const targetRuntimePath = resolvedFileName === undefined ? undefined : toRuntimeSourcePath(resolvedFileName); if (targetRuntimePath === undefined) { if (/^built-ins\/[^/]+\/react\//.test(importerModulePath) && resolvedFileName !== undefined && /^(?:react(?:\/|$)|@repo\/ui(?:\/|$))/.test(reference.specifier)) return {}; return { violation: `${importerModulePath} -> ${formatStaticModuleReference(reference)}`, }; } return { targetRuntimePath, violation: getBoundaryViolation(importerModulePath, targetRuntimePath), }; } export function getImportBoundaryViolation( importerModulePath: string, importerRuntimePath: string, specifier: string, compilerOptions: ts.CompilerOptions = runtimeCompilerConfiguration.options, ): string | undefined { return resolveStaticReference( importerModulePath, importerRuntimePath, { referenceKind: "module", specifier }, compilerOptions, ).violation; } type SourceImportBoundaryAnalysis = Readonly<{ allowedInternalTargets: readonly string[]; diagnostics: readonly string[]; }>; function analyzeSourceImports({ compilerOptions = runtimeCompilerConfiguration.options, importerModulePath, importerRuntimePath, source, }: Readonly<{ compilerOptions?: ts.CompilerOptions; importerModulePath: string; importerRuntimePath: string; source: string; }>): SourceImportBoundaryAnalysis { const diagnostics: string[] = []; const allowedInternalTargets: string[] = []; const importerFilePath = join(runtimeSourceDirectory, importerRuntimePath); for (const reference of getModuleReferences(importerFilePath, source)) { if (reference.kind === "nonliteral") { diagnostics.push(`${importerModulePath} -> ${reference.diagnostic}`); continue; } const resolution = resolveStaticReference( importerModulePath, importerRuntimePath, reference, compilerOptions, ); if (resolution.violation !== undefined) { diagnostics.push(resolution.violation); } else if (resolution.targetRuntimePath !== undefined) { allowedInternalTargets.push(resolution.targetRuntimePath); } } return { allowedInternalTargets, diagnostics }; } export function getSourceImportBoundaryDiagnostics( input: Parameters[0], ): readonly string[] { return analyzeSourceImports(input).diagnostics; } export function listRuntimeSourcePaths(): readonly string[] { return runtimeCompilerConfiguration.fileNames .map(toRuntimeSourcePath) .filter((runtimePath): runtimePath is string => runtimePath !== undefined) .sort(); } export function listProductionModulePaths(): readonly string[] { return listRuntimeSourcePaths() .filter((runtimePath) => runtimePath.startsWith("modules/")) .map((runtimePath) => runtimePath.slice("modules/".length)) .filter((modulePath) => !isTestSupportModulePath(modulePath)); } type ImportGraphAnalysis = Readonly<{ adjacency: ReadonlyMap>; diagnostics: readonly string[]; }>; function analyzeImportGraph( sourcesByModulePath: ReadonlyMap, ): ImportGraphAnalysis { const modulePaths = [...sourcesByModulePath.keys()].sort(); const modulePathLookup = new Set(modulePaths); const adjacency = new Map( modulePaths.map((modulePath) => [modulePath, new Set()]), ); const diagnostics: string[] = []; for (const importerModulePath of modulePaths) { const importerRuntimePath = `modules/${importerModulePath}`; const analysis = analyzeSourceImports({ importerModulePath, importerRuntimePath, source: sourcesByModulePath.get(importerModulePath)!, }); diagnostics.push(...analysis.diagnostics); for (const targetRuntimePath of analysis.allowedInternalTargets) { if (!targetRuntimePath.startsWith("modules/")) continue; const targetModulePath = targetRuntimePath.slice("modules/".length); if (modulePathLookup.has(targetModulePath)) { adjacency.get(importerModulePath)!.add(targetModulePath); } } } return { adjacency, diagnostics }; } function analyzeProductionImportGraph(): ImportGraphAnalysis { return analyzeImportGraph( new Map( listProductionModulePaths().map((modulePath) => [ modulePath, readFileSync(join(modulesDirectory, modulePath), "utf8"), ]), ), ); } function getModuleImportCycleDiagnostic( adjacency: ReadonlyMap>, ): string | undefined { const cycle = findShortestStableCycle(adjacency); return cycle === undefined ? undefined : `Capability module import cycle: ${cycle.join(" -> ")}.`; } export function getProductionGraphCycleDiagnostic(): string | undefined { return getModuleImportCycleDiagnostic(analyzeProductionImportGraph().adjacency); } export function getSourceImportGraphCycleDiagnostic( sourcesByModulePath: ReadonlyMap, ): string | undefined { return getModuleImportCycleDiagnostic( analyzeImportGraph(sourcesByModulePath).adjacency, ); } export function getProductionGraphBoundaryDiagnostics(): readonly string[] { const analysis = analyzeProductionImportGraph(); const cycleDiagnostic = getModuleImportCycleDiagnostic(analysis.adjacency); return [ ...analysis.diagnostics, ...(cycleDiagnostic === undefined ? [] : [cycleDiagnostic]), ].sort(); }