release: WRNexusJS 0.8.3
This commit is contained in:
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user