Codegen hooks let libraries register custom JavaScript emitters for namespace-qualified function calls. Instead of emitting generic runtime calls, hooks produce idiomatic JS tailored to the library's semantics.
import { compile } from '@clojurewasm/kiso';
import type { CodegenHook } from '@clojurewasm/kiso/codegen';
const myHook: CodegenHook = (args, helpers) => {
const name = helpers.emit(args[0]!);
return `myLib.create(${name})`;
};
const result = compile(source, {
codegenHooks: {
'my.ns/create-thing': myHook,
},
});When the compiler encounters (my.ns/create-thing "foo"), it calls myHook
instead of emitting my.ns.create_thing("foo").
// Imported from '@clojurewasm/kiso/codegen'
type CodegenHook = (args: Node[], helpers: CodegenHelpers) => string;
type CodegenHelpers = {
emit: (node: Node) => string; // Recursively emit a node as JS
indent: string; // Current indentation string
deeper: () => CodegenHelpers; // Return helpers with +1 indent level
extractLiteral: (node: Node) => unknown | null; // Extract static value or null
nsRef: (ns: string) => string; // Resolve namespace to JS import alias
};Receives the invocation's argument nodes (not the function node) and helper utilities. Returns a JavaScript expression string.
| Method | Description |
|---|---|
emit(node) |
Emit any AST node to JS. Use for dynamic sub-expressions. |
indent |
Current indentation (e.g. " " at depth 1). |
deeper() |
Returns new helpers at indent+1. Use for multi-line output. |
extractLiteral |
Returns the static JS value if node is a literal, else null. |
nsRef(ns) |
Resolve a namespace to its JS import alias (auto-imports if needed). |
The analyzed AST node type. Common node types you'll encounter in hook arguments:
| Type | Fields | Example source |
|---|---|---|
literal |
value, jsType |
"hello", 42 |
keyword |
ns, name |
:title, :my/key |
vector |
items: Node[] |
[1 2 3] |
map |
keys: Node[], vals: Node[] |
{:a 1 :b 2} |
var-ref |
ns, name, local |
my-var |
invoke |
fn: Node, args: Node[] |
(f x y) |
fn |
name, arities, variadic |
(fn [x] x) |
Keys are fully-qualified Clojure names: "namespace/function-name".
{
'su.core/define-component': defineComponentHook,
'su.core/create-stylesheet': createStylesheetHook,
'my.lib/special-form': myHook,
}The compiler matches these against var-ref nodes in invoke position.
compile(source, {
filename: 'my-app.cljs',
sourceMap: true,
codegenHooks: { ... },
});import { cljs } from '@clojurewasm/kiso/vite';
import { suCodegenHooks } from '@clojurewasm/su';
export default defineConfig({
plugins: [
cljs({ codegenHooks: suCodegenHooks }),
],
});The @clojurewasm/su package provides hooks for its macros:
import { suCodegenHooks } from '@clojurewasm/su';
// Keys: 'su.core/define-component', 'su.core/create-stylesheet'su.core.define_component(
"my-card",
hashMap(keyword("observedAttrs"), vector("title", "subtitle")),
function (_p0) { ... }
);su.defineComponent("my-card", {
observedAttrs: ["title", "subtitle"]
}, function (_p0) { ... });The hook converts hashMap/keyword config nodes to JS object literals and
vector hiccup to JS array literals where statically possible.
Convert Clojure data literals to JS object/array literals for readability:
const myHook: CodegenHook = (args, helpers) => {
const config = args[0]!;
// If it's a static map, convert keys to JS object
if (config.type === 'map') {
const pairs = config.keys.map((k, i) => {
const key = k.type === 'keyword' ? k.name : helpers.emit(k);
const val = helpers.emit(config.vals[i]!);
return `${key}: ${val}`;
});
return `myLib.init({ ${pairs.join(', ')} })`;
}
// Fallback: emit as-is
return `myLib.init(${helpers.emit(config)})`;
};Use helpers.indent and helpers.deeper() for formatted output:
const myHook: CodegenHook = (args, helpers) => {
const inner = helpers.deeper();
const items = args.map(a => `${inner.indent}${inner.emit(a)}`);
return `myLib.create(\n${items.join(',\n')}\n${helpers.indent})`;
};