release: WRNexusJS 0.6.0

This commit is contained in:
2026-08-01 01:09:58 +05:30
parent 3e565e8d03
commit 687d345882
502 changed files with 33038 additions and 11358 deletions
+23
View File
@@ -0,0 +1,23 @@
export interface ClientModuleScope {
output: Record<string, (payload?: unknown) => void>;
server: Record<string, (...args: unknown[]) => Promise<unknown>>;
props: Readonly<Record<string, unknown>>;
refs: Record<string, Element>;
}
const moduleCache = new Map<string, Promise<any>>();
export async function loadClientFunctions(
url: string,
scope: ClientModuleScope,
): Promise<Record<string, (...args: unknown[]) => unknown>> {
const module = await (moduleCache.get(url) ??
(() => {
const promise = import(/* @vite-ignore */ url);
moduleCache.set(url, promise);
return promise;
})());
if (typeof module.bindClientScope === "function") return module.bindClientScope(scope);
return module.__wrnexusClientFunctions ?? {};
}
export function invalidateClientModule(url: string): void {
moduleCache.delete(url);
}
+6
View File
@@ -29,3 +29,9 @@ export function getNavRuntime(): string {
export function getRealtimeRuntime(): string {
return REALTIME_RUNTIME;
}
export * from "./outputs.ts";
export * from "./server-client.ts";
export * from "./refs.ts";
export * from "./client-functions.ts";
export type * from "./types.ts";
+10
View File
@@ -243,6 +243,16 @@ export const NAV_RUNTIME = String.raw`
*/
function dispose(root) {
unmountPackageRuntimes(root);
try {
var stores = window.__wrnexusStoreContainer;
if (stores && typeof stores.disposePageStores === "function") {
Promise.resolve(stores.disposePageStores()).catch(function (error) {
console.error("[wrnexus] failed to dispose page stores", error);
});
}
} catch (error) {
console.error("[wrnexus] failed to access page stores", error);
}
try {
if (
typeof window.__wrnexusDisposeBehaviors ===
+39
View File
@@ -0,0 +1,39 @@
export type OutputHandler<T = unknown> = (payload: T) => void | Promise<void>;
export interface OutputHost extends HTMLElement {
__wrnexusOutputHandlers?: Map<string, Set<OutputHandler>>;
}
export function registerOutputHandler<T>(
host: OutputHost,
name: string,
handler: OutputHandler<T>,
): () => void {
const registry = (host.__wrnexusOutputHandlers ??= new Map());
const handlers = registry.get(name) ?? new Set();
handlers.add(handler as OutputHandler);
registry.set(name, handlers);
return () => {
handlers.delete(handler as OutputHandler);
if (!handlers.size) registry.delete(name);
};
}
export async function invokeOutput<T>(host: OutputHost, name: string, payload?: T): Promise<void> {
const handlers = host.__wrnexusOutputHandlers?.get(name);
if (handlers?.size) {
for (const handler of handlers) await handler(payload);
return;
}
// Compatibility path for legacy listeners outside a hydrated WRN parent.
host.dispatchEvent(new CustomEvent(name, { detail: payload }));
host.dispatchEvent(new CustomEvent(`wrnexus:${name}`, { detail: payload }));
}
export function createOutputProxy<T extends Record<string, (...args: any[]) => void>>(
host: OutputHost,
): T {
return new Proxy({} as T, {
get: (_target, property) => (payload?: unknown) =>
invokeOutput(host, String(property), payload),
});
}
+147 -2
View File
@@ -24,6 +24,7 @@ export const REACTIVE_RUNTIME = String.raw`
var pendingUpdateHooks = new Map();
var updateHooksScheduled = false;
var behaviorObserver;
var clientModuleCache = new Map();
function reportDiagnostic(code, message, element, detail) {
var payload = {
@@ -478,6 +479,21 @@ export const REACTIVE_RUNTIME = String.raw`
}
}
function loadClientModule(element) {
var url = element.getAttribute("data-wrn-client-module");
if (!url) return Promise.resolve(null);
var promise = clientModuleCache.get(url);
if (!promise) {
promise = import(url).catch(function (error) {
clientModuleCache.delete(url);
reportDiagnostic("WRN-CLIENT-MODULE", "Failed to load browser function module '" + url + "'.", element, error);
return null;
});
clientModuleCache.set(url, promise);
}
return promise;
}
function setupScope(el) {
if (el.__wrnexusScope) return;
@@ -677,13 +693,39 @@ export const REACTIVE_RUNTIME = String.raw`
}
var behaviorFunctions = {};
var componentEventTarget = el.querySelector("[data-wrn-events]") || el;
var moduleBindings = {};
var componentEventTarget = el.hasAttribute("data-wrn-events")
? el
: el.querySelector("[data-wrn-events]") || el;
var declaredEvents = new Set(
String(componentEventTarget.getAttribute("data-wrn-events") || "")
.split(",")
.map(function (name) { return name.trim(); })
.filter(Boolean),
);
var outputHandlers = componentEventTarget.__wrnexusOutputHandlers || (componentEventTarget.__wrnexusOutputHandlers = {});
var outputProxy = new Proxy({}, {
get: function (_target, property) {
return function (payload) {
return invokeComponentOutput(componentEventTarget, String(property), payload);
};
},
});
var componentRpcName = componentEventTarget.getAttribute("data-wrn-component") || componentEventTarget.getAttribute("data-wrn-hydration") || "component";
var serverProxy = new Proxy({}, {
get: function (_target, property) {
return function () {
return callServerFunction(componentRpcName, String(property), Array.prototype.slice.call(arguments));
};
},
});
var propsProxy = new Proxy({}, {
get: function (_target, property) { return peekScope(String(property)); },
set: function () { throw new TypeError("WRN-PROP-READONLY: props are readonly"); },
});
var refsProxy = new Proxy({}, {
get: function (_target, property) { return el.querySelector('[data-ref="' + String(property).replace(/"/g, '\"') + '"]'); },
});
var stateWatchers = {};
var anyStateListeners = new Set();
var cleanupCallbacks = [];
@@ -695,6 +737,11 @@ export const REACTIVE_RUNTIME = String.raw`
return dispatchComponentEvent(componentEventTarget, name, detail);
};
}
if (name === "output") return outputProxy;
if (name === "server") return serverProxy;
if (name === "props") return propsProxy;
if (name === "refs") return refsProxy;
if (Object.prototype.hasOwnProperty.call(moduleBindings, name)) return moduleBindings[name];
if (name === "$emit") {
return function (eventName, detail) {
return dispatchComponentEvent(componentEventTarget, eventName, detail);
@@ -919,6 +966,42 @@ export const REACTIVE_RUNTIME = String.raw`
});
}
function installClientModule(module) {
if (!module) return;
var stateProxy = new Proxy({}, {
get: function (_target, property) { return readScope(String(property)); },
set: function (_target, property, value) { writeScope(String(property), value); return true; },
ownKeys: function () { return Object.keys(signals); },
getOwnPropertyDescriptor: function () { return { enumerable: true, configurable: true }; },
});
var context = {
state: stateProxy,
output: outputProxy,
server: serverProxy,
props: propsProxy,
refs: refsProxy,
};
var importedBindings = module.__wrnexusImportedBindings;
if (importedBindings && typeof importedBindings === "object") {
Object.keys(importedBindings).forEach(function (name) {
var binding = importedBindings[name];
moduleBindings[name] = binding;
if (binding && typeof binding.subscribe === "function") {
cleanupCallbacks.push(binding.subscribe(function () { renderAll(); }));
}
});
}
var functions = typeof module.bindClientScope === "function"
? module.bindClientScope(context)
: module.__wrnexusClientFunctions;
if (!functions || typeof functions !== "object") return;
Object.keys(functions).forEach(function (name) {
if (typeof functions[name] === "function") behaviorFunctions[name] = functions[name];
});
}
installClientModule(el.__wrnexusClientModule);
// A binding belongs to THIS scope only when el is the node's nearest
// [data-scope] ancestor. Otherwise a nested scope owns it and we skip it,
// so an outer scope never clobbers an inner one's values.
@@ -1233,6 +1316,7 @@ export const REACTIVE_RUNTIME = String.raw`
eventLocals.event = event;
eventLocals.$event = event;
eventLocals.payload = event && Object.prototype.hasOwnProperty.call(event, "detail") ? event.detail : undefined;
try {
runStmt(
@@ -1773,6 +1857,7 @@ export const REACTIVE_RUNTIME = String.raw`
locals.event = event;
locals.$event = event;
locals.payload = event && Object.prototype.hasOwnProperty.call(event, "detail") ? event.detail : undefined;
try {
runStmt(
@@ -1794,6 +1879,19 @@ export const REACTIVE_RUNTIME = String.raw`
? { passive: true }
: undefined;
if (target === componentEventTarget && declaredEvents.has(evt)) {
var directHandler = function (payload) {
var locals = decodeLoopLocals(node);
locals.payload = payload;
locals.event = undefined;
locals.$event = undefined;
return runStmt(stmt, locals);
};
(outputHandlers[evt] || (outputHandlers[evt] = new Set())).add(directHandler);
cleanupCallbacks.push(function () {
if (outputHandlers[evt]) outputHandlers[evt].delete(directHandler);
});
}
target.addEventListener(evt, listener, options);
cleanupCallbacks.push(function () {
target.removeEventListener(evt, listener, options);
@@ -1932,7 +2030,16 @@ export const REACTIVE_RUNTIME = String.raw`
function hydrate() {
if (element.__wrnexusScope || !element.isConnected) return;
setupScope(element);
var moduleUrl = element.getAttribute("data-wrn-client-module");
if (!moduleUrl || moduleUrl === "__WRNEXUS_CLIENT_MODULE__") {
setupScope(element);
return;
}
loadClientModule(element).then(function (module) {
if (element.__wrnexusScope || !element.isConnected) return;
element.__wrnexusClientModule = module;
setupScope(element);
});
}
if (strategy === "load") {
@@ -2616,6 +2723,43 @@ export const REACTIVE_RUNTIME = String.raw`
}
}
function invokeComponentOutput(root, name, payload) {
if (!root || !name) return undefined;
var registry = root.__wrnexusOutputHandlers;
var handlers = registry && registry[name];
if (handlers && handlers.size) {
var values = [];
handlers.forEach(function (handler) { values.push(handler(payload)); });
return values.some(function (value) { return value && typeof value.then === "function"; })
? Promise.all(values)
: values[values.length - 1];
}
return dispatchComponentEvent(root, name, payload);
}
function callServerFunction(component, functionName, args) {
var csrf = document.querySelector('meta[name="wrnexus-csrf"]');
return fetch("/__wrnexus/rpc", {
method: "POST",
credentials: "same-origin",
headers: {
"content-type": "application/json",
"x-wrnexus-csrf": csrf ? csrf.getAttribute("content") || "" : "",
},
body: JSON.stringify({ component: component, function: functionName, args: args || [] }),
}).then(function (response) {
return response.json().catch(function () { return null; }).then(function (payload) {
if (!response.ok || !payload || !payload.ok) {
var error = new Error(payload && payload.error && payload.error.message || "Server function call failed");
error.code = payload && payload.error && payload.error.code || "WRN-RPC-FAILED";
error.status = response.status;
throw error;
}
return payload.value;
});
});
}
function dispatchComponentEvent(root, name, detail) {
if (!root || !name) return null;
var EventConstructor =
@@ -3789,6 +3933,7 @@ export const REACTIVE_RUNTIME = String.raw`
}
window.__wrnexusHydrateScopes = hydrateScopes;
window.__wrnexusInvalidateClientModule = function (url) { clientModuleCache.delete(url); };
window.__wrnexusHydrateCsrFetches = hydrateCsrFetches;
window.__wrnexusDisposeBehaviors = disposeBehaviors;
if (document.readyState === "loading") {
+10
View File
@@ -0,0 +1,10 @@
export function collectRefs(root: ParentNode): Record<string, Element> {
const refs: Record<string, Element> = {};
if (root instanceof Element && root.hasAttribute("data-ref"))
refs[root.getAttribute("data-ref")!] = root;
root.querySelectorAll("[data-ref]").forEach((element) => {
const name = element.getAttribute("data-ref");
if (name) refs[name] = element;
});
return refs;
}
+63
View File
@@ -0,0 +1,63 @@
export interface ServerCallOptions {
endpoint?: string;
signal?: AbortSignal;
headers?: HeadersInit;
csrfToken?: string;
}
export class WrnServerCallError extends Error {
constructor(
message: string,
readonly code: string,
readonly status: number,
readonly details?: unknown,
) {
super(message);
}
}
function csrfFromCookie(): string | undefined {
if (typeof document === "undefined") return undefined;
const raw = /(?:^|;\s*)wrnexus_csrf=([^;]+)/.exec(document.cookie)?.[1];
return raw ? decodeURIComponent(raw) : undefined;
}
export async function callServerFunction<TInput extends unknown[], TOutput>(
component: string,
functionName: string,
args: TInput,
options: ServerCallOptions = {},
): Promise<TOutput> {
const csrfToken = options.csrfToken ?? csrfFromCookie();
const response = await fetch(options.endpoint ?? "/__wrnexus/rpc", {
method: "POST",
credentials: "same-origin",
signal: options.signal,
headers: {
"content-type": "application/json",
...(csrfToken ? { "x-wrnexus-csrf": csrfToken } : {}),
...options.headers,
},
body: JSON.stringify({ component, function: functionName, args }),
});
const payload = (await response.json().catch(() => null)) as any;
if (!response.ok || !payload?.ok)
throw new WrnServerCallError(
payload?.error?.message ?? `Server call failed (${response.status})`,
payload?.error?.code ?? "WRN-RPC-FAILED",
response.status,
payload?.error?.details,
);
return payload.value as TOutput;
}
export function createServerProxy<T extends Record<string, (...args: any[]) => Promise<any>>>(
component: string,
options: ServerCallOptions = {},
): T {
return new Proxy({} as T, {
get:
(_target, property) =>
(...args: unknown[]) =>
callServerFunction(component, String(property), args, options),
});
}
+11
View File
@@ -0,0 +1,11 @@
export interface HydrationScopeApi {
get(name: string): unknown;
set(name: string, value: unknown): void;
call(name: string, ...args: unknown[]): unknown;
snapshot(): Readonly<Record<string, unknown>>;
dispose(): void;
}
export interface WrnexusBrowserGlobals {
__wrnexusHydrateScopes?(root?: ParentNode): void;
__wrnexusDisposeBehaviors?(root?: ParentNode): void;
}