open-design/scripts/check-residual-js.ts
Zakaria a46764fb1b
Some checks failed
ci / Validate workspace (push) Has been cancelled
landing-page-ci / Validate landing page (push) Has been cancelled
landing-page-deploy / Deploy landing page (push) Has been cancelled
github-metrics / Generate repository metrics SVG (push) Has been cancelled
first-commit
2026-05-04 14:58:14 -04:00

121 lines
3.5 KiB
TypeScript

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<string[]> {
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.");
}