JavaScript Macros and Abstract Code Generation
Master JavaScript code generation techniques for compile-time and runtime metaprogramming. Covers AST manipulation, Babel plugin authorship, tagged template literals as macros, code generation pipelines, source-to-source transformation, compile-time evaluation, and safe eval alternatives.
JavaScript lacks native macro support, but compile-time tools (Babel plugins, build-time transforms) and runtime techniques (tagged templates, Function constructor, code generation) provide powerful code generation capabilities.
For the metaprogramming patterns that complement code generation, see JS Metaprogramming Advanced Architecture Guide.
Tagged Template Literals as Macros
Tagged templates are the closest thing JavaScript has to macros at runtime. The tag function receives the static string parts separately from the interpolated values, which means you can process them differently. This is exactly how you build safe SQL queries (values go into a parameter array), auto-escaping HTML templates (user input gets sanitized, trusted markup does not), and scoped CSS class generators.
// Tagged templates transform template literals at call time
// They receive raw strings and interpolated values separately
function sql(strings, ...values) {
// Build parameterized query (prevents SQL injection)
const params = [];
let query = "";
for (let i = 0; i < strings.length; i++) {
query += strings[i];
if (i < values.length) {
params.push(values[i]);
query += `$${params.length}`; // PostgreSQL-style parameter
}
}
return {
text: query.trim(),
values: params,
// For debugging
toString() {
return this.text;
}
};
}
const name = "Alice";
const age = 30;
const query = sql`
SELECT * FROM users
WHERE name = ${name}
AND age > ${age}
ORDER BY created_at DESC
`;
console.log(query.text);
// SELECT * FROM users WHERE name = $1 AND age > $2 ORDER BY created_at DESC
console.log(query.values); // ["Alice", 30]
// HTML TEMPLATE WITH AUTO-ESCAPING
function html(strings, ...values) {
const escape = (str) =>
String(str)
.replace(/&/g, "&")
.replace(/</g, "<")
.replace(/>/g, ">")
.replace(/"/g, """);
let result = "";
for (let i = 0; i < strings.length; i++) {
result += strings[i]; // Raw HTML (trusted)
if (i < values.length) {
const value = values[i];
if (value && value.__safe) {
result += value.content; // Pre-sanitized content
} else {
result += escape(value); // Escape user input
}
}
}
return { content: result, __safe: true };
}
function raw(content) {
return { content, __safe: true };
}
const userInput = '<script>alert("xss")</script>';
const output = html`
<div class="user-content">
<h1>${"Welcome"}</h1>
<p>${userInput}</p>
${raw('<hr class="divider">')}
</div>
`;
console.log(output.content);
// <div class="user-content">
// <h1>Welcome</h1>
// <p><script>alert("xss")</script></p>
// <hr class="divider">
// </div>
// CSS-IN-JS GENERATOR
function css(strings, ...values) {
const className = `css-${hashCode(strings.join(""))}`;
let rules = "";
for (let i = 0; i < strings.length; i++) {
rules += strings[i];
if (i < values.length) {
rules += typeof values[i] === "function" ? values[i].name : values[i];
}
}
return {
className,
rules: `.${className} { ${rules.trim()} }`,
toString() { return this.className; }
};
}
function hashCode(str) {
let hash = 0;
for (let i = 0; i < str.length; i++) {
hash = ((hash << 5) - hash + str.charCodeAt(i)) | 0;
}
return Math.abs(hash).toString(36);
}
const buttonStyle = css`
padding: 8px 16px;
background: ${"#3b82f6"};
color: white;
border-radius: 4px;
border: none;
`;
console.log(buttonStyle.className); // "css-abc123"
console.log(buttonStyle.rules); // ".css-abc123 { padding: 8px 16px; ... }"AST-Based Code Generation
Instead of building code strings by hand (which is error-prone and hard to maintain), you can construct an Abstract Syntax Tree and then generate source code from it. The AST builder here gives you helper functions for common node types like variable declarations, function declarations, and if statements. A generate function walks the tree and outputs properly indented JavaScript. The generateValidator example shows the real payoff: you feed it a schema, and it produces a complete validation function with the right type checks and required field checks.
// Build Abstract Syntax Trees and generate code from them
// Simple AST node constructors
const AST = {
program(body) {
return { type: "Program", body };
},
variableDeclaration(kind, name, init) {
return {
type: "VariableDeclaration",
kind,
declarations: [{
type: "VariableDeclarator",
id: { type: "Identifier", name },
init
}]
};
},
functionDeclaration(name, params, body) {
return {
type: "FunctionDeclaration",
id: { type: "Identifier", name },
params: params.map(p => ({ type: "Identifier", name: p })),
body: { type: "BlockStatement", body }
};
},
returnStatement(argument) {
return { type: "ReturnStatement", argument };
},
binaryExpression(operator, left, right) {
return { type: "BinaryExpression", operator, left, right };
},
identifier(name) {
return { type: "Identifier", name };
},
literal(value) {
return { type: "Literal", value };
},
callExpression(callee, args) {
return {
type: "CallExpression",
callee: typeof callee === "string" ? { type: "Identifier", name: callee } : callee,
arguments: args
};
},
ifStatement(test, consequent, alternate = null) {
return {
type: "IfStatement",
test,
consequent: { type: "BlockStatement", body: Array.isArray(consequent) ? consequent : [consequent] },
alternate: alternate ? { type: "BlockStatement", body: Array.isArray(alternate) ? alternate : [alternate] } : null
};
}
};
// CODE GENERATOR: AST -> JavaScript source code
function generate(node, indent = 0) {
const pad = " ".repeat(indent);
switch (node.type) {
case "Program":
return node.body.map(n => generate(n, indent)).join("\n");
case "VariableDeclaration":
return `${pad}${node.kind} ${node.declarations.map(d =>
`${d.id.name}${d.init ? ` = ${generate(d.init)}` : ""}`
).join(", ")};`;
case "FunctionDeclaration":
return `${pad}function ${node.id.name}(${
node.params.map(p => p.name).join(", ")
}) {\n${generate(node.body, indent + 1)}\n${pad}}`;
case "BlockStatement":
return node.body.map(n => generate(n, indent)).join("\n");
case "ReturnStatement":
return `${pad}return ${generate(node.argument)};`;
case "BinaryExpression":
return `${generate(node.left)} ${node.operator} ${generate(node.right)}`;
case "Identifier":
return node.name;
case "Literal":
return JSON.stringify(node.value);
case "CallExpression":
return `${generate(node.callee)}(${
node.arguments.map(a => generate(a)).join(", ")
})`;
case "IfStatement":
let code = `${pad}if (${generate(node.test)}) {\n${generate(node.consequent, indent + 1)}\n${pad}}`;
if (node.alternate) {
code += ` else {\n${generate(node.alternate, indent + 1)}\n${pad}}`;
}
return code;
default:
return `/* unknown: ${node.type} */`;
}
}
// GENERATE A FUNCTION FROM A SCHEMA
function generateValidator(schema) {
const checks = [];
for (const [field, rules] of Object.entries(schema)) {
if (rules.type) {
checks.push(
AST.ifStatement(
AST.binaryExpression("!==",
AST.callExpression("typeof", [AST.identifier(`obj.${field}`)]),
AST.literal(rules.type)
),
[AST.returnStatement(AST.literal(`${field} must be a ${rules.type}`))]
)
);
}
if (rules.required) {
checks.push(
AST.ifStatement(
AST.binaryExpression("==",
AST.identifier(`obj.${field}`),
AST.literal(null)
),
[AST.returnStatement(AST.literal(`${field} is required`))]
)
);
}
}
checks.push(AST.returnStatement(AST.literal(null)));
const ast = AST.functionDeclaration("validate", ["obj"], checks);
return generate(ast);
}
const validatorCode = generateValidator({
name: { type: "string", required: true },
age: { type: "number" },
email: { type: "string", required: true }
});
console.log(validatorCode);
// function validate(obj) {
// if (typeof obj.name !== "string") { return "name must be a string"; }
// if (obj.name == null) { return "name is required"; }
// if (typeof obj.age !== "number") { return "age must be a number"; }
// if (typeof obj.email !== "string") { return "email must be a string"; }
// if (obj.email == null) { return "email is required"; }
// return null;
// }Runtime Code Generation
Sometimes you need to generate and execute code at runtime. The Function constructor is a safer alternative to eval because it creates a new function scope and only has access to the parameters you explicitly pass in. Below you will see three patterns: a compiled expression evaluator with input validation, a template compiler that turns {{ name }} syntax into a fast render function, and a schema validator that generates an optimized checking function from your field rules.
// Safe alternatives to eval for runtime code generation
// FUNCTION CONSTRUCTOR (safe scope isolation)
function createCompiledExpression(expression, variables) {
// Validate: only allow safe characters
if (/[;{}]/.test(expression)) {
throw new Error("Expression contains unsafe characters");
}
const paramNames = Object.keys(variables);
const paramValues = Object.values(variables);
// Create function with specified parameters only
const fn = new Function(...paramNames, `return (${expression});`);
return fn(...paramValues);
}
console.log(createCompiledExpression("a + b * c", { a: 1, b: 2, c: 3 })); // 7
console.log(createCompiledExpression("x > 10 ? 'big' : 'small'", { x: 15 })); // "big"
// TEMPLATE COMPILER
function compileTemplate(template) {
// Convert template string to a render function
const parts = [];
let current = 0;
// Find all {{ expression }} blocks
const regex = /\{\{(.+?)\}\}/g;
let match;
while ((match = regex.exec(template)) !== null) {
// Add text before the expression
if (match.index > current) {
parts.push({ type: "text", value: template.slice(current, match.index) });
}
// Add the expression
parts.push({ type: "expr", value: match[1].trim() });
current = match.index + match[0].length;
}
// Add remaining text
if (current < template.length) {
parts.push({ type: "text", value: template.slice(current) });
}
// Generate render function
const body = parts.map(part => {
if (part.type === "text") {
return JSON.stringify(part.value);
}
return `String(ctx.${part.value})`;
}).join(" + ");
return new Function("ctx", `return ${body || "''"};`);
}
const render = compileTemplate(
"Hello, {{ name }}! You have {{ count }} new messages."
);
console.log(render({ name: "Alice", count: 5 }));
// "Hello, Alice! You have 5 new messages."
// COMPILED SCHEMA VALIDATOR (generates optimized function)
function compileValidator(schema) {
const lines = ["const errors = [];"];
for (const [field, rules] of Object.entries(schema)) {
if (rules.required) {
lines.push(`if (data.${field} == null) errors.push("${field} is required");`);
}
if (rules.type) {
lines.push(
`if (data.${field} != null && typeof data.${field} !== "${rules.type}") ` +
`errors.push("${field} must be ${rules.type}");`
);
}
if (rules.min !== undefined) {
lines.push(
`if (typeof data.${field} === "number" && data.${field} < ${rules.min}) ` +
`errors.push("${field} must be >= ${rules.min}");`
);
}
if (rules.max !== undefined) {
lines.push(
`if (typeof data.${field} === "number" && data.${field} > ${rules.max}) ` +
`errors.push("${field} must be <= ${rules.max}");`
);
}
if (rules.pattern) {
lines.push(
`if (typeof data.${field} === "string" && !${rules.pattern}.test(data.${field})) ` +
`errors.push("${field} format is invalid");`
);
}
}
lines.push("return errors.length > 0 ? errors : null;");
return new Function("data", lines.join("\n"));
}
const validate = compileValidator({
name: { type: "string", required: true },
age: { type: "number", min: 0, max: 150 },
email: { type: "string", required: true, pattern: /^[^\s@]+@[^\s@]+$/ }
});
console.log(validate({ name: "Alice", age: 30, email: "alice@example.com" }));
// null (valid)
console.log(validate({ name: null, age: -5, email: "invalid" }));
// ["name is required", "age must be >= 0", "email format is invalid"]Source-to-Source Transformation
Source-to-source transforms take JavaScript code as input and produce modified JavaScript code as output. This is how minifiers, dead code eliminators, and build-time optimizations work. The CodeTransformer class below chains regex-based replacements, while the CodePipeline runs multiple transformation stages in sequence and logs which stages actually changed the code. For production use you would want AST-based transforms, but regex works fine for straightforward patterns like stripping console.log calls.
// Transform JavaScript source code patterns
class CodeTransformer {
#transforms = [];
addTransform(pattern, replacement) {
this.#transforms.push({
pattern: typeof pattern === "string" ? new RegExp(escapeRegex(pattern), "g") : pattern,
replacement
});
return this;
}
transform(source) {
let result = source;
for (const { pattern, replacement } of this.#transforms) {
result = result.replace(pattern, replacement);
}
return result;
}
}
function escapeRegex(str) {
return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
}
// DEAD CODE ELIMINATION (simplified)
function eliminateDeadCode(source) {
const transformer = new CodeTransformer();
// Remove if(false) blocks
transformer.addTransform(
/if\s*\(\s*false\s*\)\s*\{[^}]*\}/g,
"/* dead code removed */"
);
// Remove console.log in production
transformer.addTransform(
/console\.(log|debug|info)\([^)]*\);?\n?/g,
""
);
return transformer.transform(source);
}
const code = `
function process(x) {
console.log("processing", x);
if (false) {
doExpensiveThing();
}
return x * 2;
}
`;
console.log(eliminateDeadCode(code));
// function process(x) {
// /* dead code removed */
// return x * 2;
// }
// CODE GENERATION PIPELINE
class CodePipeline {
#stages = [];
addStage(name, transform) {
this.#stages.push({ name, transform });
return this;
}
run(source) {
let result = source;
const log = [];
for (const stage of this.#stages) {
const before = result;
result = stage.transform(result);
log.push({
stage: stage.name,
changed: before !== result,
length: result.length
});
}
return { output: result, log };
}
}
const pipeline = new CodePipeline()
.addStage("strip-comments", src => src.replace(/\/\/.*$/gm, ""))
.addStage("strip-console", src => src.replace(/console\.\w+\([^)]*\);?\n?/g, ""))
.addStage("minify-whitespace", src => src.replace(/\n\s*\n/g, "\n").trim());
const { output, log } = pipeline.run(`
// This is a helper function
function helper(x) {
console.log("debug:", x);
// Transform the value
return x * 2;
}
`);
console.log(output);
console.log(log);Build-Time Macro Patterns
Build-time macros run during your build step and produce output that ships to production. The patterns here include compile-time constants (like __DEV__ flags that get replaced by your bundler), type annotation stripping, and pre-computed lookup tables where expensive calculations happen once at build time instead of every time the code runs.
// Patterns that mimic macros using build-time processing
// COMPILE-TIME CONSTANTS (replaced during build)
// In a build plugin, these would be replaced with actual values
const __DEV__ = process.env.NODE_ENV !== "production";
const __VERSION__ = "1.0.0";
function devOnly(fn) {
if (__DEV__) return fn;
return () => {}; // No-op in production
}
const debugLog = devOnly((msg) => console.log(`[DEBUG] ${msg}`));
// COMPILE-TIME TYPE STRIPPING (simulated)
function stripTypeAnnotations(source) {
// Remove TypeScript-style type annotations
return source
.replace(/:\s*(string|number|boolean|any|void|object)\b/g, "")
.replace(/:\s*\w+\[\]/g, "")
.replace(/<[^>]+>/g, "")
.replace(/\bas\s+\w+/g, "")
.replace(/interface\s+\w+\s*\{[^}]*\}/g, "")
.replace(/type\s+\w+\s*=\s*[^;]+;/g, "");
}
const tsCode = `
interface User {
name: string;
age: number;
}
function greet(user: User): string {
const message: string = "Hello " + user.name;
return message as string;
}
`;
console.log(stripTypeAnnotations(tsCode).trim());
// function greet(user) {
// const message = "Hello " + user.name;
// return message;
// }
// PRE-COMPUTED LOOKUP TABLES
function generateLookupTable(entries) {
const lines = ["const LOOKUP = {"];
for (const [key, value] of Object.entries(entries)) {
// Pre-compute values at build time
const computed = typeof value === "function" ? value() : value;
lines.push(` ${JSON.stringify(key)}: ${JSON.stringify(computed)},`);
}
lines.push("};");
lines.push("export default LOOKUP;");
return lines.join("\n");
}
const lookupCode = generateLookupTable({
sin30: () => Math.sin(Math.PI / 6),
cos45: () => Math.cos(Math.PI / 4),
sqrt2: () => Math.sqrt(2),
phi: () => (1 + Math.sqrt(5)) / 2
});
console.log(lookupCode);
// const LOOKUP = {
// "sin30": 0.49999999999999994,
// "cos45": 0.7071067811865476,
// "sqrt2": 1.4142135623730951,
// "phi": 1.618033988749895,
// };
// export default LOOKUP;| Technique | Phase | Safety | Use Case |
|---|---|---|---|
| Tagged templates | Runtime | High (no eval) | SQL, HTML, CSS generation |
| AST construction | Build/Runtime | High (structured) | Code generators, compilers |
| Function constructor | Runtime | Medium (scoped) | Expression evaluation, templates |
| Source transforms | Build | High (no execution) | Minification, polyfills |
| Babel plugins | Build | High (AST-level) | Language extensions, optimizations |
| eval / indirect eval | Runtime | Low (full access) | Avoid in production |
Rune AI
Key Insights
- Tagged template literals act as runtime macros, receiving raw strings and values separately for safe domain-specific processing: They prevent injection attacks in SQL, HTML, and CSS by design
- AST-based code generation builds structured syntax trees that are converted to source code, ensuring syntactic correctness: This approach is used by compilers, transpilers, and code generation tools
- The Function constructor creates scoped functions from strings, providing a safer alternative to eval with explicit parameter binding: It cannot access calling scope variables, limiting the attack surface
- Source-to-source transformations using regex or AST visitors enable dead code elimination, minification, and language extensions: Build-time transforms have zero runtime overhead
- Code generation pipelines compose multiple transformation stages for systematic source processing: Each stage can be independently tested, logged, and debugged
Frequently Asked Questions
Are tagged template literals real macros?
When should I use the Function constructor vs eval?
How do Babel plugins transform code?
What are the security risks of code generation?
Conclusion
JavaScript code generation spans compile-time AST transforms to runtime tagged templates. Tagged templates provide safe, practical macros for SQL, HTML, and CSS. AST-based generation enables type-safe code synthesis. Runtime compilation via Function constructor creates optimized validators and template renderers. For the self-modifying patterns that build on code generation, see Writing Self-Modifying Code in JS Architecture. For the AST structures that V8 uses internally, revisit JavaScript ASTs and Parse Trees Explained.
More in this topic
OffscreenCanvas API in JS for UI Performance
Master the OffscreenCanvas API to offload rendering from the main thread. Covers worker-based 2D and WebGL rendering, animation loops inside workers, bitmap transfer, double buffering, chart rendering pipelines, image processing, and performance measurement strategies.
Advanced Web Workers for High Performance JS
Master Web Workers for truly parallel JavaScript execution. Covers dedicated and shared workers, structured cloning, transferable objects, SharedArrayBuffer with Atomics, worker pools, task scheduling, Comlink RPC patterns, module workers, and performance profiling strategies.
Creating Advanced UI Frameworks in JavaScript
Build a modern UI framework from scratch in JavaScript. Covers virtual DOM implementation, diff algorithms, reactive state management, component lifecycle, template compilation, event delegation, batched rendering, hooks system, server-side rendering, and hydration.