import { eraseFunctionTypes, type PageAst, type RuntimeFunctionDecl } from "@wrnexus/syntax"; const RESERVED_BINDINGS = 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", ]); const RUNTIME_BINDINGS = new Set([ "context", "state", "output", "server", "props", "refs", "event", "payload", ]); function safeIdentifier(name: string): boolean { return /^[A-Za-z_$][A-Za-z0-9_$]*$/.test(name) && !RESERVED_BINDINGS.has(name); } function functionEntry( ast: PageAst, fn: RuntimeFunctionDecl, availableFunctions: string[], ): string { 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 = eraseFunctionTypes(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} } }`; } export function generateBrowserModule(ast: PageAst): string { 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; } `; }