first commit
This commit is contained in:
@@ -0,0 +1,111 @@
|
||||
/**
|
||||
* Request pipeline helpers: middleware execution and safe module loading.
|
||||
* These are deliberately runtime-agnostic (no Bun APIs) so they could run on
|
||||
* Node too.
|
||||
*/
|
||||
|
||||
import { pathToFileURL } from "node:url";
|
||||
import { readFileSync, writeFileSync, mkdirSync, statSync } from "node:fs";
|
||||
import { dirname, join, basename } from "node:path";
|
||||
import { compileWireFile } from "@wrnexus/compiler";
|
||||
import type { Context, Middleware } from "@wrnexus/core";
|
||||
|
||||
/**
|
||||
* Run an onion-style middleware chain, ending in `final` (the route handler).
|
||||
* Each middleware receives `next`; calling it advances the chain. A middleware
|
||||
* may short-circuit by returning a Response without calling `next`.
|
||||
*/
|
||||
export function runMiddleware(
|
||||
middlewares: Middleware[],
|
||||
ctx: Context,
|
||||
final: () => Promise<Response> | Response,
|
||||
): Promise<Response> {
|
||||
let lastIndex = -1;
|
||||
|
||||
const dispatch = (index: number): Promise<Response> => {
|
||||
if (index <= lastIndex) {
|
||||
return Promise.reject(new Error("next() called multiple times"));
|
||||
}
|
||||
lastIndex = index;
|
||||
const mw = middlewares[index];
|
||||
if (!mw) return Promise.resolve(final());
|
||||
return Promise.resolve(mw(ctx, () => dispatch(index + 1)));
|
||||
};
|
||||
|
||||
return dispatch(0);
|
||||
}
|
||||
|
||||
/**
|
||||
* Cache of imported route modules. Modules are only ever loaded from absolute
|
||||
* paths discovered during the startup scan — never from request input.
|
||||
*/
|
||||
const moduleCache = new Map<string, Promise<Record<string, unknown>>>();
|
||||
|
||||
export function loadModule(file: string): Promise<Record<string, unknown>> {
|
||||
let mod = moduleCache.get(file);
|
||||
if (!mod) {
|
||||
// `.wrn` files are compiled to TypeScript first, then imported.
|
||||
const target = file.endsWith(".wrn") ? compileWireToTs(file) : file;
|
||||
// pathToFileURL handles Windows drive letters and spaces correctly.
|
||||
mod = import(pathToFileURL(target).href) as Promise<Record<string, unknown>>;
|
||||
moduleCache.set(file, mod);
|
||||
}
|
||||
return mod;
|
||||
}
|
||||
|
||||
/**
|
||||
* A single cache dir for ALL `.wrn` compilation (set once at server start).
|
||||
* When unset, compilation falls back to a sibling `.wrnexus/` next to each file.
|
||||
*/
|
||||
let compileCacheDir: string | null = null;
|
||||
|
||||
/**
|
||||
* Point all `.wrn` compilation at ONE cache dir (typically `<appRoot>/.wrnexus`)
|
||||
* instead of scattering a `.wrnexus/` folder next to every `.wrn` source. Called
|
||||
* once by the dev server at startup.
|
||||
*/
|
||||
export function setCompileCacheDir(dir: string): void {
|
||||
compileCacheDir = dir;
|
||||
}
|
||||
|
||||
/** FNV-1a hash of a string → short base36, to make unique flat cache filenames. */
|
||||
function hashPath(s: string): string {
|
||||
let h = 0x811c9dc5;
|
||||
for (let i = 0; i < s.length; i++) {
|
||||
h ^= s.charCodeAt(i);
|
||||
h = Math.imul(h, 0x01000193);
|
||||
}
|
||||
return (h >>> 0).toString(36);
|
||||
}
|
||||
|
||||
/**
|
||||
* Compile a `.wrn` file to a `.ts` file inside the shared `.wrnexus/` cache dir and
|
||||
* return the generated path. The cache dir is hidden, so the router never re-scans
|
||||
* it and the dev watcher ignores it. Output names are flat + hash-suffixed by the
|
||||
* absolute source path, so `.wrn` files from anywhere (the app AND node_modules UI
|
||||
* components) share one cache dir without colliding. Generated modules are
|
||||
* self-contained (no relative imports), so the cache location doesn't affect them.
|
||||
*/
|
||||
function compileWireToTs(file: string): string {
|
||||
const cacheDir = compileCacheDir ?? join(dirname(file), ".wrnexus");
|
||||
const name = basename(file).replace(/\.wrn$/, "");
|
||||
const out = join(cacheDir, `${name}-${hashPath(file)}.wrn.ts`);
|
||||
|
||||
// Skip recompiling when the on-disk cache is already newer than the source
|
||||
// (e.g. reused across dev restarts) — avoids a read + compile + write.
|
||||
try {
|
||||
if (statSync(out).mtimeMs >= statSync(file).mtimeMs) return out;
|
||||
} catch {
|
||||
/* cache missing → compile below */
|
||||
}
|
||||
|
||||
const code = compileWireFile(readFileSync(file, "utf8"));
|
||||
mkdirSync(cacheDir, { recursive: true });
|
||||
writeFileSync(out, code, "utf8");
|
||||
return out;
|
||||
}
|
||||
|
||||
/** Forget cached modules (used by build/dev tooling if needed). */
|
||||
export function clearModuleCache(): void {
|
||||
moduleCache.clear();
|
||||
}
|
||||
Reference in New Issue
Block a user