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
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@wrnexus/dev-server",
"version": "0.8.2",
"version": "0.8.3",
"type": "module",
"main": "src/index.ts",
"exports": {
+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,
@@ -0,0 +1,90 @@
import { expect, test } from "bun:test";
import { mkdtempSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import {
compileWireArtifactsAsync,
setCompileCacheDir,
setCompileImportOptions,
} from "../src/pipeline.ts";
test("development browser artifacts bundle local client imports", async () => {
const root = mkdtempSync(join(tmpdir(), "wrnexus-client-bundle-"));
const app = join(root, "app");
const cache = join(root, ".wrnexus");
mkdirSync(app, { recursive: true });
writeFileSync(join(app, "helper.ts"), "export function save(value: unknown) { return value; }\n");
const page = join(app, "settings.wrn");
writeFileSync(
page,
`import { save } from "./helper.ts"
page Settings {
state { value = 1 }
functions { client function persist() { return save(value) } }
view { <button @click='persist()'>Save</button> }
}`,
);
try {
setCompileCacheDir(cache);
const artifacts = await compileWireArtifactsAsync(page);
const browser = readFileSync(artifacts.browser, "utf8");
expect(browser).toContain("wrnexus-client-bundled");
expect(browser).not.toContain("file://");
expect(browser).not.toContain('from "./helper.ts"');
} finally {
setCompileCacheDir(null);
rmSync(root, { recursive: true, force: true });
}
});
test("development browser artifacts bundle configured alias imports", async () => {
const root = mkdtempSync(join(tmpdir(), "wrnexus-client-alias-bundle-"));
const app = join(root, "app");
const cache = join(root, ".wrnexus");
mkdirSync(join(app, "client"), { recursive: true });
writeFileSync(
join(app, "client", "storage.ts"),
"export function persist(value: unknown) { return value; }\n",
);
const page = join(app, "settings.wrn");
writeFileSync(
page,
`import { persist } from "~/client/storage.ts"
page Settings {
state { value = 1 }
functions { client function save() { return persist(value) } }
view { <button @click='save()'>Save</button> }
}`,
);
try {
setCompileCacheDir(cache);
setCompileImportOptions(root, {
mode: "explicit",
aliases: { "~": "./app" },
autoImport: false,
});
const artifacts = await compileWireArtifactsAsync(page);
const browser = readFileSync(artifacts.browser, "utf8");
expect(browser).toContain("wrnexus-client-bundled");
expect(browser).not.toContain("~/client/storage.ts");
expect(browser).not.toContain("file://");
mkdirSync(join(root, "alternate", "client"), { recursive: true });
writeFileSync(
join(root, "alternate", "client", "storage.ts"),
"export function persist(value: unknown) { return value; }\n",
);
setCompileImportOptions(root, {
mode: "explicit",
aliases: { "~": "./alternate" },
autoImport: false,
});
const changed = await compileWireArtifactsAsync(page);
expect(changed.browser).not.toBe(artifacts.browser);
} finally {
setCompileCacheDir(null);
rmSync(root, { recursive: true, force: true });
}
});
@@ -62,7 +62,9 @@ test("production streams request regions into the build-time static shell", asyn
test("normal production output remains free of development HMR", async () => {
const handlers = createProductionHandlers(manifest, {});
const response = await handlers.fetch(new Request("http://localhost/"), server);
expect(await (response as Response).text()).not.toContain("/__wrnexus/hmr");
const html = await (response as Response).text();
expect(html).not.toContain("/__wrnexus/hmr");
expect(html).toContain('data-wrn-route-params="{}"');
});
test("production client-load endpoint returns only the requested named result", async () => {
@@ -121,3 +123,43 @@ test("production prefers fast gzip for dynamic HTML and cached Brotli for immuta
)) as Response;
expect(head.headers.get("content-encoding")).toBeNull();
});
test("client-load preserves the active route query string", async () => {
const queryManifest: ProdManifest = {
...manifest,
pages: [
...manifest.pages,
{
raw: "/query",
mod: {
default: () => "<main>Query page</main>",
__wrnexusClientLoad: async (ctx: { url: URL }) => ({
identity: ctx.url.searchParams.get("as"),
}),
},
},
],
};
const handlers = createProductionHandlers(queryManifest, {});
const response = (await handlers.fetch(
new Request(
"http://localhost/__wrnexus/client-load?route=%2Fquery&search=%3Fas%3Drohan&name=identity",
),
server,
)) as Response;
expect(response.status).toBe(200);
expect(await response.json()).toEqual({ data: "rohan" });
});
test("production serves generated WRN browser modules from the client directory", async () => {
const directory = mkdtempSync(join(tmpdir(), "wrnexus-client-modules-"));
writeFileSync(join(directory, "settings-abc123.mjs"), "export const ready = true;\n");
const handlers = createProductionHandlers(manifest, { clientModulesDir: directory });
const response = (await handlers.fetch(
new Request("http://localhost/__wrnexus/client/settings-abc123.mjs"),
server,
)) as Response;
expect(response.status).toBe(200);
expect(response.headers.get("content-type")).toContain("text/javascript");
expect(await response.text()).toContain("ready = true");
});