import { eraseFunctionTypes, type PageAst, type RuntimeFunctionDecl, type StructuredImportDecl, type ViewNode, } 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 identifierReferenced(source: string, name: string): boolean { const escaped = name.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); return new RegExp(`(?:^|[^A-Za-z0-9_$])${escaped}(?![A-Za-z0-9_$])`).test(source); } function viewReferenceSource(nodes: ViewNode[]): string[] { return nodes.flatMap((node): string[] => { if (node.type === "text") return [node.value]; if (node.type === "each") { return [ node.list, node.key ?? "", ...viewReferenceSource(node.body), ...viewReferenceSource(node.empty), ]; } if (node.type === "if") { return node.branches.flatMap((branch) => [ branch.cond ?? "", ...viewReferenceSource(branch.body), ]); } return [...node.attrs.map((attr) => attr.value), ...viewReferenceSource(node.children)]; }); } function browserReferenceSource(ast: PageAst, functions: RuntimeFunctionDecl[]): string { return [ ...functions.flatMap((fn) => [fn.source, fn.body]), ...ast.computed.map((entry) => entry.expr), ...ast.effects, ast.lifecycle.mount ?? "", ast.lifecycle.update ?? "", ast.lifecycle.unmount ?? "", ...viewReferenceSource(ast.view), ].join("\n"); } function renderSelectedImport( entry: StructuredImportDecl, source: string, ): { code: string; bindings: string[] } | null { if (entry.typeOnly) return null; const isStore = entry.source.endsWith(".wrn") && /(?:^|\/)stores?\//.test(entry.source); if (entry.source.endsWith(".wrn") && !isStore) return null; const defaultImport = entry.defaultImport && (isStore || identifierReferenced(source, entry.defaultImport)) ? entry.defaultImport : undefined; const namespaceImport = entry.namespaceImport && (isStore || identifierReferenced(source, entry.namespaceImport)) ? entry.namespaceImport : undefined; const namedImports = entry.namedImports.filter( (item) => !item.typeOnly && (isStore || identifierReferenced(source, item.local)), ); const bindings = [ ...(defaultImport ? [defaultImport] : []), ...(namespaceImport ? [namespaceImport] : []), ...namedImports.map((item) => item.local), ].filter(safeIdentifier); const hasBindings = Boolean(defaultImport || namespaceImport || namedImports.length); const sideEffectOnly = !entry.defaultImport && !entry.namespaceImport && entry.namedImports.length === 0; if (!hasBindings && !sideEffectOnly) return null; if (sideEffectOnly) return { code: entry.raw, bindings: [] }; const clauses: string[] = []; if (defaultImport) clauses.push(defaultImport); if (namespaceImport) clauses.push(`* as ${namespaceImport}`); if (namedImports.length) { clauses.push( `{ ${namedImports .map((item) => item.imported === item.local ? item.imported : `${item.imported} as ${item.local}`, ) .join(", ")} }`, ); } return { code: `import ${clauses.join(", ")} from ${JSON.stringify(entry.source)};`, bindings, }; } function selectedBrowserImports( ast: PageAst, functions: RuntimeFunctionDecl[], ): { code: string; bindings: string[] }[] { const referenceSource = browserReferenceSource(ast, functions); return ast.structuredImports .map((entry) => renderSelectedImport(entry, referenceSource)) .filter((entry): entry is { code: string; bindings: string[] } => entry !== null); } export function browserModuleRequired(ast: PageAst): boolean { const functions = ast.runtimeFunctions.filter((fn) => ["legacy", "client", "shared"].includes(fn.runtime), ); return functions.length > 0 || selectedBrowserImports(ast, functions).length > 0; } function _functionEntry( ast: PageAst, fn: RuntimeFunctionDecl, availableFunctions: string[], ): string { const parameterNames = new Set(fn.parameters.map((parameter) => parameter.name)); /* * Names the function body declares for itself. * * Props and state are destructured into the SAME scope as the body, so a * body that declares `var size` when `size` is also a prop produced * "Identifier 'size' has already been declared" and the entire module * failed to parse -- taking every function in the component down with it, * with nothing to point at the one line responsible. Skipping the alias for * a shadowed name is also what plain JavaScript does: inside that function * the local wins. */ const declaredLocals = new Set(); for (const match of fn.body.matchAll( /\b(?:var|let|const)\s+([A-Za-z_$][\w$]*)|\bfunction\s+([A-Za-z_$][\w$]*)/g, )) { const name = match[1] ?? match[2]; if (name) declaredLocals.add(name); } const stateNames = ast.states .filter( (state) => state.runtime !== "server" && safeIdentifier(state.name) && !RUNTIME_BINDINGS.has(state.name) && !parameterNames.has(state.name) && !declaredLocals.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) && !declaredLocals.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) && !declaredLocals.has(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 = !stateNames.length ? functionAliases .map( (name) => `const ${name} = (...__wrnexusPeerArgs) => context.functions[${JSON.stringify(name)}](...__wrnexusPeerArgs);`, ) .join("\n") : `const __wrnexusFlush = () => { ${syncStateToContext} }; const __wrnexusRestore = () => { ${syncStateFromContext} }; const __wrnexusPeer = (name, args) => { __wrnexusFlush(); let result; try { result = context.functions[name](...args); } catch (error) { __wrnexusRestore(); throw error; } if (result && typeof result.then === "function") { return Promise.resolve(result).finally(__wrnexusRestore); } __wrnexusRestore(); return result; }; ${functionAliases.map((name) => `const ${name} = (...__wrnexusPeerArgs) => __wrnexusPeer(${JSON.stringify(name)}, __wrnexusPeerArgs);`).join("\n")}`; const copyBack = stateNames .map( (name) => `if (!Object.is(${name}, __wrnexusInitialState[${JSON.stringify(name)}])) context.state.${name} = ${name};`, ) .join("\n"); const commitBinding = stateNames.length ? `const __wrnexusCommit = () => { ${copyBack} }; ${!parameterNames.has("commit") && !declaredLocals.has("commit") && !functionAliases.includes("commit") ? "const commit = __wrnexusCommit;" : ""} ${ !parameterNames.has("setTimeout") && !declaredLocals.has("setTimeout") && !functionAliases.includes("setTimeout") ? `const setTimeout = (callback, delay, ...args) => globalThis.setTimeout(() => { try { return callback(...args); } finally { __wrnexusCommit(); } }, delay);` : "" }` : ""; 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} ${commitBinding} ${peerAliases} ${runtimeBindings} try { ${body} } finally { ${stateNames.length ? "__wrnexusCommit();" : ""} } }`; } 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 selectedImports = selectedBrowserImports(ast, functions); const imports = selectedImports.map((entry) => entry.code).join("\n"); const importedBindings = [...new Set(selectedImports.flatMap((entry) => entry.bindings))]; const sharedState = state.filter((name) => safeIdentifier(name) && !RUNTIME_BINDINGS.has(name)); const sharedProps = ast.props .map((entry) => entry.name) .filter( (name) => safeIdentifier(name) && !RUNTIME_BINDINGS.has(name) && !sharedState.includes(name), ); const callableAliases = functionNames.filter( (name) => safeIdentifier(name) && !RUNTIME_BINDINGS.has(name) && !sharedState.includes(name) && !sharedProps.includes(name), ); const sharedCommit = sharedState.map((name) => `context.state.${name} = ${name};`).join(" "); const sharedRestore = [ ...sharedState.map((name) => `${name} = context.state.${name};`), ...sharedProps.map((name) => `${name} = context.props.${name};`), ].join(" "); const hasAuthoredCommit = callableAliases.includes("commit"); const hasAuthoredSetTimeout = callableAliases.includes("setTimeout"); const implementations = functions .map((fn) => { const parameters = fn.parameters.map((parameter) => parameter.name).join(", "); return `${JSON.stringify(fn.name)}: ${fn.async ? "async " : ""}function(${parameters}) { try { ${eraseFunctionTypes(fn.body)} } finally { __wrnexusCommit(); } }`; }) .join(",\n"); return `// generated WRNexusJS browser module for ${ast.name} ${imports} export const __wrnexusClientFunctions = { ${functions .map( (fn) => ` ${JSON.stringify(fn.name)}: (context, ...args) => __wrnexusBindings(context)[${JSON.stringify(fn.name)}](...args)`, ) .join(",\n")} }; export const __wrnexusClientState = ${JSON.stringify(state)}; export const __wrnexusOutputs = ${JSON.stringify(ast.outputs)}; export const __wrnexusImportedBindings = { ${importedBindings.join(", ")} }; function __wrnexusCreateClientFunctions(context) { ${sharedState.length ? `let { ${sharedState.join(", ")} } = context.state;` : ""} ${sharedProps.length ? `let { ${sharedProps.join(", ")} } = context.props;` : ""} const output = context.output; const server = context.server; const props = context.props; const refs = context.refs; const __wrnexusCommit = () => { ${sharedCommit} }; const __wrnexusRestore = () => { ${sharedRestore} }; ${!hasAuthoredCommit ? "const commit = __wrnexusCommit;" : ""} ${ !hasAuthoredSetTimeout ? `const setTimeout = (callback, delay, ...args) => globalThis.setTimeout(() => { try { return callback(...args); } finally { __wrnexusCommit(); } }, delay);` : "" } const implementations = { ${implementations} }; ${callableAliases.map((name) => `const ${name} = (...args) => implementations[${JSON.stringify(name)}](...args);`).join("\n ")} const functions = {}; for (const name of Object.keys(implementations)) { functions[name] = (...args) => { __wrnexusRestore(); return implementations[name](...args); }; } return functions; } function __wrnexusBindings(context) { return context.__wrnexusBoundFunctions || (context.__wrnexusBoundFunctions = __wrnexusCreateClientFunctions(context)); } export function bindClientScope(context) { return __wrnexusBindings(context); } `; }