release: WRNexusJS 0.3.0
This commit is contained in:
+162
-18
@@ -11,11 +11,20 @@
|
||||
*/
|
||||
|
||||
import { createHash } from "node:crypto";
|
||||
import { cpSync, existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
||||
import {
|
||||
cpSync,
|
||||
existsSync,
|
||||
mkdirSync,
|
||||
readFileSync,
|
||||
readdirSync,
|
||||
rmSync,
|
||||
statSync,
|
||||
writeFileSync,
|
||||
} from "node:fs";
|
||||
import { join, resolve } from "node:path";
|
||||
import { buildRouter, type Route } from "@wrnexus/router";
|
||||
import { getReactiveRuntime } from "@wrnexus/csr";
|
||||
import { compileWireFile } from "@wrnexus/compiler";
|
||||
import { assertValidAst, generate, parse } from "@wrnexus/compiler";
|
||||
import {
|
||||
loadAppConfig,
|
||||
headToString,
|
||||
@@ -30,6 +39,8 @@ import { uiComponentsDir, uiCss } from "@wrnexus/ui";
|
||||
import { renderSchemasScript, type ObjectSchema, type SchemaDescriptor } from "@wrnexus/validation";
|
||||
import { loadLocales, resolveI18n } from "@wrnexus/i18n";
|
||||
import { pathToFileURL } from "node:url";
|
||||
import { checkPerformanceBudgets } from "@wrnexus/core";
|
||||
import { createPluginRunner } from "@wrnexus/plugin";
|
||||
|
||||
// Import the production server from the package specifier (not a source path) so
|
||||
// the generated entry resolves whether @wrnexus/dev-server is a workspace or an
|
||||
@@ -58,23 +69,47 @@ export async function runBuild(appRoot: string): Promise<void> {
|
||||
console.log(`✓ Public: ${distPublicDir}`);
|
||||
}
|
||||
|
||||
// `.wrn` route files are compiled to `.ts` so Bun.build can bundle them.
|
||||
let compiledCount = 0;
|
||||
const importPathFor = (file: string): string => {
|
||||
if (!file.endsWith(".wrn")) {
|
||||
return fwd(file);
|
||||
}
|
||||
|
||||
const ts = compileWireFile(readFileSync(file, "utf8"), file);
|
||||
|
||||
const out = join(compiledDir, `route${compiledCount++}.ts`);
|
||||
|
||||
writeFileSync(out, ts, "utf8");
|
||||
|
||||
return fwd(out);
|
||||
};
|
||||
|
||||
const config = await loadAppConfig(root);
|
||||
const pluginRunner = createPluginRunner(config.plugins, {
|
||||
root,
|
||||
mode: "production",
|
||||
command: "build",
|
||||
profile: process.env.WRNEXUS_PROFILE,
|
||||
metadata: new Map(),
|
||||
warn: (message) => console.warn(`[wrnexus:plugin] ${message}`),
|
||||
});
|
||||
await pluginRunner.configure(config as Record<string, unknown>);
|
||||
await pluginRunner.configResolved(config as Readonly<Record<string, unknown>>);
|
||||
await pluginRunner.hook("buildStart");
|
||||
|
||||
// `.wrn` route files are compiled once into deterministic intermediate modules.
|
||||
// Plugin AST/code transforms run only when configured, so existing applications
|
||||
// keep the exact compiler path and output contract by default.
|
||||
let compiledCount = 0;
|
||||
const compiledFiles = new Map<string, string>();
|
||||
const compileWrn = async (file: string): Promise<void> => {
|
||||
if (!file.endsWith(".wrn") || compiledFiles.has(file)) return;
|
||||
const source = readFileSync(file, "utf8");
|
||||
let ast = parse(source);
|
||||
assertValidAst(ast, { file, accessibility: true });
|
||||
ast = await pluginRunner.transformAst(ast, file);
|
||||
const pluginDiagnostics = await pluginRunner.diagnostics(ast, file);
|
||||
const errors = pluginDiagnostics.filter((diagnostic) => diagnostic.severity === "error");
|
||||
for (const diagnostic of pluginDiagnostics.filter((item) => item.severity !== "error")) {
|
||||
console.warn(`[${diagnostic.code}] ${file}: ${diagnostic.message}`);
|
||||
}
|
||||
if (errors.length) {
|
||||
throw new Error(
|
||||
errors.map((diagnostic) => `[${diagnostic.code}] ${diagnostic.message}`).join("\n"),
|
||||
);
|
||||
}
|
||||
let code = `// compiled from .wrn\n${generate(ast)}`;
|
||||
code = await pluginRunner.transformCode(code, file);
|
||||
const out = join(compiledDir, `route${compiledCount++}.ts`);
|
||||
writeFileSync(out, code, "utf8");
|
||||
compiledFiles.set(file, out);
|
||||
};
|
||||
const importPathFor = (file: string): string => fwd(compiledFiles.get(file) ?? file);
|
||||
// Regenerate typed DB queries (app/db/queries/*.sql → queries.gen.ts) first, so
|
||||
// any page/API importing them is built against the current SQL.
|
||||
const { regenerateQueries } = await import("./db.ts");
|
||||
@@ -105,6 +140,14 @@ export async function runBuild(appRoot: string): Promise<void> {
|
||||
}
|
||||
|
||||
const router = buildRouter(appDir, { componentDirs: [uiComponentsDir()] });
|
||||
const wrnFiles = new Set([
|
||||
...router.pages.map((route) => route.file),
|
||||
...router.api.map((route) => route.file),
|
||||
...router.realtime.map((route) => route.file),
|
||||
...router.components.map((component) => component.file),
|
||||
...router.layouts.map((layout) => layout.file),
|
||||
]);
|
||||
for (const file of wrnFiles) await compileWrn(file);
|
||||
const assetHash = createHash("sha256");
|
||||
|
||||
// 1) Components are `.wrn` modules rendered server-side — no browser chunks.
|
||||
@@ -271,6 +314,8 @@ await createProductionServer(
|
||||
mobile: ${JSON.stringify(config.mobile ?? {})},
|
||||
pwa: ${JSON.stringify(config.pwa ?? {})},
|
||||
security: ${JSON.stringify(config.security ?? {})},
|
||||
observability: ${JSON.stringify(config.observability ?? {})},
|
||||
tenancy: ${JSON.stringify(config.tenancy ?? {})},
|
||||
},
|
||||
);
|
||||
`;
|
||||
@@ -285,19 +330,118 @@ await createProductionServer(
|
||||
target: "bun",
|
||||
format: "esm",
|
||||
minify: true,
|
||||
sourcemap: config.build?.sourceMaps ? "inline" : "none",
|
||||
});
|
||||
if (!result.success) {
|
||||
throw new Error("Server build failed:\n" + result.logs.map(String).join("\n"));
|
||||
}
|
||||
writeFileSync(join(distDir, "server.js"), await result.outputs[0]!.text(), "utf8");
|
||||
|
||||
const report = createBuildReport({
|
||||
root,
|
||||
distDir,
|
||||
publicDir: distPublicDir,
|
||||
adapter: config.build?.adapter ?? "bun",
|
||||
routes: router.pages,
|
||||
runtimeFile: reactivePath,
|
||||
cssFile: hasStyles ? join(distDir, "styles.css") : join(distDir, "framework.css"),
|
||||
});
|
||||
const violations = checkPerformanceBudgets(
|
||||
config.performance?.budgets ?? {},
|
||||
report.measurements,
|
||||
);
|
||||
report.budgetViolations = violations;
|
||||
writeFileSync(join(distDir, "build-report.json"), JSON.stringify(report, null, 2) + "\n", "utf8");
|
||||
await pluginRunner.hook("buildEnd", report);
|
||||
|
||||
console.log(`✓ Server: ${join(distDir, "server.js")}`);
|
||||
console.log(
|
||||
`✓ Routes: ${router.pages.length} pages, ${router.api.length} api, ${router.realtime.length} realtime, ${mwVars.length} middleware`,
|
||||
);
|
||||
console.log(`✓ Report: ${join(distDir, "build-report.json")}`);
|
||||
if (violations.length) {
|
||||
for (const violation of violations) {
|
||||
console.warn(
|
||||
`⚠ Budget ${violation.metric}: ${violation.actual} > ${violation.budget} (+${violation.overBy})`,
|
||||
);
|
||||
}
|
||||
if (config.performance?.enforcement === "error") {
|
||||
throw new Error(
|
||||
`WRN-PERFORMANCE-BUDGET: ${violations.length} production budget(s) exceeded.`,
|
||||
);
|
||||
}
|
||||
}
|
||||
console.log(`\nRun it: bun ${fwd(join(distDir, "server.js"))}`);
|
||||
}
|
||||
|
||||
interface BuildReport {
|
||||
frameworkVersion: string;
|
||||
generatedAt: string;
|
||||
root: string;
|
||||
adapter: string;
|
||||
routes: Array<{ path: string; source: string; sourceBytes: number; dynamicParams: string[] }>;
|
||||
assets: Array<{ file: string; bytes: number }>;
|
||||
measurements: { routeJsBytes: number; routeCssBytes: number; imageBytes: number };
|
||||
budgetViolations: ReturnType<typeof checkPerformanceBudgets>;
|
||||
}
|
||||
|
||||
function fileBytes(file: string): number {
|
||||
return existsSync(file) && statSync(file).isFile() ? statSync(file).size : 0;
|
||||
}
|
||||
|
||||
function walkFiles(dir: string): string[] {
|
||||
if (!existsSync(dir)) return [];
|
||||
const files: string[] = [];
|
||||
for (const entry of readdirSync(dir)) {
|
||||
const path = join(dir, entry);
|
||||
const stat = statSync(path);
|
||||
if (stat.isDirectory()) files.push(...walkFiles(path));
|
||||
else if (stat.isFile()) files.push(path);
|
||||
}
|
||||
return files;
|
||||
}
|
||||
|
||||
function createBuildReport(input: {
|
||||
root: string;
|
||||
distDir: string;
|
||||
publicDir: string;
|
||||
adapter: string;
|
||||
routes: Route[];
|
||||
runtimeFile: string;
|
||||
cssFile: string;
|
||||
}): BuildReport {
|
||||
const assets = walkFiles(input.distDir)
|
||||
.filter((file) => !file.endsWith("build-report.json") && !fwd(file).includes("/compiled/"))
|
||||
.map((file) => ({ file: fwd(file.slice(input.distDir.length + 1)), bytes: fileBytes(file) }))
|
||||
.sort((a, b) => b.bytes - a.bytes);
|
||||
const imageExtensions = /\.(?:avif|gif|jpe?g|png|svg|webp)$/i;
|
||||
const imageBytes = Math.max(
|
||||
0,
|
||||
...walkFiles(input.publicDir)
|
||||
.filter((file) => imageExtensions.test(file))
|
||||
.map(fileBytes),
|
||||
);
|
||||
return {
|
||||
frameworkVersion: "0.3.0",
|
||||
generatedAt: new Date().toISOString(),
|
||||
root: input.root,
|
||||
adapter: input.adapter,
|
||||
routes: input.routes.map((route) => ({
|
||||
path: route.raw,
|
||||
source: fwd(route.file.replace(input.root, "").replace(/^\//, "")),
|
||||
sourceBytes: fileBytes(route.file),
|
||||
dynamicParams: route.paramNames,
|
||||
})),
|
||||
assets,
|
||||
measurements: {
|
||||
routeJsBytes: fileBytes(input.runtimeFile),
|
||||
routeCssBytes: fileBytes(input.cssFile),
|
||||
imageBytes,
|
||||
},
|
||||
budgetViolations: [],
|
||||
};
|
||||
}
|
||||
|
||||
async function buildBrowserRuntime(
|
||||
source: string,
|
||||
outFile: string,
|
||||
|
||||
Reference in New Issue
Block a user