release: WRNexusJS 0.7.0
This commit is contained in:
@@ -24,7 +24,13 @@ import {
|
||||
import { basename, extname, join, resolve } from "node:path";
|
||||
import { buildRouter, type Route } from "@wrnexus/router";
|
||||
import { getReactiveRuntime } from "@wrnexus/csr";
|
||||
import { assertValidAst, generate, parse } from "@wrnexus/compiler";
|
||||
import {
|
||||
analyzeRuntimeRequirements,
|
||||
assertValidAst,
|
||||
generate,
|
||||
parse,
|
||||
type RuntimeRequirements,
|
||||
} from "@wrnexus/compiler";
|
||||
import {
|
||||
loadAppConfig,
|
||||
headToString,
|
||||
@@ -102,12 +108,14 @@ 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 runtimeAnalysis = new Map<string, RuntimeRequirements>();
|
||||
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);
|
||||
runtimeAnalysis.set(file, analyzeRuntimeRequirements(ast));
|
||||
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")) {
|
||||
@@ -453,6 +461,7 @@ await createProductionServer(
|
||||
],
|
||||
runtimeFile: reactivePath,
|
||||
cssFile: hasStyles ? join(distDir, "styles.css") : join(distDir, "framework.css"),
|
||||
runtimeAnalysis,
|
||||
});
|
||||
report.pluginAssets = emittedPluginAssets.assets.map((asset) => ({
|
||||
id: asset.id,
|
||||
@@ -636,6 +645,11 @@ interface BuildReport {
|
||||
source: string;
|
||||
sourceBytes: number;
|
||||
dynamicParams: string[];
|
||||
execution: RuntimeRequirements["kind"];
|
||||
canPrerender: boolean;
|
||||
needsClientRuntime: boolean;
|
||||
needsServerRuntime: boolean;
|
||||
hydrationStrategy: string | null;
|
||||
}>;
|
||||
assets: Array<{ file: string; bytes: number }>;
|
||||
measurements: { routeJsBytes: number; routeCssBytes: number; imageBytes: number };
|
||||
@@ -666,6 +680,7 @@ function createBuildReport(input: {
|
||||
routes: Array<{ kind: "page" | "api" | "realtime"; route: Route }>;
|
||||
runtimeFile: string;
|
||||
cssFile: string;
|
||||
runtimeAnalysis: ReadonlyMap<string, RuntimeRequirements>;
|
||||
}): BuildReport {
|
||||
const assets = walkFiles(input.distDir)
|
||||
.filter((file) => !file.endsWith("build-report.json") && !fwd(file).includes("/compiled/"))
|
||||
@@ -689,6 +704,11 @@ function createBuildReport(input: {
|
||||
source: fwd(route.file.replace(input.root, "").replace(/^\//, "")),
|
||||
sourceBytes: fileBytes(route.file),
|
||||
dynamicParams: route.paramNames,
|
||||
execution: input.runtimeAnalysis.get(route.file)?.kind ?? "dynamic",
|
||||
canPrerender: input.runtimeAnalysis.get(route.file)?.canPrerender ?? false,
|
||||
needsClientRuntime: input.runtimeAnalysis.get(route.file)?.needsClientRuntime ?? true,
|
||||
needsServerRuntime: input.runtimeAnalysis.get(route.file)?.needsServerRuntime ?? true,
|
||||
hydrationStrategy: input.runtimeAnalysis.get(route.file)?.hydrationStrategy ?? null,
|
||||
})),
|
||||
assets,
|
||||
measurements: {
|
||||
|
||||
@@ -563,6 +563,71 @@ function writeMigrationReport(ctx: MigrationCtx): void {
|
||||
* Versioned, idempotent upgrade steps. Each MUST be safe to re-run. Source
|
||||
* migrations must preserve semantics and are protected by update backups.
|
||||
*/
|
||||
|
||||
function updateV070Config(ctx: MigrationCtx): void {
|
||||
const candidates = ["wrnexus.config.ts", "wrnexus.config.js", "wrnexus.config.mjs"];
|
||||
const file = candidates.map((name) => join(ctx.appRoot, name)).find(existsSync);
|
||||
if (!file) return;
|
||||
const current = readFileSync(file, "utf8");
|
||||
const additions: string[] = [];
|
||||
if (!/\bnavigation\s*:/.test(current)) {
|
||||
additions.push(` navigation: {
|
||||
mode: "auto",
|
||||
}`);
|
||||
}
|
||||
if (!/\bsecurity\s*:/.test(current)) {
|
||||
additions.push(` security: {
|
||||
headers: true,
|
||||
requestLimits: {
|
||||
maxUrlLength: 8192,
|
||||
maxHeaderCount: 100,
|
||||
maxHeaderBytes: 32768,
|
||||
maxQueryParameters: 100,
|
||||
maxBodyBytes: 10485760,
|
||||
timeoutMs: 30000,
|
||||
maxConcurrent: 1000,
|
||||
fetchMetadata: true,
|
||||
},
|
||||
contentSecurityPolicy: { enabled: true, useDefaults: true },
|
||||
trustedTypes: { enabled: true, requireForScript: true },
|
||||
hsts: { enabled: true, maxAge: 63072000, includeSubDomains: true, preload: true },
|
||||
}`);
|
||||
}
|
||||
if (!/\bperformance\s*:/.test(current)) {
|
||||
additions.push(` performance: {
|
||||
enforcement: "warn",
|
||||
analyze: true,
|
||||
budgets: {
|
||||
routeJsBytes: 51200,
|
||||
routeCssBytes: 25600,
|
||||
htmlBytes: 204800,
|
||||
hydrationMs: 200,
|
||||
serverRenderMs: 500,
|
||||
lcpMs: 2500,
|
||||
inpMs: 200,
|
||||
cls: 0.1,
|
||||
ttfbMs: 800,
|
||||
},
|
||||
}`);
|
||||
}
|
||||
if (!/\bobservability\s*:/.test(current)) {
|
||||
additions.push(` observability: {
|
||||
enabled: true,
|
||||
serverTiming: true,
|
||||
sampleRate: 0.1,
|
||||
exporter: "none",
|
||||
webVitals: true,
|
||||
}`);
|
||||
}
|
||||
if (!additions.length) return;
|
||||
const index = current.lastIndexOf("}");
|
||||
if (index < 0) return;
|
||||
const before = current.slice(0, index).replace(/,?\s*$/, "");
|
||||
const next = `${before},\n${additions.join(",\n")}\n${current.slice(index)}`;
|
||||
ctx.log(`~ ${file.slice(ctx.appRoot.length + 1)}: security/performance production defaults`);
|
||||
if (!ctx.dryRun) writeFileSync(file, next, "utf8");
|
||||
}
|
||||
|
||||
const MIGRATIONS: Migration[] = [
|
||||
{
|
||||
version: "0.2.8",
|
||||
@@ -1737,6 +1802,61 @@ const MIGRATIONS: Migration[] = [
|
||||
writeMigrationReport(ctx);
|
||||
},
|
||||
},
|
||||
{
|
||||
version: "0.7.0",
|
||||
id: "0.7.0-01-security-performance-foundation",
|
||||
description:
|
||||
"Adds secure-by-default request limits, caching, image optimization, observability, benchmarks, and production hardening packages.",
|
||||
apply(ctx) {
|
||||
const file = join(ctx.appRoot, "package.json");
|
||||
if (existsSync(file)) {
|
||||
const pkg = JSON.parse(readFileSync(file, "utf8")) as Record<string, any>;
|
||||
const dependencies = (pkg.dependencies ??= {});
|
||||
const devDependencies = (pkg.devDependencies ??= {});
|
||||
const added: string[] = [];
|
||||
for (const name of [
|
||||
"@wrnexus/security",
|
||||
"@wrnexus/cache",
|
||||
"@wrnexus/image",
|
||||
"@wrnexus/observability",
|
||||
]) {
|
||||
if (dependencies[name] === `^${ctx.to}`) continue;
|
||||
dependencies[name] = `^${ctx.to}`;
|
||||
added.push(name);
|
||||
}
|
||||
if (devDependencies["@wrnexus/benchmark"] !== `^${ctx.to}`) {
|
||||
devDependencies["@wrnexus/benchmark"] = `^${ctx.to}`;
|
||||
added.push("@wrnexus/benchmark (dev)");
|
||||
}
|
||||
if (added.length) {
|
||||
ctx.log(`+ security/performance dependencies: ${added.join(", ")}`);
|
||||
if (!ctx.dryRun) writeFileSync(file, JSON.stringify(pkg, null, 2) + "\n", "utf8");
|
||||
}
|
||||
}
|
||||
updateV070Config(ctx);
|
||||
const gitignore = join(ctx.appRoot, ".gitignore");
|
||||
const currentIgnore = existsSync(gitignore) ? readFileSync(gitignore, "utf8") : "";
|
||||
const ignoreEntries = [".env", ".env.*", "!.env.example", "!.env.*.example"];
|
||||
const knownIgnore = new Set(currentIgnore.split(/\r?\n/).map((line) => line.trim()));
|
||||
const missingIgnore = ignoreEntries.filter((entry) => !knownIgnore.has(entry));
|
||||
if (missingIgnore.length) {
|
||||
ctx.log(`+ .gitignore secure environment templates: ${missingIgnore.join(", ")}`);
|
||||
if (!ctx.dryRun) {
|
||||
writeFileSync(
|
||||
gitignore,
|
||||
`${currentIgnore.replace(/\s*$/, "")}\n${missingIgnore.join("\n")}\n`.replace(
|
||||
/^\n+/,
|
||||
"",
|
||||
),
|
||||
"utf8",
|
||||
);
|
||||
}
|
||||
}
|
||||
const review =
|
||||
"Review WRNexusJS 0.7 security policy, CSP allowlists, tenant authorization, upload scanners, cache adapters, and production performance budgets.";
|
||||
if (!ctx.report.needsReview.includes(review)) ctx.report.needsReview.push(review);
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
/** Release tooling uses this to require an explicit migration entry per version. */
|
||||
|
||||
Reference in New Issue
Block a user