release: WRNexusJS 0.8.3
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@wrnexus/dev-server",
|
||||
"version": "0.8.2",
|
||||
"version": "0.8.3",
|
||||
"type": "module",
|
||||
"main": "src/index.ts",
|
||||
"exports": {
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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");
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user