3293 lines
127 KiB
JavaScript
3293 lines
127 KiB
JavaScript
var __defProp = Object.defineProperty;
|
|
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
var __copyProps = (to, from, except, desc) => {
|
|
if (from && typeof from === "object" || typeof from === "function") {
|
|
for (let key of __getOwnPropNames(from))
|
|
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
}
|
|
return to;
|
|
};
|
|
var __reExport = (target, mod, secondTarget) => (__copyProps(target, mod, "default"), secondTarget && __copyProps(secondTarget, mod, "default"));
|
|
|
|
// packages/compiler/src/index.ts
|
|
import {
|
|
assertValidAst,
|
|
diagnose as diagnose2,
|
|
diagnosticFromError,
|
|
formatDiagnostic,
|
|
parse as parse2,
|
|
ParseError as ParseError2
|
|
} from "@wrnexus/syntax";
|
|
import { formatWrn } from "@wrnexus/syntax";
|
|
|
|
// packages/compiler/src/codegen.ts
|
|
import { Buffer as Buffer2 } from "buffer";
|
|
|
|
// packages/compiler/src/parser.ts
|
|
var parser_exports = {};
|
|
__reExport(parser_exports, parser_star);
|
|
import * as parser_star from "@wrnexus/syntax/parser";
|
|
|
|
// packages/compiler/src/types.ts
|
|
var types_exports = {};
|
|
__reExport(types_exports, types_star);
|
|
import * as types_star from "@wrnexus/syntax/types";
|
|
|
|
// packages/compiler/src/codegen.ts
|
|
import { stripRuntimeFunctionModifiers as stripRuntimeFunctionModifiers2 } from "@wrnexus/syntax";
|
|
|
|
// packages/compiler/src/store-codegen.ts
|
|
import { eraseFunctionTypes } from "@wrnexus/syntax";
|
|
|
|
// packages/compiler/src/type-codegen.ts
|
|
function member(name) {
|
|
return /^[A-Za-z_$][\w$]*$/.test(name) ? name : JSON.stringify(name);
|
|
}
|
|
function params(astParams) {
|
|
return astParams.map(
|
|
(param) => `${member(param.name)}${param.optional ? "?" : ""}: ${param.valueType ?? "unknown"}`
|
|
).join(", ");
|
|
}
|
|
function generateDeclarations(ast) {
|
|
const inline = ast.types.map((body) => body.trim()).filter(Boolean).join("\n\n");
|
|
if (ast.kind === "global-store" || ast.kind === "page-store") {
|
|
const state = ast.states.filter((entry) => entry.runtime !== "server").map((entry) => ` readonly ${member(entry.name)}: ${entry.valueType ?? "unknown"};`).join("\n");
|
|
const computed = ast.computed.map((entry) => ` readonly ${member(entry.name)}: ${entry.valueType ?? "unknown"};`).join("\n");
|
|
const actions = ast.runtimeFunctions.filter((fn) => fn.runtime !== "server").map(
|
|
(fn) => ` ${member(fn.name)}(${params(fn.parameters)}): ${fn.returnType ?? (fn.async ? "Promise<unknown>" : "unknown")};`
|
|
).join("\n");
|
|
return `${inline ? `${inline}
|
|
|
|
` : ""}export interface ${ast.name}State {
|
|
${state}
|
|
}
|
|
|
|
export interface ${ast.name}Computed {
|
|
${computed}
|
|
}
|
|
|
|
export interface ${ast.name}Actions {
|
|
${actions}
|
|
}
|
|
|
|
export interface ${ast.name}Instance extends ${ast.name}State, ${ast.name}Computed, ${ast.name}Actions {
|
|
reset(): void;
|
|
snapshot(): Readonly<${ast.name}State>;
|
|
}
|
|
|
|
declare const store: ${ast.name}Instance;
|
|
export default store;
|
|
`;
|
|
}
|
|
const props = ast.props.map(
|
|
(prop) => ` readonly ${member(prop.name)}${prop.required ? "" : "?"}: ${prop.valueType ?? "unknown"};`
|
|
).join("\n");
|
|
const outputs = ast.outputs.map(
|
|
(output) => ` ${member(output.name)}(${output.payload ? `${member(output.payload.name)}${output.payload.optional ? "?" : ""}: ${output.payload.valueType}` : ""}): void;`
|
|
).join("\n");
|
|
const clientFunctions = ast.runtimeFunctions.filter((fn) => fn.runtime === "client" || fn.runtime === "shared" || fn.runtime === "legacy").map(
|
|
(fn) => ` ${member(fn.name)}(${params(fn.parameters)}): ${fn.returnType ?? (fn.async ? "Promise<unknown>" : "unknown")};`
|
|
).join("\n");
|
|
const serverFunctions = ast.runtimeFunctions.filter((fn) => fn.runtime === "server").map(
|
|
(fn) => ` ${member(fn.name)}(${params(fn.parameters)}): Promise<Awaited<${fn.returnType ?? "unknown"}>>;`
|
|
).join("\n");
|
|
return `${inline ? `${inline}
|
|
|
|
` : ""}export interface ${ast.name}Props {
|
|
${props}
|
|
}
|
|
|
|
export interface ${ast.name}Outputs {
|
|
${outputs}
|
|
}
|
|
|
|
export interface ${ast.name}ClientFunctions {
|
|
${clientFunctions}
|
|
}
|
|
|
|
export interface ${ast.name}ServerCalls {
|
|
${serverFunctions}
|
|
}
|
|
`;
|
|
}
|
|
|
|
// packages/compiler/src/server-codegen.ts
|
|
import { stripRuntimeFunctionModifiers } from "@wrnexus/syntax";
|
|
function stableId(value) {
|
|
let hash = 2166136261;
|
|
for (let index = 0; index < value.length; index++) {
|
|
hash ^= value.charCodeAt(index);
|
|
hash = Math.imul(hash, 16777619);
|
|
}
|
|
return `wrn_${(hash >>> 0).toString(36)}`;
|
|
}
|
|
function remotelyReferencedServerFunctions(ast) {
|
|
const browserSources = ast.runtimeFunctions.filter((fn) => ["legacy", "client", "shared"].includes(fn.runtime)).map((fn) => fn.body);
|
|
for (const [hook, body] of Object.entries(ast.storeLifecycle)) {
|
|
if (hook !== "serverInit" && body) browserSources.push(body);
|
|
}
|
|
const names = /* @__PURE__ */ new Set();
|
|
const call = /\bserver\.([A-Za-z_$][\w$]*)\s*\(/g;
|
|
for (const source of browserSources) {
|
|
for (const match of source.matchAll(call)) names.add(match[1]);
|
|
}
|
|
return names;
|
|
}
|
|
function rpcManifest(ast) {
|
|
const exposed = remotelyReferencedServerFunctions(ast);
|
|
return ast.runtimeFunctions.filter((fn) => fn.runtime === "server" && exposed.has(fn.name)).map((fn) => ({
|
|
id: stableId(`${ast.name}:${fn.name}`),
|
|
component: ast.name,
|
|
function: fn.name,
|
|
parameters: fn.parameters.map((param) => ({
|
|
name: param.name,
|
|
type: param.valueType ?? "unknown",
|
|
optional: param.optional
|
|
})),
|
|
returnType: fn.returnType ?? (fn.async ? "Promise<unknown>" : "unknown")
|
|
}));
|
|
}
|
|
function generateServerFunctionsModule(ast) {
|
|
const source = ast.functions.map((body) => stripRuntimeFunctionModifiers(body, ["legacy", "server", "shared"])).filter(Boolean).join("\n\n");
|
|
const names = ast.runtimeFunctions.filter((fn) => ["legacy", "server", "shared"].includes(fn.runtime)).map((fn) => fn.name);
|
|
const manifest = rpcManifest(ast);
|
|
return `// generated WRNexusJS server module for ${ast.name}
|
|
${source}
|
|
|
|
export const __wrnexusServerFunctions = { ${[...new Set(names)].join(", ")} };
|
|
export const __wrnexusRpcManifest = ${JSON.stringify(manifest, null, 2)};
|
|
`;
|
|
}
|
|
|
|
// packages/compiler/src/store-codegen.ts
|
|
var RESERVED_BINDINGS = /* @__PURE__ */ new Set([
|
|
"await",
|
|
"break",
|
|
"case",
|
|
"catch",
|
|
"class",
|
|
"const",
|
|
"continue",
|
|
"debugger",
|
|
"default",
|
|
"delete",
|
|
"do",
|
|
"else",
|
|
"enum",
|
|
"export",
|
|
"extends",
|
|
"false",
|
|
"finally",
|
|
"for",
|
|
"function",
|
|
"if",
|
|
"implements",
|
|
"import",
|
|
"in",
|
|
"instanceof",
|
|
"interface",
|
|
"let",
|
|
"new",
|
|
"null",
|
|
"package",
|
|
"private",
|
|
"protected",
|
|
"public",
|
|
"return",
|
|
"static",
|
|
"super",
|
|
"switch",
|
|
"this",
|
|
"throw",
|
|
"true",
|
|
"try",
|
|
"typeof",
|
|
"var",
|
|
"void",
|
|
"while",
|
|
"with",
|
|
"yield"
|
|
]);
|
|
function safeBinding(name) {
|
|
return /^[A-Za-z_$][A-Za-z0-9_$]*$/.test(name) && !RESERVED_BINDINGS.has(name);
|
|
}
|
|
function stateObject(ast, runtime) {
|
|
const entries = ast.states.filter((state) => state.runtime === runtime).map((state) => `${JSON.stringify(state.name)}: (${state.expr})`);
|
|
return `{ ${entries.join(", ")} }`;
|
|
}
|
|
function actionSource(fn, stateNames, eraseTypes = false) {
|
|
const parameterNames = new Set(fn.parameters.map((param) => param.name));
|
|
const params2 = fn.parameters.map((param) => param.name).join(", ");
|
|
const aliases = stateNames.filter((name) => safeBinding(name) && !parameterNames.has(name));
|
|
const aliasSource = aliases.length ? `let { ${aliases.join(", ")} } = context.state;` : "";
|
|
const runtimeAliases = ["server"].filter((name) => !parameterNames.has(name)).map((name) => `const ${name} = context.${name};`).join("\n");
|
|
const copyBack = aliases.map((name) => `context.state.${name} = ${name};`).join("\n");
|
|
const body = eraseTypes ? eraseFunctionTypes(fn.body) : fn.body;
|
|
return `{ runtime: ${JSON.stringify(fn.runtime)}, handler: ${fn.async ? "async " : ""}function(context${params2 ? `, ${params2}` : ""}) { ${runtimeAliases}
|
|
${aliasSource}
|
|
try { ${body} } finally { ${copyBack} } } }`;
|
|
}
|
|
function persistedCallback(source, functionName) {
|
|
if (!source?.trim()) return void 0;
|
|
const body = eraseFunctionTypes(source);
|
|
if (functionName === "migrate") {
|
|
return `(value, fromVersion, toVersion) => {
|
|
${body}
|
|
if (typeof migrate === "function") return migrate(value, fromVersion, toVersion);
|
|
return value;
|
|
}`;
|
|
}
|
|
return `(value) => {
|
|
${body}
|
|
if (typeof validate === "function") return validate(value);
|
|
return value && typeof value === "object" && !Array.isArray(value) ? value : null;
|
|
}`;
|
|
}
|
|
function persistenceSource(ast) {
|
|
if (!ast.persist) return "undefined";
|
|
const migrate = persistedCallback(ast.persist.migrations, "migrate");
|
|
const validate = persistedCallback(ast.persist.validation, "validate");
|
|
return `{ storage: ${JSON.stringify(ast.persist.storage)}, include: ${JSON.stringify(ast.persist.include)}, version: ${ast.persist.version}${migrate ? `, migrate: ${migrate}` : ""}${validate ? `, validate: ${validate}` : ""} }`;
|
|
}
|
|
function lifecycleSource(ast, stateNames, browser) {
|
|
return Object.entries(ast.storeLifecycle).filter(([name]) => !browser || name !== "serverInit").map(([name, body]) => {
|
|
const aliases = stateNames.filter(safeBinding);
|
|
const aliasSource = aliases.length ? `let { ${aliases.join(", ")} } = context.state;` : "";
|
|
const runtimeAliases = browser ? "const server = context.server;" : "";
|
|
const copyBack = aliases.map((state) => `context.state.${state} = ${state};`).join("\n");
|
|
const emittedBody = browser ? eraseFunctionTypes(body) : body;
|
|
return `${name}: async (context) => { ${runtimeAliases} ${aliasSource} try { ${emittedBody} } finally { ${copyBack} } }`;
|
|
}).join(",\n");
|
|
}
|
|
function generateStoreModule(ast) {
|
|
if (ast.kind !== "global-store" && ast.kind !== "page-store") {
|
|
throw new Error("generateStoreModule requires a store AST");
|
|
}
|
|
const stateNames = ast.states.map((state) => state.name);
|
|
const safeStateNames = stateNames.filter(safeBinding);
|
|
const computed = ast.computed.map(
|
|
(entry) => `${JSON.stringify(entry.name)}: (state) => { ${safeStateNames.length ? `const { ${safeStateNames.join(", ")} } = state;` : ""} return (${entry.expr}); }`
|
|
).join(",\n");
|
|
const actionGroups = /* @__PURE__ */ new Map();
|
|
for (const fn of ast.runtimeFunctions) {
|
|
const group = actionGroups.get(fn.name) ?? [];
|
|
group.push(fn);
|
|
actionGroups.set(fn.name, group);
|
|
}
|
|
const actions = Array.from(
|
|
actionGroups,
|
|
([name, functions]) => `${JSON.stringify(name)}: [${functions.map((fn) => actionSource(fn, stateNames)).join(", ")}]`
|
|
).join(",\n");
|
|
const persistence = persistenceSource(ast);
|
|
const lifecycle = lifecycleSource(ast, stateNames, false);
|
|
const manifest = rpcManifest(ast);
|
|
const remoteFunctions = manifest.map((entry) => entry.function);
|
|
const rpcWrappers = remoteFunctions.map(
|
|
(name) => `${JSON.stringify(name)}: async (...received) => {
|
|
const rpcContext = received.pop();
|
|
if (!rpcContext || !rpcContext.request) throw new Error("WRN-RPC-CONTEXT: request context is required");
|
|
let container = __wrnexusRpcContainers.get(rpcContext.request);
|
|
if (!container) {
|
|
const url = new URL(rpcContext.request.url);
|
|
container = createRequestStoreContainer(rpcContext.request, url.pathname + url.search);
|
|
__wrnexusRpcContainers.set(rpcContext.request, container);
|
|
}
|
|
const store = await container.use(${ast.name});
|
|
const action = store.actions[${JSON.stringify(name)}];
|
|
if (typeof action !== "function") throw new Error(${JSON.stringify(`WRN-RPC-FUNCTION: store action '${name}' is unavailable on the server`)});
|
|
return action(...received);
|
|
}`
|
|
).join(",\n");
|
|
return `${ast.imports.join("\n")}
|
|
import { defineStore } from "@wrnexus/store";
|
|
import { createRequestStoreContainer } from "@wrnexus/store/server";
|
|
|
|
${ast.types.join("\n\n")}
|
|
|
|
export const ${ast.name} = defineStore({
|
|
name: ${JSON.stringify(ast.name)},
|
|
kind: ${JSON.stringify(ast.storeKind)},
|
|
createSharedState: () => (${stateObject(ast, "shared")}),
|
|
createClientState: () => (${stateObject(ast, "client")}),
|
|
createServerState: () => (${stateObject(ast, "server")}),
|
|
computed: { ${computed} },
|
|
actions: { ${actions} },
|
|
persist: ${persistence},
|
|
lifecycle: { ${lifecycle} },
|
|
});
|
|
|
|
export default ${ast.name};
|
|
|
|
const __wrnexusRpcContainers = new WeakMap();
|
|
export const __wrnexusServerFunctions = {
|
|
${rpcWrappers}
|
|
};
|
|
export const __wrnexusRpcManifest = ${JSON.stringify(manifest, null, 2)};
|
|
|
|
${generateDeclarations(ast)}
|
|
`;
|
|
}
|
|
function generateStoreBrowserModule(ast) {
|
|
if (ast.kind !== "global-store" && ast.kind !== "page-store") {
|
|
throw new Error("generateStoreBrowserModule requires a store AST");
|
|
}
|
|
const browserStates = ast.states.filter((state) => state.runtime !== "server");
|
|
const stateNames = browserStates.map((state) => state.name);
|
|
const safeStateNames = stateNames.filter(safeBinding);
|
|
const initialState = `{ ${browserStates.map((state) => `${JSON.stringify(state.name)}: (${state.expr})`).join(", ")} }`;
|
|
const computed = ast.computed.map(
|
|
(entry) => `${JSON.stringify(entry.name)}: (state) => { ${safeStateNames.length ? `const { ${safeStateNames.join(", ")} } = state;` : ""} return (${entry.expr}); }`
|
|
).join(",\n");
|
|
const groups = /* @__PURE__ */ new Map();
|
|
for (const fn of ast.runtimeFunctions.filter(
|
|
(entry) => ["client", "shared", "legacy"].includes(entry.runtime)
|
|
)) {
|
|
const group = groups.get(fn.name) ?? [];
|
|
group.push(fn);
|
|
groups.set(fn.name, group);
|
|
}
|
|
const actions = Array.from(
|
|
groups,
|
|
([name, functions]) => `${JSON.stringify(name)}: [${functions.map((fn) => actionSource(fn, stateNames, true)).join(", ")}]`
|
|
).join(",\n");
|
|
const persistence = persistenceSource(ast);
|
|
const lifecycleEntries = lifecycleSource(ast, stateNames, true);
|
|
return `// generated WRNexusJS browser store module for ${ast.name}
|
|
const __root = globalThis;
|
|
const __registry = __root.__wrnexusStoreRegistry || (__root.__wrnexusStoreRegistry = new Map());
|
|
const __hydrationNode = typeof document !== "undefined" ? document.querySelector("script[data-wrnexus-store-hydration]") : null;
|
|
let __hydration = {};
|
|
try { __hydration = __hydrationNode ? JSON.parse(__hydrationNode.textContent || "{}") : {}; } catch (_) {}
|
|
function __clone(value) { try { return structuredClone(value); } catch (_) { return JSON.parse(JSON.stringify(value)); } }
|
|
function __storage(kind) { if (typeof window === "undefined") return null; return kind === "local" ? window.localStorage : kind === "session" ? window.sessionStorage : null; }
|
|
function __diagnostic(code, message, details) {
|
|
const detail = { code, message, store: ${JSON.stringify(ast.name)}, details };
|
|
try { __root.dispatchEvent && __root.dispatchEvent(new CustomEvent("wrnexus:store-diagnostic", { detail })); } catch (_) {}
|
|
if (typeof console !== "undefined" && console.warn) console.warn("[wrnexus:store] " + code + ": " + message, details || "");
|
|
}
|
|
function __csrfToken() {
|
|
if (typeof document === "undefined") return undefined;
|
|
const match = /(?:^|;\\s*)wire-csrf=([^;]+)/.exec(document.cookie || "");
|
|
return match ? decodeURIComponent(match[1]) : undefined;
|
|
}
|
|
async function __callServerFunction(storeName, functionName, args, options) {
|
|
options = options || {};
|
|
const csrf = options.csrfToken || __csrfToken();
|
|
const traceId = options.traceId || (globalThis.crypto && crypto.randomUUID ? crypto.randomUUID() : String(Date.now()));
|
|
const response = await fetch(options.endpoint || "/__wrnexus/rpc", {
|
|
method: "POST",
|
|
credentials: "same-origin",
|
|
signal: options.signal,
|
|
headers: Object.assign({ "content-type": "application/json", "x-request-id": traceId }, csrf ? { "x-csrf-token": csrf } : {}, options.headers || {}),
|
|
body: JSON.stringify({ component: storeName, function: functionName, args: args }),
|
|
});
|
|
const payload = await response.json().catch(function () { return null; });
|
|
if (!response.ok || !payload || !payload.ok) {
|
|
const error = new Error(payload && payload.error && payload.error.message || "Server call failed (" + response.status + ")");
|
|
error.code = payload && payload.error && payload.error.code || "WRN-RPC-FAILED";
|
|
error.status = response.status;
|
|
error.details = payload && payload.error && payload.error.details;
|
|
error.traceId = payload && payload.error && payload.error.traceId || traceId;
|
|
throw error;
|
|
}
|
|
return payload.value;
|
|
}
|
|
function __compatible(expected, value) {
|
|
if (expected === null || value === null) return expected === value || expected === null;
|
|
if (Array.isArray(expected)) return Array.isArray(value);
|
|
return typeof expected === typeof value;
|
|
}
|
|
function __create(definition) {
|
|
const routeId = typeof location !== "undefined" ? location.pathname + location.search : "default";
|
|
const key = definition.kind === "page" ? definition.name + "@" + routeId : definition.name;
|
|
if (__registry.has(key)) return __registry.get(key);
|
|
let currentDefinition = definition;
|
|
const listeners = new Set();
|
|
const initial = currentDefinition.createState();
|
|
let restored = null;
|
|
if (currentDefinition.persist) {
|
|
try {
|
|
const storage = __storage(currentDefinition.persist.storage);
|
|
const rawValue = storage && storage.getItem("wrnexus:store:" + currentDefinition.name);
|
|
const parsed = rawValue ? JSON.parse(rawValue) : null;
|
|
if (parsed) {
|
|
let candidate = parsed.state;
|
|
const fromVersion = Number(parsed.version || 0);
|
|
if (fromVersion !== currentDefinition.persist.version) {
|
|
if (typeof currentDefinition.persist.migrate === "function") candidate = currentDefinition.persist.migrate(candidate, fromVersion, currentDefinition.persist.version);
|
|
else { __diagnostic("WRN-PERSIST-VERSION", "Persisted state version cannot be restored without a migration.", { fromVersion, toVersion: currentDefinition.persist.version }); candidate = null; }
|
|
}
|
|
if (candidate && typeof currentDefinition.persist.validate === "function") candidate = currentDefinition.persist.validate(candidate);
|
|
if (candidate && typeof candidate === "object" && !Array.isArray(candidate)) {
|
|
restored = {};
|
|
currentDefinition.persist.include.forEach(function (name) { if (Object.prototype.hasOwnProperty.call(candidate, name)) restored[name] = candidate[name]; });
|
|
} else if (candidate != null) {
|
|
__diagnostic("WRN-PERSIST-INVALID", "Persisted state failed validation and was reset.", candidate);
|
|
}
|
|
}
|
|
} catch (error) { __diagnostic("WRN-PERSIST-RESTORE", "Persisted state could not be restored and was reset.", error); }
|
|
}
|
|
const raw = Object.assign({}, initial, restored || {}, __hydration[currentDefinition.name] || {});
|
|
let mutable = false;
|
|
let actionName = "direct";
|
|
function persistState() {
|
|
if (!currentDefinition.persist) return;
|
|
try {
|
|
const picked = {};
|
|
currentDefinition.persist.include.forEach(function (name) { picked[name] = raw[name]; });
|
|
const storage = __storage(currentDefinition.persist.storage);
|
|
if (storage) storage.setItem("wrnexus:store:" + currentDefinition.name, JSON.stringify({ version: currentDefinition.persist.version, state: picked }));
|
|
} catch (error) { __diagnostic("WRN-PERSIST-WRITE", "Persisted state could not be written.", error); }
|
|
}
|
|
const state = new Proxy(raw, {
|
|
set(target, property, value) {
|
|
if (!mutable) throw new TypeError("WRN-STORE-READONLY: " + currentDefinition.name + "." + String(property) + " must be changed by a store action.");
|
|
if (Object.is(target[property], value)) return true;
|
|
target[property] = value;
|
|
persistState();
|
|
listeners.forEach(function (listener) { listener(instance.snapshot(), { store: currentDefinition.name, action: actionName, changed: [String(property)] }); });
|
|
return true;
|
|
},
|
|
deleteProperty(target, property) {
|
|
if (!mutable) throw new TypeError("WRN-STORE-READONLY: store state is readonly outside actions");
|
|
return Reflect.deleteProperty(target, property);
|
|
},
|
|
});
|
|
const actions = {};
|
|
const server = new Proxy({}, { get: function (_target, property) { return function () { return __callServerFunction(currentDefinition.name, String(property), Array.prototype.slice.call(arguments)); }; } });
|
|
function installActions() {
|
|
Object.keys(actions).forEach(function (name) { delete actions[name]; });
|
|
Object.entries(currentDefinition.actions || {}).forEach(function (pair) {
|
|
const name = pair[0], candidates = pair[1];
|
|
const selected = candidates.find(function (entry) { return entry.runtime === "client"; }) || candidates.find(function (entry) { return entry.runtime === "shared"; }) || candidates.find(function (entry) { return entry.runtime === "legacy"; });
|
|
if (!selected) return;
|
|
actions[name] = async function () {
|
|
const args = Array.prototype.slice.call(arguments);
|
|
const previousMutable = mutable, previousAction = actionName;
|
|
mutable = true; actionName = name;
|
|
try { return await selected.handler({ state, snapshot: function () { return __clone(state); }, reset: function () { return instance.reset(); }, runtime: "client", routeId, server }, ...args); }
|
|
finally { mutable = previousMutable; actionName = previousAction; }
|
|
};
|
|
});
|
|
}
|
|
installActions();
|
|
const core = {
|
|
name: currentDefinition.name,
|
|
kind: currentDefinition.kind,
|
|
state,
|
|
actions,
|
|
whenReady: Promise.resolve(),
|
|
reset() {
|
|
mutable = true; actionName = "$reset";
|
|
try {
|
|
const next = currentDefinition.createState();
|
|
Object.keys(raw).forEach(function (name) { if (!(name in next)) delete raw[name]; });
|
|
Object.assign(raw, next); persistState();
|
|
listeners.forEach(function (listener) { listener(instance.snapshot(), { store: currentDefinition.name, action: "$reset", changed: Object.keys(next) }); });
|
|
} finally { mutable = false; actionName = "direct"; }
|
|
},
|
|
snapshot() { return Object.freeze(__clone(raw)); },
|
|
subscribe(listener) { listeners.add(listener); return function () { listeners.delete(listener); }; },
|
|
async dispose() {
|
|
mutable = true; actionName = "$dispose";
|
|
try { await currentDefinition.lifecycle && currentDefinition.lifecycle.dispose && currentDefinition.lifecycle.dispose({ state, runtime: "client", routeId, server }); }
|
|
finally { mutable = false; actionName = "direct"; listeners.clear(); __registry.delete(key); }
|
|
},
|
|
async __hotUpdate(nextDefinition) {
|
|
const previous = __clone(raw);
|
|
const nextShape = nextDefinition.createState();
|
|
const preserved = [], reset = [], added = [], removed = [];
|
|
Object.keys(previous).forEach(function (name) {
|
|
if (!(name in nextShape)) { removed.push(name); return; }
|
|
if (__compatible(nextShape[name], previous[name])) { nextShape[name] = previous[name]; preserved.push(name); }
|
|
else reset.push(name);
|
|
});
|
|
Object.keys(nextShape).forEach(function (name) { if (!(name in previous)) added.push(name); });
|
|
currentDefinition = nextDefinition;
|
|
mutable = true; actionName = "$hmr";
|
|
try {
|
|
Object.keys(raw).forEach(function (name) { delete raw[name]; });
|
|
Object.assign(raw, nextShape);
|
|
installActions(); persistState();
|
|
} finally { mutable = false; actionName = "direct"; }
|
|
const result = { store: currentDefinition.name, preserved, reset, added, removed };
|
|
listeners.forEach(function (listener) { listener(instance.snapshot(), { store: currentDefinition.name, action: "$hmr", changed: added.concat(reset, removed) }); });
|
|
try { __root.dispatchEvent && __root.dispatchEvent(new CustomEvent("wrnexus:store-hmr", { detail: result })); } catch (_) {}
|
|
return result;
|
|
},
|
|
};
|
|
const instance = new Proxy(core, {
|
|
get(target, property, receiver) {
|
|
if (Reflect.has(target, property)) return Reflect.get(target, property, receiver);
|
|
if (property in actions) return actions[property];
|
|
if (property in (currentDefinition.computed || {})) return currentDefinition.computed[property](state);
|
|
return state[property];
|
|
},
|
|
set() { throw new TypeError("WRN-STORE-READONLY: store state is readonly outside actions"); },
|
|
});
|
|
__registry.set(key, instance);
|
|
const hydrationSource = __hydration[currentDefinition.name];
|
|
const init = async function () {
|
|
const run = async function (name, hook) {
|
|
if (!hook) return;
|
|
mutable = true; actionName = name;
|
|
try { await hook({ state, runtime: "client", routeId, server }); }
|
|
finally { mutable = false; actionName = "direct"; }
|
|
};
|
|
await run("$clientInit", currentDefinition.lifecycle && currentDefinition.lifecycle.clientInit);
|
|
if (hydrationSource) await run("$hydrate", currentDefinition.lifecycle && currentDefinition.lifecycle.hydrate);
|
|
};
|
|
core.whenReady = init();
|
|
return instance;
|
|
}
|
|
if (!__root.__wrnexusApplyStoreHotUpdate) {
|
|
__root.__wrnexusApplyStoreHotUpdate = async function (name, definition) {
|
|
const results = [];
|
|
for (const item of Array.from(__registry.values())) if (item.name === name && typeof item.__hotUpdate === "function") results.push(await item.__hotUpdate(definition));
|
|
return results;
|
|
};
|
|
}
|
|
if (!__root.__wrnexusStoreContainer) {
|
|
__root.__wrnexusStoreContainer = {
|
|
async disposePageStores() { for (const item of Array.from(__registry.values())) if (item.kind === "page") await item.dispose(); },
|
|
async hotUpdate(name, definition) { return __root.__wrnexusApplyStoreHotUpdate(name, definition); },
|
|
inspect() { return Array.from(__registry.values()).map(function (item) { return { name: item.name, kind: item.kind, state: item.snapshot() }; }); },
|
|
};
|
|
}
|
|
export const ${ast.name}Definition = {
|
|
name: ${JSON.stringify(ast.name)},
|
|
kind: ${JSON.stringify(ast.storeKind)},
|
|
createState: () => (${initialState}),
|
|
computed: { ${computed} },
|
|
actions: { ${actions} },
|
|
persist: ${persistence},
|
|
lifecycle: { ${lifecycleEntries} },
|
|
};
|
|
export const ${ast.name} = __create(${ast.name}Definition);
|
|
export default ${ast.name};
|
|
`;
|
|
}
|
|
|
|
// packages/compiler/src/analysis.ts
|
|
function identifiers(value) {
|
|
return new Set(value.match(/[A-Za-z_$][\w$]*/g) ?? []);
|
|
}
|
|
function literalBoolean(expression2) {
|
|
if (expression2 === null) return true;
|
|
const value = expression2.trim();
|
|
if (value === "true") return true;
|
|
if (value === "false" || value === "null" || value === "undefined" || value === "0" || value === "''" || value === '""')
|
|
return false;
|
|
if (/^-?(?:[1-9]\d*|0?\.\d+)$/.test(value) || /^(['"]).+\1$/.test(value)) return true;
|
|
return void 0;
|
|
}
|
|
function optimizeNodes(nodes, report) {
|
|
const output = [];
|
|
for (const node of nodes) {
|
|
if (node.type === "element")
|
|
output.push({
|
|
...node,
|
|
attrs: node.attrs.map((attribute) => ({ ...attribute })),
|
|
children: optimizeNodes(node.children, report)
|
|
});
|
|
else if (node.type === "each")
|
|
output.push({
|
|
...node,
|
|
body: optimizeNodes(node.body, report),
|
|
empty: optimizeNodes(node.empty, report)
|
|
});
|
|
else if (node.type === "if") {
|
|
let selected;
|
|
let dynamic = false;
|
|
for (const branch of node.branches) {
|
|
const value = literalBoolean(branch.cond);
|
|
if (value === void 0) {
|
|
dynamic = true;
|
|
break;
|
|
}
|
|
report.eliminated++;
|
|
if (value) {
|
|
selected = branch.body;
|
|
break;
|
|
}
|
|
}
|
|
if (dynamic)
|
|
output.push({
|
|
...node,
|
|
branches: node.branches.map((branch) => ({
|
|
...branch,
|
|
body: optimizeNodes(branch.body, report)
|
|
}))
|
|
});
|
|
else if (selected) output.push(...optimizeNodes(selected, report));
|
|
} else output.push({ ...node });
|
|
}
|
|
return output;
|
|
}
|
|
function optimizeAst(ast) {
|
|
const report = { eliminated: 0 };
|
|
return {
|
|
ast: { ...ast, view: optimizeNodes(ast.view, report) },
|
|
eliminatedBranches: report.eliminated
|
|
};
|
|
}
|
|
function analyzeOptimizations(ast) {
|
|
const used = /* @__PURE__ */ new Set();
|
|
let staticNodes = 0;
|
|
let reactiveRegions = 0;
|
|
const componentNames = /* @__PURE__ */ new Set();
|
|
const staticClasses = /* @__PURE__ */ new Set();
|
|
const visit = (nodes) => {
|
|
for (const node of nodes) {
|
|
if (node.type === "text") {
|
|
const refs = identifiers(node.value);
|
|
refs.forEach((name) => used.add(name));
|
|
if (node.value.includes("{")) reactiveRegions++;
|
|
else staticNodes++;
|
|
} else if (node.type === "element") {
|
|
if (/^[A-Z]/.test(node.tag)) componentNames.add(node.tag);
|
|
let reactive = false;
|
|
for (const attribute of node.attrs) {
|
|
identifiers(attribute.value).forEach((name) => used.add(name));
|
|
reactive ||= attribute.event || attribute.value.includes("{");
|
|
if (attribute.name === "class" && !attribute.value.includes("{")) {
|
|
for (const name of attribute.value.split(/\s+/)) if (name) staticClasses.add(name);
|
|
}
|
|
}
|
|
if (reactive) reactiveRegions++;
|
|
else staticNodes++;
|
|
visit(node.children);
|
|
} else if (node.type === "each") {
|
|
identifiers(`${node.list} ${node.key ?? ""}`).forEach((name) => used.add(name));
|
|
reactiveRegions++;
|
|
visit(node.body);
|
|
visit(node.empty);
|
|
} else {
|
|
for (const branch of node.branches) {
|
|
identifiers(branch.cond ?? "").forEach((name) => used.add(name));
|
|
visit(branch.body);
|
|
}
|
|
reactiveRegions++;
|
|
}
|
|
}
|
|
};
|
|
visit(ast.view);
|
|
const handlerReferences = new Set(used);
|
|
const executable = [
|
|
...ast.runtimeFunctions.map((fn) => fn.body),
|
|
...ast.functions,
|
|
...ast.effects.map((effect) => effect.body),
|
|
...ast.watches.map((watch) => watch.body),
|
|
...ast.actions.map((action) => action.body)
|
|
].join("\n");
|
|
identifiers(executable).forEach((name) => used.add(name));
|
|
const localCss = new Set(
|
|
ast.styles.flatMap(
|
|
(style) => [...style.matchAll(/\.([_a-zA-Z][\w-]*)/g)].map((match) => match[1])
|
|
)
|
|
);
|
|
const optimized = optimizeAst(ast);
|
|
const assignmentCounts = ast.runtimeFunctions.map(
|
|
(fn) => ast.states.filter(
|
|
(state) => new RegExp(`\\b${state.name}\\s*(?:[+*/-]?=|\\+\\+|--)`).test(fn.body)
|
|
).length
|
|
);
|
|
return {
|
|
staticNodes,
|
|
reactiveRegions,
|
|
eliminatedBranches: optimized.eliminatedBranches,
|
|
unusedState: ast.states.filter((state) => !used.has(state.name)).map((state) => state.name),
|
|
unusedHandlers: ast.runtimeFunctions.filter((fn) => fn.runtime !== "server" && !handlerReferences.has(fn.name)).map((fn) => fn.name),
|
|
constantProps: ast.props.filter(
|
|
(prop) => /^(?:-?\d+(?:\.\d+)?|true|false|null|(['"]).*\1)$/.test(prop.default.trim())
|
|
).map((prop) => prop.name),
|
|
unusedLocalCssClasses: [...localCss].filter((name) => !staticClasses.has(name)).sort(),
|
|
batchableStateUpdates: assignmentCounts.filter((count) => count > 1).reduce((sum, count) => sum + count - 1, 0),
|
|
memoizableComponents: [...componentNames].sort(),
|
|
preloadDependencies: ast.structuredImports.filter((entry) => !entry.typeOnly && !entry.source.startsWith("node:")).map((entry) => entry.source),
|
|
serverOnlyModules: ast.structuredImports.filter((entry) => entry.source.startsWith("node:") || ast.runtime === "server").map((entry) => entry.source)
|
|
};
|
|
}
|
|
function hasEvent(nodes) {
|
|
for (const node of nodes) {
|
|
if (node.type === "element") {
|
|
if (node.attrs.some((attribute) => attribute.event)) return true;
|
|
if (hasEvent(node.children)) return true;
|
|
} else if (node.type === "each") {
|
|
if (hasEvent(node.body) || hasEvent(node.empty)) return true;
|
|
} else if (node.type === "if") {
|
|
if (node.branches.some((branch) => hasEvent(branch.body))) return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
function analyzeRuntimeRequirements(ast) {
|
|
const reasons = [];
|
|
const clientFunctions = ast.runtimeFunctions.some((fn) => fn.runtime !== "server");
|
|
const clientState = ast.states.some((state) => state.runtime !== "server");
|
|
const interactive = clientFunctions || clientState || ast.effects.length > 0 || ast.watches.length > 0 || hasEvent(ast.view);
|
|
if (interactive) reasons.push("client interactivity");
|
|
const requestData = ast.loads.length > 0 || ast.actions.length > 0 || ast.dataApis.length > 0 || ast.apis.length > 0 || ast.realtimes.length > 0 || ast.runtimeFunctions.some((fn) => fn.runtime === "server") || ast.states.some((state) => state.runtime === "server");
|
|
if (requestData) reasons.push("server/request data");
|
|
const authenticated = /^(?:required|true)$/i.test(ast.security.auth ?? "");
|
|
if (authenticated) reasons.push("authentication required");
|
|
const streaming = /^(?:true|required)$/i.test(ast.security.streaming ?? "");
|
|
if (streaming) reasons.push("streaming enabled");
|
|
let kind;
|
|
if (streaming) kind = "streaming-ssr";
|
|
else if (authenticated) kind = "authenticated-ssr";
|
|
else if (requestData && interactive) kind = "dynamic";
|
|
else if (requestData) kind = "request-ssr";
|
|
else if (interactive) kind = "static-interactive";
|
|
else kind = "static";
|
|
if (ast.renderMode === "static") {
|
|
kind = "static";
|
|
reasons.push("explicit static rendering");
|
|
} else if (ast.renderMode === "server") {
|
|
kind = requestData ? "request-ssr" : "static";
|
|
reasons.push("explicit server rendering");
|
|
} else if (ast.renderMode === "client") {
|
|
kind = "static-interactive";
|
|
reasons.push("explicit client rendering");
|
|
} else if (ast.renderMode === "partial-static") {
|
|
kind = "streaming-ssr";
|
|
reasons.push("partial-static shell with streamed dynamic regions");
|
|
}
|
|
const clientDisabled = ast.renderMode === "static" || ast.renderMode === "server";
|
|
const serverDisabled = ast.renderMode === "client";
|
|
return {
|
|
kind,
|
|
canPrerender: kind === "static" || kind === "static-interactive",
|
|
needsClientRuntime: !clientDisabled && (interactive || ast.renderMode === "client") && ast.hydrate !== "none" && ast.runtime !== "server",
|
|
needsServerRuntime: !serverDisabled && (requestData || authenticated || streaming || ast.renderMode === "server" || ["server", "edge", "worker", "service-worker"].includes(ast.runtime ?? "")),
|
|
hydrationStrategy: clientDisabled ? null : interactive ? ast.hydrate ?? "load" : null,
|
|
reasons,
|
|
optimization: analyzeOptimizations(ast),
|
|
cachePolicy: { ...ast.cache ?? {} },
|
|
requiredPermission: ast.security.permission ?? null
|
|
};
|
|
}
|
|
|
|
// packages/compiler/src/codegen.ts
|
|
function isComponentTag(tag) {
|
|
return /^[A-Z][A-Za-z0-9_$]*$/.test(tag);
|
|
}
|
|
var HTML_BOOLEAN_ATTRIBUTES = /* @__PURE__ */ new Set([
|
|
"allowfullscreen",
|
|
"async",
|
|
"autofocus",
|
|
"autoplay",
|
|
"checked",
|
|
"controls",
|
|
"default",
|
|
"defer",
|
|
"disabled",
|
|
"formnovalidate",
|
|
"hidden",
|
|
"inert",
|
|
"ismap",
|
|
"itemscope",
|
|
"loop",
|
|
"multiple",
|
|
"muted",
|
|
"nomodule",
|
|
"novalidate",
|
|
"open",
|
|
"playsinline",
|
|
"readonly",
|
|
"required",
|
|
"reversed",
|
|
"selected"
|
|
]);
|
|
function isHtmlBooleanAttribute(name) {
|
|
return HTML_BOOLEAN_ATTRIBUTES.has(name.toLowerCase());
|
|
}
|
|
var URL_ATTRIBUTES = /* @__PURE__ */ new Set([
|
|
"href",
|
|
"src",
|
|
"action",
|
|
"formaction",
|
|
"poster",
|
|
"cite",
|
|
"background",
|
|
"xlink:href"
|
|
]);
|
|
function stripAsciiControlAndSpace(value) {
|
|
let result = "";
|
|
for (const character of value) {
|
|
if (character.charCodeAt(0) > 32) result += character;
|
|
}
|
|
return result;
|
|
}
|
|
function sanitizeUrlAttribute(value) {
|
|
const compact = stripAsciiControlAndSpace(value.trim());
|
|
const lower = compact.toLowerCase();
|
|
if (/^(?:javascript|vbscript|file):/.test(lower)) return "about:blank";
|
|
if (/^data:(?!image\/(?:png|gif|jpeg|webp|avif);)/.test(lower)) return "about:blank";
|
|
return value;
|
|
}
|
|
function safeAttributeValue(name, value) {
|
|
if (!URL_ATTRIBUTES.has(name.toLowerCase()) || value.includes("{")) return value;
|
|
return sanitizeUrlAttribute(value);
|
|
}
|
|
function attrEscape(value) {
|
|
return value.replace(/&/g, "&").replace(/"/g, """).replace(/</g, "<").replace(/>/g, ">");
|
|
}
|
|
function templateEscape(html) {
|
|
return html.replace(/\\/g, "\\\\").replace(/`/g, "\\`").replace(/\$\{/g, "\\${");
|
|
}
|
|
function styleEscape(css) {
|
|
return css.replace(/<\/style/gi, "<\\/style");
|
|
}
|
|
function attrValue(attrs, name) {
|
|
return attrs.find((attr) => !attr.event && attr.name === name)?.value;
|
|
}
|
|
function renderAttr(attr) {
|
|
if (attr.event) return ` ${eventAttribute(attr.name)}="${attrEscape(attr.value)}"`;
|
|
switch (attr.name) {
|
|
case "api":
|
|
case "ssrGet":
|
|
case "ssrText":
|
|
case "csrGet":
|
|
case "csrText":
|
|
return "";
|
|
default:
|
|
return attr.boolean ? ` ${attr.name}` : ` ${attr.name}="${attrEscape(safeAttributeValue(attr.name, attr.value))}"`;
|
|
}
|
|
}
|
|
function eventAttribute(name) {
|
|
if (name.startsWith("window:")) {
|
|
return `data-on-window-${name.slice("window:".length)}`;
|
|
}
|
|
if (name.startsWith("document:")) {
|
|
return `data-on-document-${name.slice("document:".length)}`;
|
|
}
|
|
if (name.startsWith("browser-")) {
|
|
return `data-on-wrnexus-browser-${name.slice(8)}`;
|
|
}
|
|
if (name.startsWith("mobile-")) {
|
|
return `data-on-wrnexus-mobile-${name.slice(7)}`;
|
|
}
|
|
return `data-on-${name}`;
|
|
}
|
|
function reactiveAttrValue(raw, reactive) {
|
|
let found = false;
|
|
const value = raw.replace(/\{([^{}]+)\}/g, (whole, inner) => {
|
|
const expr = inner.trim();
|
|
if (!exprRefsState(expr, reactive.stateNames)) return whole;
|
|
found = true;
|
|
try {
|
|
const result = new Function("with(this){return (" + expr + ");}").call(reactive.scope);
|
|
return result == null ? "" : String(result);
|
|
} catch {
|
|
return whole;
|
|
}
|
|
});
|
|
return found ? value : null;
|
|
}
|
|
function renderAttrs(attrs, csrId, reactive = null, dynamicExpressions) {
|
|
let bindIndex = 0;
|
|
const rendered = attrs.map((attr) => {
|
|
const base = renderAttr(attr);
|
|
if (!reactive || attr.event || attr.boolean || !base || !attr.value.includes("{"))
|
|
return base;
|
|
const expression2 = wholeAttributeExpression(attr.value);
|
|
if (expression2 && exprRefsState(expression2, reactive.runtimeStateNames) && dynamicExpressions) {
|
|
dynamicExpressions.push(`\${__wrnexusPropAttr(${expression2})}`);
|
|
const sentinel = `\0WRNEACH${dynamicExpressions.length - 1}\0`;
|
|
const marker2 = JSON.stringify([attr.name, attr.value]);
|
|
return ` ${attr.name}="${sentinel}" data-wrn-bind-${bindIndex++}="${attrEscape(marker2)}"`;
|
|
}
|
|
const initial = reactiveAttrValue(attr.value, reactive);
|
|
if (initial === null) return base;
|
|
const marker = JSON.stringify([attr.name, attr.value]);
|
|
return ` ${attr.name}="${attrEscape(URL_ATTRIBUTES.has(attr.name.toLowerCase()) ? sanitizeUrlAttribute(initial) : initial)}" data-wrn-bind-${bindIndex++}="${attrEscape(marker)}"`;
|
|
}).join("");
|
|
return csrId ? `${rendered} data-wrnexus-csr="${attrEscape(csrId)}"` : rendered;
|
|
}
|
|
function substituteTMarkers(text) {
|
|
return text.replace(
|
|
/\{t:([^{}]+)\}/g,
|
|
(_m, key) => `<span data-t="${attrEscape(key.trim())}"></span>`
|
|
);
|
|
}
|
|
function htmlTextEscape(value) {
|
|
return value.replace(/[&<>]/g, (c) => c === "&" ? "&" : c === "<" ? "<" : ">");
|
|
}
|
|
function evalStateSeeds(states) {
|
|
const scope = {};
|
|
for (const s of states) {
|
|
try {
|
|
scope[s.name] = new Function("with(this){return (" + s.expr + ");}").call(scope);
|
|
} catch {
|
|
scope[s.name] = void 0;
|
|
}
|
|
}
|
|
return scope;
|
|
}
|
|
function substituteReactiveText(raw, reactive) {
|
|
const text = substituteTMarkers(raw);
|
|
if (!reactive || reactive.stateNames.size === 0) return text;
|
|
return text.replace(/\{([^{}]+)\}/g, (whole, inner) => {
|
|
const expr = inner.trim();
|
|
if (expr.startsWith("t:") || !exprRefsState(expr, reactive.stateNames)) return whole;
|
|
let value;
|
|
try {
|
|
value = new Function("with(this){return (" + expr + ");}").call(reactive.scope);
|
|
} catch {
|
|
return whole;
|
|
}
|
|
const baked = htmlTextEscape(value == null ? "" : String(value));
|
|
return `<span data-text="${attrEscape(expr)}">${baked}</span>`;
|
|
});
|
|
}
|
|
function bakeLoopText(raw) {
|
|
let out = "";
|
|
let last = 0;
|
|
let m;
|
|
const re = /\{([^{}]+)\}/g;
|
|
while (m = re.exec(raw)) {
|
|
out += escLit(raw.slice(last, m.index));
|
|
const expr = m[1].trim();
|
|
if (expr.startsWith("t:")) {
|
|
out += escLit(`<span data-t="${attrEscape(expr.slice(2).trim())}"></span>`);
|
|
} else {
|
|
out += "${__wrnexusEscapeHtml(" + expr + ")}";
|
|
}
|
|
last = m.index + m[0].length;
|
|
}
|
|
return out + escLit(raw.slice(last));
|
|
}
|
|
function bakeLoopAttr(raw) {
|
|
if (!raw.includes("{")) return escLit(attrEscape(raw));
|
|
let out = "";
|
|
let last = 0;
|
|
let m;
|
|
const re = /\{([^{}]+)\}/g;
|
|
while (m = re.exec(raw)) {
|
|
out += escLit(attrEscape(raw.slice(last, m.index)));
|
|
out += "${__wrnexusEscapeHtml(" + m[1].trim() + ")}";
|
|
last = m.index + m[0].length;
|
|
}
|
|
return out + escLit(attrEscape(raw.slice(last)));
|
|
}
|
|
function renderLoopBody(node) {
|
|
if (node.type === "text") {
|
|
return bakeLoopText(node.value);
|
|
}
|
|
if (node.type === "each") {
|
|
return compileEachExpr(node);
|
|
}
|
|
if (node.type === "if") {
|
|
return compileIfExpr(node);
|
|
}
|
|
const componentTag = isComponentTag(node.tag);
|
|
const attrs = node.attrs.filter((attr) => attr.name !== "data-component").map((attr) => {
|
|
const name = attr.event ? eventAttribute(attr.name) : attr.name;
|
|
if (attr.boolean) {
|
|
return escLit(` ${name}`);
|
|
}
|
|
return escLit(` ${name}="`) + bakeLoopAttr(attr.value) + escLit(`"`);
|
|
}).join("");
|
|
const inner = node.children.map(renderLoopBody).join("");
|
|
if (node.tag === "Static") return inner;
|
|
if (node.tag === "Dynamic")
|
|
return escLit('<wrn-dynamic-region data-wrn-dynamic="true">') + inner + escLit("</wrn-dynamic-region>");
|
|
if (node.tag === "KeepAlive") {
|
|
const key = node.attrs.find((attribute) => attribute.name === "key")?.value ?? "default";
|
|
return escLit('<div data-wrn-keepalive="') + bakeLoopAttr(key) + escLit(`">`) + inner + escLit("</div>");
|
|
}
|
|
if (node.tag === "Portal") {
|
|
const target = node.attrs.find((attribute) => attribute.name === "to")?.value ?? "body";
|
|
return escLit('<div data-wrn-portal="') + bakeLoopAttr(target) + escLit('">') + inner + escLit("</div>");
|
|
}
|
|
if (node.tag === "Transition") {
|
|
const name = node.attrs.find((attribute) => attribute.name === "name")?.value ?? "wrn-transition";
|
|
return escLit('<div data-wrn-transition="') + bakeLoopAttr(name) + escLit('">') + inner + escLit("</div>");
|
|
}
|
|
if (node.tag === "Component") {
|
|
const selected = node.attrs.find((attribute) => attribute.name === "is")?.value ?? "";
|
|
return escLit('<div data-wrn-dynamic-component="') + bakeLoopAttr(selected) + escLit('">') + inner + escLit("</div>");
|
|
}
|
|
if (componentTag) {
|
|
return escLit(`<div data-component="${attrEscape(node.tag)}"`) + attrs + escLit(">") + inner + escLit("</div>");
|
|
}
|
|
if (parser_exports.VOID_ELEMENTS.has(node.tag.toLowerCase())) {
|
|
return escLit(`<${node.tag}`) + attrs + escLit(">");
|
|
}
|
|
return escLit(`<${node.tag}`) + attrs + escLit(">") + inner + escLit(`</${node.tag}>`);
|
|
}
|
|
function compileEachExpr(node) {
|
|
const item = node.item;
|
|
const index = node.index ?? "__wi";
|
|
const body = node.body.map(renderLoopBody).join("");
|
|
const empty = node.empty.map(renderLoopBody).join("");
|
|
return "${(() => { const __wl = Array.isArray(" + node.list + ") ? (" + node.list + ") : []; return __wl.length ? __wl.map((" + item + ", " + index + ") => `" + body + '`).join("") : `' + empty + "`; })()}";
|
|
}
|
|
function compileIfExpr(node) {
|
|
let expr = "``";
|
|
for (let k = node.branches.length - 1; k >= 0; k--) {
|
|
const b = node.branches[k];
|
|
const bodySrc = "`" + b.body.map(renderLoopBody).join("") + "`";
|
|
expr = b.cond === null ? bodySrc : "(" + b.cond + ") ? " + bodySrc + " : " + expr;
|
|
}
|
|
return "${" + expr + "}";
|
|
}
|
|
function collectControlExprs(nodes, out = []) {
|
|
for (const node of nodes) {
|
|
if (node.type === "text") continue;
|
|
if (node.type === "each") {
|
|
out.push(node.list);
|
|
collectControlExprs(node.body, out);
|
|
collectControlExprs(node.empty, out);
|
|
} else if (node.type === "if") {
|
|
for (const b of node.branches) {
|
|
if (b.cond) out.push(b.cond);
|
|
collectControlExprs(b.body, out);
|
|
}
|
|
} else if (node.type === "element") {
|
|
collectControlExprs(node.children, out);
|
|
}
|
|
}
|
|
return out;
|
|
}
|
|
function renderNode(node, ssrBindings, csrBindings, apiBindings, loops, reactive = null) {
|
|
if (node.type === "text") return substituteReactiveText(node.value, reactive);
|
|
if (node.type === "each" || node.type === "if") {
|
|
loops.push(node.type === "each" ? compileEachExpr(node) : compileIfExpr(node));
|
|
return `\0WRNEACH${loops.length - 1}\0`;
|
|
}
|
|
if (node.tag === "Static" || node.tag === "Dynamic") {
|
|
const inner2 = node.children.map((child) => renderNode(child, ssrBindings, csrBindings, apiBindings, loops, reactive)).join("");
|
|
return node.tag === "Static" ? inner2 : `<wrn-dynamic-region data-wrn-dynamic="true">${inner2}</wrn-dynamic-region>`;
|
|
}
|
|
if (node.tag === "Portal" || node.tag === "Transition" || node.tag === "Component") {
|
|
const inner2 = node.children.map((child) => renderNode(child, ssrBindings, csrBindings, apiBindings, loops, reactive)).join("");
|
|
const attribute = node.tag === "Portal" ? "data-wrn-portal" : node.tag === "Transition" ? "data-wrn-transition" : "data-wrn-dynamic-component";
|
|
const source = node.tag === "Portal" ? "to" : node.tag === "Transition" ? "name" : "is";
|
|
const fallback = node.tag === "Portal" ? "body" : node.tag === "Transition" ? "wrn-transition" : "";
|
|
const original = node.attrs.find((item) => item.name === source);
|
|
const rendered = original ? renderAttrs([{ ...original, name: attribute }], void 0, reactive, loops) : ` ${attribute}="${attrEscape(fallback)}"`;
|
|
return `<div${rendered}>${inner2}</div>`;
|
|
}
|
|
if (node.tag === "Async") {
|
|
const source = attrValue(node.attrs, "source") ?? "data";
|
|
const retries = attrValue(node.attrs, "retries") ?? "2";
|
|
const serverResolved = attrValue(node.attrs, "data-wrn-async-server") === "true";
|
|
const asyncIndex = serverResolved ? loops.push("") - 1 : -1;
|
|
const branch = (name) => {
|
|
const element = node.children.find(
|
|
(child) => child.type === "element" && child.tag === name
|
|
);
|
|
return (element?.children ?? []).map((child) => renderNode(child, ssrBindings, csrBindings, apiBindings, loops, reactive)).join("");
|
|
};
|
|
const loading = branch("Loading");
|
|
const success = branch("Success");
|
|
const error = branch("Error");
|
|
let initial = loading;
|
|
if (serverResolved) {
|
|
const nested = (value) => value.replace(/\\/g, "\\\\").replace(/`/g, "\\`");
|
|
const sourcePattern = source.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
const serverSuccess = success.replace(
|
|
new RegExp(`\\{\\s*(${sourcePattern}(?:\\.[A-Za-z_$][\\w$]*)*)\\s*\\}`, "g"),
|
|
(_whole, expression2) => `\${__wrnexusEscapeHtml(${expression2})}`
|
|
);
|
|
loops[asyncIndex] = `\${ctx[${JSON.stringify(source)}] !== undefined ? \`${nested(serverSuccess)}\` : \`${nested(loading)}\`}`;
|
|
initial = `\0WRNEACH${asyncIndex}\0`;
|
|
}
|
|
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>`;
|
|
}
|
|
if (node.tag === "KeepAlive") {
|
|
const key = attrValue(node.attrs, "key") ?? "default";
|
|
const inner2 = node.children.map((child) => renderNode(child, ssrBindings, csrBindings, apiBindings, loops, reactive)).join("");
|
|
return `<div data-wrn-keepalive="${attrEscape(key)}">${inner2}</div>`;
|
|
}
|
|
if (isComponentTag(node.tag)) {
|
|
return renderPageComponentInvocation(
|
|
node,
|
|
ssrBindings,
|
|
csrBindings,
|
|
apiBindings,
|
|
loops,
|
|
reactive
|
|
);
|
|
}
|
|
const apiName = attrValue(node.attrs, "api");
|
|
const apiBinding = apiName ? apiBindings.get(apiName) : void 0;
|
|
if (apiName && !apiBinding) {
|
|
throw new Error(`Unknown .wrn api binding "${apiName}"`);
|
|
}
|
|
const ssrGet = attrValue(node.attrs, "ssrGet");
|
|
const ssrText = attrValue(node.attrs, "ssrText");
|
|
const csrGet = attrValue(node.attrs, "csrGet");
|
|
const csrText = attrValue(node.attrs, "csrText");
|
|
const csrId = apiBinding?.mode === "client" ? csrMarker(csrBindings, renderBinding(apiBinding)) : csrGet && csrText ? csrMarker(csrBindings, {
|
|
method: "GET",
|
|
path: apiRoutePath(csrGet),
|
|
body: expressionBody(csrText),
|
|
helpers: ""
|
|
}) : void 0;
|
|
if (parser_exports.VOID_ELEMENTS.has(node.tag.toLowerCase())) {
|
|
return `<${node.tag}${renderAttrs(node.attrs, csrId, reactive, loops)}>`;
|
|
}
|
|
const inner = apiBinding?.mode === "ssr" ? ssrMarker(ssrBindings, renderBinding(apiBinding)) : ssrGet && ssrText ? ssrMarker(ssrBindings, {
|
|
method: "GET",
|
|
path: apiRoutePath(ssrGet),
|
|
body: expressionBody(ssrText),
|
|
helpers: ""
|
|
}) : node.children.map(
|
|
(child) => renderNode(child, ssrBindings, csrBindings, apiBindings, loops, reactive)
|
|
).join("");
|
|
return `<${node.tag}${renderAttrs(node.attrs, csrId, reactive, loops)}>${inner}</${node.tag}>`;
|
|
}
|
|
function renderPageComponentInvocation(node, ssrBindings, csrBindings, apiBindings, loops, reactive) {
|
|
const attrs = node.attrs.filter((attr) => attr.name !== "data-component").map((attr) => renderPageComponentAttr(attr, loops)).join("");
|
|
const inner = node.children.map((child) => renderNode(child, ssrBindings, csrBindings, apiBindings, loops, reactive)).join("");
|
|
return `<div data-component="${attrEscape(node.tag)}"${attrs}>${inner}</div>`;
|
|
}
|
|
function renderNestedComponentInvocation(node, ctx) {
|
|
let bindIndex = 0;
|
|
const attrs = node.attrs.filter((attr) => attr.name !== "data-component").map((attr) => {
|
|
const spread = /^\{\.\.\.([A-Za-z_$][\w$]*)\}$/.exec(attr.name);
|
|
if (spread) {
|
|
return `\${__wireSpreadAttrs(${ctx.resolveExpr(spread[1])})}`;
|
|
}
|
|
if (attr.event) {
|
|
return escLit(` ${eventAttribute(attr.name)}="`) + escLit(attrEscape(attr.value)) + escLit(`"`);
|
|
}
|
|
if (attr.boolean) {
|
|
return ` ${attr.name}`;
|
|
}
|
|
const wholeExpression = wholeAttributeExpression(attr.value);
|
|
const compiledValue = wholeExpression ? `\${__wireProp(${ctx.resolveExpr(wholeExpression)})}` : compileAttrValue(attr.value, ctx);
|
|
const rendered = ` ${attr.name}="${compiledValue}"`;
|
|
if (wholeExpression || !attr.value.includes("{") || !exprRefsState(attr.value, ctx.stateNames)) {
|
|
return rendered;
|
|
}
|
|
const marker = attrEscape(JSON.stringify([attr.name, attr.value]));
|
|
return rendered + ` data-wrn-bind-${bindIndex++}="${escLit(marker)}"`;
|
|
}).join("");
|
|
const loops = loopVarsOf(node);
|
|
const childCtx = loops.length > 0 ? {
|
|
...ctx,
|
|
forwardRestAttrs: false,
|
|
loopVars: /* @__PURE__ */ new Set([...ctx.loopVars ?? [], ...loops])
|
|
} : { ...ctx, forwardRestAttrs: false };
|
|
const inner = node.children.map((child) => renderComponentNode(child, childCtx)).join("");
|
|
return `<div data-component="${attrEscape(node.tag)}"${ctx.forwardRestAttrs ? "${__wireSpreadAttrs(__attrs)}" : ""}${attrs}>${inner}</div>`;
|
|
}
|
|
function ssrMarker(bindings, binding) {
|
|
const marker = `<!--wrnexus-ssr:${bindings.length}-->`;
|
|
bindings.push({ marker, ...binding });
|
|
return marker;
|
|
}
|
|
function csrMarker(bindings, binding) {
|
|
const id = String(bindings.length);
|
|
bindings.push({ id, ...binding });
|
|
return id;
|
|
}
|
|
function renderBinding(binding) {
|
|
return {
|
|
method: binding.method,
|
|
path: binding.path,
|
|
body: binding.body,
|
|
helpers: binding.helpers
|
|
};
|
|
}
|
|
function hasClientBehavior(nodes) {
|
|
return nodes.some((node) => {
|
|
if (node.type === "text") return /\{(?!t:)[^{}]+\}/.test(node.value);
|
|
if (node.type === "each") {
|
|
return hasClientBehavior(node.body) || hasClientBehavior(node.empty);
|
|
}
|
|
if (node.type === "if") {
|
|
return node.branches.some((branch) => hasClientBehavior(branch.body));
|
|
}
|
|
return node.attrs.some((attr) => attr.event || attr.name === "csrGet" || attr.name === "csrText") || hasClientBehavior(node.children);
|
|
});
|
|
}
|
|
function apiRoutePath(path) {
|
|
const trimmed = path.trim();
|
|
if (!trimmed.startsWith("/")) {
|
|
throw new Error(`.wrn API paths must start with "/": ${path}`);
|
|
}
|
|
if (trimmed.includes("\0") || trimmed.includes("\\") || /(^|\/)\.\.(\/|$)/.test(trimmed)) {
|
|
throw new Error(`Unsafe .wrn API path: ${path}`);
|
|
}
|
|
if (trimmed === "/api" || trimmed.startsWith("/api/")) return trimmed;
|
|
return `/api${trimmed}`;
|
|
}
|
|
function expressionBody(expr) {
|
|
return `return (${expr});`;
|
|
}
|
|
function dataBody(source) {
|
|
const trimmed = source.trim();
|
|
if (!trimmed) return "return undefined;";
|
|
return /\breturn\b/.test(trimmed) ? trimmed : expressionBody(trimmed);
|
|
}
|
|
function modeHelpers(ast, mode, sharedHelpers) {
|
|
return [
|
|
sharedHelpers,
|
|
...ast.modeFunctions.filter((block) => block.mode === mode).map((block) => block.body.trim()).filter(Boolean)
|
|
].filter(Boolean).join("\n\n");
|
|
}
|
|
function apiBindingMap(ast, sharedHelpers) {
|
|
const bindings = /* @__PURE__ */ new Map();
|
|
for (const block of ast.dataApis) {
|
|
if (bindings.has(block.name)) {
|
|
throw new Error(`Duplicate .wrn api binding "${block.name}"`);
|
|
}
|
|
bindings.set(block.name, {
|
|
mode: block.mode,
|
|
method: block.method,
|
|
path: apiRoutePath(block.path),
|
|
body: dataBody(block.body),
|
|
helpers: modeHelpers(ast, block.mode, sharedHelpers)
|
|
});
|
|
}
|
|
return bindings;
|
|
}
|
|
function ssrRuntimeSource() {
|
|
return `const __wrnexusHtmlEscapes = { "&": "&", "<": "<", ">": ">", "\\"": """, "'": "'" };
|
|
function __wrnexusEscapeHtml(value: unknown): string {
|
|
return String(value).replace(/[&<>"']/g, (ch) => __wrnexusHtmlEscapes[ch] ?? ch);
|
|
}
|
|
|
|
function __wrnexusEvalData(data: unknown, body: string, helpers = "", ctx: any): unknown {
|
|
const adapters = {
|
|
cookies: ctx.cookies,
|
|
session: ctx.session,
|
|
localStorage: ctx.localStorage,
|
|
};
|
|
return new Function("$data", "$adapters", "const cookies = $adapters.cookies;\\nconst session = $adapters.session;\\nconst localStorage = $adapters.localStorage;\\nwith ($data ?? {}) {\\n" + helpers + "\\n" + body + "\\n}")(data, adapters);
|
|
}
|
|
|
|
function __wrnexusPropAttr(
|
|
value: unknown,
|
|
): string {
|
|
const serialized =
|
|
value !== null &&
|
|
typeof value === "object"
|
|
? JSON.stringify(value)
|
|
: String(value == null ? "" : value);
|
|
|
|
return serialized.replace(
|
|
/[&<>"]/g,
|
|
(character) =>
|
|
character === "&"
|
|
? "&"
|
|
: character === "<"
|
|
? "<"
|
|
: character === ">"
|
|
? ">"
|
|
: """,
|
|
);
|
|
}
|
|
|
|
async function __wrnexusCallApi(path: string, method: string, ctx: any): Promise<unknown> {
|
|
if (typeof ctx.__wrnexusCallApi === "function") {
|
|
return await ctx.__wrnexusCallApi(path, method);
|
|
}
|
|
|
|
const url = new URL(path, ctx.req.url);
|
|
const res = await fetch(new Request(url, { method, headers: ctx.req.headers }));
|
|
if (!res.ok) {
|
|
throw new Error(".wrn data API request failed with status " + res.status);
|
|
}
|
|
|
|
const type = res.headers.get("content-type") || "";
|
|
return type.includes("application/json") ? await res.json() : await res.text();
|
|
}
|
|
|
|
async function __wrnexusRenderSsrBindings(html: string, ctx: any): Promise<string> {
|
|
for (const binding of __wrnexusSsrBindings) {
|
|
const data = await __wrnexusCallApi(binding.path, binding.method, ctx);
|
|
const value = __wrnexusEvalData(data, binding.body, binding.helpers, ctx);
|
|
html = html.replace(binding.marker, __wrnexusEscapeHtml(value));
|
|
}
|
|
return html;
|
|
}`;
|
|
}
|
|
function stableHash(value) {
|
|
let hash = 2166136261;
|
|
for (let index = 0; index < value.length; index++) {
|
|
hash ^= value.charCodeAt(index);
|
|
hash = Math.imul(hash, 16777619);
|
|
}
|
|
return (hash >>> 0).toString(36);
|
|
}
|
|
function hydrationId(ast) {
|
|
const shape = JSON.stringify({
|
|
kind: ast.kind,
|
|
name: ast.name,
|
|
props: ast.props.map((entry) => entry.name),
|
|
events: ast.events.map((entry) => entry.name),
|
|
states: ast.states.map((entry) => entry.name),
|
|
computed: ast.computed.map((entry) => entry.name),
|
|
view: ast.view
|
|
});
|
|
return `${ast.name}:${stableHash(shape)}`;
|
|
}
|
|
function localStyleId(ast) {
|
|
return `wrn-${ast.kind}-${stableHash(`${ast.kind}:${ast.name}`)}`;
|
|
}
|
|
function localStyleTag(ast, styles) {
|
|
if (!styles.length) return "";
|
|
const id = localStyleId(ast);
|
|
const css = styles.map(styleEscape).join("\n");
|
|
return `<style data-wrnexus-style="${attrEscape(ast.name)}" data-wrnexus-style-id="${attrEscape(id)}" data-wrnexus-style-owner="${attrEscape(ast.name)}" data-wrnexus-style-kind="${attrEscape(ast.kind)}">
|
|
${css}
|
|
</style>`;
|
|
}
|
|
function localStyleExport(ast, styles) {
|
|
if (!styles.length) return null;
|
|
return `export const __wrnexusStyles = ${JSON.stringify(
|
|
[
|
|
{
|
|
id: localStyleId(ast),
|
|
owner: ast.name,
|
|
kind: ast.kind,
|
|
css: styles.join("\n")
|
|
}
|
|
],
|
|
null,
|
|
2
|
|
)};`;
|
|
}
|
|
function isStoreImportSource(source) {
|
|
return /(?:^|\/)stores?\//.test(source) || source.startsWith("@wrnexus/store");
|
|
}
|
|
function importedStoreBindings(ast) {
|
|
return ast.structuredImports.filter(
|
|
(entry) => entry.defaultImport && entry.source.endsWith(".wrn") && isStoreImportSource(entry.source)
|
|
).map((entry) => ({
|
|
local: entry.defaultImport,
|
|
internal: `__wrnexusStoreDefinition_${entry.defaultImport}`
|
|
}));
|
|
}
|
|
function generatedImports(ast) {
|
|
const stores = new Map(importedStoreBindings(ast).map((entry) => [entry.local, entry.internal]));
|
|
return ast.structuredImports.map((entry) => {
|
|
if (!entry.defaultImport) return entry.raw;
|
|
const internal = stores.get(entry.defaultImport);
|
|
return internal ? entry.raw.replace(
|
|
new RegExp(
|
|
`^(\\s*import\\s+(?:type\\s+)?)(?:${entry.defaultImport.replace(/[.*+?^${}()|[\\]\\]/g, "\\$&")})(\\s+from\\s+)`
|
|
),
|
|
`$1${internal}$2`
|
|
) : entry.raw;
|
|
});
|
|
}
|
|
function isSafeGeneratedIdentifier(name) {
|
|
return /^[A-Za-z_$][A-Za-z0-9_$]*$/.test(name);
|
|
}
|
|
function generateSsrStateAliases(stateNames) {
|
|
const names = [...new Set(stateNames)].filter(isSafeGeneratedIdentifier);
|
|
if (!names.length) {
|
|
return "";
|
|
}
|
|
return `const { ${names.join(", ")} } = __state;
|
|
`;
|
|
}
|
|
function hydrationAttribute(ast) {
|
|
const strategy = ["static", "server"].includes(ast.renderMode ?? "") ? "none" : ast.hydrate ?? "load";
|
|
const hasBrowserModule = ast.runtimeFunctions.some(
|
|
(fn) => ["legacy", "client", "shared"].includes(fn.runtime)
|
|
);
|
|
const moduleAttribute = hasBrowserModule ? ' data-wrn-client-module="__WRNEXUS_CLIENT_MODULE__"' : "";
|
|
return ` data-wrn-hydration="${attrEscape(hydrationId(ast))}" data-wrn-hydrate="${attrEscape(strategy)}" data-wrn-runtime="${attrEscape(ast.runtime ?? "universal")}"${moduleAttribute}`;
|
|
}
|
|
function targetFunctions(ast, target) {
|
|
const runtimes = target === "browser" ? ["legacy", "client", "shared"] : ["legacy", "server", "shared"];
|
|
return ast.functions.map((body) => stripRuntimeFunctionModifiers2(body, [...runtimes])).map((body) => body.trim()).filter(Boolean).join("\n\n");
|
|
}
|
|
function publicOutputNames(ast) {
|
|
return [
|
|
.../* @__PURE__ */ new Set([
|
|
...ast.outputs.map((output) => output.name),
|
|
...ast.events.map((event) => event.name)
|
|
])
|
|
];
|
|
}
|
|
function prepareActionForms(nodes, actions) {
|
|
for (const node of nodes) {
|
|
if (node.type === "text") continue;
|
|
if (node.type === "each") {
|
|
prepareActionForms(node.body, actions);
|
|
prepareActionForms(node.empty, actions);
|
|
continue;
|
|
}
|
|
if (node.type === "if") {
|
|
node.branches.forEach((branch) => prepareActionForms(branch.body, actions));
|
|
continue;
|
|
}
|
|
prepareActionForms(node.children, actions);
|
|
if (node.tag.toLowerCase() !== "form") continue;
|
|
const submit = node.attrs.find((attr) => attr.event && attr.name === "submit");
|
|
if (!submit || !actions.has(submit.value.trim())) continue;
|
|
const name = submit.value.trim();
|
|
node.attrs = node.attrs.filter((attr) => attr !== submit);
|
|
if (!node.attrs.some((attr) => !attr.event && attr.name === "method")) {
|
|
node.attrs.push({ name: "method", value: "post", event: false });
|
|
}
|
|
node.attrs.push({ name: "data-wrn-action", value: name, event: false });
|
|
node.children.unshift({
|
|
type: "element",
|
|
tag: "input",
|
|
attrs: [
|
|
{ name: "type", value: "hidden", event: false },
|
|
{ name: "name", value: "_wrnexus_action", event: false },
|
|
{ name: "value", value: name, event: false }
|
|
],
|
|
children: []
|
|
});
|
|
}
|
|
}
|
|
function markServerAsyncBoundaries(nodes, serverLoads) {
|
|
for (const node of nodes) {
|
|
if (node.type === "text") continue;
|
|
if (node.type === "each") {
|
|
markServerAsyncBoundaries(node.body, serverLoads);
|
|
markServerAsyncBoundaries(node.empty, serverLoads);
|
|
continue;
|
|
}
|
|
if (node.type === "if") {
|
|
node.branches.forEach((branch) => markServerAsyncBoundaries(branch.body, serverLoads));
|
|
continue;
|
|
}
|
|
if (node.tag === "Async") {
|
|
const source = attrValue(node.attrs, "source") ?? "data";
|
|
if (serverLoads.has(source) && !node.attrs.some((attribute) => attribute.name === "data-wrn-async-server")) {
|
|
node.attrs.push({ name: "data-wrn-async-server", value: "true", event: false });
|
|
}
|
|
}
|
|
markServerAsyncBoundaries(node.children, serverLoads);
|
|
}
|
|
}
|
|
function generate(ast) {
|
|
ast = optimizeAst(ast).ast;
|
|
if (ast.kind === "global-store" || ast.kind === "page-store") return generateStoreModule(ast);
|
|
if (ast.kind === "component" || ast.kind === "layout") {
|
|
return generateComponent(ast);
|
|
}
|
|
const out = [];
|
|
prepareActionForms(ast.view, new Set(ast.actions.map((action) => action.name)));
|
|
markServerAsyncBoundaries(
|
|
ast.view,
|
|
new Set(
|
|
ast.loads.filter((load) => load.mode === "server" && !load.deferred && load.name).map((load) => load.name)
|
|
)
|
|
);
|
|
if (ast.actions.length > 0) {
|
|
out.push(
|
|
`import { createActionClient } from "@wrnexus/csr";
|
|
import type { InferSchema } from "@wrnexus/validation";`
|
|
);
|
|
}
|
|
if (ast.imports.length > 0) out.push(generatedImports(ast).join("\n"));
|
|
const ssrBindings = [];
|
|
const csrBindings = [];
|
|
const helpers = targetFunctions(ast, "server");
|
|
const apiBindings = apiBindingMap(ast, helpers);
|
|
const typeSource = ast.types.map((body2) => body2.trim()).filter(Boolean).join("\n\n");
|
|
if (typeSource) out.push(typeSource);
|
|
if (helpers) {
|
|
out.push(`// --- .wrn functions ---
|
|
${helpers}`);
|
|
}
|
|
out.push(`export const meta = ${JSON.stringify({ title: ast.name, ...ast.seo }, null, 2)};`);
|
|
if (ast.layout)
|
|
out.push(
|
|
`export const layout = ${ast.layoutIsSymbol ? ast.layout : JSON.stringify(ast.layout)};`
|
|
);
|
|
out.push(`export const __wrnexusRuntime = ${JSON.stringify(ast.runtime ?? "universal")};`);
|
|
out.push(`export const __wrnexusRender = ${JSON.stringify(ast.renderMode ?? "hybrid")};`);
|
|
out.push(
|
|
`export const __wrnexusHydrate = ${JSON.stringify(["static", "server"].includes(ast.renderMode ?? "") ? "none" : ast.hydrate ?? "load")};`
|
|
);
|
|
out.push(`export const __wrnexusHydrationId = ${JSON.stringify(hydrationId(ast))};`);
|
|
if (Object.keys(ast.cache ?? {}).length > 0)
|
|
out.push(`export const __wrnexusCache = ${JSON.stringify(ast.cache, null, 2)};`);
|
|
if (Object.keys(ast.security).length > 0) {
|
|
out.push(`export const __wrnexusSecurity = ${JSON.stringify(ast.security, null, 2)};`);
|
|
}
|
|
if (Object.keys(ast.navigation).length > 0) {
|
|
out.push(`export const __wrnexusNavigation = ${JSON.stringify(ast.navigation, null, 2)};`);
|
|
}
|
|
const browserStates = ast.states.filter((state) => state.runtime !== "server");
|
|
const seedScope = evalStateSeeds(ast.states);
|
|
for (const entry of ast.computed) {
|
|
try {
|
|
seedScope[entry.name] = new Function("with(this){return (" + entry.expr + ");}").call(
|
|
seedScope
|
|
);
|
|
} catch {
|
|
seedScope[entry.name] = void 0;
|
|
}
|
|
}
|
|
const reactiveNames = [
|
|
...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 reactive = reactiveNames.length > 0 ? { stateNames: new Set(reactiveNames), runtimeStateNames, scope: seedScope } : null;
|
|
const loops = [];
|
|
let html = ast.view.map((node) => renderNode(node, ssrBindings, csrBindings, apiBindings, loops, reactive)).join("");
|
|
const styles = ast.styles.map((body2) => body2.trim()).filter(Boolean);
|
|
const pageBehavior = ast.runtime === "server" ? null : componentBehavior(ast);
|
|
const needsClientRuntime = ast.runtime !== "server" && (browserStates.length > 0 || ast.computed.length > 0 || hasClientBehavior(ast.view) || pageBehavior !== null);
|
|
if (needsClientRuntime) {
|
|
const scopePlaceholder = "__WRNEXUS_DYNAMIC_SCOPE__";
|
|
html = `<div data-scope="${scopePlaceholder}"${behaviorAttribute(pageBehavior)}${hydrationAttribute(ast)}>${html}</div>`;
|
|
}
|
|
const pageStyleTag = localStyleTag(ast, styles);
|
|
if (pageStyleTag) {
|
|
html = `${pageStyleTag}${html}`;
|
|
}
|
|
if (ast.renderMode === "client") {
|
|
const clientRoot = hydrationId(ast);
|
|
html = `<div data-wrn-client-root="${clientRoot}" aria-busy="true"></div><template data-wrn-client-template="${clientRoot}">${html}</template>`;
|
|
}
|
|
const pageStyleExport = localStyleExport(ast, styles);
|
|
if (pageStyleExport) out.push(pageStyleExport);
|
|
if (csrBindings.length > 0) {
|
|
out.push(`export const __wrnexusCsr = ${JSON.stringify(csrBindings, null, 2)};`);
|
|
}
|
|
if (pageBehavior) {
|
|
out.push(`export const __wrnexusBehavior = ${JSON.stringify(pageBehavior, null, 2)};`);
|
|
}
|
|
let body = templateEscape(html);
|
|
let staticShellBody;
|
|
if (ast.renderMode === "partial-static") {
|
|
const shellHtml = html.replace(
|
|
/<wrn-dynamic-region\b[^>]*>[\s\S]*?<\/wrn-dynamic-region>/gi,
|
|
'<wrn-dynamic-region data-wrn-dynamic="true"></wrn-dynamic-region>'
|
|
);
|
|
staticShellBody = templateEscape(shellHtml);
|
|
}
|
|
const dynamicStateScope = ast.states.map(
|
|
(state) => `${JSON.stringify(state.name)}: (() => { try { return (${state.expr}); } catch { return undefined; } })()`
|
|
).join(", ");
|
|
const stateType = ast.states.length > 0 ? `{ ${ast.states.map((state) => `${JSON.stringify(state.name)}: ${state.valueType ?? "unknown"}`).join("; ")} }` : "Record<string, never>";
|
|
const hydrationStateNames = JSON.stringify(browserStates.map((state) => state.name));
|
|
const ssrStateAliases = generateSsrStateAliases(ast.states.map((state) => state.name));
|
|
const storeBindings = importedStoreBindings(ast);
|
|
const storeDeclarations = storeBindings.map(
|
|
(entry) => ` const ${entry.local} = await ctx.__wrnexusUseStore(${entry.internal});`
|
|
).join("\n");
|
|
const serverLoadAliases = ast.loads.filter((load) => load.mode === "server" && !load.deferred && load.name).map((load) => ` const ${load.name} = ctx[${JSON.stringify(load.name)}];`).join("\n");
|
|
loops.forEach((code, idx) => {
|
|
body = body.replace(`\0WRNEACH${idx}\0`, () => code);
|
|
if (staticShellBody?.includes(`\0WRNEACH${idx}\0`)) {
|
|
staticShellBody = staticShellBody.replace(`\0WRNEACH${idx}\0`, () => code);
|
|
}
|
|
});
|
|
const loopConsts = [];
|
|
if (loops.length > 0) {
|
|
const lists = collectControlExprs(ast.view);
|
|
for (const [name, binding] of apiBindings) {
|
|
if (binding.mode !== "ssr") continue;
|
|
if (!lists.some((expr) => new RegExp(`\\b${name}\\b`).test(expr))) continue;
|
|
loopConsts.push(
|
|
` const ${name} = __wrnexusEvalData(await __wrnexusCallApi(${JSON.stringify(binding.path)}, ${JSON.stringify(binding.method)}, ctx), ${JSON.stringify(binding.body)}, ${JSON.stringify(binding.helpers)}, ctx);`
|
|
);
|
|
}
|
|
}
|
|
const needsSsrRuntime = ssrBindings.length > 0 || loops.length > 0 || ast.states.some((state) => /\bctx\b/.test(state.expr));
|
|
if (needsSsrRuntime) {
|
|
out.push(ssrRuntimeSource());
|
|
out.push(`const __wrnexusSsrBindings = ${JSON.stringify(ssrBindings, null, 2)};`);
|
|
const decls = loopConsts.length > 0 ? loopConsts.join("\n") + "\n" : "";
|
|
out.push(
|
|
`export default async function ${ast.name}(ctx: any) {
|
|
${storeDeclarations}
|
|
${serverLoadAliases}
|
|
${decls}
|
|
const __state: ${stateType} = { ${dynamicStateScope} };
|
|
${ssrStateAliases}
|
|
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));
|
|
|
|
return key + ": " + encoded;
|
|
})
|
|
.join(", ")
|
|
.replace(/&/g, "&")
|
|
.replace(/"/g, """)
|
|
.replace(/</g, "<")
|
|
.replace(/>/g, ">");
|
|
|
|
const html = \`${body}\`.replace(
|
|
"__WRNEXUS_DYNAMIC_SCOPE__",
|
|
__scopeValue,
|
|
);
|
|
|
|
return await __wrnexusRenderSsrBindings(html, ctx);
|
|
}`
|
|
);
|
|
} else {
|
|
out.push(
|
|
`export default ${storeBindings.length > 0 ? "async " : ""}function ${ast.name}(ctx: any) {
|
|
${storeDeclarations}
|
|
${serverLoadAliases}
|
|
const __state: ${stateType} = { ${dynamicStateScope} };
|
|
${ssrStateAliases}
|
|
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));
|
|
|
|
return key + ": " + encoded;
|
|
})
|
|
.join(", ")
|
|
.replace(/&/g, "&")
|
|
.replace(/"/g, """)
|
|
.replace(/</g, "<")
|
|
.replace(/>/g, ">");
|
|
|
|
return \`${body}\`.replace(
|
|
"__WRNEXUS_DYNAMIC_SCOPE__",
|
|
__scopeValue,
|
|
);
|
|
}`
|
|
);
|
|
}
|
|
if (staticShellBody !== void 0) {
|
|
out.push(
|
|
`export async function __wrnexusBuildStaticShell(ctx: any = {}) {
|
|
${storeDeclarations}
|
|
${serverLoadAliases}
|
|
${loopConsts.length > 0 ? loopConsts.join("\n") : ""}
|
|
const __state: ${stateType} = { ${dynamicStateScope} };
|
|
${ssrStateAliases}
|
|
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));
|
|
return key + ": " + encoded;
|
|
})
|
|
.join(", ")
|
|
.replace(/&/g, "&")
|
|
.replace(/"/g, """)
|
|
.replace(/</g, "<")
|
|
.replace(/>/g, ">");
|
|
return \`${staticShellBody}\`.replace("__WRNEXUS_DYNAMIC_SCOPE__", __scopeValue);
|
|
}`
|
|
);
|
|
}
|
|
if (ast.loads.length > 0) {
|
|
const serverLoads = ast.loads.filter((entry) => entry.mode === "server" && !entry.deferred);
|
|
const publicClientLoads = ast.loads.filter(
|
|
(entry) => entry.mode === "client" || entry.deferred
|
|
);
|
|
const namedByName = new Map(
|
|
ast.loads.filter((entry) => entry.name).map((entry) => [entry.name, entry])
|
|
);
|
|
const clientNames = new Set(
|
|
publicClientLoads.flatMap((entry) => entry.name ? [entry.name] : [])
|
|
);
|
|
const includeDependencies = (name) => {
|
|
for (const dependency of namedByName.get(name)?.dependsOn ?? []) {
|
|
if (clientNames.has(dependency)) continue;
|
|
clientNames.add(dependency);
|
|
includeDependencies(dependency);
|
|
}
|
|
};
|
|
for (const name of [...clientNames]) includeDependencies(name);
|
|
const clientLoads = ast.loads.filter((entry) => !entry.name || clientNames.has(entry.name));
|
|
const renderLoads = (exportName, execution, exposed) => {
|
|
const declarations = execution.filter((entry) => entry.name).map((entry) => {
|
|
const dependencies = (entry.dependsOn ?? []).map((dependency) => `const ${dependency} = await __load_${dependency}();`).join("\n");
|
|
return ` let __promise_${entry.name}: Promise<unknown> | undefined;
|
|
const __load_${entry.name} = () => (__promise_${entry.name} ??= (async () => {
|
|
${dependencies}
|
|
${entry.body}
|
|
})());`;
|
|
}).join("\n");
|
|
const visible = exposed.filter((entry) => entry.name);
|
|
return `export async function ${exportName}(ctx: any) {
|
|
${exposed.filter((entry) => !entry.name).map((entry) => entry.body).join("\n")}
|
|
${declarations}
|
|
${visible.length ? ` const __values = await Promise.all([${visible.map((entry) => `__load_${entry.name}()`).join(", ")}]);
|
|
return { ${visible.map((entry, index) => `${JSON.stringify(entry.name)}: __values[${index}]`).join(", ")} };` : ""}
|
|
}`;
|
|
};
|
|
if (serverLoads.length > 0) out.push(renderLoads("__wrnexusLoad", serverLoads, serverLoads));
|
|
if (publicClientLoads.length > 0)
|
|
out.push(renderLoads("__wrnexusClientLoad", clientLoads, publicClientLoads));
|
|
}
|
|
if (ast.actions.length > 0) {
|
|
for (const action of ast.actions) {
|
|
if (!action.schema) {
|
|
out.push(
|
|
`export async function ${action.name}(${action.args.join(", ")}) {${action.body}}`
|
|
);
|
|
continue;
|
|
}
|
|
out.push(`export async function ${action.name}(input: any, ctx: any) {
|
|
const invalidate = (...tags: string[]) => {
|
|
const bucket = (ctx.locals.__wrnexusInvalidatedTags ??= []);
|
|
bucket.push(...tags.flat());
|
|
};
|
|
${action.body}
|
|
}`);
|
|
}
|
|
out.push(
|
|
`export const __wrnexusActions = { ${ast.actions.map(
|
|
(action) => `${action.name}: { run: ${action.name}, schema: ${action.schema ?? "undefined"} }`
|
|
).join(", ")} };`
|
|
);
|
|
out.push(`export const __wrnexusActionClients = {
|
|
${ast.actions.map(
|
|
(action) => ` ${action.name}: createActionClient<${action.schema ? `InferSchema<typeof ${action.schema}>` : "Record<string, unknown>"}, Awaited<ReturnType<typeof ${action.name}>>>("", ${JSON.stringify(action.name)}),`
|
|
).join("\n")}
|
|
};`);
|
|
}
|
|
if (ast.apis.length > 0) {
|
|
ast.apis.forEach((api, index) => {
|
|
const name = `__wrnexusApi_${api.method}_${index}`;
|
|
out.push(`// ${api.method} ${apiRoutePath(api.path)}
|
|
const ${name} = async (ctx: any) => {${api.body}};`);
|
|
});
|
|
const entries = ast.apis.map(
|
|
(api, index) => ` ${JSON.stringify(`${api.method} ${apiRoutePath(api.path)}`)}: __wrnexusApi_${api.method}_${index},`
|
|
);
|
|
out.push(`export const __wrnexusApi = {
|
|
${entries.join("\n")}
|
|
};`);
|
|
const exported = /* @__PURE__ */ new Set();
|
|
ast.apis.forEach((api, index) => {
|
|
if (exported.has(api.method)) return;
|
|
exported.add(api.method);
|
|
out.push(`export const ${api.method} = __wrnexusApi_${api.method}_${index};`);
|
|
});
|
|
}
|
|
if (ast.realtimes.length > 0) {
|
|
const handlers = ast.realtimes.flatMap(
|
|
(rt) => rt.handlers.map((h) => {
|
|
const params2 = ["ws", ...h.args].join(", ");
|
|
return ` ${h.event}(${params2}: any) {${h.body}},`;
|
|
})
|
|
);
|
|
out.push(`export const websocket = {
|
|
${handlers.join("\n")}
|
|
};`);
|
|
}
|
|
return out.join("\n\n") + "\n";
|
|
}
|
|
function parseForExpr(value) {
|
|
const m = /^\s*([A-Za-z_$][\w$]*)\s*(?:,\s*([A-Za-z_$][\w$]*)\s*)?\s+in\s+([\s\S]+?)(?:\s+key\s+([\s\S]+?))?\s*$/.exec(
|
|
value
|
|
);
|
|
if (!m) return null;
|
|
return { item: m[1], index: m[2], list: m[3].trim(), key: m[4]?.trim() };
|
|
}
|
|
function loopVarsOf(node) {
|
|
if (node.type !== "element") return [];
|
|
const attr = node.attrs.find((a) => !a.event && a.name === "data-for");
|
|
if (!attr) return [];
|
|
const parsed = parseForExpr(attr.value);
|
|
return parsed ? [parsed.item, ...parsed.index ? [parsed.index] : []] : [];
|
|
}
|
|
var JS_RESERVED = /* @__PURE__ */ new Set([
|
|
"class",
|
|
"for",
|
|
"default",
|
|
"function",
|
|
"return",
|
|
"if",
|
|
"else",
|
|
"new",
|
|
"delete",
|
|
"typeof",
|
|
"in",
|
|
"instanceof",
|
|
"void",
|
|
"do",
|
|
"while",
|
|
"switch",
|
|
"case",
|
|
"break",
|
|
"continue",
|
|
"this",
|
|
"super",
|
|
"import",
|
|
"export",
|
|
"extends",
|
|
"var",
|
|
"let",
|
|
"const",
|
|
"null",
|
|
"true",
|
|
"false",
|
|
"try",
|
|
"catch",
|
|
"finally",
|
|
"throw",
|
|
"yield",
|
|
"await",
|
|
"enum",
|
|
"with",
|
|
"debugger",
|
|
"implements",
|
|
"interface",
|
|
"package",
|
|
"private",
|
|
"protected",
|
|
"public",
|
|
"static"
|
|
]);
|
|
function safeRef(name) {
|
|
return JS_RESERVED.has(name) ? `__p_${name}` : name;
|
|
}
|
|
function escLit(s) {
|
|
return s.replace(/\\/g, "\\\\").replace(/`/g, "\\`").replace(/\$\{/g, "\\${");
|
|
}
|
|
function componentBehavior(ast) {
|
|
const functions = (0, types_exports.eraseFunctionTypes)(targetFunctions(ast, "browser"));
|
|
const computed = ast.computed.map((entry) => ({ name: entry.name, expr: entry.expr.trim() }));
|
|
const effects = ast.effects.map((entry) => entry.body.trim()).filter(Boolean);
|
|
const lifecycle = {
|
|
...ast.lifecycle.mount?.trim() ? { mount: ast.lifecycle.mount.trim() } : {},
|
|
...ast.lifecycle.update?.trim() ? { update: ast.lifecycle.update.trim() } : {},
|
|
...ast.lifecycle.unmount?.trim() ? { unmount: ast.lifecycle.unmount.trim() } : {}
|
|
};
|
|
const watches = ast.watches.map((watch) => ({
|
|
state: watch.state,
|
|
body: watch.body.trim()
|
|
}));
|
|
if (!functions && ast.outputs.length === 0 && computed.length === 0 && effects.length === 0 && Object.keys(lifecycle).length === 0 && watches.length === 0) {
|
|
return null;
|
|
}
|
|
return {
|
|
functions,
|
|
outputs: ast.outputs,
|
|
computed,
|
|
effects,
|
|
lifecycle,
|
|
watches
|
|
};
|
|
}
|
|
function behaviorAttribute(behavior) {
|
|
if (!behavior) {
|
|
return "";
|
|
}
|
|
const encoded = Buffer2.from(JSON.stringify(behavior), "utf8").toString("base64");
|
|
return ` data-wrn-behavior="${encoded}"`;
|
|
}
|
|
var INTERP_RE = /\{([^{}]+)\}/g;
|
|
function exprRefsState(expr, stateNames) {
|
|
for (const name of stateNames) {
|
|
if (new RegExp(`\\b${name}\\b`).test(expr)) return true;
|
|
}
|
|
return false;
|
|
}
|
|
function exprRefsComponentReactiveValue(expr, ctx) {
|
|
return exprRefsState(expr, ctx.stateNames) || exprRefsState(expr, ctx.functionNames);
|
|
}
|
|
function viewHasEvents(nodes) {
|
|
return nodes.some((node) => {
|
|
if (node.type === "text") return false;
|
|
if (node.type === "each") {
|
|
return viewHasEvents(node.body) || viewHasEvents(node.empty);
|
|
}
|
|
if (node.type === "if") {
|
|
return node.branches.some((branch) => viewHasEvents(branch.body));
|
|
}
|
|
return node.attrs.some((attr) => attr.event) || viewHasEvents(node.children);
|
|
});
|
|
}
|
|
function viewHasServerEach(nodes) {
|
|
return nodes.some((node) => {
|
|
if (node.type === "text") return false;
|
|
if (node.type === "each") return true;
|
|
if (node.type === "if") {
|
|
return node.branches.some((branch) => viewHasServerEach(branch.body));
|
|
}
|
|
return viewHasServerEach(node.children);
|
|
});
|
|
}
|
|
function viewHasRestAttributeSpread(nodes) {
|
|
return nodes.some((node) => {
|
|
if (node.type === "text") return false;
|
|
if (node.type === "each") {
|
|
return viewHasRestAttributeSpread(node.body) || viewHasRestAttributeSpread(node.empty);
|
|
}
|
|
if (node.type === "if") {
|
|
return node.branches.some((branch) => viewHasRestAttributeSpread(branch.body));
|
|
}
|
|
return node.attrs.some((attr) => /^\{\.\.\.([A-Za-z_$][\w$]*)\}$/.test(attr.name)) || viewHasRestAttributeSpread(node.children);
|
|
});
|
|
}
|
|
function compileText(raw, ctx) {
|
|
let out = "";
|
|
let last = 0;
|
|
let m;
|
|
INTERP_RE.lastIndex = 0;
|
|
while (m = INTERP_RE.exec(raw)) {
|
|
out += escLit(raw.slice(last, m.index));
|
|
const expr = m[1].trim();
|
|
if (expr.startsWith("t:")) {
|
|
out += escLit(`<span data-t="${attrEscape(expr.slice(2).trim())}"></span>`);
|
|
} else if (ctx.loopVars && exprRefsState(expr, ctx.loopVars)) {
|
|
out += escLit(`{${expr}}`);
|
|
} else if (exprRefsComponentReactiveValue(expr, ctx)) {
|
|
out += escLit(`<span data-text="${attrEscape(expr)}">`) + `\${__wireHtml(${ctx.resolveExpr(expr)})}` + escLit(`</span>`);
|
|
} else if (expr === "content") {
|
|
out += `\${__wireRaw(${ctx.resolveExpr(expr)})}`;
|
|
} else {
|
|
out += `\${__wireHtml(${ctx.resolveExpr(expr)})}`;
|
|
}
|
|
last = m.index + m[0].length;
|
|
}
|
|
return out + escLit(raw.slice(last));
|
|
}
|
|
function compileAttrValue(raw, ctx) {
|
|
if (!raw.includes("{")) return escLit(attrEscape(raw));
|
|
let out = "";
|
|
let last = 0;
|
|
let m;
|
|
INTERP_RE.lastIndex = 0;
|
|
while (m = INTERP_RE.exec(raw)) {
|
|
out += escLit(attrEscape(raw.slice(last, m.index)));
|
|
const expr = m[1].trim();
|
|
if (ctx.loopVars && exprRefsState(expr, ctx.loopVars)) {
|
|
out += escLit(`{${expr}}`);
|
|
} else {
|
|
out += `\${__wireAttr(${ctx.resolveExpr(expr)})}`;
|
|
}
|
|
last = m.index + m[0].length;
|
|
}
|
|
return out + escLit(attrEscape(raw.slice(last)));
|
|
}
|
|
function renderComponentIfNode(node, ctx) {
|
|
let expression2 = "``";
|
|
for (let index = node.branches.length - 1; index >= 0; index--) {
|
|
const branch = node.branches[index];
|
|
const body = branch.body.map((child) => renderComponentNode(child, ctx)).join("");
|
|
const bodyExpression = "`" + body + "`";
|
|
expression2 = branch.cond === null ? bodyExpression : `(${ctx.resolveExpr(branch.cond)}) ? ${bodyExpression} : ${expression2}`;
|
|
}
|
|
return "${" + expression2 + "}";
|
|
}
|
|
function renderComponentEachNode(node, ctx) {
|
|
const item = node.item;
|
|
const index = node.index ?? "__wi";
|
|
const list = ctx.resolveExpr(node.list);
|
|
const childCtx = {
|
|
...ctx,
|
|
serverLocals: /* @__PURE__ */ new Set([...ctx.serverLocals ?? [], item, index])
|
|
};
|
|
const body = node.body.map((child) => renderComponentNode(child, childCtx)).join("");
|
|
const empty = node.empty.map((child) => renderComponentNode(child, ctx)).join("");
|
|
return "${(() => { const __wl = Array.isArray(" + list + ") ? (" + list + ") : []; return __wl.length ? __wl.map((" + item + ", " + index + ") => `" + body + '`).join("") : `' + empty + "`; })()}";
|
|
}
|
|
function serverLoopLocalsAttribute(ctx) {
|
|
const locals = [...ctx.serverLocals ?? []];
|
|
if (locals.length === 0) {
|
|
return "";
|
|
}
|
|
const entries = locals.map((name) => `${JSON.stringify(name)}: ${name}`).join(", ");
|
|
return ` data-wrn-loop-locals="\${__wrnexusEncodeLoopLocals({ ${entries} })}"`;
|
|
}
|
|
function unwrapDirectiveExpression(raw) {
|
|
const value = raw.trim();
|
|
if (!value.startsWith("{") || !value.endsWith("}")) {
|
|
return value;
|
|
}
|
|
let depth = 0;
|
|
let quote = null;
|
|
let escaped = false;
|
|
for (let index = 0; index < value.length; index++) {
|
|
const char = value[index];
|
|
if (escaped) {
|
|
escaped = false;
|
|
continue;
|
|
}
|
|
if (quote) {
|
|
if (char === "\\") {
|
|
escaped = true;
|
|
} else if (char === quote) {
|
|
quote = null;
|
|
}
|
|
continue;
|
|
}
|
|
if (char === '"' || char === "'" || char === "`") {
|
|
quote = char;
|
|
continue;
|
|
}
|
|
if (char === "{") depth++;
|
|
if (char === "}") depth--;
|
|
if (depth === 0 && index < value.length - 1) {
|
|
return value;
|
|
}
|
|
}
|
|
return depth === 0 ? value.slice(1, -1).trim() : value;
|
|
}
|
|
function renderComponentNode(node, ctx) {
|
|
if (node.type === "text") return compileText(node.value, ctx);
|
|
if (node.type === "each") {
|
|
return renderComponentEachNode(node, ctx);
|
|
}
|
|
if (node.type === "if") {
|
|
return renderComponentIfNode(node, ctx);
|
|
}
|
|
if (node.tag === "Static" || node.tag === "Dynamic") {
|
|
const inner2 = node.children.map((child) => renderComponentNode(child, ctx)).join("");
|
|
return node.tag === "Static" ? inner2 : `<wrn-dynamic-region data-wrn-dynamic="true">${inner2}</wrn-dynamic-region>`;
|
|
}
|
|
if (node.tag === "KeepAlive") {
|
|
const key = node.attrs.find((attribute) => attribute.name === "key")?.value ?? "default";
|
|
const inner2 = node.children.map((child) => renderComponentNode(child, ctx)).join("");
|
|
return `<div data-wrn-keepalive="${compileAttrValue(key, ctx)}">${inner2}</div>`;
|
|
}
|
|
if (node.tag === "Portal" || node.tag === "Transition" || node.tag === "Component") {
|
|
const inner2 = node.children.map((child) => renderComponentNode(child, ctx)).join("");
|
|
const attribute = node.tag === "Portal" ? "data-wrn-portal" : node.tag === "Transition" ? "data-wrn-transition" : "data-wrn-dynamic-component";
|
|
const source = node.tag === "Portal" ? "to" : node.tag === "Transition" ? "name" : "is";
|
|
const fallback = node.tag === "Portal" ? "body" : node.tag === "Transition" ? "wrn-transition" : "";
|
|
const raw = node.attrs.find((item) => item.name === source)?.value ?? fallback;
|
|
return `<div ${attribute}="${compileAttrValue(raw, ctx)}">${inner2}</div>`;
|
|
}
|
|
if (isComponentTag(node.tag)) {
|
|
return renderNestedComponentInvocation(node, ctx);
|
|
}
|
|
const loopVariables = loopVarsOf(node);
|
|
const elementContext = {
|
|
...ctx,
|
|
forwardRestAttrs: false,
|
|
...loopVariables.length > 0 ? { loopVars: /* @__PURE__ */ new Set([...ctx.loopVars ?? [], ...loopVariables]) } : {}
|
|
};
|
|
let bindIndex = 0;
|
|
const staticClasses = [];
|
|
const conditionalClasses = [];
|
|
for (const attr of node.attrs) {
|
|
if (!attr.event && attr.name === "class") {
|
|
staticClasses.push(attr.value);
|
|
}
|
|
if (!attr.event && attr.name.startsWith("class:")) {
|
|
conditionalClasses.push({
|
|
className: attr.name.slice("class:".length),
|
|
expression: unwrapDirectiveExpression(attr.value)
|
|
});
|
|
}
|
|
}
|
|
const isExplicitComponentMount = node.attrs.some(
|
|
(attribute) => attribute.name === "data-component"
|
|
);
|
|
const attrs = node.attrs.filter((a) => a.name !== "class" && !a.name.startsWith("class:")).map((a) => {
|
|
const spread = /^\{\.\.\.([A-Za-z_$][\w$]*)\}$/.exec(a.name);
|
|
if (spread) {
|
|
return `\${__wireSpreadAttrs(${elementContext.resolveExpr(spread[1])})}`;
|
|
}
|
|
if (a.event) {
|
|
return ` ${eventAttribute(a.name)}="${escLit(attrEscape(a.value))}"`;
|
|
}
|
|
if (a.boolean) {
|
|
return ` ${a.name}`;
|
|
}
|
|
if (isHtmlBooleanAttribute(a.name)) {
|
|
const expression2 = wholeAttributeExpression(a.value);
|
|
if (expression2) {
|
|
const referencesState2 = exprRefsComponentReactiveValue(a.value, ctx);
|
|
const referencesLoopVariable2 = elementContext.loopVars ? exprRefsState(a.value, elementContext.loopVars) : false;
|
|
const referencesServerLocal2 = ctx.serverLocals ? exprRefsState(a.value, ctx.serverLocals) : false;
|
|
const marker2 = referencesState2 || referencesLoopVariable2 || referencesServerLocal2 ? ` data-wrn-bind-${bindIndex++}="${escLit(
|
|
attrEscape(JSON.stringify([a.name, a.value]))
|
|
)}"` : "";
|
|
return `\${__wireBooleanAttr(${JSON.stringify(a.name)}, ${elementContext.resolveExpr(expression2)})}${marker2}`;
|
|
}
|
|
if (a.value === "false") return "";
|
|
if (a.value === "true" || a.value === "") return ` ${a.name}`;
|
|
}
|
|
const wholeExpression = wholeAttributeExpression(a.value);
|
|
const compiledValue = isExplicitComponentMount && wholeExpression ? `\${__wireProp(${elementContext.resolveExpr(wholeExpression)})}` : compileAttrValue(a.value, elementContext);
|
|
const rendered = ` ${a.name}="${compiledValue}"`;
|
|
const referencesState = exprRefsComponentReactiveValue(a.value, ctx);
|
|
const referencesLoopVariable = elementContext.loopVars ? exprRefsState(a.value, elementContext.loopVars) : false;
|
|
const referencesServerLocal = ctx.serverLocals ? exprRefsState(a.value, ctx.serverLocals) : false;
|
|
if (!a.value.includes("{") || !referencesState && !referencesLoopVariable && !referencesServerLocal) {
|
|
return rendered;
|
|
}
|
|
const marker = attrEscape(JSON.stringify([a.name, a.value]));
|
|
return `${rendered} data-wrn-bind-${bindIndex++}="${escLit(marker)}"`;
|
|
}).join("");
|
|
const initialConditionalClasses = conditionalClasses.map(({ className, expression: expression2 }) => {
|
|
const referencesLoopVariable = elementContext.loopVars ? exprRefsState(expression2, elementContext.loopVars) : false;
|
|
if (referencesLoopVariable) {
|
|
return "";
|
|
}
|
|
return `\${(${ctx.resolveExpr(expression2)}) ? ${JSON.stringify(` ${className}`)} : ""}`;
|
|
}).join("");
|
|
const staticClassValue = staticClasses.join(" ");
|
|
const classReferencesState = exprRefsComponentReactiveValue(staticClassValue, ctx);
|
|
const classReferencesLoopVariable = elementContext.loopVars ? exprRefsState(staticClassValue, elementContext.loopVars) : false;
|
|
const classReferencesServerLocal = ctx.serverLocals ? exprRefsState(staticClassValue, ctx.serverLocals) : false;
|
|
const classHasReactiveExpression = staticClassValue.includes("{") && (classReferencesState || classReferencesLoopVariable || classReferencesServerLocal);
|
|
const classAttribute = staticClasses.length > 0 || conditionalClasses.length > 0 ? ` class="${compileAttrValue(staticClassValue, elementContext)}${initialConditionalClasses}"` : "";
|
|
const classReactiveBinding = classHasReactiveExpression ? ` data-wrn-bind-class="${escLit(attrEscape(JSON.stringify(["class", staticClassValue])))}"` : "";
|
|
const classBindings = conditionalClasses.map(({ className, expression: expression2 }, index) => {
|
|
const marker = attrEscape(JSON.stringify([className, expression2]));
|
|
return ` data-wrn-class-${index}="${escLit(marker)}"`;
|
|
}).join("");
|
|
const loopLocalsAttribute = serverLoopLocalsAttribute(ctx);
|
|
const allAttrs = `${loopLocalsAttribute}${ctx.forwardRestAttrs ? "${__wireSpreadAttrs(__attrs)}" : ""}${ctx.eventNames?.length ? ` data-wrn-events="${attrEscape(ctx.eventNames.join(","))}"` : ""}${classAttribute}${classReactiveBinding}${classBindings}${attrs}`;
|
|
if (parser_exports.VOID_ELEMENTS.has(node.tag.toLowerCase())) {
|
|
return `<${node.tag}${allAttrs}>`;
|
|
}
|
|
const inner = node.children.map((child) => renderComponentNode(child, elementContext)).join("");
|
|
return `<${node.tag}${allAttrs}>${inner}</${node.tag}>`;
|
|
}
|
|
function generateComponent(ast) {
|
|
const out = [];
|
|
if (ast.imports.length > 0) out.push(generatedImports(ast).join("\n"));
|
|
const hasServerEach = viewHasServerEach(ast.view);
|
|
const effectiveProps = ast.kind === "layout" && !ast.props.some((prop) => prop.name === "content") ? [
|
|
{
|
|
name: "content",
|
|
default: '""',
|
|
valueType: "string",
|
|
required: false
|
|
},
|
|
...ast.props
|
|
] : ast.props;
|
|
const browserStates = ast.states.filter((state) => state.runtime !== "server");
|
|
const stateNames = /* @__PURE__ */ new Set([
|
|
...browserStates.map((entry) => entry.name),
|
|
...ast.computed.map((entry) => entry.name)
|
|
]);
|
|
const nameRefs = /* @__PURE__ */ new Map();
|
|
for (const p of effectiveProps) {
|
|
nameRefs.set(p.name, safeRef(p.name));
|
|
}
|
|
if (!nameRefs.has("attrs")) {
|
|
nameRefs.set("attrs", "__attrs");
|
|
}
|
|
for (const s of ast.states) nameRefs.set(s.name, safeRef(s.name));
|
|
for (const entry of ast.computed) nameRefs.set(entry.name, safeRef(entry.name));
|
|
const resolveExpr = (expr) => {
|
|
let result = expr;
|
|
for (const [name, ref] of nameRefs) {
|
|
if (name !== ref) result = result.replace(new RegExp(`\\b${name}\\b`, "g"), ref);
|
|
}
|
|
return result;
|
|
};
|
|
const ctx = {
|
|
stateNames,
|
|
functionNames: new Set(
|
|
ast.runtimeFunctions.filter((fn) => fn.runtime !== "server").map((fn) => fn.name)
|
|
),
|
|
resolveExpr,
|
|
eventNames: publicOutputNames(ast)
|
|
};
|
|
const serverFunctions = targetFunctions(ast, "server");
|
|
const hasExplicitRestSpread = viewHasRestAttributeSpread(ast.view);
|
|
const rootElementIndex = ast.view.findIndex((node) => node.type === "element");
|
|
const automaticallyForwardRootAttrs = !hasExplicitRestSpread && !effectiveProps.some((prop) => prop.name === "attrs") && rootElementIndex >= 0;
|
|
const viewCode = ast.view.map(
|
|
(node, index) => renderComponentNode(
|
|
node,
|
|
automaticallyForwardRootAttrs && index === rootElementIndex ? { ...ctx, forwardRestAttrs: true } : ctx
|
|
)
|
|
).join("");
|
|
const styles = ast.styles.map((body) => body.trim()).filter(Boolean);
|
|
const styleTag = escLit(localStyleTag(ast, styles));
|
|
const behavior = componentBehavior(ast);
|
|
const needsScope = ast.runtime !== "server" && (browserStates.length > 0 || ast.computed.length > 0 || viewHasEvents(ast.view) || behavior !== null);
|
|
if (hasServerEach || needsScope) {
|
|
out.push(`import { Buffer as __WrnexusBuffer } from "node:buffer";`);
|
|
}
|
|
const scopeKeys = [
|
|
...effectiveProps.map((prop) => prop.name),
|
|
...browserStates.map((state) => state.name)
|
|
];
|
|
const behaviorAttr = behaviorAttribute(behavior);
|
|
const decls = [];
|
|
for (const prop of effectiveProps) {
|
|
if (prop.required) {
|
|
decls.push(
|
|
` if (__p[${JSON.stringify(prop.name)}] === undefined) throw new TypeError(${JSON.stringify(
|
|
`${ast.name} requires prop '${prop.name}' (${prop.valueType ?? "unknown"})`
|
|
)});`
|
|
);
|
|
}
|
|
decls.push(
|
|
` const ${nameRefs.get(prop.name)}: ${prop.valueType ?? "any"} = __coerce(__p[${JSON.stringify(prop.name)}], (${resolveExpr(prop.default)}), ${JSON.stringify((0, types_exports.runtimeTypeOf)(prop.valueType))});`
|
|
);
|
|
}
|
|
if (!effectiveProps.some((prop) => prop.name === "attrs")) {
|
|
decls.push(
|
|
` const __attrs = __restProps(__p, new Set(${JSON.stringify(effectiveProps.map((prop) => prop.name))}));`
|
|
);
|
|
}
|
|
for (const state of ast.states) {
|
|
decls.push(
|
|
` let ${nameRefs.get(state.name)}${state.valueType ? `: ${state.valueType}` : ""} = (${resolveExpr(state.expr)});`
|
|
);
|
|
}
|
|
for (const entry of ast.computed) {
|
|
decls.push(` const ${nameRefs.get(entry.name)} = (${resolveExpr(entry.expr)});`);
|
|
}
|
|
const returnExpr = needsScope ? "`" + styleTag + `<div data-scope="\${__scope}" data-wrn-scope="\${__scopePayload}"${behaviorAttr}${hydrationAttribute(ast)}>` + viewCode + "</div>`" : "`" + styleTag + viewCode + "`";
|
|
const scopeLine = needsScope && scopeKeys.length > 0 ? ` const __scopeState = { ${scopeKeys.map((key) => `${JSON.stringify(key)}: ${nameRefs.get(key)}`).join(
|
|
", "
|
|
)} };
|
|
const __scope = __wrnexusScopeDecl(__scopeState);
|
|
const __scopePayload = __WrnexusBuffer.from(JSON.stringify(__scopeState), "utf8").toString("base64");
|
|
` : needsScope ? ` const __scopeState = {};
|
|
const __scope = "";
|
|
const __scopePayload = __WrnexusBuffer.from("{}", "utf8").toString("base64");
|
|
` : "";
|
|
if (ast.kind === "layout") {
|
|
out.push(`export const __wrnexusLayout = ${JSON.stringify(ast.name)};`);
|
|
} else {
|
|
out.push(`export const __wrnexusComponent = ${JSON.stringify(ast.name)};`);
|
|
}
|
|
out.push(`export const __wrnexusRuntime = ${JSON.stringify(ast.runtime ?? "universal")};`);
|
|
out.push(`export const __wrnexusRender = ${JSON.stringify(ast.renderMode ?? "hybrid")};`);
|
|
out.push(
|
|
`export const __wrnexusHydrate = ${JSON.stringify(["static", "server"].includes(ast.renderMode ?? "") ? "none" : ast.hydrate ?? "load")};`
|
|
);
|
|
out.push(`export const __wrnexusHydrationId = ${JSON.stringify(hydrationId(ast))};`);
|
|
if (Object.keys(ast.cache ?? {}).length > 0)
|
|
out.push(`export const __wrnexusCache = ${JSON.stringify(ast.cache, null, 2)};`);
|
|
if (Object.keys(ast.security).length > 0) {
|
|
out.push(`export const __wrnexusSecurity = ${JSON.stringify(ast.security, null, 2)};`);
|
|
}
|
|
if (Object.keys(ast.navigation).length > 0) {
|
|
out.push(`export const __wrnexusNavigation = ${JSON.stringify(ast.navigation, null, 2)};`);
|
|
}
|
|
const componentStyleExport = localStyleExport(ast, styles);
|
|
if (componentStyleExport) out.push(componentStyleExport);
|
|
if (behavior) {
|
|
out.push(`export const __wrnexusBehavior = ${JSON.stringify(behavior, null, 2)};`);
|
|
}
|
|
const typeSource = ast.types.map((body) => body.trim()).filter(Boolean).join("\n\n");
|
|
if (typeSource) out.push(typeSource);
|
|
if (effectiveProps.length > 0) {
|
|
out.push(
|
|
`export interface ${ast.name}Props {
|
|
[attribute: string]: unknown;
|
|
${effectiveProps.map(
|
|
(prop) => ` ${JSON.stringify(prop.name)}${prop.required ? "" : "?"}: ${prop.valueType ?? "unknown"};`
|
|
).join("\n")}
|
|
}`
|
|
);
|
|
}
|
|
if (ast.outputs.length > 0) {
|
|
out.push(
|
|
`export interface ${ast.name}Outputs {
|
|
${ast.outputs.map(
|
|
(output) => ` ${JSON.stringify(output.name)}(${output.payload ? `${output.payload.name}${output.payload.optional ? "?" : ""}: ${output.payload.valueType}` : ""}): void;`
|
|
).join("\n")}
|
|
}`
|
|
);
|
|
}
|
|
out.push(`function __coerce(v: any, def: any, declared: string = "unknown"): any {
|
|
if (v === undefined || v === null) {
|
|
return def;
|
|
}
|
|
|
|
if (declared === "number" || typeof def === "number") {
|
|
const parsed = Number(v);
|
|
if (!Number.isFinite(parsed)) throw new TypeError("Expected a finite number prop");
|
|
return parsed;
|
|
}
|
|
|
|
if (declared === "boolean" || typeof def === "boolean") {
|
|
if (v === true || v === "" || v === "true" || v === 1 || v === "1") return true;
|
|
if (v === false || v === "false" || v === 0 || v === "0") return false;
|
|
throw new TypeError("Expected a boolean prop");
|
|
}
|
|
|
|
if (declared === "array" || Array.isArray(def)) {
|
|
if (Array.isArray(v)) {
|
|
return v;
|
|
}
|
|
|
|
if (typeof v === "string") {
|
|
try {
|
|
const parsed = JSON.parse(v);
|
|
return Array.isArray(parsed) ? parsed : def;
|
|
} catch {
|
|
if (declared === "array") throw new TypeError("Expected an array prop");
|
|
return def;
|
|
}
|
|
}
|
|
|
|
return def;
|
|
}
|
|
|
|
if (declared === "object" || (def !== null && typeof def === "object")) {
|
|
if (
|
|
v !== null &&
|
|
typeof v === "object" &&
|
|
!Array.isArray(v)
|
|
) {
|
|
return v;
|
|
}
|
|
|
|
if (typeof v === "string") {
|
|
try {
|
|
const parsed = JSON.parse(v);
|
|
|
|
return (
|
|
parsed !== null &&
|
|
typeof parsed === "object" &&
|
|
!Array.isArray(parsed)
|
|
)
|
|
? parsed
|
|
: def;
|
|
} catch {
|
|
if (declared === "object") throw new TypeError("Expected an object prop");
|
|
return def;
|
|
}
|
|
}
|
|
|
|
return def;
|
|
}
|
|
|
|
if (declared === "bigint") return BigInt(v);
|
|
if (declared === "function" && typeof v !== "function") {
|
|
throw new TypeError("Expected a function prop");
|
|
}
|
|
return declared === "unknown" && def === undefined ? v : String(v);
|
|
}
|
|
|
|
function __restProps(
|
|
props: Record<string, any>,
|
|
declared: Set<string>,
|
|
): Record<string, any> {
|
|
return Object.fromEntries(
|
|
Object.entries(props).filter(([name]) => !declared.has(name)),
|
|
);
|
|
}
|
|
|
|
function __wireHtml(v: any): string {
|
|
return String(v == null ? "" : v).replace(
|
|
/[&<>]/g,
|
|
(c) =>
|
|
c === "&"
|
|
? "&"
|
|
: c === "<"
|
|
? "<"
|
|
: ">",
|
|
);
|
|
}
|
|
|
|
function __wireAttr(v: any): string {
|
|
return String(v == null ? "" : v).replace(
|
|
/[&<>"]/g,
|
|
(c) =>
|
|
c === "&"
|
|
? "&"
|
|
: c === "<"
|
|
? "<"
|
|
: c === ">"
|
|
? ">"
|
|
: """,
|
|
);
|
|
}
|
|
|
|
function __wireBooleanAttr(name: string, value: any): string {
|
|
return value === true ||
|
|
value === "true" ||
|
|
value === "" ||
|
|
value === 1 ||
|
|
value === "1" ||
|
|
value === name
|
|
? " " + name
|
|
: "";
|
|
}
|
|
|
|
function __wireSpreadAttrs(value: any): string {
|
|
if (value === null || typeof value !== "object" || Array.isArray(value)) return "";
|
|
|
|
const booleanAttributes = new Set(${JSON.stringify([...HTML_BOOLEAN_ATTRIBUTES])});
|
|
const attributes: string[] = [];
|
|
|
|
for (const [name, raw] of Object.entries(value)) {
|
|
const lowerName = name.toLowerCase();
|
|
if (
|
|
!/^[A-Za-z_:][A-Za-z0-9_.:-]*$/.test(name) ||
|
|
lowerName.startsWith("on") ||
|
|
lowerName === "style" ||
|
|
lowerName === "slot" ||
|
|
lowerName === "data-component" ||
|
|
lowerName.startsWith("data-wrn")
|
|
) {
|
|
continue;
|
|
}
|
|
|
|
if (booleanAttributes.has(lowerName)) {
|
|
attributes.push(__wireBooleanAttr(name, raw));
|
|
continue;
|
|
}
|
|
|
|
if (raw === false || raw === null || raw === undefined) continue;
|
|
attributes.push(" " + name + '="' + __wireAttr(raw) + '"');
|
|
}
|
|
|
|
return attributes.join("");
|
|
}
|
|
|
|
function __wireProp(v: any): string {
|
|
const value =
|
|
v !== null && typeof v === "object"
|
|
? JSON.stringify(v)
|
|
: String(v == null ? "" : v);
|
|
|
|
return __wireAttr(value);
|
|
}
|
|
|
|
function __wireRaw(v: any): string {
|
|
return String(v == null ? "" : v);
|
|
}`);
|
|
if (hasServerEach) {
|
|
out.push(`function __wrnexusEncodeLoopLocals(value: Record<string, any>): string {
|
|
return __WrnexusBuffer.from(JSON.stringify(value), "utf8").toString("base64");
|
|
}`);
|
|
}
|
|
if (needsScope) {
|
|
out.push(`function __wrnexusSerializeScopeValue(value: any): string {
|
|
if (value === undefined) {
|
|
return "undefined";
|
|
}
|
|
|
|
if (value === null) {
|
|
return "null";
|
|
}
|
|
|
|
if (typeof value === "number") {
|
|
return Number.isFinite(value)
|
|
? String(value)
|
|
: "null";
|
|
}
|
|
|
|
if (typeof value === "boolean") {
|
|
return value ? "true" : "false";
|
|
}
|
|
|
|
if (typeof value === "string") {
|
|
return JSON.stringify(value);
|
|
}
|
|
|
|
try {
|
|
const serialized = JSON.stringify(value);
|
|
|
|
return serialized === undefined
|
|
? "undefined"
|
|
: serialized;
|
|
} catch {
|
|
return "null";
|
|
}
|
|
}
|
|
|
|
function __wrnexusScopeDecl(obj: Record<string, any>): string {
|
|
return Object.keys(obj)
|
|
.map(
|
|
(key) =>
|
|
key +
|
|
": " +
|
|
__wrnexusSerializeScopeValue(
|
|
obj[key],
|
|
),
|
|
)
|
|
.join(", ")
|
|
.replace(/&/g, "&")
|
|
.replace(/"/g, """)
|
|
.replace(/</g, "<")
|
|
.replace(/>/g, ">");
|
|
}`);
|
|
}
|
|
const serverFunctionSource = serverFunctions ? `${serverFunctions}
|
|
` : "";
|
|
out.push(
|
|
`export function render(props: ${effectiveProps.length > 0 ? `${ast.name}Props` : "Record<string, any>"} = {} as ${effectiveProps.length > 0 ? `${ast.name}Props` : "Record<string, any>"}): string {
|
|
const __p = props || {};
|
|
` + (decls.length > 0 ? decls.join("\n") + "\n" : "") + serverFunctionSource + scopeLine + ` return ${returnExpr};
|
|
}`
|
|
);
|
|
out.push(
|
|
`export default { name: ${JSON.stringify(ast.name)}, kind: ${JSON.stringify(ast.kind)}, render };`
|
|
);
|
|
return out.join("\n\n") + "\n";
|
|
}
|
|
function wholeAttributeExpression(value) {
|
|
const match = /^\s*\{([\s\S]+)\}\s*$/.exec(value);
|
|
return match?.[1]?.trim() || null;
|
|
}
|
|
function renderPageComponentAttr(attr, dynamicExpressions) {
|
|
if (attr.event) {
|
|
return ` ${eventAttribute(attr.name)}="${attrEscape(attr.value)}"`;
|
|
}
|
|
if (attr.boolean) {
|
|
return ` ${attr.name}`;
|
|
}
|
|
const expression2 = wholeAttributeExpression(attr.value);
|
|
if (!expression2) {
|
|
return ` ${attr.name}="${attrEscape(safeAttributeValue(attr.name, attr.value))}"`;
|
|
}
|
|
dynamicExpressions.push(`\${__wrnexusPropAttr(${expression2})}`);
|
|
const marker = `\0WRNEACH${dynamicExpressions.length - 1}\0`;
|
|
return ` ${attr.name}="${marker}"`;
|
|
}
|
|
|
|
// packages/compiler/src/native-codegen.ts
|
|
var NativeCompileError = class extends Error {
|
|
constructor(message) {
|
|
super(message);
|
|
this.name = "NativeCompileError";
|
|
}
|
|
};
|
|
var tagMap = {
|
|
div: "View",
|
|
main: "View",
|
|
section: "View",
|
|
article: "View",
|
|
nav: "View",
|
|
header: "View",
|
|
footer: "View",
|
|
aside: "View",
|
|
form: "View",
|
|
ul: "View",
|
|
ol: "View",
|
|
li: "View",
|
|
p: "Text",
|
|
span: "Text",
|
|
strong: "Text",
|
|
em: "Text",
|
|
small: "Text",
|
|
label: "Text",
|
|
h1: "Text",
|
|
h2: "Text",
|
|
h3: "Text",
|
|
h4: "Text",
|
|
h5: "Text",
|
|
h6: "Text",
|
|
button: "Pressable",
|
|
a: "Pressable",
|
|
input: "TextInput",
|
|
textarea: "TextInput",
|
|
img: "Image",
|
|
view: "View",
|
|
text: "Text",
|
|
pressable: "Pressable",
|
|
textinput: "TextInput",
|
|
image: "Image",
|
|
scrollview: "ScrollView",
|
|
safeareaview: "SafeAreaView",
|
|
flatlist: "FlatList",
|
|
activityindicator: "ActivityIndicator"
|
|
};
|
|
var attrMap = {
|
|
class: "style",
|
|
className: "style",
|
|
src: "source",
|
|
alt: "accessibilityLabel",
|
|
placeholder: "placeholder",
|
|
disabled: "disabled",
|
|
value: "value",
|
|
href: "__href",
|
|
"aria-label": "accessibilityLabel"
|
|
};
|
|
function expression(value) {
|
|
const exact = /^\{([\s\S]+)\}$/.exec(value.trim());
|
|
return exact?.[1]?.trim() ?? null;
|
|
}
|
|
function textJsx(value) {
|
|
const pieces = [];
|
|
let last = 0;
|
|
for (const match of value.matchAll(/\{([^{}]+)\}/g)) {
|
|
if (match.index > last) pieces.push(value.slice(last, match.index));
|
|
const expr = match[1].trim();
|
|
pieces.push(expr.startsWith("t:") ? `{${JSON.stringify(expr.slice(2).trim())}}` : `{${expr}}`);
|
|
last = match.index + match[0].length;
|
|
}
|
|
pieces.push(value.slice(last));
|
|
return pieces.join("").replace(/([<>])/g, (char) => char === "<" ? "<" : ">");
|
|
}
|
|
function eventBody(value, states) {
|
|
let body = expression(value) ?? value;
|
|
for (const state of states) {
|
|
const cap = state[0].toUpperCase() + state.slice(1);
|
|
body = body.replace(new RegExp(`\\b${state}\\+\\+`, "g"), `set${cap}(value => value + 1)`).replace(new RegExp(`\\b${state}--`, "g"), `set${cap}(value => value - 1)`).replace(new RegExp(`\\b${state}\\s*=\\s*([^;]+)`, "g"), `set${cap}($1)`);
|
|
}
|
|
return `() => { ${body} }`;
|
|
}
|
|
function renderAttrs2(attrs, states) {
|
|
return attrs.map((attr) => {
|
|
if (attr.event) {
|
|
if (attr.name.startsWith("browser-")) return "";
|
|
const eventName = attr.name.startsWith("mobile-") ? attr.name.slice(7) : attr.name;
|
|
const event = eventName === "click" || eventName === "press" ? "onPress" : eventName === "input" || eventName === "change" ? "onChangeText" : `on${eventName[0].toUpperCase()}${eventName.slice(1)}`;
|
|
return ` ${event}={${eventBody(attr.value, states)}}`;
|
|
}
|
|
if (attr.name === "data-native-browser" || attr.name.startsWith("data-native-on-browser-"))
|
|
return "";
|
|
if (attr.name === "data-native-options" || attr.name === "data-native-only" || attr.name === "data-native-requires" || attr.name === "data-native-unsupported")
|
|
return "";
|
|
if (attr.name === "data-native-mobile") {
|
|
throw new NativeCompileError(
|
|
`Declarative native capability "${attr.value}" currently targets browser/Capacitor pages. In Expo output, call the installed Expo package from an @mobile-event handler.`
|
|
);
|
|
}
|
|
const name = attrMap[attr.name] ?? attr.name;
|
|
if (name === "__href") return ` onPress={() => router.push(${JSON.stringify(attr.value)})}`;
|
|
if (name === "source") {
|
|
const expr2 = expression(attr.value);
|
|
return ` source={${expr2 ? `{ uri: ${expr2} }` : `{ uri: ${JSON.stringify(attr.value)} }`}}`;
|
|
}
|
|
if (name === "style" && attr.name !== "style") {
|
|
return ` style={[${attr.value.split(/\s+/).filter(Boolean).map((value) => `styles[${JSON.stringify(value)}]`).join(", ")} ]}`;
|
|
}
|
|
if (name === "style") {
|
|
const inlineExpression = expression(attr.value);
|
|
if (inlineExpression) return ` style={${inlineExpression}}`;
|
|
throw new NativeCompileError(
|
|
'Inline CSS strings are not portable to native; use class="name" and a page style block'
|
|
);
|
|
}
|
|
if (attr.boolean) return ` ${name}`;
|
|
const expr = expression(attr.value);
|
|
return expr ? ` ${name}={${expr}}` : ` ${name}=${JSON.stringify(attr.value)}`;
|
|
}).join("");
|
|
}
|
|
function renderNode2(node, states, key) {
|
|
if (node.type === "text") return textJsx(node.value);
|
|
if (node.type === "each") {
|
|
const params2 = node.index ? `${node.item}, ${node.index}` : `${node.item}, __index`;
|
|
const body = node.body.map(
|
|
(child, index) => renderNode2(child, states, index === 0 ? node.index ?? "__index" : void 0)
|
|
).join("");
|
|
const empty = node.empty.map((child) => renderNode2(child, states)).join("");
|
|
return `{(${node.list})?.length ? (${node.list}).map((${params2}) => <>${body}</>) : <>${empty}</>}`;
|
|
}
|
|
if (node.type === "if") {
|
|
const result = node.branches.reduceRight(
|
|
(fallback, branch) => branch.cond === null ? `<>${branch.body.map((child) => renderNode2(child, states)).join("")}</>` : `(${branch.cond}) ? <>${branch.body.map((child) => renderNode2(child, states)).join("")}</> : ${fallback}`,
|
|
"null"
|
|
);
|
|
return `{${result}}`;
|
|
}
|
|
const nativeOnly = node.attrs.find(
|
|
(attr) => !attr.event && attr.name === "data-native-only"
|
|
)?.value;
|
|
if (nativeOnly === "browser" || nativeOnly === "web") return "";
|
|
const nativeTag = tagMap[node.tag.toLowerCase()] ?? (/^[A-Z]/.test(node.tag) ? node.tag : void 0);
|
|
if (!nativeTag)
|
|
throw new NativeCompileError(`HTML element <${node.tag}> has no native equivalent`);
|
|
const attrs = renderAttrs2(node.attrs, states) + (key ? ` key={${key}}` : "");
|
|
if (nativeTag === "TextInput" || nativeTag === "Image" || nativeTag === "ActivityIndicator")
|
|
return `<${nativeTag}${attrs} />`;
|
|
const children = node.children.map((child) => {
|
|
if (child.type !== "text") return renderNode2(child, states);
|
|
if (!child.value.trim()) return "";
|
|
const text = textJsx(child.value);
|
|
return nativeTag === "Text" ? text : `<Text>${text}</Text>`;
|
|
}).join("");
|
|
return `<${nativeTag}${attrs}>${children}</${nativeTag}>`;
|
|
}
|
|
function nativeStyles(blocks) {
|
|
const entries = [];
|
|
for (const block of blocks) {
|
|
for (const match of block.matchAll(/\.([A-Za-z_][\w-]*)\s*\{([^}]*)\}/g)) {
|
|
const props = [];
|
|
for (const declaration of match[2].split(";")) {
|
|
const colon = declaration.indexOf(":");
|
|
if (colon < 0) continue;
|
|
const name = declaration.slice(0, colon).trim().replace(/-([a-z])/g, (_, c) => c.toUpperCase());
|
|
let value = declaration.slice(colon + 1).trim();
|
|
if (/^-?\d+(?:\.\d+)?px$/.test(value)) value = Number(value.slice(0, -2));
|
|
props.push(
|
|
`${JSON.stringify(name)}: ${typeof value === "number" ? value : JSON.stringify(value)}`
|
|
);
|
|
}
|
|
entries.push(`${JSON.stringify(match[1])}: { ${props.join(", ")} }`);
|
|
}
|
|
}
|
|
return `const styles = StyleSheet.create({ ${entries.join(",\n")} });`;
|
|
}
|
|
function generateNative(ast) {
|
|
if (ast.kind !== "page")
|
|
throw new NativeCompileError("Native route compilation currently accepts page files only");
|
|
if (ast.dataApis.length)
|
|
throw new NativeCompileError(
|
|
"Data API blocks are not yet portable to native screens; fetch through the generated native backend helper"
|
|
);
|
|
const states = new Set(ast.states.map((state) => state.name));
|
|
const hooks = ast.states.map((state) => {
|
|
const cap = state.name[0].toUpperCase() + state.name.slice(1);
|
|
return ` const [${state.name}, set${cap}] = useState${state.valueType ? `<${state.valueType}>` : ""}(${state.expr});`;
|
|
}).join("\n");
|
|
const body = ast.view.map((node) => renderNode2(node, states)).join("");
|
|
const typeSource = ast.types.map((block) => block.trim()).filter(Boolean).join("\n\n");
|
|
return `// generated from .wrn for Expo/React Native
|
|
import React, { useState } from "react";
|
|
import { ActivityIndicator, FlatList, Image, Pressable, SafeAreaView, ScrollView, StyleSheet, Text, TextInput, View } from "react-native";
|
|
import { useRouter } from "expo-router";
|
|
${ast.imports.join("\n")}
|
|
|
|
${typeSource}
|
|
|
|
export default function ${ast.name}() {
|
|
const router = useRouter();
|
|
${hooks}
|
|
return <>${body}</>;
|
|
}
|
|
|
|
${nativeStyles(ast.styles)}
|
|
`;
|
|
}
|
|
|
|
// packages/compiler/src/index.ts
|
|
import {
|
|
assertValidAst as assertValidAst2,
|
|
diagnose as diagnose3,
|
|
diagnosticFromError as diagnosticFromError2,
|
|
formatDiagnostic as formatDiagnostic2,
|
|
parse as parse3,
|
|
ParseError as ParseError3
|
|
} from "@wrnexus/syntax";
|
|
|
|
// packages/compiler/src/component-contract.ts
|
|
function unionOptions(type) {
|
|
if (!type || !type.includes("|")) return void 0;
|
|
const values = type.split("|").map((part) => part.trim()).filter((part) => /^(?:"[^"]*"|'[^']*')$/.test(part)).map((part) => part.slice(1, -1));
|
|
return values.length ? values : void 0;
|
|
}
|
|
function createComponentContract(ast) {
|
|
return {
|
|
name: ast.name,
|
|
kind: ast.kind,
|
|
props: ast.props.map((prop) => ({
|
|
name: prop.name,
|
|
type: prop.valueType ?? "unknown",
|
|
required: prop.required,
|
|
...prop.default !== "undefined" ? { default: prop.default } : {},
|
|
...unionOptions(prop.valueType) ? { options: unionOptions(prop.valueType) } : {}
|
|
})),
|
|
outputs: ast.outputs.map((output) => ({
|
|
name: output.name,
|
|
...output.payload ? { payloadName: output.payload.name, payloadType: output.payload.valueType } : {}
|
|
})),
|
|
functions: ast.runtimeFunctions.map((fn) => ({
|
|
name: fn.name,
|
|
runtime: fn.runtime,
|
|
async: fn.async,
|
|
parameters: fn.parameters.map((param) => ({
|
|
name: param.name,
|
|
type: param.valueType ?? "unknown",
|
|
optional: param.optional
|
|
})),
|
|
returnType: fn.returnType ?? (fn.async ? "Promise<unknown>" : "unknown")
|
|
})),
|
|
states: ast.states.map((state) => ({
|
|
name: state.name,
|
|
runtime: state.runtime,
|
|
type: state.valueType ?? "unknown",
|
|
initializer: state.expr
|
|
})),
|
|
computed: ast.computed.map((entry) => ({
|
|
name: entry.name,
|
|
type: entry.valueType ?? "unknown",
|
|
expression: entry.expr
|
|
})),
|
|
imports: ast.structuredImports.map((entry) => ({
|
|
source: entry.source,
|
|
typeOnly: entry.typeOnly,
|
|
...entry.defaultImport ? { defaultImport: entry.defaultImport } : {},
|
|
namedImports: entry.namedImports.map((named) => named.local)
|
|
}))
|
|
};
|
|
}
|
|
|
|
// packages/compiler/src/client-codegen.ts
|
|
import { eraseFunctionTypes as eraseFunctionTypes3 } from "@wrnexus/syntax";
|
|
var RESERVED_BINDINGS2 = /* @__PURE__ */ new Set([
|
|
"await",
|
|
"break",
|
|
"case",
|
|
"catch",
|
|
"class",
|
|
"const",
|
|
"continue",
|
|
"debugger",
|
|
"default",
|
|
"delete",
|
|
"do",
|
|
"else",
|
|
"enum",
|
|
"export",
|
|
"extends",
|
|
"false",
|
|
"finally",
|
|
"for",
|
|
"function",
|
|
"if",
|
|
"implements",
|
|
"import",
|
|
"in",
|
|
"instanceof",
|
|
"interface",
|
|
"let",
|
|
"new",
|
|
"null",
|
|
"package",
|
|
"private",
|
|
"protected",
|
|
"public",
|
|
"return",
|
|
"static",
|
|
"super",
|
|
"switch",
|
|
"this",
|
|
"throw",
|
|
"true",
|
|
"try",
|
|
"typeof",
|
|
"var",
|
|
"void",
|
|
"while",
|
|
"with",
|
|
"yield"
|
|
]);
|
|
var RUNTIME_BINDINGS = /* @__PURE__ */ new Set([
|
|
"context",
|
|
"state",
|
|
"output",
|
|
"server",
|
|
"props",
|
|
"refs",
|
|
"event",
|
|
"payload"
|
|
]);
|
|
function safeIdentifier(name) {
|
|
return /^[A-Za-z_$][A-Za-z0-9_$]*$/.test(name) && !RESERVED_BINDINGS2.has(name);
|
|
}
|
|
function functionEntry(ast, fn, availableFunctions) {
|
|
const parameterNames = new Set(fn.parameters.map((parameter) => parameter.name));
|
|
const stateNames = ast.states.filter(
|
|
(state) => state.runtime !== "server" && safeIdentifier(state.name) && !RUNTIME_BINDINGS.has(state.name) && !parameterNames.has(state.name)
|
|
).map((state) => state.name);
|
|
const stateSet = new Set(stateNames);
|
|
const propNames = ast.props.filter(
|
|
(prop) => safeIdentifier(prop.name) && !RUNTIME_BINDINGS.has(prop.name) && !parameterNames.has(prop.name) && !stateSet.has(prop.name)
|
|
).map((prop) => prop.name);
|
|
const functionAliases = availableFunctions.filter(
|
|
(name) => safeIdentifier(name) && !RUNTIME_BINDINGS.has(name) && !parameterNames.has(name) && !stateSet.has(name) && !propNames.includes(name)
|
|
);
|
|
const parameters = fn.parameters.map((parameter) => parameter.name).join(", ");
|
|
const initialStateSnapshot = stateNames.length ? `const __wrnexusInitialState = { ${stateNames.map((name) => `${JSON.stringify(name)}: context.state.${name}`).join(", ")} };` : "";
|
|
const stateAliases = stateNames.length ? `let { ${stateNames.join(", ")} } = context.state;` : "";
|
|
const propAliases = propNames.length ? `const { ${propNames.join(", ")} } = context.props;` : "";
|
|
const syncStateToContext = stateNames.map((name) => `context.state.${name} = ${name};`).join(" ");
|
|
const syncStateFromContext = stateNames.map((name) => `${name} = context.state.${name};`).join(" ");
|
|
const peerAliases = functionAliases.map((name) => {
|
|
const call = `context.functions[${JSON.stringify(name)}](...__wrnexusPeerArgs)`;
|
|
if (!stateNames.length) {
|
|
return `const ${name} = (...__wrnexusPeerArgs) => ${call};`;
|
|
}
|
|
return `const ${name} = (...__wrnexusPeerArgs) => {
|
|
${syncStateToContext}
|
|
let __wrnexusPeerResult;
|
|
try {
|
|
__wrnexusPeerResult = ${call};
|
|
} catch (__wrnexusPeerError) {
|
|
${syncStateFromContext}
|
|
throw __wrnexusPeerError;
|
|
}
|
|
if (__wrnexusPeerResult && typeof __wrnexusPeerResult.then === "function") {
|
|
return Promise.resolve(__wrnexusPeerResult).finally(() => { ${syncStateFromContext} });
|
|
}
|
|
${syncStateFromContext}
|
|
return __wrnexusPeerResult;
|
|
};`;
|
|
}).join("\n");
|
|
const copyBack = stateNames.map(
|
|
(name) => `if (!Object.is(${name}, __wrnexusInitialState[${JSON.stringify(name)}])) context.state.${name} = ${name};`
|
|
).join("\n");
|
|
const body = eraseFunctionTypes3(fn.body);
|
|
const runtimeBindings = [
|
|
!parameterNames.has("output") ? "const output = context.output;" : "",
|
|
!parameterNames.has("server") ? "const server = context.server;" : "",
|
|
!parameterNames.has("props") ? "const props = context.props;" : "",
|
|
!parameterNames.has("refs") ? "const refs = context.refs;" : ""
|
|
].filter(Boolean).join("\n ");
|
|
return `${JSON.stringify(fn.name)}: ${fn.async ? "async " : ""}function(context${parameters ? `, ${parameters}` : ""}) {
|
|
${initialStateSnapshot}
|
|
${stateAliases}
|
|
${propAliases}
|
|
${peerAliases}
|
|
${runtimeBindings}
|
|
try {
|
|
${body}
|
|
} finally {
|
|
${copyBack}
|
|
}
|
|
}`;
|
|
}
|
|
function generateBrowserModule(ast) {
|
|
const functions = ast.runtimeFunctions.filter(
|
|
(fn) => ["legacy", "client", "shared"].includes(fn.runtime)
|
|
);
|
|
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);
|
|
return `// generated WRNexusJS browser module for ${ast.name}
|
|
${imports}
|
|
export const __wrnexusClientFunctions = {
|
|
${functions.map((fn) => ` ${functionEntry(ast, fn, functionNames)}`).join(",\n")}
|
|
};
|
|
export const __wrnexusClientState = ${JSON.stringify(state)};
|
|
export const __wrnexusOutputs = ${JSON.stringify(ast.outputs)};
|
|
export const __wrnexusImportedBindings = { ${importedBindings.join(", ")} };
|
|
export function bindClientScope(context) {
|
|
const functions = {};
|
|
const scopedContext = { ...context, functions };
|
|
for (const [name, handler] of Object.entries(__wrnexusClientFunctions)) {
|
|
functions[name] = (...args) => handler(scopedContext, ...args);
|
|
}
|
|
return functions;
|
|
}
|
|
`;
|
|
}
|
|
|
|
// packages/compiler/src/targets.ts
|
|
function generateTargets(ast) {
|
|
return {
|
|
server: ast.kind === "global-store" || ast.kind === "page-store" ? generateStoreModule(ast) : generateServerFunctionsModule(ast),
|
|
browser: ast.kind === "global-store" || ast.kind === "page-store" ? generateStoreBrowserModule(ast) : generateBrowserModule(ast),
|
|
declarations: generateDeclarations(ast),
|
|
contract: createComponentContract(ast),
|
|
rpc: rpcManifest(ast)
|
|
};
|
|
}
|
|
|
|
// packages/compiler/src/import-resolver.ts
|
|
import { existsSync, realpathSync } from "fs";
|
|
import { dirname, extname, join, resolve } from "path";
|
|
function candidates(path) {
|
|
return extname(path) ? [path] : [
|
|
path,
|
|
`${path}.wrn`,
|
|
`${path}.ts`,
|
|
`${path}.d.ts`,
|
|
join(path, "index.wrn"),
|
|
join(path, "index.ts")
|
|
];
|
|
}
|
|
function resolveWrnImport(declaration, importer, options) {
|
|
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)) : resolve(dirname(importer), source);
|
|
const found = candidates(base).find(existsSync);
|
|
if (found) return { declaration, resolved: realpathSync(found) };
|
|
const severity = (options.mode ?? "compatible") === "explicit" ? "error" : "warning";
|
|
return {
|
|
declaration,
|
|
diagnostic: {
|
|
code: "WRN-IMPORT-NOT-FOUND",
|
|
message: `Cannot resolve import '${source}' from ${importer}`,
|
|
severity
|
|
}
|
|
};
|
|
}
|
|
function resolveWrnImports(declarations, importer, options) {
|
|
return declarations.map((declaration) => resolveWrnImport(declaration, importer, options));
|
|
}
|
|
|
|
// packages/compiler/src/source-map.ts
|
|
function createWrnSourceMap(source, generated) {
|
|
const sourceLines = source.split(/\r?\n/).length;
|
|
const generatedLines = generated.split(/\r?\n/).length;
|
|
const mappings = Array.from({ length: Math.min(sourceLines, generatedLines) }, (_, index) => ({
|
|
generatedLine: index + 1,
|
|
sourceLine: index + 1,
|
|
sourceColumn: 1,
|
|
kind: "line"
|
|
}));
|
|
return { version: 1, source, generated, mappings };
|
|
}
|
|
|
|
// packages/compiler/src/runtime-capabilities.ts
|
|
var CAPABILITIES = {
|
|
bun: /* @__PURE__ */ new Set([
|
|
"filesystem",
|
|
"tcp",
|
|
"process",
|
|
"websocket",
|
|
"crypto",
|
|
"streams",
|
|
"background-tasks"
|
|
]),
|
|
node: /* @__PURE__ */ new Set([
|
|
"filesystem",
|
|
"tcp",
|
|
"process",
|
|
"websocket",
|
|
"crypto",
|
|
"streams",
|
|
"background-tasks"
|
|
]),
|
|
edge: /* @__PURE__ */ new Set(["websocket", "crypto", "streams", "background-tasks"]),
|
|
worker: /* @__PURE__ */ new Set(["websocket", "crypto", "streams", "background-tasks"]),
|
|
"service-worker": /* @__PURE__ */ new Set(["crypto", "streams", "background-tasks"]),
|
|
browser: /* @__PURE__ */ new Set(["websocket", "crypto", "streams"])
|
|
};
|
|
var MODULE_CAPABILITIES = [
|
|
[/^(?:node:)?(?:fs|path|os)(?:\/|$)/, "filesystem"],
|
|
[/^(?:node:)?(?:net|tls|dgram|http2)(?:\/|$)/, "tcp"],
|
|
[/^(?:node:)?(?:child_process|cluster|worker_threads)(?:\/|$)/, "process"]
|
|
];
|
|
function runtimeCapabilities(runtime) {
|
|
return CAPABILITIES[runtime];
|
|
}
|
|
function analyzeRuntimeImports(source, runtime) {
|
|
const modules = [
|
|
...source.matchAll(/\b(?:import\s+(?:[\s\S]*?\s+from\s+)?|require\s*\()\s*["']([^"']+)["']/g)
|
|
].map((match) => match[1]);
|
|
const available = runtimeCapabilities(runtime);
|
|
return modules.flatMap((module) => {
|
|
const requirement = MODULE_CAPABILITIES.find(([pattern]) => pattern.test(module));
|
|
if (!requirement || available.has(requirement[1])) return [];
|
|
return [
|
|
{
|
|
code: "WRN-RUNTIME-CAPABILITY",
|
|
runtime,
|
|
module,
|
|
capability: requirement[1],
|
|
message: `Module '${module}' requires ${requirement[1]}, which is unavailable in the ${runtime} runtime.`
|
|
}
|
|
];
|
|
});
|
|
}
|
|
|
|
// packages/compiler/src/index.ts
|
|
import { Lexer, LexError } from "@wrnexus/syntax";
|
|
import { eraseFunctionTypes as eraseFunctionTypes4, inferredRuntimeType, runtimeTypeOf as runtimeTypeOf2 } from "@wrnexus/syntax";
|
|
|
|
// packages/compiler/src/cache.ts
|
|
import { createHash } from "crypto";
|
|
import { diagnose, parse, ParseError } from "@wrnexus/syntax";
|
|
function compileSource(source, filePath) {
|
|
const richDiagnostics = diagnose(source, { file: filePath, accessibility: true });
|
|
const errors = richDiagnostics.filter((diagnostic) => diagnostic.severity === "error");
|
|
if (errors.length > 0) {
|
|
throw new ParseError(
|
|
errors.map((diagnostic) => diagnostic.message).join("\n"),
|
|
errors[0].code
|
|
);
|
|
}
|
|
const ast = parse(source);
|
|
return {
|
|
code: `// compiled from .wrn
|
|
${generate(ast)}`,
|
|
ast,
|
|
diagnostics: richDiagnostics.map((diagnostic) => `${diagnostic.code}: ${diagnostic.message}`),
|
|
richDiagnostics
|
|
};
|
|
}
|
|
function compilationKey(source, file = "<inline .wrn>", salt = "") {
|
|
return createHash("sha256").update(file).update("\0").update(salt).update("\0").update(source).digest("hex");
|
|
}
|
|
function createCompilationCache(options = {}) {
|
|
const maxEntries = options.maxEntries ?? 500;
|
|
if (!Number.isInteger(maxEntries) || maxEntries < 1)
|
|
throw new RangeError("maxEntries must be positive");
|
|
const now = options.now ?? Date.now;
|
|
const entries = /* @__PURE__ */ new Map();
|
|
let hits = 0;
|
|
let misses = 0;
|
|
function touch(key, value) {
|
|
entries.delete(key);
|
|
entries.set(key, value);
|
|
while (entries.size > maxEntries) entries.delete(entries.keys().next().value);
|
|
}
|
|
return {
|
|
compile(source, file = "<inline .wrn>", salt = "") {
|
|
const key = compilationKey(source, file, salt);
|
|
const existing = entries.get(key);
|
|
if (existing) {
|
|
hits++;
|
|
touch(key, existing);
|
|
return existing;
|
|
}
|
|
misses++;
|
|
const result = compileSource(source, file);
|
|
const entry = {
|
|
...result,
|
|
key,
|
|
file,
|
|
sourceHash: createHash("sha256").update(source).digest("hex"),
|
|
createdAt: now()
|
|
};
|
|
touch(key, entry);
|
|
return entry;
|
|
},
|
|
get(key) {
|
|
const entry = entries.get(key);
|
|
if (entry) touch(key, entry);
|
|
return entry;
|
|
},
|
|
invalidate(file) {
|
|
let removed = 0;
|
|
for (const [key, entry] of entries) {
|
|
if (!file || entry.file === file) {
|
|
entries.delete(key);
|
|
removed++;
|
|
}
|
|
}
|
|
return removed;
|
|
},
|
|
clear() {
|
|
entries.clear();
|
|
},
|
|
size: () => entries.size,
|
|
stats: () => ({ hits, misses, entries: entries.size })
|
|
};
|
|
}
|
|
var DependencyGraph = class {
|
|
#dependencies = /* @__PURE__ */ new Map();
|
|
#dependents = /* @__PURE__ */ new Map();
|
|
set(file, dependencies) {
|
|
this.remove(file);
|
|
const values = new Set(dependencies);
|
|
this.#dependencies.set(file, values);
|
|
for (const dependency of values) {
|
|
const set = this.#dependents.get(dependency) ?? /* @__PURE__ */ new Set();
|
|
set.add(file);
|
|
this.#dependents.set(dependency, set);
|
|
}
|
|
}
|
|
remove(file) {
|
|
for (const dependency of this.#dependencies.get(file) ?? []) {
|
|
const set = this.#dependents.get(dependency);
|
|
set?.delete(file);
|
|
if (set?.size === 0) this.#dependents.delete(dependency);
|
|
}
|
|
this.#dependencies.delete(file);
|
|
}
|
|
dependencies(file) {
|
|
return [...this.#dependencies.get(file) ?? []].sort();
|
|
}
|
|
dependents(file) {
|
|
return [...this.#dependents.get(file) ?? []].sort();
|
|
}
|
|
affected(file) {
|
|
const found = /* @__PURE__ */ new Set();
|
|
const queue = [file];
|
|
while (queue.length) {
|
|
const current = queue.shift();
|
|
for (const dependent of this.#dependents.get(current) ?? []) {
|
|
if (found.has(dependent)) continue;
|
|
found.add(dependent);
|
|
queue.push(dependent);
|
|
}
|
|
}
|
|
return [...found].sort();
|
|
}
|
|
};
|
|
|
|
// packages/compiler/src/index.ts
|
|
function compileNativeWireFile(source) {
|
|
const ast = parse2(source);
|
|
assertValidAst(ast);
|
|
return generateNative(ast);
|
|
}
|
|
function compileWireFile(source, filePath = "<inline .wrn>") {
|
|
try {
|
|
const ast = parse2(source);
|
|
assertValidAst(ast, { file: filePath, accessibility: true });
|
|
return `// compiled from .wrn
|
|
${generate(ast)}`;
|
|
} catch (error) {
|
|
const diagnostic = diagnosticFromError(source, error, { file: filePath });
|
|
throw new Error(`Failed to parse ${filePath}:
|
|
|
|
${formatDiagnostic(source, diagnostic)}`, {
|
|
cause: error
|
|
});
|
|
}
|
|
}
|
|
function compile(source, filePath = "<inline .wrn>") {
|
|
const richDiagnostics = diagnose2(source, { file: filePath, accessibility: true });
|
|
const errors = richDiagnostics.filter((diagnostic) => diagnostic.severity === "error");
|
|
if (errors.length > 0) {
|
|
throw new ParseError2(
|
|
errors.map((diagnostic) => diagnostic.message).join("\n"),
|
|
errors[0].code
|
|
);
|
|
}
|
|
const ast = parse2(source);
|
|
return {
|
|
code: `// compiled from .wrn
|
|
${generate(ast)}`,
|
|
ast,
|
|
diagnostics: richDiagnostics.map((diagnostic) => `${diagnostic.code}: ${diagnostic.message}`),
|
|
richDiagnostics
|
|
};
|
|
}
|
|
export {
|
|
DependencyGraph,
|
|
LexError,
|
|
Lexer,
|
|
NativeCompileError,
|
|
ParseError3 as ParseError,
|
|
analyzeOptimizations,
|
|
analyzeRuntimeImports,
|
|
analyzeRuntimeRequirements,
|
|
assertValidAst2 as assertValidAst,
|
|
compilationKey,
|
|
compile,
|
|
compileNativeWireFile,
|
|
compileWireFile,
|
|
createCompilationCache,
|
|
createComponentContract,
|
|
createWrnSourceMap,
|
|
diagnose3 as diagnose,
|
|
diagnosticFromError2 as diagnosticFromError,
|
|
eraseFunctionTypes4 as eraseFunctionTypes,
|
|
formatDiagnostic2 as formatDiagnostic,
|
|
formatWrn,
|
|
generate,
|
|
generateBrowserModule,
|
|
generateDeclarations,
|
|
generateNative,
|
|
generateServerFunctionsModule,
|
|
generateStoreBrowserModule,
|
|
generateStoreModule,
|
|
generateTargets,
|
|
inferredRuntimeType,
|
|
optimizeAst,
|
|
parse3 as parse,
|
|
resolveWrnImport,
|
|
resolveWrnImports,
|
|
rpcManifest,
|
|
runtimeCapabilities,
|
|
runtimeTypeOf2 as runtimeTypeOf
|
|
};
|