Skip to content

AI Scaffolding Handlebars

AI Scaffolding uses Handlebars as its templating engine to create dynamic files based on user configuration.

Understanding how to effectively use Handlebars is essential for creating and extending packages.

How Handlebars Works in AI Scaffolding

During the execution phase, the CLI tool processes template files with the Handlebars engine, substituting variables and expressions with actual values from the configuration object created during the prompt phase.

Special Comment Formats

AI Scaffolding supports multiple formats for Handlebars directives to ensure compatibility with different file types and to avoid breaking syntax highlighting in IDEs.

This is particularly important when working with files that have their own syntax requirements.

Available Formats

FormatSuitable forExample
{{}}Basic templating in .hbs files{{projectName}}
/*#{{}}*/JavaScript/TypeScript files/*#{{#if useFeature}}*/
{/*#{{}}*/}JSX/TSX/React files{/*#{{global.isPnpm}}*/}

Basic Usage

Variables

Access any value from the config object:

.json
{
  "name": "{{name}}",
  "version": "1.0.0"
}
.ts
const projectName = "{{projectName}}";

Conditional Blocks

Conditionally include or exclude content:

hardhat.config.ts
/*#{{#if packages.hardhat.useIgnition}}*/
import '@nomicfoundation/hardhat-ignition'
/*#{{/if}}*/
 
// JSX/React with comment format
{/*#{{#if global.shouldAddNextjs}}*/}
import NextLink from 'next/link';
{/*#{{/if}}*/}

Loops

Iterate over arrays and objects:

// TypeScript example
const networks = {
/*#{{#each packages.hardhat.networks}}*/
  /*#{{#ifneq this 'hardhat'}}*/
  ['{{this}}']: {
    url: process.env.{{uppercase this}}_RPC_URL || '',
    accounts: process.env.PRIVATE_KEY ? [process.env.PRIVATE_KEY] : [],
  },
  /*#{{/ifneq}}*/
/*#{{/each}}*/
}

Custom Helpers

AI Scaffolding extends Handlebars with several custom helpers to make templating more powerful:

String Transformation

// Convert to uppercase
{{uppercase variable}}  // VARIABLE
 
// Convert to lowercase
{{lowercase variable}}  // variable

Conditional Helpers

