release: WRNexusJS 0.8.3
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@wrnexus/cli",
|
||||
"version": "0.8.2",
|
||||
"version": "0.8.3",
|
||||
"type": "module",
|
||||
"main": "src/index.ts",
|
||||
"exports": {
|
||||
|
||||
+108
-20
@@ -29,6 +29,7 @@ import {
|
||||
analyzeRuntimeRequirements,
|
||||
assertValidAst,
|
||||
generate,
|
||||
generateTargets,
|
||||
parse,
|
||||
type RuntimeRequirements,
|
||||
type DeploymentRuntime,
|
||||
@@ -106,6 +107,7 @@ export async function runBuild(appRoot: string): Promise<void> {
|
||||
const appDir = join(root, "app");
|
||||
const distDir = join(root, "dist");
|
||||
const compiledDir = join(distDir, "compiled");
|
||||
const clientModulesDir = join(distDir, "client");
|
||||
const reactivePath = join(distDir, "reactive.js");
|
||||
const publicDir = join(root, "public");
|
||||
const distPublicDir = join(distDir, "public");
|
||||
@@ -117,6 +119,7 @@ export async function runBuild(appRoot: string): Promise<void> {
|
||||
// Clean output.
|
||||
rmSync(distDir, { recursive: true, force: true });
|
||||
mkdirSync(compiledDir, { recursive: true });
|
||||
mkdirSync(clientModulesDir, { recursive: true });
|
||||
if (existsSync(publicDir)) {
|
||||
cpSync(publicDir, distPublicDir, { recursive: true });
|
||||
console.log(`✓ Public: ${distPublicDir}`);
|
||||
@@ -169,15 +172,29 @@ export async function runBuild(appRoot: string): Promise<void> {
|
||||
// keep the exact compiler path and output contract by default.
|
||||
let compiledCount = 0;
|
||||
const compiledFiles = new Map<string, string>();
|
||||
const clientFiles = new Map<string, string>();
|
||||
const runtimeAnalysis = new Map<string, RuntimeRequirements>();
|
||||
const partialStaticFiles = new Set<string>();
|
||||
const compileWrn = async (file: string): Promise<void> => {
|
||||
if (!file.endsWith(".wrn") || compiledFiles.has(file)) return;
|
||||
const out = join(compiledDir, `route${compiledCount++}.ts`);
|
||||
// Reserve the artifact before resolving imports so cycles terminate and
|
||||
// mutually dependent generated modules can point at deterministic paths.
|
||||
compiledFiles.set(file, out);
|
||||
const source = readFileSync(file, "utf8");
|
||||
const artifactId = compiledCount++;
|
||||
const out = join(compiledDir, `route${artifactId}.ts`);
|
||||
const clientHash = createHash("sha256")
|
||||
.update(fwd(file))
|
||||
.update("\0")
|
||||
.update(source)
|
||||
.digest("hex")
|
||||
.slice(0, 16);
|
||||
const clientOut = join(clientModulesDir, `${basename(file, ".wrn")}-${clientHash}.mjs`);
|
||||
const clientUrl = `/__wrnexus/client/${basename(clientOut)}`;
|
||||
const clientEntry = join(compiledDir, `client${artifactId}.entry.mjs`);
|
||||
|
||||
// Reserve both artifacts before resolving imports so dependency cycles
|
||||
// terminate and every generated import has a deterministic target.
|
||||
compiledFiles.set(file, out);
|
||||
clientFiles.set(file, clientOut);
|
||||
|
||||
let ast = parse(source);
|
||||
assertValidAst(ast, { file, accessibility: true });
|
||||
ast = await pluginRunner.transformAst(ast, file);
|
||||
@@ -193,18 +210,35 @@ export async function runBuild(appRoot: string): Promise<void> {
|
||||
errors.map((diagnostic) => `[${diagnostic.code}] ${diagnostic.message}`).join("\n"),
|
||||
);
|
||||
}
|
||||
|
||||
try {
|
||||
let code = `// compiled from .wrn\n${generate(ast)}`;
|
||||
const targets = generateTargets(ast);
|
||||
let code = `// compiled from ${fwd(relative(root, file))}\n${generate(ast)}`.replaceAll(
|
||||
"__WRNEXUS_CLIENT_MODULE__",
|
||||
clientUrl,
|
||||
);
|
||||
let browserCode = `// browser module compiled from ${fwd(relative(root, file))}\n${targets.browser}`;
|
||||
code = await pluginRunner.transformCode(code, file);
|
||||
browserCode = await pluginRunner.transformCode(browserCode, file);
|
||||
|
||||
for (const [id, target] of virtualModules) {
|
||||
const sourcePattern = id.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
||||
const relativeTarget = relative(dirname(out), target).replace(/\\/g, "/");
|
||||
const specifier = relativeTarget.startsWith(".") ? relativeTarget : `./${relativeTarget}`;
|
||||
const mainRelative = relative(dirname(out), target).replace(/\\/g, "/");
|
||||
const mainSpecifier = mainRelative.startsWith(".") ? mainRelative : `./${mainRelative}`;
|
||||
code = code.replace(
|
||||
new RegExp(`(["'])${sourcePattern}\\1`, "g"),
|
||||
JSON.stringify(specifier),
|
||||
JSON.stringify(mainSpecifier),
|
||||
);
|
||||
const browserRelative = relative(dirname(clientEntry), target).replace(/\\/g, "/");
|
||||
const browserSpecifier = browserRelative.startsWith(".")
|
||||
? browserRelative
|
||||
: `./${browserRelative}`;
|
||||
browserCode = browserCode.replace(
|
||||
new RegExp(`(["'])${sourcePattern}\\1`, "g"),
|
||||
JSON.stringify(browserSpecifier),
|
||||
);
|
||||
}
|
||||
|
||||
const resolvedImports = resolveWrnImports(ast.structuredImports, file, {
|
||||
appRoot: root,
|
||||
mode: config.imports?.mode ?? "compatible",
|
||||
@@ -214,23 +248,74 @@ export async function runBuild(appRoot: string): Promise<void> {
|
||||
if (imported.diagnostic?.severity === "error") {
|
||||
throw new Error(`${imported.diagnostic.code}: ${imported.diagnostic.message}`);
|
||||
}
|
||||
if (!imported.resolved || !imported.declaration.source.startsWith(".")) continue;
|
||||
let target = imported.resolved;
|
||||
if (target.endsWith(".wrn")) {
|
||||
await compileWrn(target);
|
||||
target = compiledFiles.get(target)!;
|
||||
}
|
||||
if (!imported.resolved) continue;
|
||||
const sourcePattern = imported.declaration.source.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
||||
const relativeTarget = relative(dirname(out), target).replace(/\\/g, "/");
|
||||
const specifier = relativeTarget.startsWith(".") ? relativeTarget : `./${relativeTarget}`;
|
||||
code = code.replace(
|
||||
new RegExp(`(["'])${sourcePattern}\\1`, "g"),
|
||||
JSON.stringify(specifier),
|
||||
|
||||
const importAliases = { "@": "./app", ...(config.imports?.aliases ?? {}) };
|
||||
const isApplicationImport =
|
||||
imported.declaration.source.startsWith(".") ||
|
||||
Object.keys(importAliases).some(
|
||||
(alias) =>
|
||||
imported.declaration.source === alias ||
|
||||
imported.declaration.source.startsWith(`${alias}/`),
|
||||
);
|
||||
if (isApplicationImport) {
|
||||
let target = imported.resolved;
|
||||
if (target.endsWith(".wrn")) {
|
||||
await compileWrn(target);
|
||||
target = compiledFiles.get(target)!;
|
||||
}
|
||||
const relativeTarget = relative(dirname(out), target).replace(/\\/g, "/");
|
||||
const specifier = relativeTarget.startsWith(".") ? relativeTarget : `./${relativeTarget}`;
|
||||
code = code.replace(
|
||||
new RegExp(`(["'])${sourcePattern}\\1`, "g"),
|
||||
JSON.stringify(specifier),
|
||||
);
|
||||
}
|
||||
|
||||
// Browser modules keep only imports referenced by client/shared code.
|
||||
// Resolve those imports relative to the original .wrn source before
|
||||
// bundling so no filesystem or bare package specifier reaches browsers.
|
||||
if (isApplicationImport && new RegExp(`(["'])${sourcePattern}\\1`).test(browserCode)) {
|
||||
let browserTarget = imported.resolved;
|
||||
if (browserTarget.endsWith(".wrn")) {
|
||||
await compileWrn(browserTarget);
|
||||
browserTarget = clientFiles.get(browserTarget)!;
|
||||
}
|
||||
const browserRelative = relative(dirname(clientEntry), browserTarget).replace(/\\/g, "/");
|
||||
const browserSpecifier = browserRelative.startsWith(".")
|
||||
? browserRelative
|
||||
: `./${browserRelative}`;
|
||||
browserCode = browserCode.replace(
|
||||
new RegExp(`(["'])${sourcePattern}\\1`, "g"),
|
||||
JSON.stringify(browserSpecifier),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
writeFileSync(out, code, "utf8");
|
||||
writeFileSync(clientEntry, browserCode, "utf8");
|
||||
const browserResult = await Bun.build({
|
||||
entrypoints: [clientEntry],
|
||||
target: "browser",
|
||||
format: "esm",
|
||||
splitting: false,
|
||||
minify: true,
|
||||
sourcemap: config.build?.sourceMaps ? "inline" : "none",
|
||||
});
|
||||
if (!browserResult.success || !browserResult.outputs.length) {
|
||||
throw new Error(
|
||||
`WRN-CLIENT-BUNDLE: failed to bundle ${file}\n${browserResult.logs.map(String).join("\n")}`,
|
||||
);
|
||||
}
|
||||
writeFileSync(out, code, "utf8");
|
||||
writeFileSync(clientOut, await browserResult.outputs[0]!.text(), "utf8");
|
||||
rmSync(clientEntry, { force: true });
|
||||
} catch (error) {
|
||||
compiledFiles.delete(file);
|
||||
clientFiles.delete(file);
|
||||
rmSync(out, { force: true });
|
||||
rmSync(clientOut, { force: true });
|
||||
rmSync(clientEntry, { force: true });
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
@@ -369,6 +454,8 @@ export async function runBuild(appRoot: string): Promise<void> {
|
||||
console.log(`✓ Partial shells: ${partialShells.size} build-time route shell(s)`);
|
||||
}
|
||||
const assetHash = createHash("sha256");
|
||||
for (const file of clientFiles.values()) assetHash.update(readFileSync(file));
|
||||
if (clientFiles.size) console.log(`✓ Client modules: ${clientFiles.size} bundled module(s)`);
|
||||
const emittedPluginAssets = await emitPluginAssets(
|
||||
pluginContributions,
|
||||
distDir,
|
||||
@@ -533,6 +620,7 @@ await createProductionServer(
|
||||
},
|
||||
{
|
||||
reactivePath: join(import.meta.dir, "reactive.js"),
|
||||
clientModulesDir: join(import.meta.dir, "client"),
|
||||
themePath: join(import.meta.dir, "theme.css"),
|
||||
themeJsPath: join(import.meta.dir, "theme.js"),
|
||||
theme: ${JSON.stringify(theme)},
|
||||
|
||||
@@ -3,6 +3,7 @@ import { basename, extname, join, relative, resolve } from "node:path";
|
||||
import { buildRouter, createRouteManifest, nameRoutes } from "@wrnexus/router";
|
||||
import { checkWrnFile, type WrnTypeDiagnostic } from "@wrnexus/typecheck";
|
||||
import { parse } from "@wrnexus/syntax";
|
||||
import { generate, generateTargets } from "@wrnexus/compiler";
|
||||
import { regenerateRoutes } from "./routes.ts";
|
||||
import { loadAppConfig } from "@wrnexus/styles";
|
||||
import { createPluginRunner, discoverPlugins, type PluginContributions } from "@wrnexus/plugin";
|
||||
@@ -258,11 +259,51 @@ export async function generateApplicationTypesWithPlugins(
|
||||
return generateApplicationTypes(root, await runner.contributions());
|
||||
}
|
||||
|
||||
function codegenDiagnostics(file: string): WrnTypeDiagnostic[] {
|
||||
try {
|
||||
const ast = parse(readFileSync(file, "utf8"));
|
||||
const main = generate(ast);
|
||||
const targets = generateTargets(ast);
|
||||
const bun = (
|
||||
globalThis as unknown as {
|
||||
Bun?: {
|
||||
Transpiler?: new (options: { loader: string }) => {
|
||||
transformSync(code: string): unknown;
|
||||
};
|
||||
};
|
||||
}
|
||||
).Bun;
|
||||
if (bun?.Transpiler) {
|
||||
const transpiler = new bun.Transpiler({ loader: "ts" });
|
||||
transpiler.transformSync(main);
|
||||
transpiler.transformSync(targets.browser);
|
||||
transpiler.transformSync(targets.server);
|
||||
}
|
||||
return [];
|
||||
} catch (error) {
|
||||
return [
|
||||
{
|
||||
code: "WRN-CODEGEN",
|
||||
category: "error",
|
||||
message: error instanceof Error ? error.message : String(error),
|
||||
file,
|
||||
line: 1,
|
||||
column: 1,
|
||||
length: 1,
|
||||
hint: "Fix the reported SSR/client code generation issue before starting or building the app.",
|
||||
},
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
export function checkApplication(appRoot: string): WrnTypeDiagnostic[] {
|
||||
const root = resolve(appRoot);
|
||||
return files(join(root, "app"), (path) => extname(path) === ".wrn").flatMap((file) =>
|
||||
checkWrnFile(file, { appRoot: root }),
|
||||
);
|
||||
return files(join(root, "app"), (path) => extname(path) === ".wrn").flatMap((file) => {
|
||||
const diagnostics = checkWrnFile(file, { appRoot: root });
|
||||
return diagnostics.some((item) => item.category === "error")
|
||||
? diagnostics
|
||||
: [...diagnostics, ...codegenDiagnostics(file)];
|
||||
});
|
||||
}
|
||||
|
||||
export async function runTypecheck(appRoot: string): Promise<boolean> {
|
||||
|
||||
@@ -1989,6 +1989,16 @@ const MIGRATIONS: Migration[] = [
|
||||
// Runtime stability fixes require no application source rewrite.
|
||||
},
|
||||
},
|
||||
{
|
||||
version: "0.8.3",
|
||||
id: "0.8.3-client-ssr-realtime-stability",
|
||||
description:
|
||||
"Fixes browser function bundling, SSR computed values, Async aliases, typed loop props, realtime identity isolation, loader invalidation, and strict package imports.",
|
||||
apply() {
|
||||
// Framework runtime and compiler fixes require no application source rewrite.
|
||||
// The compile-cache generation changes automatically invalidate old artifacts.
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
/** Release tooling uses this to require an explicit migration entry per version. */
|
||||
|
||||
Reference in New Issue
Block a user