60 lines
1.8 KiB
TypeScript
60 lines
1.8 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).
|
|
*/
|
|
export function scanDir(baseDir: 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)) {
|
|
out.push({
|
|
file: abs,
|
|
rel: relative(baseDir, abs).split(sep).join("/"),
|
|
});
|
|
}
|
|
}
|
|
};
|
|
|
|
walk(baseDir);
|
|
return out;
|
|
}
|