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.

JavaScriptadvanced
18 min read

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.

javascriptjavascript
// 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, "&amp;")
      .replace(/</g, "&lt;")
      .replace(/>/g, "&gt;")
      .replace(/"/g, "&quot;");
 
  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>&lt;script&gt;alert(&quot;xss&quot;)&lt;/script&gt;</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.

javascriptjavascript
// 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.

javascriptjavascript
// 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.

javascriptjavascript
// 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.

javascriptjavascript
// 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;
TechniquePhaseSafetyUse Case
Tagged templatesRuntimeHigh (no eval)SQL, HTML, CSS generation
AST constructionBuild/RuntimeHigh (structured)Code generators, compilers
Function constructorRuntimeMedium (scoped)Expression evaluation, templates
Source transformsBuildHigh (no execution)Minification, polyfills
Babel pluginsBuildHigh (AST-level)Language extensions, optimizations
eval / indirect evalRuntimeLow (full access)Avoid in production
Rune AI

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
RunePowered by Rune AI

Frequently Asked Questions

Are tagged template literals real macros?

Tagged template literals are not macros in the traditional sense (compile-time code transformation). They are runtime functions that receive template parts and produce values. However, they serve similar purposes: domain-specific syntax (SQL, HTML, CSS), input sanitization, and custom string processing. True macros operate on source code before execution, while tagged templates operate on values at runtime. Some build tools (like babel-plugin-macros) enable compile-time tagged template processing that approaches true macro behavior.

When should I use the Function constructor vs eval?

Prefer the Function constructor over eval in all cases. The Function constructor creates a function with its own scope that cannot access the calling function's local variables (only globals). eval executes code in the current scope, accessing and modifying local variables, which is both a security risk and a performance problem (engines cannot optimize code containing eval). If you must evaluate dynamic code, use `new Function()` with explicit parameter passing to control exactly what the code can access.

How do Babel plugins transform code?

Babel plugins operate on the Abstract Syntax Tree (AST). Babel parses JavaScript source into an AST, plugins visit and transform AST nodes, and Babel generates new JavaScript from the modified AST. A plugin exports a visitor object with methods named after AST node types (Identifier, CallExpression, etc.). When Babel encounters a matching node, it calls the visitor method, which can modify, replace, or remove the node. This structured approach is safer than regex-based source transforms because it understands code structure.

What are the security risks of code generation?

The primary risk is code injection: user-controlled input becoming part of generated code that gets executed. Tagged templates mitigate this by separating static template parts from dynamic values. The Function constructor limits scope access but still executes arbitrary code. Always validate and sanitize inputs to code generation functions. Never pass user input directly to eval, new Function, or template literal expressions without validation. For production systems, prefer compile-time generation where possible, as it eliminates runtime code injection entirely.

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.