release: WRNexusJS 0.8.3
Quality / quality (ubuntu-latest) (push) Failing after 12m9s
Quality / quality (windows-latest) (push) Canceled after 0s

This commit is contained in:
2026-08-03 19:47:30 +05:30
parent e8f630f12d
commit 4cebacadfe
156 changed files with 2608 additions and 473 deletions
+2 -2
View File
@@ -40,7 +40,7 @@ import {
setCompileCacheDir,
setCompileImportOptions,
setDevCompilerPipeline,
wrnBrowserArtifactUrl,
wrnBrowserArtifactUrlAsync,
} from "./pipeline.ts";
import { createRpcHandler } from "@wrnexus/ssr/rpc";
import { createHandlers, type WsData } from "./runtime.ts";
@@ -632,7 +632,7 @@ export async function startServer(opts: ServeOptions): Promise<RunningServer> {
storeUpdates.push({
name: declaration[2]!,
kind: declaration[1]!,
url: wrnBrowserArtifactUrl(absolute),
url: await wrnBrowserArtifactUrlAsync(absolute),
});
} catch (error) {
console.warn(`[wrnexus] failed to prepare store HMR for ${absolute}`, error);
+123 -23
View File
@@ -14,7 +14,7 @@ import {
unlinkSync,
existsSync,
} from "node:fs";
import { dirname, join, basename, extname, resolve } from "node:path";
import { dirname, join, basename, extname, relative, resolve } from "node:path";
import {
compile,
generate,
@@ -91,6 +91,16 @@ export function setCompileImportOptions(
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) {
@@ -101,6 +111,19 @@ function projectRootForFile(file: string): string {
}
}
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,
@@ -132,8 +155,7 @@ function rewriteArtifactImports(
}
}
if (!entry.resolved || !entry.declaration.source) continue;
if (!entry.declaration.source.startsWith(".") && !entry.declaration.source.startsWith("@/"))
continue;
if (!isApplicationImportSource(entry.declaration.source, importOptions.aliases)) continue;
let replacement = entry.resolved;
if (entry.resolved.endsWith(".wrn")) {
const dependencySource = readFileSync(entry.resolved, "utf8");
@@ -148,13 +170,22 @@ function rewriteArtifactImports(
output = output.replace(entry.declaration.raw, "");
continue;
}
replacement = `/__wrnexus/client/${basename(dependency.browser).replace(/\.client\.mjs$/, ".mjs")}`;
replacement = dependency.browser;
} else {
replacement = target === "server" ? dependency.server : dependency.main;
}
}
const escaped = entry.declaration.source.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
const specifier = replacement.startsWith("/") ? replacement : pathToFileURL(replacement).href;
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;
@@ -169,10 +200,15 @@ async function rewriteArtifactImportsAsync(
let output = code;
for (const [id, replacement] of devCompilerPipeline?.virtualModules ?? []) {
const escaped = id.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
output = output.replace(
new RegExp(`(["'])${escaped}\\1`, "g"),
JSON.stringify(pathToFileURL(replacement).href),
);
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);
@@ -189,8 +225,7 @@ async function rewriteArtifactImportsAsync(
for (const entry of resolved) {
if (entry.diagnostic?.severity === "error") throw new Error(entry.diagnostic.message);
if (!entry.resolved || !entry.declaration.source) continue;
if (!entry.declaration.source.startsWith(".") && !entry.declaration.source.startsWith("@/"))
continue;
if (!isApplicationImportSource(entry.declaration.source, importOptions.aliases)) continue;
let replacement = entry.resolved;
if (replacement.endsWith(".wrn")) {
const dependencySource = readFileSync(replacement, "utf8");
@@ -204,11 +239,20 @@ async function rewriteArtifactImportsAsync(
output = output.replace(entry.declaration.raw, "");
continue;
}
replacement = `/__wrnexus/client/${basename(dependency.browser).replace(/\.client\.mjs$/, ".mjs")}`;
replacement = dependency.browser;
} else replacement = target === "server" ? dependency.server : dependency.main;
}
const escaped = entry.declaration.source.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
const specifier = replacement.startsWith("/") ? replacement : pathToFileURL(replacement).href;
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;
@@ -257,14 +301,14 @@ export async function loadModule(file: string): Promise<Record<string, unknown>>
* When unset, compilation falls back to a sibling `.wrnexus/` next to each file.
*/
let compileCacheDir: string | null = null;
const WRN_COMPILE_CACHE_VERSION = "v2";
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): void {
export function setCompileCacheDir(dir: string | null): void {
compileCacheDir = dir;
}
@@ -357,18 +401,68 @@ const compileMetrics: WrnCompileMetrics = {
};
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> {
if (!devCompilerPipeline) return Promise.resolve(compileWireArtifacts(file, version));
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)}${suffix}`;
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`),
@@ -394,11 +488,11 @@ export function compileWireArtifactsAsync(file: string, version = 0): Promise<Wr
};
for (const target of ["main", "browser", "server"] as const) {
const rewritten = await rewriteArtifactImportsAsync(outputs[target], ast, file, target);
writeFileSync(
artifacts[target],
await devCompilerPipeline!.transformCode(rewritten, file),
"utf8",
);
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,
@@ -437,7 +531,7 @@ export function compileWireArtifacts(file: string, version = 0): WrnCompileArtif
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)}${suffix}`;
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`),
@@ -516,6 +610,12 @@ export function wrnBrowserArtifactUrl(file: string): string {
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;
+10
View File
@@ -9,6 +9,7 @@
* supervised `dev --production-runtime` mode can explicitly enable it.
*/
import { join } from "node:path";
import type { Middleware, Mode, SecurityConfig, SeoConfig } from "@wrnexus/core";
import {
compileRoutePattern,
@@ -88,6 +89,8 @@ export interface ProdOptions {
stylesIncludeFramework?: boolean;
/** Absolute path to the pre-built reactive runtime. */
reactivePath?: string;
/** Absolute directory containing bundled per-WRN browser modules. */
clientModulesDir?: string;
/** Absolute path to the pre-built theme stylesheet (`theme.css`). */
themePath?: string;
/** Absolute path to the pre-built theme runtime (`theme.js`). */
@@ -267,6 +270,13 @@ async function serveFile(path: string | undefined, headers: Record<string, strin
function createProdAssetServer(opts: ProdOptions): AssetServer {
return {
async serve(pathname: string): Promise<Response | null> {
if (pathname.startsWith("/__wrnexus/client/")) {
const name = pathname.slice("/__wrnexus/client/".length);
if (!opts.clientModulesDir || !/^[A-Za-z0-9._-]+\.mjs$/.test(name)) {
return new Response("Not Found", { status: 404 });
}
return serveFile(join(opts.clientModulesDir, name), JS_HEADERS);
}
if (pathname === "/__wrnexus/reactive.js") {
if (opts.reactivePath) {
const file = Bun.file(opts.reactivePath);
+12 -2
View File
@@ -1279,11 +1279,14 @@ export function createHandlers(deps: RuntimeDeps): Handlers {
async function handleClientLoad(ctx: Context): Promise<Response> {
const routePath = ctx.url.searchParams.get("route") ?? "";
const routeSearch = ctx.url.searchParams.get("search") ?? "";
const name = ctx.url.searchParams.get("name") ?? "";
if (
!routePath.startsWith("/") ||
routePath.startsWith("/__wrnexus/") ||
!isSafeRequestPath(routePath) ||
(routeSearch !== "" &&
(!routeSearch.startsWith("?") || routeSearch.includes("#") || routeSearch.length > 4096)) ||
!/^[A-Za-z_$][\w$]{0,63}$/.test(name)
) {
return new Response("Not Found", { status: 404 });
@@ -1294,8 +1297,11 @@ export function createHandlers(deps: RuntimeDeps): Handlers {
const load = pageModule.__wrnexusClientLoad;
if (typeof load !== "function") return new Response("Not Found", { status: 404 });
try {
ctx.params = page.params;
const values = await load(ctx);
const routeUrl = new URL(routePath + routeSearch, ctx.url.origin);
if (routeUrl.pathname !== routePath) return new Response("Not Found", { status: 404 });
const routeRequest = new Request(routeUrl, { method: "GET", headers: ctx.req.headers });
const loadCtx: Context = { ...ctx, req: routeRequest, url: routeUrl, params: page.params };
const values = await load(loadCtx);
if (!values || typeof values !== "object" || !(name in values)) {
return new Response("Not Found", { status: 404 });
}
@@ -1627,6 +1633,10 @@ export function createHandlers(deps: RuntimeDeps): Handlers {
if (partial) {
body = typeof precomputedShell === "string" ? precomputedShell : (pagePartial?.shell ?? body);
}
const routeParamsMarker = `<span hidden data-wrn-route-params="${escapeHtml(
JSON.stringify(ctx.params ?? {}),
)}"></span>`;
body = `${routeParamsMarker}${body}`;
if (body.includes("data-wrn-action=")) {
body = body.replace(
/(<form\b[^>]*\bdata-wrn-action=(?:"[^"]+"|'[^']+')[^>]*>)/gi,