import { readFile, readdir } from "node:fs/promises"; import path from "node:path"; export type RealtimeBoundaryRuleId = | "NATIVE_REALTIME_API_OUTSIDE_ADAPTER" | "PRESENTATION_INTERVAL_OWNER" | "UNSELECTED_REALTIME_RUNTIME_COMPOSED"; export type RealtimeBoundaryViolation = Readonly<{ ruleId: RealtimeBoundaryRuleId; file: string; line: number; }>; const SOURCE_EXTENSION = /\.(?:[cm]?ts|tsx)$/u; const OWNED_NATIVE_ROOTS = [ "src/adapters/realtime/", "src/adapters/web-push/", ] as const; const REALTIME_ADAPTER_IMPORT = /(?:from\s*|import\s*\()\s*["'][^"']*\/adapters\/(?:realtime|web-push)(?:\/[^"']*)?["']/gu; const NATIVE_REALTIME_PATTERNS = [ /\bnew\s+(?:WebSocket|EventSource|Notification)\s*\(/gu, /\bNotification\s*\.\s*requestPermission\s*\(/gu, /\.\s*showNotification\s*\(/gu, /\.\s*pushManager\s*\.\s*(?:subscribe|getSubscription)\s*\(/gu, /\bReflect\s*\.\s*get\s*\([^,]+,\s*["'](?:WebSocket|EventSource|Notification|pushManager)["']/gu, ] as const; const PRESENTATION_INTERVAL = /\bsetInterval\s*\(/gu; export async function scanRealtimeBoundaries( sourceRoot: string, ): Promise { const absoluteRoot = path.resolve(sourceRoot); const files = await collectSourceFiles(absoluteRoot); const violations: RealtimeBoundaryViolation[] = []; for (const file of files) { const source = await readFile(file, "utf8"); const logicalFile = logicalSourcePath(absoluteRoot, file); inspectFile(source, logicalFile, violations); } return Object.freeze( violations .sort( (left, right) => left.file.localeCompare(right.file) || left.line - right.line || left.ruleId.localeCompare(right.ruleId), ) .map((violation) => Object.freeze(violation)), ); } function inspectFile( source: string, logicalFile: string, violations: RealtimeBoundaryViolation[], ): void { const nativeOwned = OWNED_NATIVE_ROOTS.some((root) => logicalFile.startsWith(root), ); const presentationOwned = logicalFile.startsWith("src/presentation/") || /^src\/features\/[^/]+\/presentation\//u.test(logicalFile); const compositionBoundary = logicalFile.startsWith("src/bootstrap/") || /^src\/features\/installed-feature-/u.test(logicalFile); const report = ( ruleId: RealtimeBoundaryRuleId, index: number, ): void => { violations.push({ ruleId, file: logicalFile, line: lineAt(source, index), }); }; if (!nativeOwned) { for (const pattern of NATIVE_REALTIME_PATTERNS) { for (const match of source.matchAll(pattern)) { report( "NATIVE_REALTIME_API_OUTSIDE_ADAPTER", match.index, ); } } } if (presentationOwned) { for (const match of source.matchAll(PRESENTATION_INTERVAL)) { report("PRESENTATION_INTERVAL_OWNER", match.index); } } if (compositionBoundary) { for (const match of source.matchAll(REALTIME_ADAPTER_IMPORT)) { report("UNSELECTED_REALTIME_RUNTIME_COMPOSED", match.index); } } } async function collectSourceFiles( directory: string, ): Promise { const output: string[] = []; const entries = await readdir(directory, { withFileTypes: true }); for (const entry of entries) { const resolved = path.join(directory, entry.name); if ( entry.isDirectory() && !["node_modules", "dist", "artifacts", ".tmp"].includes( entry.name, ) ) { output.push(...(await collectSourceFiles(resolved))); } else if (entry.isFile() && SOURCE_EXTENSION.test(entry.name)) { output.push(resolved); } } return output; } function logicalSourcePath(root: string, file: string): string { const workspaceRelative = path .relative(process.cwd(), file) .split(path.sep) .join("/"); if (root === path.resolve("src")) return workspaceRelative; return path.relative(root, file).split(path.sep).join("/"); } function lineAt(source: string, index: number): number { let line = 1; for (let offset = 0; offset < index; offset += 1) { if (source.charCodeAt(offset) === 10) line += 1; } return line; }