Files
WRNexusJS/packages/dev-server/src/pipeline.ts
T
Clintchiz 4cebacadfe
Quality / quality (ubuntu-latest) (push) Failing after 12m9s
Quality / quality (windows-latest) (push) Canceled after 0s
release: WRNexusJS 0.8.3
2026-08-03 19:47:30 +05:30

645 lines
23 KiB
TypeScript

/**
* 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 {
copyFileSync,
readFileSync,
writeFileSync,
mkdirSync,
statSync,
unlinkSync,
existsSync,
} from "node:fs";
import { dirname, join, basename, extname, relative, resolve } from "node:path";
import {
compile,
generate,
generateTargets,
resolveWrnImports,
type PageAst,
} 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>>>();
const moduleVersions = new Map<string, number>();
const browserArtifactPaths = new Map<string, string>();
type ImportMode = "legacy" | "compatible" | "explicit";
interface CompileImportOptions {
mode: ImportMode;
aliases: Record<string, string>;
autoImport: boolean;
}
const compileImportOptions = new Map<string, CompileImportOptions>();
const warnedImportDiagnostics = new Set<string>();
interface DevCompilerPipeline {
transformAst(ast: PageAst, file: string): Promise<PageAst>;
transformCode(code: string, file: string): Promise<string>;
virtualModules: Map<string, string>;
}
let devCompilerPipeline: DevCompilerPipeline | null = null;
/** Install the configured plugin compiler pipeline for development compilation. */
export function setDevCompilerPipeline(pipeline: DevCompilerPipeline | null): void {
devCompilerPipeline = pipeline;
}
export function setCompileImportOptions(
appRoot: string,
options: { mode?: ImportMode; aliases?: Record<string, string>; autoImport?: boolean } = {},
): void {
compileImportOptions.set(resolve(appRoot), {
mode: options.mode ?? "compatible",
aliases: { "@": "./app", ...(options.aliases ?? {}) },
autoImport: options.autoImport ?? true,
});
}
const compileInProgress = new Map<string, WrnCompileArtifacts>();
function isApplicationImportSource(
source: string,
aliases: Record<string, string> | undefined,
): boolean {
if (source.startsWith(".")) return true;
return Object.keys(aliases ?? {}).some(
(alias) => source === alias || source.startsWith(`${alias}/`),
);
}
function projectRootForFile(file: string): string {
let current = dirname(resolve(file));
while (true) {
if (existsSync(join(current, "app"))) return current;
const parent = dirname(current);
if (parent === current) return dirname(resolve(file));
current = parent;
}
}
function importOptionsHash(file: string): string {
const root = resolve(projectRootForFile(file));
const options = compileImportOptions.get(root) ?? {
mode: "compatible" as const,
aliases: { "@": "./app" },
autoImport: true,
};
const aliases = Object.fromEntries(
Object.entries(options.aliases).sort(([left], [right]) => left.localeCompare(right)),
);
return hashPath(JSON.stringify({ ...options, aliases }));
}
function rewriteArtifactImports(
code: string,
ast: PageAst,
importer: string,
target: "main" | "server" | "browser",
): string {
if (!ast.structuredImports.length) return code;
const root = projectRootForFile(importer);
const importOptions = compileImportOptions.get(resolve(root)) ?? {
mode: "compatible" as const,
aliases: { "@": "./app" },
autoImport: true,
};
const resolved = resolveWrnImports(ast.structuredImports, importer, {
appRoot: root,
mode: importOptions.mode,
aliases: importOptions.aliases,
});
let output = code;
for (const entry of resolved) {
if (entry.diagnostic) {
const key = `${importer}:${entry.diagnostic.code}:${entry.declaration.source}`;
if (entry.diagnostic.severity === "error") {
throw new Error(`${entry.diagnostic.code}: ${entry.diagnostic.message}`);
}
if (!warnedImportDiagnostics.has(key)) {
warnedImportDiagnostics.add(key);
console.warn(`[wrnexus] ${entry.diagnostic.code}: ${entry.diagnostic.message}`);
}
}
if (!entry.resolved || !entry.declaration.source) continue;
if (!isApplicationImportSource(entry.declaration.source, importOptions.aliases)) continue;
let replacement = entry.resolved;
if (entry.resolved.endsWith(".wrn")) {
const dependencySource = readFileSync(entry.resolved, "utf8");
const isStore = /\b(?:global|page)\s+store\s+[A-Za-z_$][\w$]*\s*\{/.test(dependencySource);
const dependency = compileWireArtifacts(
entry.resolved,
moduleVersions.get(entry.resolved) ?? 0,
);
if (target === "browser") {
if (!isStore) {
// Components and layouts are compile-time dependencies in browser modules.
output = output.replace(entry.declaration.raw, "");
continue;
}
replacement = dependency.browser;
} else {
replacement = target === "server" ? dependency.server : dependency.main;
}
}
const escaped = entry.declaration.source.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
const specifier =
target === "browser"
? (() => {
const cacheDir = compileCacheDir ?? join(dirname(importer), ".wrnexus");
const relativeTarget = relative(cacheDir, replacement).replace(/\\/g, "/");
return relativeTarget.startsWith(".") ? relativeTarget : `./${relativeTarget}`;
})()
: replacement.startsWith("/")
? replacement
: pathToFileURL(replacement).href;
output = output.replace(new RegExp(`(["'])${escaped}\\1`, "g"), JSON.stringify(specifier));
}
return output;
}
async function rewriteArtifactImportsAsync(
code: string,
ast: PageAst,
importer: string,
target: "main" | "server" | "browser",
): Promise<string> {
let output = code;
for (const [id, replacement] of devCompilerPipeline?.virtualModules ?? []) {
const escaped = id.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
const specifier =
target === "browser"
? (() => {
const cacheDir = compileCacheDir ?? join(dirname(importer), ".wrnexus");
const relativeTarget = relative(cacheDir, replacement).replace(/\\/g, "/");
return relativeTarget.startsWith(".") ? relativeTarget : `./${relativeTarget}`;
})()
: pathToFileURL(replacement).href;
output = output.replace(new RegExp(`(["'])${escaped}\\1`, "g"), JSON.stringify(specifier));
}
if (!ast.structuredImports.length) return output;
const root = projectRootForFile(importer);
const importOptions = compileImportOptions.get(resolve(root)) ?? {
mode: "compatible" as const,
aliases: { "@": "./app" },
autoImport: true,
};
const resolved = resolveWrnImports(ast.structuredImports, importer, {
appRoot: root,
mode: importOptions.mode,
aliases: importOptions.aliases,
});
for (const entry of resolved) {
if (entry.diagnostic?.severity === "error") throw new Error(entry.diagnostic.message);
if (!entry.resolved || !entry.declaration.source) continue;
if (!isApplicationImportSource(entry.declaration.source, importOptions.aliases)) continue;
let replacement = entry.resolved;
if (replacement.endsWith(".wrn")) {
const dependencySource = readFileSync(replacement, "utf8");
const isStore = /\b(?:global|page)\s+store\s+[A-Za-z_$][\w$]*\s*\{/.test(dependencySource);
const dependency = await compileWireArtifactsAsync(
replacement,
moduleVersions.get(replacement) ?? 0,
);
if (target === "browser") {
if (!isStore) {
output = output.replace(entry.declaration.raw, "");
continue;
}
replacement = dependency.browser;
} else replacement = target === "server" ? dependency.server : dependency.main;
}
const escaped = entry.declaration.source.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
const specifier =
target === "browser"
? (() => {
const cacheDir = compileCacheDir ?? join(dirname(importer), ".wrnexus");
const relativeTarget = relative(cacheDir, replacement).replace(/\\/g, "/");
return relativeTarget.startsWith(".") ? relativeTarget : `./${relativeTarget}`;
})()
: replacement.startsWith("/")
? replacement
: pathToFileURL(replacement).href;
output = output.replace(new RegExp(`(["'])${escaped}\\1`, "g"), JSON.stringify(specifier));
}
return output;
}
export async function loadModule(file: string): Promise<Record<string, unknown>> {
let mod = moduleCache.get(file);
if (!mod) {
mod = (async () => {
const version = moduleVersions.get(file) ?? 0;
// `.wrn` files are compiled to TypeScript first, then imported.
let target = file.endsWith(".wrn")
? (await compileWireArtifactsAsync(file, version)).main
: file;
let temporary = false;
// Bun intentionally caches local TS/JS modules by filesystem path and ignores
// URL query strings. A short-lived versioned sibling keeps relative imports
// correct while giving the changed module a genuinely new import identity.
if (version && !file.endsWith(".wrn")) {
const extension = extname(file);
const stem = basename(file, extension);
target = join(dirname(file), `${stem}.wrnexus-hmr-${version}${extension}`);
copyFileSync(file, target);
temporary = true;
}
// pathToFileURL handles Windows drive letters and spaces correctly.
let imported = import(pathToFileURL(target).href) as Promise<Record<string, unknown>>;
if (temporary) {
imported = imported.finally(() => {
try {
unlinkSync(target);
} catch {
/* best-effort cleanup after Bun has loaded the module */
}
});
}
return imported;
})();
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;
const WRN_COMPILE_CACHE_VERSION = "v3";
/**
* 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 | null): 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 importedValueBindings(ast: PageAst): Set<string> {
const names = new Set<string>();
for (const entry of ast.structuredImports) {
if (entry.typeOnly) continue;
if (entry.defaultImport) names.add(entry.defaultImport);
if (entry.namespaceImport) names.add(entry.namespaceImport);
for (const item of entry.namedImports) if (!item.typeOnly) names.add(item.local);
}
return names;
}
function validateConfiguredImports(source: string, ast: PageAst, file: string): void {
const root = projectRootForFile(file);
const options = compileImportOptions.get(resolve(root));
if (!options || options.mode === "legacy") return;
const imported = importedValueBindings(ast);
const usedComponents = new Set(
Array.from(source.matchAll(/<([A-Z][A-Za-z0-9_$]*)\b/g), (match) => match[1]!),
);
const compilerBuiltins = new Set([
"Async",
"Component",
"Error",
"Loading",
"Portal",
"Success",
"Transition",
]);
const missing = [...usedComponents].filter(
(name) => !compilerBuiltins.has(name) && !imported.has(name),
);
if (ast.layoutIsSymbol && ast.layout && !imported.has(ast.layout)) missing.push(ast.layout);
if (!missing.length) return;
const unique = [...new Set(missing)];
const message = `WRN-IMPORT-IMPLICIT: ${file} uses ${unique.join(", ")} without explicit imports.`;
if (options.mode === "explicit") throw new Error(message);
const key = `${file}:WRN-IMPORT-IMPLICIT:${unique.join(",")}`;
if (!warnedImportDiagnostics.has(key)) {
warnedImportDiagnostics.add(key);
console.warn(`[wrnexus] ${message}`);
}
}
export interface WrnCompileArtifacts {
main: string;
browser: string;
server: string;
declarations: string;
contract: string;
rpc: string;
}
export interface WrnCompileMetrics {
hits: number;
misses: number;
compilations: number;
errors: number;
totalDurationMs: number;
lastDurationMs: number;
}
const compileMetrics: WrnCompileMetrics = {
hits: 0,
misses: 0,
compilations: 0,
errors: 0,
totalDurationMs: 0,
lastDurationMs: 0,
};
const asyncCompileInProgress = new Map<string, Promise<WrnCompileArtifacts>>();
async function bundleBrowserArtifact(
code: string,
file: string,
cacheDir: string,
stem: string,
): Promise<string> {
const hasModuleImport = /(?:^|\n)\s*import(?:\s|["'])|\bimport\s*\(/m.test(code);
if (!hasModuleImport || code.includes("wrnexus-client-bundled")) return code;
const bun = (globalThis as any).Bun;
if (!bun?.build) {
throw new Error(
`WRN-CLIENT-BUNDLE: ${file} has browser imports, but the Bun bundler is unavailable.`,
);
}
const entry = join(cacheDir, `${stem}.browser-entry.mjs`);
writeFileSync(entry, code, "utf8");
try {
const result = await bun.build({
entrypoints: [entry],
target: "browser",
format: "esm",
splitting: false,
minify: false,
sourcemap: "inline",
});
if (!result.success || !result.outputs?.length) {
const detail = (result.logs ?? []).map(String).join("\n");
throw new Error(`WRN-CLIENT-BUNDLE: failed to bundle ${file}${detail ? `\n${detail}` : ""}`);
}
return `// wrnexus-client-bundled\n${await result.outputs[0].text()}`;
} finally {
try {
unlinkSync(entry);
} catch {
// Best-effort cleanup; cache pruning removes stale temporary entries.
}
}
}
export function compileWireArtifactsAsync(file: string, version = 0): Promise<WrnCompileArtifacts> {
const key = `${file}:${version}`;
const active = asyncCompileInProgress.get(key);
if (active) return active;
const task = (async () => {
if (!devCompilerPipeline) {
const artifacts = compileWireArtifacts(file, version);
const code = readFileSync(artifacts.browser, "utf8");
const bundled = await bundleBrowserArtifact(
code,
file,
dirname(artifacts.browser),
basename(artifacts.browser, ".client.mjs"),
);
if (bundled !== code) writeFileSync(artifacts.browser, bundled, "utf8");
return artifacts;
}
const cacheDir = compileCacheDir ?? join(dirname(file), ".wrnexus");
const name = basename(file).replace(/\.wrn$/, "");
const suffix = version ? `-hmr-${version}` : "";
const source = readFileSync(file, "utf8");
// Plugin output affects the artifact, so use a separate cache generation.
const stem = `${name}-${WRN_COMPILE_CACHE_VERSION}-plugin-${hashPath(file)}-${hashPath(source)}-${importOptionsHash(file)}${suffix}`;
const artifacts: WrnCompileArtifacts = {
main: join(cacheDir, `${stem}.wrn.ts`),
browser: join(cacheDir, `${stem}.client.mjs`),
server: join(cacheDir, `${stem}.server.ts`),
declarations: join(cacheDir, `${stem}.d.ts`),
contract: join(cacheDir, `${stem}.contract.json`),
rpc: join(cacheDir, `${stem}.rpc.json`),
};
const result = compile(source, file);
validateConfiguredImports(source, result.ast, file);
const ast = await devCompilerPipeline!.transformAst(result.ast, file);
const targets = generateTargets(ast);
mkdirSync(cacheDir, { recursive: true });
const browserPath = `/__wrnexus/client/${stem}.mjs`;
const outputs = {
main: `// compiled from .wrn\n${generate(ast)}`.replaceAll(
"__WRNEXUS_CLIENT_MODULE__",
browserPath,
),
browser: targets.browser,
server: targets.server,
declarations: targets.declarations,
};
for (const target of ["main", "browser", "server"] as const) {
const rewritten = await rewriteArtifactImportsAsync(outputs[target], ast, file, target);
let transformed = await devCompilerPipeline!.transformCode(rewritten, file);
if (target === "browser") {
transformed = await bundleBrowserArtifact(transformed, file, cacheDir, stem);
}
writeFileSync(artifacts[target], transformed, "utf8");
}
writeFileSync(
artifacts.declarations,
await devCompilerPipeline!.transformCode(outputs.declarations, file),
"utf8",
);
writeFileSync(artifacts.contract, JSON.stringify(targets.contract, null, 2) + "\n", "utf8");
writeFileSync(artifacts.rpc, JSON.stringify(targets.rpc, null, 2) + "\n", "utf8");
browserArtifactPaths.set(browserPath, artifacts.browser);
compileMetrics.compilations++;
return artifacts;
})().finally(() => asyncCompileInProgress.delete(key));
asyncCompileInProgress.set(key, task);
return task;
}
export function getWrnCompileMetrics(): Readonly<WrnCompileMetrics> {
return { ...compileMetrics };
}
export function resetWrnCompileMetrics(): void {
Object.assign(compileMetrics, {
hits: 0,
misses: 0,
compilations: 0,
errors: 0,
totalDurationMs: 0,
lastDurationMs: 0,
});
}
export function compileWireArtifacts(file: string, version = 0): WrnCompileArtifacts {
const active = compileInProgress.get(file);
if (active) return active;
const cacheDir = compileCacheDir ?? join(dirname(file), ".wrnexus");
const name = basename(file).replace(/\.wrn$/, "");
const suffix = version ? `-hmr-${version}` : "";
const source = readFileSync(file, "utf8");
const stem = `${name}-${WRN_COMPILE_CACHE_VERSION}-${hashPath(file)}-${hashPath(source)}-${importOptionsHash(file)}${suffix}`;
const artifacts: WrnCompileArtifacts = {
main: join(cacheDir, `${stem}.wrn.ts`),
browser: join(cacheDir, `${stem}.client.mjs`),
server: join(cacheDir, `${stem}.server.ts`),
declarations: join(cacheDir, `${stem}.d.ts`),
contract: join(cacheDir, `${stem}.contract.json`),
rpc: join(cacheDir, `${stem}.rpc.json`),
};
compileInProgress.set(file, artifacts);
try {
try {
if (Object.values(artifacts).every((path) => statSync(path).isFile())) {
compileMetrics.hits++;
browserArtifactPaths.set(`/__wrnexus/client/${stem}.mjs`, artifacts.browser);
return artifacts;
}
} catch {
// Compile missing artifact set below.
}
compileMetrics.misses++;
const started = performance.now();
try {
const result = compile(source, file);
validateConfiguredImports(source, result.ast, file);
const targets = generateTargets(result.ast);
mkdirSync(cacheDir, { recursive: true });
const browserPath = `/__wrnexus/client/${stem}.mjs`;
const mainCode = rewriteArtifactImports(
result.code.replaceAll("__WRNEXUS_CLIENT_MODULE__", browserPath),
result.ast,
file,
"main",
);
writeFileSync(artifacts.main, mainCode, "utf8");
writeFileSync(
artifacts.browser,
rewriteArtifactImports(targets.browser, result.ast, file, "browser"),
"utf8",
);
browserArtifactPaths.set(browserPath, artifacts.browser);
writeFileSync(
artifacts.server,
rewriteArtifactImports(targets.server, result.ast, file, "server"),
"utf8",
);
writeFileSync(artifacts.declarations, targets.declarations, "utf8");
writeFileSync(artifacts.contract, JSON.stringify(targets.contract, null, 2) + "\n", "utf8");
writeFileSync(artifacts.rpc, JSON.stringify(targets.rpc, null, 2) + "\n", "utf8");
compileMetrics.compilations++;
return artifacts;
} catch (error) {
compileMetrics.errors++;
throw error;
} finally {
const duration = performance.now() - started;
compileMetrics.lastDurationMs = duration;
compileMetrics.totalDurationMs += duration;
}
} finally {
compileInProgress.delete(file);
}
}
export async function loadWrnServerModule(file: string): Promise<Record<string, unknown>> {
const version = moduleVersions.get(file) ?? 0;
const artifact = compileWireArtifacts(file, version).server;
return import(pathToFileURL(artifact).href) as Promise<Record<string, unknown>>;
}
export function wrnBrowserArtifact(file: string): string {
return compileWireArtifacts(file, moduleVersions.get(file) ?? 0).browser;
}
export function wrnBrowserArtifactUrl(file: string): string {
const artifact = compileWireArtifacts(file, moduleVersions.get(file) ?? 0).browser;
return `/__wrnexus/client/${basename(artifact).replace(/\.client\.mjs$/, ".mjs")}`;
}
/** Async browser artifact URL used by HMR so imported client modules are bundled before delivery. */
export async function wrnBrowserArtifactUrlAsync(file: string): Promise<string> {
const artifact = (await compileWireArtifactsAsync(file, moduleVersions.get(file) ?? 0)).browser;
return `/__wrnexus/client/${basename(artifact).replace(/\.client\.mjs$/, ".mjs")}`;
}
export function serveWrnBrowserArtifact(pathname: string): Response | null {
const artifact = browserArtifactPaths.get(pathname);
if (!artifact || !existsSync(artifact)) return null;
return new Response(readFileSync(artifact, "utf8"), {
headers: {
"content-type": "text/javascript; charset=utf-8",
"cache-control": "no-store, max-age=0",
pragma: "no-cache",
expires: "0",
},
});
}
/** Forget one module and force its next dynamic import to bypass Bun's import cache. */
export function invalidateModule(file: string): void {
moduleCache.delete(file);
moduleVersions.set(file, (moduleVersions.get(file) ?? 0) + 1);
}
/** Forget cached modules (used by build/dev tooling if needed). */
export function clearModuleCache(): void {
for (const file of moduleCache.keys()) {
moduleVersions.set(file, (moduleVersions.get(file) ?? 0) + 1);
}
moduleCache.clear();
}