release: WRNexusJS 0.6.0

This commit is contained in:
2026-08-01 01:09:58 +05:30
parent 3e565e8d03
commit 687d345882
502 changed files with 33038 additions and 11358 deletions
+214 -21
View File
@@ -12,9 +12,10 @@ import {
mkdirSync,
statSync,
unlinkSync,
existsSync,
} from "node:fs";
import { dirname, join, basename, extname } from "node:path";
import { compileWireFile } from "@wrnexus/compiler";
import { dirname, join, basename, extname, resolve } from "node:path";
import { compile, generateTargets, resolveWrnImports, type PageAst } from "@wrnexus/compiler";
import type { Context, Middleware } from "@wrnexus/core";
/**
@@ -48,13 +49,105 @@ export function runMiddleware(
*/
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>();
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 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 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 (!entry.declaration.source.startsWith(".") && !entry.declaration.source.startsWith("@/"))
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 = `/__wrnexus/client/${basename(dependency.browser).replace(/\.client\.mjs$/, ".mjs")}`;
} else {
replacement = target === "server" ? dependency.server : dependency.main;
}
}
const escaped = entry.declaration.source.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
const specifier = replacement.startsWith("/") ? replacement : pathToFileURL(replacement).href;
output = output.replace(new RegExp(`(["'])${escaped}\\1`, "g"), JSON.stringify(specifier));
}
return output;
}
export function loadModule(file: string): Promise<Record<string, unknown>> {
let mod = moduleCache.get(file);
if (!mod) {
const version = moduleVersions.get(file) ?? 0;
// `.wrn` files are compiled to TypeScript first, then imported.
let target = file.endsWith(".wrn") ? compileWireToTs(file, version) : file;
let target = file.endsWith(".wrn") ? compileWireArtifacts(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
@@ -116,31 +209,131 @@ function hashPath(s: string): string {
* 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, version = 0): string {
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 missing = [...usedComponents].filter((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 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");
// Include the source contents in the cache identity. Package managers, git
// checkouts, archive extraction, and linked dependencies can all replace a
// file while preserving (or moving backwards) its mtime. An mtime-only cache
// then serves an older compiled component even across a clean build.
const out = join(
cacheDir,
`${name}-${WRN_COMPILE_CACHE_VERSION}-${hashPath(file)}-${hashPath(source)}${suffix}.wrn.ts`,
);
// The content hash makes this safe even when source timestamps are preserved.
const stem = `${name}-${WRN_COMPILE_CACHE_VERSION}-${hashPath(file)}-${hashPath(source)}${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 {
if (statSync(out).isFile()) return out;
} catch {
/* cache missing → compile below */
try {
if (Object.values(artifacts).every((path) => statSync(path).isFile())) {
browserArtifactPaths.set(`/__wrnexus/client/${stem}.mjs`, artifacts.browser);
return artifacts;
}
} catch {
// Compile missing artifact set below.
}
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");
return artifacts;
} finally {
compileInProgress.delete(file);
}
}
const code = compileWireFile(source, file);
mkdirSync(cacheDir, { recursive: true });
writeFileSync(out, code, "utf8");
return out;
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")}`;
}
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. */