import { readdir } from "node:fs/promises"; import path from "node:path"; const repoRoot = path.resolve(import.meta.dirname, ".."); const residualExtensions = new Set([".js", ".mjs", ".cjs"]); const skippedDirectories = new Set([ ".agents", ".astro", ".claude", ".claude-sessions", ".codex", ".cursor", ".git", ".od", ".od-e2e", ".opencode", ".task", ".tmp", ".vite", "dist", "node_modules", "out", ]); const allowedExactPaths = new Set([ "packages/platform/esbuild.config.mjs", "packages/sidecar/esbuild.config.mjs", "packages/sidecar-proto/esbuild.config.mjs", // Maintainer utility scripts ported from the media branch. They are // executed directly by Node and are not loaded by the app runtime. "scripts/import-prompt-templates.mjs", "scripts/postinstall.mjs", "apps/packaged/esbuild.config.mjs", // Browser service workers must be served as JavaScript files. "apps/web/public/od-notifications-sw.js", "scripts/bake-html-ppt-examples.mjs", "scripts/scaffold-html-ppt-skills.mjs", "scripts/sync-hyperframes-skill.mjs", "scripts/verify-media-models.mjs", "tools/dev/bin/tools-dev.mjs", "tools/dev/esbuild.config.mjs", "tools/pack/bin/tools-pack.mjs", "tools/pack/esbuild.config.mjs", "tools/pack/resources/mac/notarize.cjs", ]); const allowedPathPrefixes = [ "apps/daemon/dist/", "apps/web/.next/", "apps/web/out/", "generated/", "e2e/playwright-report/", "e2e/reports/html/", "e2e/reports/playwright-html-report/", "e2e/reports/test-results/", // Vendored upstream HyperFrames skill helper scripts. "skills/hyperframes/scripts/", // Vendored upstream html-ppt skill runtime assets (lewislulu/html-ppt-skill). "skills/html-ppt/assets/", "test-results/", "vendor/", ]; function toRepositoryPath(filePath: string): string { return path.relative(repoRoot, filePath).split(path.sep).join("/"); } function isAllowedOutputPath(repositoryPath: string): boolean { if (allowedExactPaths.has(repositoryPath)) return true; return allowedPathPrefixes.some((prefix) => repositoryPath.startsWith(prefix)); } function isSkippedDirectoryName(directoryName: string): boolean { return skippedDirectories.has(directoryName) || directoryName === ".next" || directoryName.startsWith(".next-"); } async function collectResidualJavaScript(directory: string): Promise { const entries = await readdir(directory, { withFileTypes: true }); const residualFiles: string[] = []; for (const entry of entries) { const fullPath = path.join(directory, entry.name); const repositoryPath = toRepositoryPath(fullPath); if (entry.isDirectory()) { if (isSkippedDirectoryName(entry.name) || isAllowedOutputPath(`${repositoryPath}/`)) { continue; } residualFiles.push(...(await collectResidualJavaScript(fullPath))); continue; } if (!entry.isFile() || !residualExtensions.has(path.extname(entry.name))) { continue; } if (isAllowedOutputPath(repositoryPath)) { continue; } residualFiles.push(repositoryPath); } return residualFiles; } const residualFiles = await collectResidualJavaScript(repoRoot); if (residualFiles.length > 0) { console.error("Residual project-owned JavaScript files found:"); for (const filePath of residualFiles) { console.error(`- ${filePath}`); } console.error("Convert these files to TypeScript or add a documented generated/vendor/output allowlist entry."); process.exitCode = 1; } else { console.log("Residual JavaScript check passed: project-owned code is TypeScript-only."); }