Task 10 fix round 1: the coordinator's plan doc (41fb82b9) recorded that
generated authz type files should be skipped before the isSafeIslandName
check, but the code change never landed. isSafeIslandName rejects the dot
in the stripped basename "permissions.gen", so every app running Task 12's
codegen would warn on every boot.
Add a quiet skip for *.gen.ts / *.gen.js immediately after the extension
guard, before the name check. Add tests: a .gen.ts file is skipped without
a console.warn (spied), and a .gen.js file is skipped the same way while a
legitimately named .js declaration is still discovered.
Also corrects the scanDir extraExtensions doc comment, which incorrectly
implied app/schemas passes it too (only app/authz does).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
69 lines
2.3 KiB
TypeScript
69 lines
2.3 KiB
TypeScript
/**
|
|
* Filesystem scanning for the file-based router.
|
|
*
|
|
* Scanning happens once at startup. We build a table of routes from the files
|
|
* that exist on disk; request paths are later matched against that table.
|
|
* Crucially, request input is NEVER turned into a file path — this is what
|
|
* makes the router immune to path-traversal.
|
|
*/
|
|
|
|
import { existsSync, readdirSync, statSync } from "node:fs";
|
|
import { join, relative, sep } from "node:path";
|
|
|
|
/** Extensions we are willing to load as route modules. */
|
|
const ALLOWED_EXTENSIONS = [".ts", ".tsx", ".wrn"] as const;
|
|
|
|
export interface ScannedFile {
|
|
/** Absolute path to the file on disk. */
|
|
file: string;
|
|
/** Path relative to the scanned base directory, using forward slashes. */
|
|
rel: string;
|
|
}
|
|
|
|
function hasAllowedExtension(name: string): boolean {
|
|
return ALLOWED_EXTENSIONS.some((ext) => name.endsWith(ext));
|
|
}
|
|
|
|
/** Hidden files/dirs (dotfiles) and underscore-prefixed files are ignored. */
|
|
function isIgnored(name: string): boolean {
|
|
return name.startsWith(".") || name.startsWith("_");
|
|
}
|
|
|
|
/**
|
|
* Recursively collect allowed route files under `baseDir`.
|
|
* Returns [] if the directory does not exist (a route kind may be unused).
|
|
*
|
|
* `extraExtensions` widens the allow-list for a caller that scans a non-route
|
|
* directory and accepts plain `.js` modules (currently only `app/authz`); it
|
|
* defaults to empty so every other caller — route scanning (`app/pages`,
|
|
* `app/api`, `app/realtime`, ...) as well as `app/schemas`, which does not
|
|
* pass it and so still only sees `.ts`/`.tsx`/`.wrn` — is unaffected.
|
|
*/
|
|
export function scanDir(baseDir: string, extraExtensions: readonly string[] = []): ScannedFile[] {
|
|
if (!existsSync(baseDir)) return [];
|
|
|
|
const out: ScannedFile[] = [];
|
|
|
|
const walk = (dir: string): void => {
|
|
for (const entry of readdirSync(dir)) {
|
|
if (isIgnored(entry)) continue;
|
|
const abs = join(dir, entry);
|
|
const stats = statSync(abs);
|
|
if (stats.isDirectory()) {
|
|
walk(abs);
|
|
} else if (
|
|
stats.isFile() &&
|
|
(hasAllowedExtension(entry) || extraExtensions.some((ext) => entry.endsWith(ext)))
|
|
) {
|
|
out.push({
|
|
file: abs,
|
|
rel: relative(baseDir, abs).split(sep).join("/"),
|
|
});
|
|
}
|
|
}
|
|
};
|
|
|
|
walk(baseDir);
|
|
return out;
|
|
}
|