// Equal comparison
{{#ifeq value1 value2}}
  // Content shown if value1 === value2
{{/ifeq}}
 
// Not equal comparison
{{#ifneq value1 value2}}
  // Content shown if value1 !== value2
{{/ifneq}}
 
// Logical OR
{{#or value1 value2}}
  // Content shown if value1 || value2 is truthy
{{/or}}
 
// Logical NOR
{{#nor value1 value2}}
  // Content shown if neither value1 nor value2 is truthy
{{/nor}}

Config Object

The entire configuration object is available to Handlebars templates. Here's the general structure:

{
  projectName: string,
  packageManager: "npm" | "yarn" | "pnpm",
  packages: {
    hardhat: { ... },
    vite: { ... },
    nextjs: { ... },
    nestjs: { ... }
  },
  global: {
    useSilentData: boolean,
    shouldAddHardhat: boolean,
    shouldAddVite: boolean,
    shouldAddNextjs: boolean,
    shouldAddNestjs: boolean,
    networks: Network[],
    isPnpm: boolean,
    isYarn: boolean,
    isNpm: boolean,
    defaultNetwork: Network
  }
}

Examples

Hardhat Configuration

hardhat.config.ts
import { HardhatUserConfig } from 'hardhat/config'
import '@nomicfoundation/hardhat-toolbox'
import { config as Config } from 'dotenv'
 
/*#{{#if packages.hardhat.useIgnition}}*/
import '@nomicfoundation/hardhat-ignition'
/*#{{/if}}*/
 
/*#{{#if packages.hardhat.useSilentData}}*/
import '@appliedblockchain/silentdatarollup-hardhat-plugin'
import { SignatureType } from '@appliedblockchain/silentdatarollup-core'
/*#{{/if}}*/
 
// Load .env file
Config()
 
const config: HardhatUserConfig = {
  solidity: {
    version: '0.8.28',
    settings: {
      optimizer: {
        enabled: true,
        runs: 200,
      }
    }
  },
  networks: {
    localhost: {
      url: 'http://127.0.0.1:8545',
      chainId: 31337,
    },
    /*#{{#each packages.hardhat.networks}}*/
    /*#{{#ifneq this 'hardhat'}}*/
    ["{{this}}"]: {
      url: process.env.{{uppercase this}}_RPC_URL || '',
      accounts: process.env.PRIVATE_KEY ? [process.env.PRIVATE_KEY] : [],
    },
    /*#{{/ifneq}}*/
    /*#{{/each}}*/
  }
}
 
export default config

Package.json

package.json
{
  "name": "{{name}}",
  "version": "0.1.0",
  "private": true,
  "scripts": {
    "dev": "next dev",
    "build": "next build",
    "start": "next start",
    "lint": "next lint"
    {{#if useTypeScript}},
    "type-check": "tsc --noEmit"
    {{/if}}
  },
  "dependencies": {
    {{#if useEthers}}
    "ethers": "^6.8.0",
    {{/if}}
    "next": "13.4.19",
    "react": "18.2.0",
    "react-dom": "18.2.0"
  },
  "devDependencies": {
    {{#if useTypeScript}}
    "typescript": "^5.0.0",
    "@types/react": "^18.2.0",
    "@types/node": "^18.0.0",
    {{/if}}
    "eslint": "8.49.0",
    "eslint-config-next": "13.4.19"
  }
}

React Component

Navbar.tsx
export function Navbar() {
  /*#{{#unless packages.vite.useRainbowKit}}#*/
  const { address, isConnected } = useAccount()
  const { disconnect } = useDisconnect()
  /*#{{/unless}}#*/
 
  return (
    <nav className="bg-white shadow-lg">
      <div className="max-w-7xl mx-auto px-4">
        <div className="flex justify-between h-16">
          <div className="flex items-center">
            <span className="text-xl font-bold text-gray-800">{{projectName}} DApp ✨</span>
          </div>
          <div className="flex items-center gap-4">
            {/*#{{#if packages.vite.useRainbowKit}}#*/}
              <ConnectButton />
            {/*#{{else}}#*/}
            {!isConnected ? (
              <ConnectWallet />
            ) : (
              <>
                <div className="text-gray-600">
                  {address?.slice(0, 6)}...{address?.slice(-4)}
                </div>
                <button
                  onClick={() => disconnect()}
                  className="bg-red-500 hover:bg-red-600 text-white font-bold py-2 px-4 rounded"
                >
                  Disconnect
                </button>
              </>
            )}
            {/*#{{/if}}#*/}
          </div>
        </div>
      </div>
    </nav>
  )
}

Best Practices

  1. Use Appropriate Format: Choose the correct Handlebars format based on file type
  2. Comment Blocks Clearly: Add clear comments around complex Handlebars logic
  3. Keep Templates DRY: Avoid duplicating template logic across files
  4. Use Custom Helpers: Extend the Handlebars templating system with your own helpers
  5. Idententation: Use consistent indentation in template files

Adding Custom Helpers

To extend the Handlebars templating system with your own helpers, add them to src/handlebars.ts:

handlebars.ts
import Handlebars from 'handlebars'
 
// Existing helpers
Handlebars.registerHelper('uppercase', (str) => str.toUpperCase());
Handlebars.registerHelper('lowercase', (str) => str.toLowerCase());
 
// Add your custom helper
Handlebars.registerHelper('joinWithComma', (array) => array.join(', '));
Handlebars.registerHelper('formatDate', () => new Date().toISOString().split('T')[0]);

Troubleshooting

Common Issues

  1. Variable Not Found: Ensure the variable exists in the config object
  2. Helper Not Working: Check helper name and parameters
  3. Syntax Errors: Verify that opening and closing tags match
  4. Incorrect Format: Make sure you're using the right format for the file type
  5. Missing close tag: Ensure that all opening tags have a corresponding closing tag