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
+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;