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
| Format | Suitable for | Example |
|---|---|---|
{{}} | 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:
{
"name": "{{name}}",
"version": "1.0.0"
}const projectName = "{{projectName}}";Conditional Blocks
Conditionally include or exclude content:
/*#{{#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}} // variableConditional 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
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 configPackage.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
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
- Use Appropriate Format: Choose the correct Handlebars format based on file type
- Comment Blocks Clearly: Add clear comments around complex Handlebars logic
- Keep Templates DRY: Avoid duplicating template logic across files
- Use Custom Helpers: Extend the Handlebars templating system with your own helpers
- 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:
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
- Variable Not Found: Ensure the variable exists in the config object
- Helper Not Working: Check helper name and parameters
- Syntax Errors: Verify that opening and closing tags match
- Incorrect Format: Make sure you're using the right format for the file type
- Missing close tag: Ensure that all opening tags have a corresponding closing tag