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
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@wrnexus/ai",
"version": "0.8.2",
"version": "0.8.3",
"private": true,
"type": "module",
"description": "Zero-dependency Claude (Anthropic) client for WrNexus apps.",
@@ -1,3 +1,5 @@
import { Badge, Button, Card } from "@wrnexus/ui"
component AccountStatus {
props {
status: string = "active"
@@ -1,3 +1,5 @@
import { Button } from "@wrnexus/ui"
component AuthProviderButtons {
outputs {
select(payload: { provider: string; href: string })
@@ -1,3 +1,5 @@
import { Alert } from "@wrnexus/ui"
component AuthSecurityNotice {
props {
title: string = "Security notice"
+2
View File
@@ -1,3 +1,5 @@
import { Card } from "@wrnexus/ui"
component AuthShell {
props {
title: string = "Welcome"
@@ -1,3 +1,5 @@
import { Button, Input, PinInput } from "@wrnexus/ui"
component AuthenticatorSetup {
props {
setupAction = "/api/auth/totp/setup"
@@ -1,3 +1,5 @@
import { Avatar, Badge, Button, Card } from "@wrnexus/ui"
component DeviceSessions {
props {
sessions: unknown[] = []
@@ -1,3 +1,5 @@
import { Button, Input } from "@wrnexus/ui"
component ForgotPassword {
props {
action = "/api/auth/password/request"
@@ -1,3 +1,5 @@
import { Alert, Button } from "@wrnexus/ui"
component ImpersonationBanner {
props {
visible: boolean = true
@@ -1,3 +1,5 @@
import { Button, Input, StrongPassword } from "@wrnexus/ui"
component InvitationAccept {
props {
action = "/api/auth/invitations/accept"
@@ -1,3 +1,5 @@
import { Button, Input } from "@wrnexus/ui"
component MagicLinkSignIn {
props {
action = "/api/auth/magic-link/request"
+2
View File
@@ -1,3 +1,5 @@
import { Button, Input, PinInput } from "@wrnexus/ui"
component OtpSignIn {
props {
requestAction = "/api/auth/otp/login/request"
@@ -1,3 +1,5 @@
import { Button } from "@wrnexus/ui"
component PasskeyButton {
outputs {
passkeyRegistered(payload: { credentialId: string; response?: object })
@@ -1,3 +1,5 @@
import { Alert, Button, Card } from "@wrnexus/ui"
component RecoveryCodes {
props {
codes: unknown[] = []
@@ -1,3 +1,5 @@
import { Button, StrongPassword } from "@wrnexus/ui"
component ResetPassword {
props {
action = "/api/auth/password/reset"
+3
View File
@@ -1,3 +1,6 @@
import PasskeyButton from "./PasskeyButton.wrn"
import { Button, Checkbox, Input, TogglePassword } from "@wrnexus/ui"
component SignIn {
props {
action = "/api/auth/login"
+2
View File
@@ -1,3 +1,5 @@
import { Button, Checkbox, Input, StrongPassword } from "@wrnexus/ui"
component SignUp {
props {
action = "/api/auth/register"
@@ -1,3 +1,5 @@
import { Button, PinInput, Select } from "@wrnexus/ui"
component TwoFactorChallenge {
props {
action = "/api/auth/mfa/complete"
+2
View File
@@ -1,3 +1,5 @@
import { Button, Input } from "@wrnexus/ui"
component VerifyEmail {
props {
kind = "email"
+2
View File
@@ -1,3 +1,5 @@
import { Button, PinInput } from "@wrnexus/ui"
component VerifyPhone {
props {
kind = "phone"
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@wrnexus/auth",
"version": "0.8.2",
"version": "0.8.3",
"description": "Complete authentication, account security, MFA, passkeys, recovery, devices, risk, and audit system for WRNexusJS.",
"type": "module",
"sideEffects": false,
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@wrnexus/authz",
"version": "0.8.2",
"version": "0.8.3",
"private": true,
"type": "module",
"main": "src/index.ts",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@wrnexus/benchmark",
"version": "0.8.2",
"version": "0.8.3",
"type": "module",
"description": "Deterministic benchmark runner and performance regression budgets for WRNexusJS.",
"main": "src/index.ts",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@wrnexus/cache",
"version": "0.8.2",
"version": "0.8.3",
"type": "module",
"description": "Tag-aware memory and response caching with stale-while-revalidate for WRNexusJS.",
"main": "src/index.ts",
@@ -1,3 +1,6 @@
import Captcha from "./Captcha.wrn"
import { Card } from "@wrnexus/ui"
component CaptchaField {
props {
title: string = "Security verification"
@@ -1,3 +1,5 @@
import { Alert } from "@wrnexus/ui"
component CaptchaStatus {
props {
success: boolean = false
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@wrnexus/captcha",
"version": "0.8.2",
"version": "0.8.3",
"description": "First-class CAPTCHA challenges, providers, verification guards, page gates, and WRNexusJS UI.",
"type": "module",
"sideEffects": false,
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@wrnexus/cli",
"version": "0.8.2",
"version": "0.8.3",
"type": "module",
"main": "src/index.ts",
"exports": {
+108 -20
View File
@@ -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)},
+44 -3
View File
@@ -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> {
+10
View File
@@ -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. */
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@wrnexus/compiler",
"version": "0.8.2",
"version": "0.8.3",
"type": "module",
"main": "src/index.ts",
"exports": {
+115 -17
View File
@@ -1,4 +1,10 @@
import { eraseFunctionTypes, type PageAst, type RuntimeFunctionDecl } from "@wrnexus/syntax";
import {
eraseFunctionTypes,
type PageAst,
type RuntimeFunctionDecl,
type StructuredImportDecl,
type ViewNode,
} from "@wrnexus/syntax";
const RESERVED_BINDINGS = new Set([
"await",
@@ -63,6 +69,111 @@ function safeIdentifier(name: string): boolean {
return /^[A-Za-z_$][A-Za-z0-9_$]*$/.test(name) && !RESERVED_BINDINGS.has(name);
}
function identifierReferenced(source: string, name: string): boolean {
const escaped = name.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
return new RegExp(`(?:^|[^A-Za-z0-9_$])${escaped}(?![A-Za-z0-9_$])`).test(source);
}
function viewReferenceSource(nodes: ViewNode[]): string[] {
return nodes.flatMap((node): string[] => {
if (node.type === "text") return [node.value];
if (node.type === "each") {
return [
node.list,
node.key ?? "",
...viewReferenceSource(node.body),
...viewReferenceSource(node.empty),
];
}
if (node.type === "if") {
return node.branches.flatMap((branch) => [
branch.cond ?? "",
...viewReferenceSource(branch.body),
]);
}
return [...node.attrs.map((attr) => attr.value), ...viewReferenceSource(node.children)];
});
}
function browserReferenceSource(ast: PageAst, functions: RuntimeFunctionDecl[]): string {
return [
...functions.flatMap((fn) => [fn.source, fn.body]),
...ast.computed.map((entry) => entry.expr),
...ast.effects,
ast.lifecycle.mount ?? "",
ast.lifecycle.update ?? "",
ast.lifecycle.unmount ?? "",
...viewReferenceSource(ast.view),
].join("\n");
}
function renderSelectedImport(
entry: StructuredImportDecl,
source: string,
): { code: string; bindings: string[] } | null {
if (entry.typeOnly) return null;
const isStore = entry.source.endsWith(".wrn") && /(?:^|\/)stores?\//.test(entry.source);
if (entry.source.endsWith(".wrn") && !isStore) return null;
const defaultImport =
entry.defaultImport && (isStore || identifierReferenced(source, entry.defaultImport))
? entry.defaultImport
: undefined;
const namespaceImport =
entry.namespaceImport && (isStore || identifierReferenced(source, entry.namespaceImport))
? entry.namespaceImport
: undefined;
const namedImports = entry.namedImports.filter(
(item) => !item.typeOnly && (isStore || identifierReferenced(source, item.local)),
);
const bindings = [
...(defaultImport ? [defaultImport] : []),
...(namespaceImport ? [namespaceImport] : []),
...namedImports.map((item) => item.local),
].filter(safeIdentifier);
const hasBindings = Boolean(defaultImport || namespaceImport || namedImports.length);
const sideEffectOnly =
!entry.defaultImport && !entry.namespaceImport && entry.namedImports.length === 0;
if (!hasBindings && !sideEffectOnly) return null;
if (sideEffectOnly) return { code: entry.raw, bindings: [] };
const clauses: string[] = [];
if (defaultImport) clauses.push(defaultImport);
if (namespaceImport) clauses.push(`* as ${namespaceImport}`);
if (namedImports.length) {
clauses.push(
`{ ${namedImports
.map((item) =>
item.imported === item.local ? item.imported : `${item.imported} as ${item.local}`,
)
.join(", ")} }`,
);
}
return {
code: `import ${clauses.join(", ")} from ${JSON.stringify(entry.source)};`,
bindings,
};
}
function selectedBrowserImports(
ast: PageAst,
functions: RuntimeFunctionDecl[],
): { code: string; bindings: string[] }[] {
const referenceSource = browserReferenceSource(ast, functions);
return ast.structuredImports
.map((entry) => renderSelectedImport(entry, referenceSource))
.filter((entry): entry is { code: string; bindings: string[] } => entry !== null);
}
export function browserModuleRequired(ast: PageAst): boolean {
const functions = ast.runtimeFunctions.filter((fn) =>
["legacy", "client", "shared"].includes(fn.runtime),
);
return functions.length > 0 || selectedBrowserImports(ast, functions).length > 0;
}
function functionEntry(
ast: PageAst,
fn: RuntimeFunctionDecl,
@@ -164,22 +275,9 @@ export function generateBrowserModule(ast: PageAst): string {
);
const functionNames = functions.map((fn) => fn.name);
const state = ast.states.filter((entry) => entry.runtime !== "server").map((entry) => entry.name);
const storeImports = ast.structuredImports.filter(
(entry) =>
!entry.typeOnly && entry.source.endsWith(".wrn") && /(?:^|\/)stores?\//.test(entry.source),
);
const imports = ast.structuredImports
.filter((entry) => !entry.typeOnly)
.filter((entry) => !entry.source.endsWith(".wrn") || /(?:^|\/)stores?\//.test(entry.source))
.map((entry) => entry.raw)
.join("\n");
const importedBindings = storeImports
.flatMap((entry) => [
...(entry.defaultImport ? [entry.defaultImport] : []),
...(entry.namespaceImport ? [entry.namespaceImport] : []),
...entry.namedImports.map((item) => item.local),
])
.filter(safeIdentifier);
const selectedImports = selectedBrowserImports(ast, functions);
const imports = selectedImports.map((entry) => entry.code).join("\n");
const importedBindings = [...new Set(selectedImports.flatMap((entry) => entry.bindings))];
return `// generated WRNexusJS browser module for ${ast.name}
${imports}
export const __wrnexusClientFunctions = {
+220 -52
View File
@@ -22,6 +22,7 @@ import { eraseFunctionTypes, runtimeTypeOf } from "./types.ts";
import { stripRuntimeFunctionModifiers } from "@wrnexus/syntax";
import { generateStoreModule } from "./store-codegen.ts";
import { optimizeAst } from "./analysis.ts";
import { browserModuleRequired } from "./client-codegen.ts";
interface RenderBinding {
method: string;
@@ -264,12 +265,21 @@ function evalStateSeeds(states: { name: string; expr: string }[]): Record<string
* runtime keeps it live. Non-state `{expr}` and un-evaluable expressions are
* left as literal client mustaches.
*/
function substituteReactiveText(raw: string, reactive: PageReactive | null): string {
function substituteReactiveText(
raw: string,
reactive: PageReactive | null,
dynamicExpressions?: string[],
): string {
const text = substituteTMarkers(raw);
if (!reactive || reactive.stateNames.size === 0) return text;
return text.replace(/\{([^{}]+)\}/g, (whole, inner: string) => {
const expr = inner.trim();
if (expr.startsWith("t:") || !exprRefsState(expr, reactive.stateNames)) return whole;
if (exprRefsState(expr, reactive.runtimeStateNames) && dynamicExpressions) {
dynamicExpressions.push(`\${__wrnexusEscapeHtml(${expr})}`);
const sentinel = `\x00WRNEACH${dynamicExpressions.length - 1}\x00`;
return `<span data-text="${attrEscape(expr)}">${sentinel}</span>`;
}
let value: unknown;
try {
value = new Function("with(this){return (" + expr + ");}").call(reactive.scope);
@@ -308,7 +318,9 @@ function bakeLoopText(raw: string): string {
}
/** Bake a loop-body attribute value (same rules as text; escapeHtml is attribute-safe). */
function bakeLoopAttr(raw: string): string {
function bakeLoopAttr(raw: string, typed = false): string {
const wholeExpression = wholeAttributeExpression(raw);
if (typed && wholeExpression) return "${__wrnexusPropAttr(" + wholeExpression + ")}";
if (!raw.includes("{")) return escLit(attrEscape(raw));
let out = "";
let last = 0;
@@ -347,7 +359,7 @@ function renderLoopBody(node: ViewNode): string {
return escLit(` ${name}`);
}
return escLit(` ${name}="`) + bakeLoopAttr(attr.value) + escLit(`"`);
return escLit(` ${name}="`) + bakeLoopAttr(attr.value, componentTag) + escLit(`"`);
})
.join("");
@@ -492,7 +504,7 @@ function renderNode(
loops: string[],
reactive: PageReactive | null = null,
): string {
if (node.type === "text") return substituteReactiveText(node.value, reactive); // {t:key} + state baking
if (node.type === "text") return substituteReactiveText(node.value, reactive, loops); // {t:key} + state baking
// Server control block (loop / conditional) → a sentinel that survives
// templateEscape, swapped for its real `${…}` code after escaping.
@@ -532,13 +544,15 @@ function renderNode(
if (node.tag === "Async") {
const source = attrValue(node.attrs, "source") ?? "data";
const retries = attrValue(node.attrs, "retries") ?? "2";
const tags = attrValue(node.attrs, "tags") ?? source;
const serverResolved = attrValue(node.attrs, "data-wrn-async-server") === "true";
const asyncIndex = serverResolved ? loops.push("") - 1 : -1;
const branch = (name: string) => {
const element = node.children.find(
const branchElement = (name: string) =>
node.children.find(
(child): child is Extract<ViewNode, { type: "element" }> =>
child.type === "element" && child.tag === name,
);
const branch = (name: string) => {
const element = branchElement(name);
return (element?.children ?? [])
.map((child) => renderNode(child, ssrBindings, csrBindings, apiBindings, loops, reactive))
.join("");
@@ -546,19 +560,42 @@ function renderNode(
const loading = branch("Loading");
const success = branch("Success");
const error = branch("Error");
const successElement = branchElement("Success");
const errorElement = branchElement("Error");
const identifier = (value: string | undefined, fallback: string) =>
value && isSafeGeneratedIdentifier(value) ? value : fallback;
const successAlias = identifier(
successElement ? attrValue(successElement.attrs, "data") : undefined,
identifier(source, "data"),
);
const errorAlias = identifier(
errorElement ? attrValue(errorElement.attrs, "error") : undefined,
"error",
);
const nested = (value: string) => value.replace(/\\/g, "\\\\").replace(/`/g, "\\`");
const scoped = (value: string, alias: string, expression: string) => {
const index =
loops.push(
`\${(() => { const ${alias} = ${expression}; return \`${nested(value)}\`; })()}`,
) - 1;
return `\x00WRNEACH${index}\x00`;
};
const successTemplate = scoped(success, successAlias, `(ctx[${JSON.stringify(source)}] ?? {})`);
const errorTemplate = scoped(error, errorAlias, `{ message: "" }`);
let initial = loading;
if (serverResolved) {
const nested = (value: string) => value.replace(/\\/g, "\\\\").replace(/`/g, "\\`");
const sourcePattern = source.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
const sourcePattern = successAlias.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
const serverSuccess = success.replace(
new RegExp(`\\{\\s*(${sourcePattern}(?:\\.[A-Za-z_$][\\w$]*)*)\\s*\\}`, "g"),
(_whole, expression: string) => `\${__wrnexusEscapeHtml(${expression})}`,
);
loops[asyncIndex] =
`\${ctx[${JSON.stringify(source)}] !== undefined ? \`${nested(serverSuccess)}\` : \`${nested(loading)}\`}`;
initial = `\x00WRNEACH${asyncIndex}\x00`;
const index =
loops.push(
`\${ctx[${JSON.stringify(source)}] !== undefined ? (() => { const ${successAlias} = ctx[${JSON.stringify(source)}]; return \`${nested(serverSuccess)}\`; })() : \`${nested(loading)}\`}`,
) - 1;
initial = `\x00WRNEACH${index}\x00`;
}
return `<section data-wrn-async="${attrEscape(source)}" data-wrn-async-retries="${attrEscape(retries)}"${serverResolved ? ' data-wrn-async-resolved="true"' : ""} aria-busy="${serverResolved ? "false" : "true"}"><div data-wrn-async-content>${initial}</div><template data-wrn-async-loading>${loading}</template><template data-wrn-async-success>${success}</template><template data-wrn-async-error>${error}</template></section>`;
return `<section data-wrn-async="${attrEscape(source)}" data-wrn-async-tags="${attrEscape(tags)}" data-wrn-async-retries="${attrEscape(retries)}"${serverResolved ? ' data-wrn-async-resolved="true"' : ""} aria-busy="${serverResolved ? "false" : "true"}"><div data-wrn-async-content>${initial}</div><template data-wrn-async-loading>${loading}</template><template data-wrn-async-success data-wrn-async-alias="${attrEscape(successAlias)}">${successTemplate}</template><template data-wrn-async-error data-wrn-async-alias="${attrEscape(errorAlias)}">${errorTemplate}</template></section>`;
}
if (node.tag === "KeepAlive") {
@@ -965,13 +1002,133 @@ function generateSsrStateAliases(stateNames: string[]): string {
return `const { ${names.join(", ")} } = __state;\n`;
}
function orderPageStates(entries: PageAst["states"]): PageAst["states"] {
if (entries.length < 2) return entries;
const byName = new Map(entries.map((entry) => [entry.name, entry]));
const visiting = new Set<string>();
const visited = new Set<string>();
const ordered: PageAst["states"] = [];
const visit = (name: string): void => {
if (visited.has(name)) return;
if (visiting.has(name)) {
throw new Error(`WRN-STATE-CYCLE: state value '${name}' has a dependency cycle.`);
}
const entry = byName.get(name);
if (!entry) return;
visiting.add(name);
for (const dependency of byName.keys()) {
if (
dependency !== name &&
new RegExp(`\\b${dependency.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}\\b`).test(entry.expr)
) {
visit(dependency);
}
}
visiting.delete(name);
visited.add(name);
ordered.push(entry);
};
for (const entry of entries) visit(entry.name);
return ordered;
}
function generateSsrStateInitializer(entries: PageAst["states"]): string {
const ordered = orderPageStates(entries);
const declarations = ordered
.filter((entry) => isSafeGeneratedIdentifier(entry.name))
.map(
(entry) =>
`const ${entry.name} = (() => { try { return (${entry.expr}); } catch { return undefined; } })();`,
)
.join(" ");
const values = entries
.map((entry) => {
if (isSafeGeneratedIdentifier(entry.name)) {
return `${JSON.stringify(entry.name)}: ${entry.name}`;
}
return `${JSON.stringify(entry.name)}: (() => { try { return (${entry.expr}); } catch { return undefined; } })()`;
})
.join(", ");
return `(() => { ${declarations} return { ${values} }; })()`;
}
function orderPageComputed(entries: PageAst["computed"]): PageAst["computed"] {
if (entries.length < 2) return entries;
const byName = new Map(entries.map((entry) => [entry.name, entry]));
const visiting = new Set<string>();
const visited = new Set<string>();
const ordered: PageAst["computed"] = [];
const visit = (name: string): void => {
if (visited.has(name)) return;
if (visiting.has(name)) {
throw new Error(`WRN-COMPUTED-CYCLE: computed value '${name}' has a dependency cycle.`);
}
const entry = byName.get(name);
if (!entry) return;
visiting.add(name);
for (const dependency of byName.keys()) {
if (
dependency !== name &&
new RegExp(`\\b${dependency.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}\\b`).test(entry.expr)
) {
visit(dependency);
}
}
visiting.delete(name);
visited.add(name);
ordered.push(entry);
};
for (const entry of entries) visit(entry.name);
return ordered;
}
function generateSsrComputedAliases(entries: PageAst["computed"]): string {
return orderPageComputed(entries)
.filter((entry) => isSafeGeneratedIdentifier(entry.name))
.map(
(entry) =>
`const ${entry.name} = (() => { try { return (${entry.expr}); } catch { return undefined; } })();`,
)
.join("\n");
}
function runtimeComputedNames(
states: PageAst["states"],
computed: PageAst["computed"],
runtimeRoots: Iterable<string> = [],
): Set<string> {
const entries = [...states, ...computed];
const runtime = new Set([
...runtimeRoots,
...entries.filter((entry) => /\bctx\b/.test(entry.expr)).map((entry) => entry.name),
]);
let changed = true;
while (changed) {
changed = false;
for (const entry of entries) {
if (runtime.has(entry.name)) continue;
if (
[...runtime].some((name) =>
new RegExp(`\\b${name.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}\\b`).test(entry.expr),
)
) {
runtime.add(entry.name);
changed = true;
}
}
}
return runtime;
}
function hydrationAttribute(ast: PageAst): string {
const strategy = ["static", "server"].includes(ast.renderMode ?? "")
? "none"
: (ast.hydrate ?? "load");
const hasBrowserModule = ast.runtimeFunctions.some((fn) =>
["legacy", "client", "shared"].includes(fn.runtime),
);
const hasBrowserModule = browserModuleRequired(ast);
const moduleAttribute = hasBrowserModule
? ' data-wrn-client-module="__WRNEXUS_CLIENT_MODULE__"'
: "";
@@ -1119,9 +1276,11 @@ export function generate(ast: PageAst): string {
}
// --- View -> default page component ---
const orderedStates = orderPageStates(ast.states);
const browserStates = ast.states.filter((state) => state.runtime !== "server");
const seedScope = evalStateSeeds(ast.states);
for (const entry of ast.computed) {
const seedScope = evalStateSeeds(orderedStates);
const orderedComputed = orderPageComputed(ast.computed);
for (const entry of orderedComputed) {
try {
seedScope[entry.name] = new Function("with(this){return (" + entry.expr + ");}").call(
seedScope,
@@ -1134,9 +1293,14 @@ export function generate(ast: PageAst): string {
...browserStates.map((entry) => entry.name),
...ast.computed.map((entry) => entry.name),
];
const runtimeStateNames = new Set(
ast.states.filter((entry) => /\bctx\b/.test(entry.expr)).map((entry) => entry.name),
);
const storeBindings = importedStoreBindings(ast);
const runtimeRoots = [
...storeBindings.map((entry) => entry.local),
...ast.loads
.filter((load) => load.mode === "server" && !load.deferred && load.name)
.map((load) => load.name!),
];
const runtimeStateNames = runtimeComputedNames(orderedStates, orderedComputed, runtimeRoots);
const reactive: PageReactive | null =
reactiveNames.length > 0
? { stateNames: new Set(reactiveNames), runtimeStateNames, scope: seedScope }
@@ -1187,12 +1351,7 @@ export function generate(ast: PageAst): string {
);
staticShellBody = templateEscape(shellHtml);
}
const dynamicStateScope = ast.states
.map(
(state) =>
`${JSON.stringify(state.name)}: (() => { try { return (${state.expr}); } catch { return undefined; } })()`,
)
.join(", ");
const dynamicStateInitializer = generateSsrStateInitializer(orderedStates);
const stateType =
ast.states.length > 0
? `{ ${ast.states
@@ -1202,8 +1361,8 @@ export function generate(ast: PageAst): string {
const hydrationStateNames = JSON.stringify(browserStates.map((state) => state.name));
const ssrStateAliases = generateSsrStateAliases(ast.states.map((state) => state.name));
const ssrComputedAliases = generateSsrComputedAliases(orderedComputed);
const storeBindings = importedStoreBindings(ast);
const storeDeclarations = storeBindings
.map(
(entry) => ` const ${entry.local} = await ctx.__wrnexusUseStore(${entry.internal});`,
@@ -1214,12 +1373,16 @@ export function generate(ast: PageAst): string {
.map((load) => ` const ${load.name} = ctx[${JSON.stringify(load.name)}];`)
.join("\n");
loops.forEach((code, idx) => {
body = body.replace(`\x00WRNEACH${idx}\x00`, () => code);
// Inner dynamic expressions are registered before the wrapper that scopes
// them (for example an Async branch alias). Resolve from the outside in so a
// wrapper sentinel is expanded before its nested sentinels are visited.
for (let idx = loops.length - 1; idx >= 0; idx--) {
const code = loops[idx]!;
body = body.replaceAll(`\x00WRNEACH${idx}\x00`, code);
if (staticShellBody?.includes(`\x00WRNEACH${idx}\x00`)) {
staticShellBody = staticShellBody.replace(`\x00WRNEACH${idx}\x00`, () => code);
staticShellBody = staticShellBody.replaceAll(`\x00WRNEACH${idx}\x00`, code);
}
});
}
// Server loops iterate raw SSR data. Declare a named const for every `ssr` data
// binding a loop references, so `{#each <name> as …}` can iterate the real value.
@@ -1235,10 +1398,7 @@ export function generate(ast: PageAst): string {
}
}
const needsSsrRuntime =
ssrBindings.length > 0 ||
loops.length > 0 ||
ast.states.some((state) => /\bctx\b/.test(state.expr));
const needsSsrRuntime = ssrBindings.length > 0 || loops.length > 0 || runtimeStateNames.size > 0;
if (needsSsrRuntime) {
out.push(ssrRuntimeSource());
out.push(`const __wrnexusSsrBindings = ${JSON.stringify(ssrBindings, null, 2)};`);
@@ -1248,16 +1408,18 @@ export function generate(ast: PageAst): string {
${storeDeclarations}
${serverLoadAliases}
${decls}
const __state: ${stateType} = { ${dynamicStateScope} };
const __state: ${stateType} = ${dynamicStateInitializer};
${ssrStateAliases}
${ssrComputedAliases}
const __hydrationState = Object.fromEntries(${hydrationStateNames}.map((key) => [key, (__state as any)[key]]));
const __scopeValue = Object.entries(__hydrationState)
.map(([key, value]) => {
const encoded =
typeof value === "number" || typeof value === "boolean"
? String(value)
: JSON.stringify(value == null ? "" : String(value));
let encoded: string;
try {
encoded = value === undefined ? "undefined" : JSON.stringify(value);
} catch {
encoded = JSON.stringify(String(value));
}
return key + ": " + encoded;
})
.join(", ")
@@ -1279,16 +1441,18 @@ export function generate(ast: PageAst): string {
`export default ${storeBindings.length > 0 ? "async " : ""}function ${ast.name}(ctx: any) {
${storeDeclarations}
${serverLoadAliases}
const __state: ${stateType} = { ${dynamicStateScope} };
const __state: ${stateType} = ${dynamicStateInitializer};
${ssrStateAliases}
${ssrComputedAliases}
const __hydrationState = Object.fromEntries(${hydrationStateNames}.map((key) => [key, (__state as any)[key]]));
const __scopeValue = Object.entries(__hydrationState)
.map(([key, value]) => {
const encoded =
typeof value === "number" || typeof value === "boolean"
? String(value)
: JSON.stringify(value == null ? "" : String(value));
let encoded: string;
try {
encoded = value === undefined ? "undefined" : JSON.stringify(value);
} catch {
encoded = JSON.stringify(String(value));
}
return key + ": " + encoded;
})
.join(", ")
@@ -1311,14 +1475,18 @@ export function generate(ast: PageAst): string {
${storeDeclarations}
${serverLoadAliases}
${loopConsts.length > 0 ? loopConsts.join("\n") : ""}
const __state: ${stateType} = { ${dynamicStateScope} };
const __state: ${stateType} = ${dynamicStateInitializer};
${ssrStateAliases}
${ssrComputedAliases}
const __hydrationState = Object.fromEntries(${hydrationStateNames}.map((key) => [key, (__state as any)[key]]));
const __scopeValue = Object.entries(__hydrationState)
.map(([key, value]) => {
const encoded = typeof value === "number" || typeof value === "boolean"
? String(value)
: JSON.stringify(value == null ? "" : String(value));
let encoded: string;
try {
encoded = value === undefined ? "undefined" : JSON.stringify(value);
} catch {
encoded = JSON.stringify(String(value));
}
return key + ": " + encoded;
})
.join(", ")
+23 -6
View File
@@ -1,4 +1,4 @@
import { existsSync, realpathSync } from "node:fs";
import { existsSync, realpathSync, statSync } from "node:fs";
import { dirname, extname, join, resolve } from "node:path";
import type { StructuredImportDecl } from "@wrnexus/syntax";
@@ -33,12 +33,29 @@ export function resolveWrnImport(
options: ImportResolverOptions,
): ResolvedImport {
const source = declaration.source;
if (!source.startsWith(".") && !source.startsWith("@/")) return { declaration, resolved: source };
const aliasRoot = options.aliases?.["@"] ?? "./app";
const base = source.startsWith("@/")
? resolve(options.appRoot, aliasRoot, source.slice(2))
const aliases: Record<string, string> = {
"@": "./app",
...(options.aliases ?? {}),
};
const alias = Object.keys(aliases)
.filter((key) => key.length > 0 && (source === key || source.startsWith(`${key}/`)))
.sort((left, right) => right.length - left.length)[0];
if (!source.startsWith(".") && !alias) return { declaration, resolved: source };
const base = alias
? resolve(
options.appRoot,
aliases[alias]!,
source === alias ? "" : source.slice(alias.length + 1),
)
: resolve(dirname(importer), source);
const found = candidates(base).find(existsSync);
const found = candidates(base).find((candidate) => {
if (!existsSync(candidate)) return false;
try {
return statSync(candidate).isFile();
} catch {
return false;
}
});
if (found) return { declaration, resolved: realpathSync(found) };
const severity = (options.mode ?? "compatible") === "explicit" ? "error" : "warning";
return {
@@ -0,0 +1,40 @@
import { expect, test } from "bun:test";
import { mkdtempSync, mkdirSync, rmSync, writeFileSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { parse, resolveWrnImports } from "../src/index.ts";
test("configured import aliases resolve application client modules", () => {
const root = mkdtempSync(join(tmpdir(), "wrnexus-import-alias-"));
const app = join(root, "app");
const page = join(app, "settings.wrn");
const helper = join(app, "client", "storage.ts");
const clientIndex = join(app, "client", "index.ts");
mkdirSync(join(app, "client"), { recursive: true });
writeFileSync(helper, "export function persist(value: unknown) { return value; }\n");
writeFileSync(clientIndex, "export const storageReady = true;\n");
try {
const ast = parse(`import { persist } from "~/client/storage.ts"
page Settings { view { <button>Save</button> } }`);
const [resolved] = resolveWrnImports(ast.structuredImports, page, {
appRoot: root,
mode: "explicit",
aliases: { "~": "./app" },
});
expect(resolved?.diagnostic).toBeUndefined();
expect(resolved?.resolved).toBe(helper);
const directoryAst = parse(`import { storageReady } from "~client"
page ClientIndex { view { <p>Ready</p> } }`);
const [directoryResolved] = resolveWrnImports(directoryAst.structuredImports, page, {
appRoot: root,
mode: "explicit",
aliases: { "~client": "./app/client" },
});
expect(directoryResolved?.diagnostic).toBeUndefined();
expect(directoryResolved?.resolved).toBe(clientIndex);
} finally {
rmSync(root, { recursive: true, force: true });
}
});
@@ -0,0 +1,160 @@
import { expect, test } from "bun:test";
import { generate, generateTargets, parse } from "../src/index.ts";
test("page computed values derived from request state are declared during SSR", () => {
const code = generate(
parse(`import UserCard from "./UserCard.wrn"
page Chat {
state {
currentUserId = ctx.url.searchParams.get("as") ?? "asha"
users = [{ id: "asha", name: "Asha" }, { id: "rohan", name: "Rohan" }]
}
computed {
currentUser = users.find((user) => user.id === currentUserId) ?? users[0]
}
view {
<h1>{currentUser.name}</h1>
{#each users as user}
<UserCard user='{user}' />
<div data-user='{user.id}'></div>
{/each}
}
}`),
);
expect(code).toContain("const currentUser =");
expect(code).toContain("__wrnexusEscapeHtml(currentUser.name)");
expect(code).toContain("__wrnexusPropAttr(user)");
expect(code).toContain("__wrnexusEscapeHtml(user.id)");
expect(code).not.toContain("__wrnexusPropAttr(user.id)");
expect(code).toContain('encoded = value === undefined ? "undefined" : JSON.stringify(value)');
expect(code).not.toContain("String((__state as any)[key])");
});
test("computed values that reference ctx directly stay dynamic during SSR", () => {
const code = generate(
parse(`page DirectRequestComputed {
computed { identity = ctx.url.searchParams.get("as") ?? "asha" }
view { <p>{identity}</p> }
}`),
);
expect(code).toContain("const identity =");
expect(code).toContain("__wrnexusEscapeHtml(identity)");
});
test("computed values derived from server loads stay dynamic during SSR", () => {
const code = generate(
parse(`page LoadedComputed {
load server overview { return { count: 4 } }
computed { total = overview.count + 1 }
view { <p>{total}</p> }
}`),
);
expect(code).toContain('const overview = ctx["overview"]');
expect(code).toContain("__wrnexusEscapeHtml(total)");
});
test("request-derived state dependencies stay dynamic during SSR", async () => {
const code = generate(
parse(`page Identity {
state {
userId = ctx.url.searchParams.get("as") ?? "asha"
label = "User: " + userId
}
view { <p>{label}</p> }
}`),
);
expect(code).toContain("__wrnexusEscapeHtml(label)");
expect(code).not.toContain('<span data-text="label"></span>');
const javascript = new Bun.Transpiler({ loader: "ts" }).transformSync(code);
const moduleUrl = `data:text/javascript;base64,${Buffer.from(javascript).toString("base64")}`;
const rendered = await (
await import(moduleUrl)
).default({
url: new URL("http://localhost/?as=rohan"),
});
expect(rendered).toContain("User: rohan");
});
test("Async aliases and invalidation tags are scoped in generated templates", () => {
const code = generate(
parse(`page Uploads {
load client uploads { return { name: "report.pdf" } }
view {
<Async source="uploads" tags="uploads,files">
<Loading>Loading</Loading>
<Success data="file"><a href="/files/{file.name}">{file.name}</a></Success>
<Error error="problem"><p>{problem.message}</p></Error>
</Async>
}
}`),
);
expect(code).toContain('data-wrn-async-tags="uploads,files"');
expect(code).toContain('data-wrn-async-alias="file"');
expect(code).toContain('data-wrn-async-alias="problem"');
expect(code).toContain("const file =");
expect(code).toContain("const problem =");
});
test("browser codegen prunes SSR-only UI imports and keeps client dependencies", () => {
const browser = generateTargets(
parse(`import { Button } from "@wrnexus/ui"
import { save } from "./helper.ts"
component Settings {
state { enabled = true }
functions {
client function persist(): void { save(enabled) }
}
view {
<Button label="Save" />
<button @click='persist()'>Save</button>
}
}`),
).browser;
expect(browser).not.toContain("@wrnexus/ui");
expect(browser).toContain('import { save } from "./helper.ts"');
expect(browser).toContain('"persist": function');
});
test("reactive view imports trigger a browser module without hydrating static SSR helpers", () => {
const reactiveAst = parse(`import { formatName } from "./format.ts"
page Profile {
state { name = "Asha" }
view { <p>{formatName(name)}</p> }
}`);
const staticAst = parse(`import { readFileSync } from "node:fs"
page StaticProfile {
runtime = "server"
view { <p>{readFileSync("profile.txt", "utf8")}</p> }
}`);
expect(generate(reactiveAst)).toContain('data-wrn-client-module="__WRNEXUS_CLIENT_MODULE__"');
expect(generateTargets(reactiveAst).browser).toContain(
'import { formatName } from "./format.ts"',
);
expect(generate(staticAst)).not.toContain("data-wrn-client-module");
});
test("state dependency cycles fail code generation", () => {
expect(() =>
generate(
parse(`page CyclicState {
state { first = second + 1 second = first + 1 }
view { <p>{first}</p> }
}`),
),
).toThrow("WRN-STATE-CYCLE");
});
test("computed dependency cycles fail code generation", () => {
expect(() =>
generate(
parse(`page Cyclic {
computed { first = second + 1 second = first + 1 }
view { <p>{first}</p> }
}`),
),
).toThrow("WRN-COMPUTED-CYCLE");
});
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@wrnexus/content",
"version": "0.8.2",
"version": "0.8.3",
"type": "module",
"description": "Typed Markdown content collections, loaders, indexes, feeds, and preview workflows for WRNexusJS.",
"main": "./src/index.ts",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@wrnexus/core",
"version": "0.8.2",
"version": "0.8.3",
"type": "module",
"main": "src/index.ts",
"exports": {
-1
View File
@@ -135,7 +135,6 @@ const DEFAULT_PERMISSIONS_POLICY: PermissionsPolicyConfig = {
payment: [],
"picture-in-picture": [],
"sync-xhr": [],
unload: [],
usb: [],
"xr-spatial-tracking": [],
};
+5
View File
@@ -72,3 +72,8 @@ test("resolveRequestUrl honors standard Forwarded headers behind trusted proxies
expect(resolveRequestUrl(request, true).href).toBe("https://workroot.in/path");
expect(resolveRequestUrl(request, false).href).toBe("http://127.0.0.1:3000/path");
});
test("default permissions policy does not emit unsupported unload", () => {
const res = withSecurityHeaders(req(), new Response("x"), "development");
expect(res.headers.get("permissions-policy") ?? "").not.toContain("unload=");
});
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@wrnexus/csr",
"version": "0.8.2",
"version": "0.8.3",
"type": "module",
"main": "src/index.ts",
"exports": {
+5
View File
@@ -702,5 +702,10 @@ export const NAV_RUNTIME = String.raw`
false,
);
};
/** Re-render the current route without adding a duplicate history entry. */
window.__wrnexusRefresh = function () {
navigate(location.href, true);
};
})();
`.trim();
+150 -12
View File
@@ -363,6 +363,26 @@ export const REACTIVE_RUNTIME = String.raw`
},
};
}
function routeSnapshot() {
var url = new URL(window.location.href);
var params = window.__wrnexusRouteParams || {};
var marker = document.querySelector("[data-wrn-route-params]");
if (marker) {
try { params = JSON.parse(marker.getAttribute("data-wrn-route-params") || "{}"); }
catch (_) { params = {}; }
window.__wrnexusRouteParams = params;
marker.remove();
}
return {
url: url.href,
pathname: url.pathname,
search: url.search,
hash: url.hash,
searchParams: url.searchParams,
params: params,
};
}
function sanitizeReactiveUrl(value) {
var raw = String(value == null ? "" : value);
var compact = raw.trim().replace(/[\u0000-\u0020]+/g, "").toLowerCase();
@@ -755,6 +775,20 @@ export const REACTIVE_RUNTIME = String.raw`
var anyStateListeners = new Set();
var cleanupCallbacks = [];
var disposed = false;
var routeValue = signal(routeSnapshot());
function updateRouteValue() {
routeValue.set(routeSnapshot());
}
window.addEventListener("wrnexus:navigated", updateRouteValue);
window.addEventListener("popstate", updateRouteValue);
window.addEventListener("hashchange", updateRouteValue);
cleanupCallbacks.push(function () {
window.removeEventListener("wrnexus:navigated", updateRouteValue);
window.removeEventListener("popstate", updateRouteValue);
window.removeEventListener("hashchange", updateRouteValue);
});
function readGlobal(name) {
if (declaredEvents.has(name)) {
@@ -785,6 +819,10 @@ export const REACTIVE_RUNTIME = String.raw`
if (name === "location") return window.location;
if (name === "history") return window.history;
if (name === "navigator") return window.navigator;
if (name === "$route" || name === "route") {
if (currentRenderer) routeValue.subscribe(currentRenderer);
return routeValue.get();
}
if (name === "setTimeout") return window.setTimeout.bind(window);
if (name === "clearTimeout") return window.clearTimeout.bind(window);
if (name === "setInterval") return window.setInterval.bind(window);
@@ -965,6 +1003,12 @@ export const REACTIVE_RUNTIME = String.raw`
function installBehaviorFunctions(source) {
extractBehaviorFunctions(source || "").forEach(function (definition) {
// A generated browser module contains the real JavaScript implementation
// of client/shared functions. Keep that implementation when it has already
// been installed instead of replacing it with the small CSP-safe fallback
// interpreter. The fallback intentionally supports only a bounded subset of
// JavaScript and must never shadow the compiled function.
if (typeof behaviorFunctions[definition.name] === "function") return;
behaviorFunctions[
definition.name
] = function () {
@@ -3665,15 +3709,8 @@ export const REACTIVE_RUNTIME = String.raw`
return special.value;
}
// Template literals are evaluated without eval so they remain compatible
// with a strict CSP. Each interpolation uses the same bounded expression
// evaluator as every other reactive binding.
if (
expr.length >= 2 &&
expr[0] === "\`" &&
expr[expr.length - 1] === "\`"
) {
var template = expr.slice(1, -1);
function evaluateTemplateLiteral(source) {
var template = source.slice(1, -1);
var rendered = "";
var cursor = 0;
@@ -3703,7 +3740,7 @@ export const REACTIVE_RUNTIME = String.raw`
else if (character === "}" && --depth === 0) break;
}
if (depth !== 0) throw new Error("Unclosed template interpolation in '" + expr + "'");
if (depth !== 0) throw new Error("Unclosed template interpolation in '" + source + "'");
rendered += String(evaluateExpression(template.slice(expressionStart, expressionEnd), read));
cursor = expressionEnd + 1;
continue;
@@ -3715,6 +3752,18 @@ export const REACTIVE_RUNTIME = String.raw`
return rendered;
}
// Template literals are evaluated without eval so they remain compatible
// with a strict CSP. Each interpolation uses the same bounded expression
// evaluator as every other reactive binding. They are accepted both as a
// complete expression and as a primary inside a larger expression.
if (
expr.length >= 2 &&
expr[0] === "\`" &&
expr[expr.length - 1] === "\`"
) {
return evaluateTemplateLiteral(expr);
}
var tokens =
tokenizeExpression(expr);
@@ -3731,6 +3780,7 @@ export const REACTIVE_RUNTIME = String.raw`
var t = next();
if (!t) throw new Error("Unexpected end of expression in '" + expr + "'");
if (t.type === "number" || t.type === "string") return { value: t.value };
if (t.type === "template") return { value: evaluateTemplateLiteral(t.value) };
if (t.type === "ident") {
if (t.value === "true") return { value: true };
if (t.value === "false") return { value: false };
@@ -3874,6 +3924,37 @@ export const REACTIVE_RUNTIME = String.raw`
tokens.push({ type: "string", value: value });
continue;
}
if (ch === "\`") {
var templateStart = i++;
var templateDepth = 0;
var templateQuote = "";
while (i < input.length) {
ch = input[i++];
if (templateQuote) {
if (ch === "\\") i++;
else if (ch === templateQuote) templateQuote = "";
continue;
}
if (ch === "\\") {
i++;
continue;
}
if (ch === "\`" && templateDepth === 0) break;
if (ch === "$" && input[i] === "{") {
templateDepth++;
i++;
continue;
}
if (templateDepth > 0) {
if (ch === '"' || ch === "'") templateQuote = ch;
else if (ch === "{") templateDepth++;
else if (ch === "}") templateDepth--;
}
}
if (input[i - 1] !== "\`") throw new Error("Unclosed template literal");
tokens.push({ type: "template", value: input.slice(templateStart, i) });
continue;
}
if (/[A-Za-z_$]/.test(ch)) {
var s = i++;
while (i < input.length && /[A-Za-z0-9_$]/.test(input[i])) i++;
@@ -4047,6 +4128,23 @@ export const REACTIVE_RUNTIME = String.raw`
Array.from(node.childNodes || []).forEach(visit);
}
visit(root);
root.querySelectorAll("*").forEach(function (node) {
Array.from(node.attributes || []).forEach(function (attribute) {
if (attribute.name.indexOf("data-wrn-async-") === 0) return;
if (attribute.value.indexOf("{") === -1) return;
var nextValue = String(attribute.value).replace(/\{\s*([A-Za-z_$][\w$]*(?:\.[A-Za-z_$][\w$]*)*)\s*\}/g, function (whole, path) {
var parts = path.split(".");
if (parts[0] === prefix) parts.shift();
if (prefix === "error" && parts[0] !== "message") return whole;
var value = asyncValueAt(model, parts.join("."));
return typeof value === "object" ? JSON.stringify(value) : String(value);
});
if (/^(?:href|src|action|formaction)$/i.test(attribute.name)) {
nextValue = sanitizeReactiveUrl(nextValue);
}
node.setAttribute(attribute.name, nextValue);
});
});
root.querySelectorAll("[data-wrn-async-value]").forEach(function (node) {
var path = node.getAttribute("data-wrn-async-value") || "";
var value = asyncValueAt(model, path);
@@ -4058,7 +4156,9 @@ export const REACTIVE_RUNTIME = String.raw`
var content = boundary.querySelector("[data-wrn-async-content]");
if (!template || !content) return;
var fragment = template.content.cloneNode(true);
interpolateAsync(fragment, model, state === "error" ? "error" : boundary.getAttribute("data-wrn-async"));
var alias = template.getAttribute("data-wrn-async-alias") ||
(state === "error" ? "error" : boundary.getAttribute("data-wrn-async"));
interpolateAsync(fragment, model, alias);
content.replaceChildren(fragment);
boundary.setAttribute("aria-busy", state === "loading" ? "true" : "false");
boundary.setAttribute("data-wrn-async-state", state);
@@ -4073,7 +4173,7 @@ export const REACTIVE_RUNTIME = String.raw`
}
var name = boundary.getAttribute("data-wrn-async") || "";
if (!/^[A-Za-z_$][\w$]{0,63}$/.test(name)) return showAsync(boundary, "error", { message: "Invalid data source" });
var url = "/__wrnexus/client-load?route=" + encodeURIComponent(location.pathname) + "&name=" + encodeURIComponent(name);
var url = "/__wrnexus/client-load?route=" + encodeURIComponent(location.pathname) + "&search=" + encodeURIComponent(location.search) + "&name=" + encodeURIComponent(name);
var attempts = Math.min(5, Math.max(0, Number(boundary.getAttribute("data-wrn-async-retries")) || 0));
var controller = new AbortController();
boundary.__wrnexusAsyncAbort = controller;
@@ -4102,6 +4202,44 @@ export const REACTIVE_RUNTIME = String.raw`
(root || document).querySelectorAll("[data-wrn-async]").forEach(setupAsyncBoundary);
}
var invalidationRefreshPending = false;
function asyncBoundaryTags(boundary) {
var raw = boundary.getAttribute("data-wrn-async-tags") || boundary.getAttribute("data-wrn-async") || "";
return raw.split(",").map(function (tag) { return tag.trim(); }).filter(Boolean);
}
function refreshAsyncBoundary(boundary) {
if (boundary.__wrnexusAsyncAbort) {
try { boundary.__wrnexusAsyncAbort.abort(); } catch (_) {}
}
boundary.__wrnexusAsync = false;
boundary.removeAttribute("data-wrn-async-resolved");
setupAsyncBoundary(boundary);
}
function handleCacheInvalidation(event) {
var detail = event && event.detail && typeof event.detail === "object" ? event.detail : {};
var tags = Array.isArray(detail.tags) ? detail.tags.map(String) : [];
if (!tags.length) return;
var matchedServerBoundary = false;
document.querySelectorAll("[data-wrn-async]").forEach(function (boundary) {
var matches = asyncBoundaryTags(boundary).some(function (tag) { return tags.indexOf(tag) !== -1; });
if (!matches) return;
if (boundary.getAttribute("data-wrn-async-resolved") === "true") {
matchedServerBoundary = true;
return;
}
refreshAsyncBoundary(boundary);
});
if (matchedServerBoundary && !invalidationRefreshPending) {
invalidationRefreshPending = true;
Promise.resolve().then(function () {
invalidationRefreshPending = false;
if (typeof window.__wrnexusRefresh === "function") window.__wrnexusRefresh();
else window.location.reload();
});
}
}
window.addEventListener("wrnexus:cache:invalidate", handleCacheInvalidation);
function mountClientRoots(root) {
(root || document).querySelectorAll("template[data-wrn-client-template]").forEach(function (template) {
var id = template.getAttribute("data-wrn-client-template");
+33 -9
View File
@@ -23,10 +23,29 @@ export const REALTIME_RUNTIME = String.raw`
if (!("WebSocket" in window)) return;
var wire = (window.wire = window.wire || {});
if (wire.room) return; // already installed
var open = {}; // name -> room connection
var open = {}; // normalized room+query key -> room connection
function normalizedQuery(query) {
var params = new URLSearchParams(String(query || "").replace(/^\?/, ""));
var pairs = [];
params.forEach(function (value, name) { pairs.push([name, value]); });
pairs.sort(function (a, b) {
return a[0] === b[0] ? String(a[1]).localeCompare(String(b[1])) : String(a[0]).localeCompare(String(b[0]));
});
var normalized = new URLSearchParams();
pairs.forEach(function (pair) { normalized.append(pair[0], pair[1]); });
return normalized.toString();
}
function roomKey(name, query) {
var normalized = normalizedQuery(query);
return String(name || "") + (normalized ? "?" + normalized : "");
}
function openRoom(name, query) {
if (open[name]) return open[name];
query = normalizedQuery(query);
var key = roomKey(name, query);
if (open[key]) return open[key];
var ws = null, queue = [], listeners = [], attempts = 0, timer = null, closed = false;
function url() {
@@ -69,6 +88,8 @@ export const REALTIME_RUNTIME = String.raw`
var api = {
name: name,
key: key,
query: query,
send: function (obj) {
var payload = typeof obj === "string" ? obj : JSON.stringify(obj);
if (ws && ws.readyState === 1) ws.send(payload);
@@ -80,9 +101,9 @@ export const REALTIME_RUNTIME = String.raw`
listeners.push({ type: type, cb: cb });
return api;
},
close: function () { closed = true; clearTimeout(timer); if (ws) try { ws.close(); } catch (_) {} ws = null; delete open[name]; },
close: function () { closed = true; clearTimeout(timer); if (ws) try { ws.close(); } catch (_) {} ws = null; delete open[key]; },
};
open[name] = api;
open[key] = api;
connect();
return api;
}
@@ -114,11 +135,14 @@ export const REALTIME_RUNTIME = String.raw`
}
function bindContainer(el) {
if (el.__wireRoomBound) return;
el.__wireRoomBound = true;
var name = el.getAttribute("data-room");
var user = el.getAttribute("data-room-user");
var room = openRoom(name, user ? "user=" + encodeURIComponent(user) : "");
var query = el.getAttribute("data-room-query") || (user ? "user=" + encodeURIComponent(user) : "");
var key = roomKey(name, query);
if (el.__wireRoomBound && el.__wireRoomKey === key) return;
el.__wireRoomBound = true;
el.__wireRoomKey = key;
var room = openRoom(name, query);
el.__wireRoom = room;
var log = el.querySelector("[data-room-log]");
@@ -161,7 +185,7 @@ export const REALTIME_RUNTIME = String.raw`
var input = form.elements[i];
if (input.name) data[input.name] = input.value;
}
room.send(data);
if (el.__wireRoom) el.__wireRoom.send(data);
for (var j = 0; j < form.elements.length; j++) {
if (form.elements[j].hasAttribute("data-room-reset")) form.elements[j].value = "";
}
@@ -173,8 +197,8 @@ export const REALTIME_RUNTIME = String.raw`
var containers = (root || document).querySelectorAll("[data-room]");
var present = {};
for (var i = 0; i < containers.length; i++) {
present[containers[i].getAttribute("data-room")] = true;
bindContainer(containers[i]);
if (containers[i].__wireRoomKey) present[containers[i].__wireRoomKey] = true;
}
// Close rooms whose container has left the page (client-side navigation).
for (var nm in open) if (!present[nm]) open[nm].close();
+121
View File
@@ -452,3 +452,124 @@ test("$emit inside component functions does not depend on a global browser event
expect(win.document.querySelector("span")!.textContent).toBe("true");
expect(detail).toEqual({ source: "click" });
});
test("template literals work inside browser API call arguments", () => {
const behavior = Buffer.from(
JSON.stringify({
functions: `function save() {
window.localStorage.setItem(\`prefs:\${count}\`, JSON.stringify(count))
message = \`Saved \${count}\`
}`,
watches: [],
lifecycle: {},
}),
).toString("base64");
const win = mount(
`<div data-scope="count: 3, message: 'ready'" data-wrn-behavior="${behavior}">` +
`<button data-on-click="save()">{message}</button></div>`,
);
win.document.querySelector("button")!.click();
expect(win.localStorage.getItem("prefs:3")).toBe("3");
expect(win.document.querySelector("button")!.textContent).toBe("Saved 3");
});
test("compiled client functions are not overwritten by fallback behavior parsing", () => {
const behavior = Buffer.from(
JSON.stringify({
functions: `function save() { message = \`fallback\` }`,
watches: [],
lifecycle: {},
}),
).toString("base64");
const win = new Window() as unknown as Window & Record<string, unknown>;
win.document.body.innerHTML =
`<div id="scope" data-scope="message: 'ready'" data-wrn-behavior="${behavior}">` +
`<button data-on-click="save()">{message}</button></div>`;
const scope = win.document.getElementById("scope") as unknown as HTMLElement & {
__wrnexusClientModule?: unknown;
};
scope.__wrnexusClientModule = {
bindClientScope(context: { state: Record<string, unknown> }) {
return {
save() {
context.state.message = "native";
},
};
},
};
(globalThis as Record<string, unknown>).window = win;
(globalThis as Record<string, unknown>).document = win.document;
(globalThis as Record<string, unknown>).location = win.location;
(globalThis as Record<string, unknown>).NodeFilter = (
win as unknown as { NodeFilter: unknown }
).NodeFilter;
(globalThis as Record<string, unknown>).MutationObserver = (
win as unknown as { MutationObserver: unknown }
).MutationObserver;
(0, eval)(REACTIVE_RUNTIME);
(win as unknown as { __wrnexusHydrateScopes?: (root: unknown) => void }).__wrnexusHydrateScopes?.(
win.document,
);
win.document.querySelector("button")!.click();
expect(win.document.querySelector("button")!.textContent).toBe("native");
});
test("$route updates after client navigation", () => {
const win = new Window({ url: "http://localhost/" }) as unknown as Window &
Record<string, unknown>;
win.document.body.innerHTML =
`<span hidden data-wrn-route-params='{"id":"42"}'></span>` +
`<div data-scope=""><strong>{$route.pathname}</strong><em>{$route.params.id}</em></div>`;
(globalThis as Record<string, unknown>).window = win;
(globalThis as Record<string, unknown>).document = win.document;
(globalThis as Record<string, unknown>).location = win.location;
(globalThis as Record<string, unknown>).NodeFilter = (
win as unknown as { NodeFilter: unknown }
).NodeFilter;
(globalThis as Record<string, unknown>).MutationObserver = (
win as unknown as { MutationObserver: unknown }
).MutationObserver;
(0, eval)(REACTIVE_RUNTIME);
(win as unknown as { __wrnexusHydrateScopes?: (root: unknown) => void }).__wrnexusHydrateScopes?.(
win.document,
);
expect(win.document.querySelector("strong")!.textContent).toBe("/");
expect(win.document.querySelector("em")!.textContent).toBe("42");
expect(win.document.querySelector("[data-wrn-route-params]")).toBeNull();
win.history.pushState({}, "", "/settings?tab=chat");
win.dispatchEvent(
new (win as unknown as { CustomEvent: typeof CustomEvent }).CustomEvent(
"wrnexus:navigated",
) as unknown as Parameters<Window["dispatchEvent"]>[0],
);
expect(win.document.querySelector("strong")!.textContent).toBe("/settings");
});
test("cache invalidation refetches matching client Async boundaries", async () => {
let requests = 0;
(globalThis as Record<string, unknown>).fetch = () => {
requests++;
return Promise.resolve(Response.json({ data: { value: requests } }));
};
const win = mount(
`<section data-wrn-async="uploads" data-wrn-async-tags="uploads" data-wrn-async-retries="0">` +
`<div data-wrn-async-content></div>` +
`<template data-wrn-async-loading>Loading</template>` +
`<template data-wrn-async-success data-wrn-async-alias="uploads"><b>{uploads.value}</b></template>` +
`<template data-wrn-async-error data-wrn-async-alias="error">{error.message}</template>` +
`</section>`,
);
const runtime = win as unknown as { __wrnexusHydrateAsyncBoundaries?: (root: unknown) => void };
runtime.__wrnexusHydrateAsyncBoundaries?.(win.document);
await new Promise((resolve) => setTimeout(resolve, 0));
expect(win.document.querySelector("b")?.textContent).toBe("1");
win.dispatchEvent(
new (win as unknown as { CustomEvent: typeof CustomEvent }).CustomEvent(
"wrnexus:cache:invalidate",
{ detail: { tags: ["uploads"] } },
) as unknown as Parameters<Window["dispatchEvent"]>[0],
);
await new Promise((resolve) => setTimeout(resolve, 0));
expect(requests).toBe(2);
expect(win.document.querySelector("b")?.textContent).toBe("2");
});
+24
View File
@@ -121,3 +121,27 @@ test("programmatic wire.room() sends and receives", () => {
sockets[0]!.fireMessage({ type: "ping", n: 1 });
expect(got).toEqual([{ type: "ping", n: 1 }]);
});
test("same room with different users opens identity-specific sockets", () => {
boot(`
<div data-room="teamspace" data-room-user="asha"></div>
<div data-room="teamspace" data-room-user="rohan"></div>
`);
expect(sockets.map((socket) => socket.url).sort()).toEqual([
"ws://localhost/realtime/teamspace?user=asha",
"ws://localhost/realtime/teamspace?user=rohan",
]);
});
test("declarative room reconnects when its user identity changes", () => {
const win = boot(
`<div id="room" data-room="teamspace" data-room-user="asha"></div>`,
) as unknown as Window & {
wire: { bindRooms: (root?: unknown) => void };
};
expect(sockets[0]?.url).toBe("ws://localhost/realtime/teamspace?user=asha");
win.document.getElementById("room")!.setAttribute("data-room-user", "neha");
win.wire.bindRooms(win.document);
expect(sockets[0]?.readyState).toBe(3);
expect(sockets[1]?.url).toBe("ws://localhost/realtime/teamspace?user=neha");
});
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@wrnexus/db",
"version": "0.8.2",
"version": "0.8.3",
"private": true,
"type": "module",
"main": "./src/index.ts",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@wrnexus/dev-server",
"version": "0.8.2",
"version": "0.8.3",
"type": "module",
"main": "src/index.ts",
"exports": {
+2 -2
View File
@@ -40,7 +40,7 @@ import {
setCompileCacheDir,
setCompileImportOptions,
setDevCompilerPipeline,
wrnBrowserArtifactUrl,
wrnBrowserArtifactUrlAsync,
} from "./pipeline.ts";
import { createRpcHandler } from "@wrnexus/ssr/rpc";
import { createHandlers, type WsData } from "./runtime.ts";
@@ -632,7 +632,7 @@ export async function startServer(opts: ServeOptions): Promise<RunningServer> {
storeUpdates.push({
name: declaration[2]!,
kind: declaration[1]!,
url: wrnBrowserArtifactUrl(absolute),
url: await wrnBrowserArtifactUrlAsync(absolute),
});
} catch (error) {
console.warn(`[wrnexus] failed to prepare store HMR for ${absolute}`, error);
+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;
+10
View File
@@ -9,6 +9,7 @@
* supervised `dev --production-runtime` mode can explicitly enable it.
*/
import { join } from "node:path";
import type { Middleware, Mode, SecurityConfig, SeoConfig } from "@wrnexus/core";
import {
compileRoutePattern,
@@ -88,6 +89,8 @@ export interface ProdOptions {
stylesIncludeFramework?: boolean;
/** Absolute path to the pre-built reactive runtime. */
reactivePath?: string;
/** Absolute directory containing bundled per-WRN browser modules. */
clientModulesDir?: string;
/** Absolute path to the pre-built theme stylesheet (`theme.css`). */
themePath?: string;
/** Absolute path to the pre-built theme runtime (`theme.js`). */
@@ -267,6 +270,13 @@ async function serveFile(path: string | undefined, headers: Record<string, strin
function createProdAssetServer(opts: ProdOptions): AssetServer {
return {
async serve(pathname: string): Promise<Response | null> {
if (pathname.startsWith("/__wrnexus/client/")) {
const name = pathname.slice("/__wrnexus/client/".length);
if (!opts.clientModulesDir || !/^[A-Za-z0-9._-]+\.mjs$/.test(name)) {
return new Response("Not Found", { status: 404 });
}
return serveFile(join(opts.clientModulesDir, name), JS_HEADERS);
}
if (pathname === "/__wrnexus/reactive.js") {
if (opts.reactivePath) {
const file = Bun.file(opts.reactivePath);
+12 -2
View File
@@ -1279,11 +1279,14 @@ export function createHandlers(deps: RuntimeDeps): Handlers {
async function handleClientLoad(ctx: Context): Promise<Response> {
const routePath = ctx.url.searchParams.get("route") ?? "";
const routeSearch = ctx.url.searchParams.get("search") ?? "";
const name = ctx.url.searchParams.get("name") ?? "";
if (
!routePath.startsWith("/") ||
routePath.startsWith("/__wrnexus/") ||
!isSafeRequestPath(routePath) ||
(routeSearch !== "" &&
(!routeSearch.startsWith("?") || routeSearch.includes("#") || routeSearch.length > 4096)) ||
!/^[A-Za-z_$][\w$]{0,63}$/.test(name)
) {
return new Response("Not Found", { status: 404 });
@@ -1294,8 +1297,11 @@ export function createHandlers(deps: RuntimeDeps): Handlers {
const load = pageModule.__wrnexusClientLoad;
if (typeof load !== "function") return new Response("Not Found", { status: 404 });
try {
ctx.params = page.params;
const values = await load(ctx);
const routeUrl = new URL(routePath + routeSearch, ctx.url.origin);
if (routeUrl.pathname !== routePath) return new Response("Not Found", { status: 404 });
const routeRequest = new Request(routeUrl, { method: "GET", headers: ctx.req.headers });
const loadCtx: Context = { ...ctx, req: routeRequest, url: routeUrl, params: page.params };
const values = await load(loadCtx);
if (!values || typeof values !== "object" || !(name in values)) {
return new Response("Not Found", { status: 404 });
}
@@ -1627,6 +1633,10 @@ export function createHandlers(deps: RuntimeDeps): Handlers {
if (partial) {
body = typeof precomputedShell === "string" ? precomputedShell : (pagePartial?.shell ?? body);
}
const routeParamsMarker = `<span hidden data-wrn-route-params="${escapeHtml(
JSON.stringify(ctx.params ?? {}),
)}"></span>`;
body = `${routeParamsMarker}${body}`;
if (body.includes("data-wrn-action=")) {
body = body.replace(
/(<form\b[^>]*\bdata-wrn-action=(?:"[^"]+"|'[^']+')[^>]*>)/gi,
@@ -0,0 +1,90 @@
import { expect, test } from "bun:test";
import { mkdtempSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import {
compileWireArtifactsAsync,
setCompileCacheDir,
setCompileImportOptions,
} from "../src/pipeline.ts";
test("development browser artifacts bundle local client imports", async () => {
const root = mkdtempSync(join(tmpdir(), "wrnexus-client-bundle-"));
const app = join(root, "app");
const cache = join(root, ".wrnexus");
mkdirSync(app, { recursive: true });
writeFileSync(join(app, "helper.ts"), "export function save(value: unknown) { return value; }\n");
const page = join(app, "settings.wrn");
writeFileSync(
page,
`import { save } from "./helper.ts"
page Settings {
state { value = 1 }
functions { client function persist() { return save(value) } }
view { <button @click='persist()'>Save</button> }
}`,
);
try {
setCompileCacheDir(cache);
const artifacts = await compileWireArtifactsAsync(page);
const browser = readFileSync(artifacts.browser, "utf8");
expect(browser).toContain("wrnexus-client-bundled");
expect(browser).not.toContain("file://");
expect(browser).not.toContain('from "./helper.ts"');
} finally {
setCompileCacheDir(null);
rmSync(root, { recursive: true, force: true });
}
});
test("development browser artifacts bundle configured alias imports", async () => {
const root = mkdtempSync(join(tmpdir(), "wrnexus-client-alias-bundle-"));
const app = join(root, "app");
const cache = join(root, ".wrnexus");
mkdirSync(join(app, "client"), { recursive: true });
writeFileSync(
join(app, "client", "storage.ts"),
"export function persist(value: unknown) { return value; }\n",
);
const page = join(app, "settings.wrn");
writeFileSync(
page,
`import { persist } from "~/client/storage.ts"
page Settings {
state { value = 1 }
functions { client function save() { return persist(value) } }
view { <button @click='save()'>Save</button> }
}`,
);
try {
setCompileCacheDir(cache);
setCompileImportOptions(root, {
mode: "explicit",
aliases: { "~": "./app" },
autoImport: false,
});
const artifacts = await compileWireArtifactsAsync(page);
const browser = readFileSync(artifacts.browser, "utf8");
expect(browser).toContain("wrnexus-client-bundled");
expect(browser).not.toContain("~/client/storage.ts");
expect(browser).not.toContain("file://");
mkdirSync(join(root, "alternate", "client"), { recursive: true });
writeFileSync(
join(root, "alternate", "client", "storage.ts"),
"export function persist(value: unknown) { return value; }\n",
);
setCompileImportOptions(root, {
mode: "explicit",
aliases: { "~": "./alternate" },
autoImport: false,
});
const changed = await compileWireArtifactsAsync(page);
expect(changed.browser).not.toBe(artifacts.browser);
} finally {
setCompileCacheDir(null);
rmSync(root, { recursive: true, force: true });
}
});
@@ -62,7 +62,9 @@ test("production streams request regions into the build-time static shell", asyn
test("normal production output remains free of development HMR", async () => {
const handlers = createProductionHandlers(manifest, {});
const response = await handlers.fetch(new Request("http://localhost/"), server);
expect(await (response as Response).text()).not.toContain("/__wrnexus/hmr");
const html = await (response as Response).text();
expect(html).not.toContain("/__wrnexus/hmr");
expect(html).toContain('data-wrn-route-params="{}"');
});
test("production client-load endpoint returns only the requested named result", async () => {
@@ -121,3 +123,43 @@ test("production prefers fast gzip for dynamic HTML and cached Brotli for immuta
)) as Response;
expect(head.headers.get("content-encoding")).toBeNull();
});
test("client-load preserves the active route query string", async () => {
const queryManifest: ProdManifest = {
...manifest,
pages: [
...manifest.pages,
{
raw: "/query",
mod: {
default: () => "<main>Query page</main>",
__wrnexusClientLoad: async (ctx: { url: URL }) => ({
identity: ctx.url.searchParams.get("as"),
}),
},
},
],
};
const handlers = createProductionHandlers(queryManifest, {});
const response = (await handlers.fetch(
new Request(
"http://localhost/__wrnexus/client-load?route=%2Fquery&search=%3Fas%3Drohan&name=identity",
),
server,
)) as Response;
expect(response.status).toBe(200);
expect(await response.json()).toEqual({ data: "rohan" });
});
test("production serves generated WRN browser modules from the client directory", async () => {
const directory = mkdtempSync(join(tmpdir(), "wrnexus-client-modules-"));
writeFileSync(join(directory, "settings-abc123.mjs"), "export const ready = true;\n");
const handlers = createProductionHandlers(manifest, { clientModulesDir: directory });
const response = (await handlers.fetch(
new Request("http://localhost/__wrnexus/client/settings-abc123.mjs"),
server,
)) as Response;
expect(response.status).toBe(200);
expect(response.headers.get("content-type")).toContain("text/javascript");
expect(await response.text()).toContain("ready = true");
});
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@wrnexus/dev-toolbar",
"version": "0.8.2",
"version": "0.8.3",
"private": true,
"type": "module",
"sideEffects": false,
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@wrnexus/encryption",
"version": "0.8.2",
"version": "0.8.3",
"private": true,
"type": "module",
"main": "./src/index.ts",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@wrnexus/graphql",
"version": "0.8.2",
"version": "0.8.3",
"type": "module",
"main": "./src/index.ts",
"exports": {
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@wrnexus/helpers",
"version": "0.8.2",
"version": "0.8.3",
"private": true,
"type": "module",
"description": "Safe convenience helpers for WrNexus request contexts and common application flows.",
@@ -1,3 +1,5 @@
import { Badge, Card } from "@wrnexus/ui"
component LocaleStatus {
props {
locale: string = "en"
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@wrnexus/i18n",
"version": "0.8.2",
"version": "0.8.3",
"private": true,
"type": "module",
"main": "./src/index.ts",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@wrnexus/identity",
"version": "0.8.2",
"version": "0.8.3",
"type": "module",
"description": "Enterprise federation, provisioning, machine identity, and privacy governance for WRNexusJS.",
"main": "src/index.ts",
+3
View File
@@ -1,3 +1,6 @@
import OptimizedImage from "./OptimizedImage.wrn"
import { Card } from "@wrnexus/ui"
component ImageCard {
props {
title: string = ""
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@wrnexus/image",
"version": "0.8.2",
"version": "0.8.3",
"type": "module",
"description": "Responsive image planning, secure remote image policies, and performance auditing for WRNexusJS.",
"main": "./src/index.ts",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@wrnexus/jwt",
"version": "0.8.2",
"version": "0.8.3",
"private": true,
"type": "module",
"main": "./src/index.ts",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@wrnexus/language-server",
"version": "0.8.2",
"version": "0.8.3",
"type": "module",
"description": "Editor-neutral Language Server Protocol implementation for WRNexus .wrn files.",
"main": "src/index.ts",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@wrnexus/mcp",
"version": "0.8.2",
"version": "0.8.3",
"type": "module",
"description": "Model Context Protocol server exposing WRNexus application and framework context.",
"main": "src/index.ts",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@wrnexus/mobile",
"version": "0.8.2",
"version": "0.8.3",
"type": "module",
"main": "src/index.ts",
"exports": {
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@wrnexus/native",
"version": "0.8.2",
"version": "0.8.3",
"type": "module",
"main": "src/index.ts",
"exports": {
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@wrnexus/oauth",
"version": "0.8.2",
"version": "0.8.3",
"private": true,
"type": "module",
"main": "src/index.ts",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@wrnexus/observability",
"version": "0.8.2",
"version": "0.8.3",
"type": "module",
"description": "Metrics, Web Vitals collection, request instrumentation, and exporter adapters for WRNexusJS.",
"main": "src/index.ts",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@wrnexus/playground",
"version": "0.8.2",
"version": "0.8.3",
"type": "module",
"description": "Secure, shareable WRNexus compiler and UI playground.",
"main": "src/index.ts",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@wrnexus/plugin",
"version": "0.8.2",
"version": "0.8.3",
"type": "module",
"main": "src/index.ts",
"exports": {
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@wrnexus/pubsub",
"version": "0.8.2",
"version": "0.8.3",
"private": true,
"type": "module",
"main": "src/index.ts",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@wrnexus/pwa",
"version": "0.8.2",
"version": "0.8.3",
"type": "module",
"description": "Service workers, manifests, offline queues, background sync, push, and conflict resolution for WRNexusJS.",
"main": "./src/index.ts",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@wrnexus/queue",
"version": "0.8.2",
"version": "0.8.3",
"private": true,
"type": "module",
"main": "src/index.ts",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@wrnexus/reactive",
"version": "0.8.2",
"version": "0.8.3",
"type": "module",
"main": "src/index.ts",
"exports": {
@@ -1,3 +1,5 @@
import { ChatBubble } from "@wrnexus/ui"
component RealtimeMessageBubble {
props {
id: string = ""
@@ -1,3 +1,5 @@
import { Button, Textarea } from "@wrnexus/ui"
component MessageComposer {
props {
name: string = "text"
@@ -1,3 +1,5 @@
import { Avatar, Badge, Card } from "@wrnexus/ui"
component PresenceList {
props {
members: unknown[] = []
@@ -1,3 +1,6 @@
import RoomStatus from "./RoomStatus.wrn"
import { Card } from "@wrnexus/ui"
component RealtimeRoom {
props {
room: string = ""
@@ -1,3 +1,5 @@
import { Badge, Card } from "@wrnexus/ui"
component RoomMeta {
props {
name: string = "Room"
@@ -1,3 +1,5 @@
import { Badge } from "@wrnexus/ui"
component RoomStatus {
props {
label: string = "Connecting…"
@@ -1,3 +1,5 @@
import { Alert } from "@wrnexus/ui"
component TypingIndicator {
props {
names: unknown[] = []
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@wrnexus/realtime",
"version": "0.8.2",
"version": "0.8.3",
"description": "Typed realtime rooms, message helpers, presence utilities, package UI blocks, and WRNexusJS integration.",
"type": "module",
"sideEffects": false,
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@wrnexus/router",
"version": "0.8.2",
"version": "0.8.3",
"type": "module",
"main": "src/index.ts",
"exports": {
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@wrnexus/security",
"version": "0.8.2",
"version": "0.8.3",
"type": "module",
"description": "Security policies, safe serialization, SSRF protection, request limits, and secure cookie helpers for WRNexusJS.",
"main": "src/index.ts",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@wrnexus/ssr",
"version": "0.8.2",
"version": "0.8.3",
"type": "module",
"main": "src/index.ts",
"exports": {
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@wrnexus/store",
"version": "0.8.2",
"version": "0.8.3",
"type": "module",
"main": "src/index.ts",
"exports": {
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@wrnexus/styles",
"version": "0.8.2",
"version": "0.8.3",
"type": "module",
"main": "src/index.ts",
"exports": {
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@wrnexus/syntax",
"version": "0.8.2",
"version": "0.8.3",
"type": "module",
"main": "src/index.ts",
"exports": {
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@wrnexus/test",
"version": "0.8.2",
"version": "0.8.3",
"private": true,
"type": "module",
"main": "src/index.ts",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@wrnexus/tracking",
"version": "0.8.2",
"version": "0.8.3",
"private": true,
"type": "module",
"main": "src/index.ts",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@wrnexus/typecheck",
"version": "0.8.2",
"version": "0.8.3",
"type": "module",
"main": "src/index.ts",
"exports": {
+5
View File
@@ -1,3 +1,8 @@
import Button from "./button.wrn"
import Checkbox from "./Checkbox.wrn"
import Input from "./Input.wrn"
import PinInput from "./PinInput.wrn"
component AuthForm {
outputs {
submit(payload: { event: Event; mode: string; action: string })
+2
View File
@@ -1,3 +1,5 @@
import FeatureCard from "./FeatureCard.wrn"
component FeatureGrid {
props {
size: string = "default"
@@ -1,3 +1,5 @@
import TextLink from "./TextLink.wrn"
component FeatureIconCard {
props {
icon: string = "icon-[lucide--sparkles]"
+2
View File
@@ -1,3 +1,5 @@
import Badge from "./Badge.wrn"
component Hero {
props {
eyebrow: string = ""
+2
View File
@@ -1,3 +1,5 @@
import Button from "./button.wrn"
component HeroActions {
props {
actions: unknown[] = []
@@ -1,3 +1,6 @@
import SectionHeader from "./SectionHeader.wrn"
import TextLink from "./TextLink.wrn"
component MarketingSectionHeader {
props {
id: string = ""

Some files were not shown because too many files have changed in this diff Show More