Skip to content

Package Architecture

This guide explains how to create new packages for AI Scaffolding and how the package architecture works.

Package Structure

Each package in AI Scaffolding follows a consistent structure:

Directory Layout

Package Files
packages/<package-name>/
├── files/           # Template files for the package
│   ├── contracts/   # Package-specific content
│   ├── src/         # Package-specific content
│   └── ...
├── prompt.ts        # Interactive prompts for the package
└── builder.ts       # Logic for generating the package files

Creating a New Package

To create a new package for AI Scaffolding, you need to implement the following components:

1. Create Prompt Module

First, create a prompt.ts file that defines the configuration options for your package:

Package Files
packages/<package-name>/
├── files/           # Template files for the package
│   ├── contracts/   # Package-specific content
│   ├── src/         # Package-specific content
│   └── ...
├── prompt.ts        # Interactive prompts for the package 
└── builder.ts       # Logic for generating the package files
packages/<package-name>/prompt.ts
import { checkbox, confirm, input } from '@inquirer/prompts'
import { PackagePrompt } from '../../types.js';
 
export interface MyPackageConfig {
  useFeatureA: boolean
  useFeatureB: boolean
  // Add your configuration options
  [key: string]: unknown
}
 
const prompt: PackagePrompt<MyPackageConfig> = async (ctx) => {
  // Get package name
  const name = await input({
    message: 'Enter the name of your package:',
    default: 'mypackage',
    transformer: (value: string, { isFinal }) => 
      isFinal ? `@${ctx.projectName.toLowerCase()}/${value}` : value
  });
 
  // Collect configuration options
  const useFeatureA = await confirm({
    message: 'Do you want to include Feature A?',
    default: true
  });
 
  const useFeatureB = await confirm({
    message: 'Do you want to include Feature B?',
    default: false
  });
 
  // Return the configuration
  return {
    name,
    useFeatureA,
    useFeatureB
  }
}
 
export default prompt;

2. Implement Builder Class

Next, create a builder.ts file that extends the PackageBuilder base class:

Package Files
packages/<package-name>/
├── files/           # Template files for the package
│   ├── contracts/   # Package-specific content
│   ├── src/         # Package-specific content
│   └── ...
├── prompt.ts        # Interactive prompts for the package
└── builder.ts       # Logic for generating the package files 
packages/<package-name>/builder.ts
import path from 'path'
import { fileURLToPath } from 'url'
import picocolors from 'picocolors'
import { ProjectConfigV2 } from '../../types.js'
import { ListrTaskArg, PackageBuilder, ListrTaskList } from '../../builders/PackageBuilder.js'
 
const __dirname = path.dirname(fileURLToPath(import.meta.url));
const { white } = picocolors;
 
export default class MyPackageBuilder extends PackageBuilder {
  constructor(config: ProjectConfigV2) {
    super(
      config,
      path.join(__dirname, 'files'),
      config.packages.mypackage.name
    );
  }
 
  public build(task: ListrTaskArg, nextSteps: string[]): ListrTaskList {
    this._addNextSteps(nextSteps);
 
    return task.newListr([
      {
        title: 'Copy package files',
        task: this.copyPackageFiles.bind(this)
      },
      {
        title: 'Create package.json',
        task: this.createPackageJson.bind(this)
      },
      {
        title: 'Create README.md',
        task: async () => {
          await this.createReadme({
            packageManagerCmd: this.getPackageManagerCmd()
          });
        }
      },
      {
        title: 'Parse template files',
        task: async () => {
          await this.parseAllFiles({
            exclude: ['README.md', 'package.json']
          });
        }
      }
    ]);
  }
 
  private _addNextSteps(nextSteps: string[]) {
    // Add any package-specific next steps
    const pkgCmd = this.config.packageManager;
    const { packages } = this.config;
 
    nextSteps.push(
      white(`Run \`${pkgCmd} ${packages.mypackage.name} start\``)
    );
  }
}

3. Prepare Template Files

Create a files directory with all the template files for your package:

Package Files
packages/<package-name>/
├── files/           # Template files for the package 
│   ├── contracts/   # Package-specific content 
│   ├── src/         # Package-specific content 
│   └── ...
├── prompt.ts        # Interactive prompts for the package
└── builder.ts       # Logic for generating the package files
packages/<package-name>/files/
├── package.json    # Will be processed as a Handlebars template
├── README.md       # Will be processed as a Handlebars template
├── src/            # Package source files
│   ├── index.ts    # Package source files
│   └── ...
└── ...             # Other package files

4. Register Your Package

Finally, register your package in src/packages/index.ts:

src/packages/index.ts
import mypackagePrompt, { MyPackageConfig } from './mypackage/prompt.js'
import MyPackageBuilder from './mypackage/builder.js'
 
export const Packages: PackageExport = {
  hardhat: {
    prompt: hardhatPrompt as PackagePrompt<HardhatConfig>,
    builder: HardhatBuilder
  },
  vite: {
    prompt: vitePrompt as PackagePrompt<ViteConfig>,
    builder: ViteBuilder
  },
  // Add your new package here 
  mypackage: { 
    prompt: mypackagePrompt as PackagePrompt<MyPackageConfig>, 
    builder: MyPackageBuilder 
  } 
};
 
export async function packagesPrompt() {
  const packages = await checkbox<string>({
    message: 'Select packages to install:',
    choices: [
      { name: 'Hardhat', value: 'hardhat' },
      { name: 'Vite', value: 'vite' },
      // Add your new package to the choices 
      { name: 'My Package', value: 'mypackage' }, 
    ],
    required: true,
    pageSize: 20,
  });
  // ...
}
 
// Export your config type
export { MyPackageConfig } from './mypackage/prompt.js'

Advanced Package Features

Conditional File Generation

You can conditionally generate files based on user configuration:

packages/<package-name>/builder.ts
public build(task: ListrTaskArg, nextSteps: string[]): ListrTaskList {
  // ...
  
  if (this.config.packages.mypackage.useFeatureA) { 
    // Only copy feature A files if the user selected that option 
    await this.copyFiles('feature-a-templates', 'feature-a'); 
  } 
  
  // ...
}

Integration with Other Packages

To interact with other packages in the project:

packages/<package-name>/builder.ts
public build(task: ListrTaskArg, nextSteps: string[]): ListrTaskList {
  // Check if hardhat is enabled to add integrations 
  if (this.config.global.shouldAddHardhat) { 
    await this.copyFiles('hardhat-integration', 'hardhat-integration'); 
  } 
  
  // ...
}

Testing Your Package

To test your package during development:

  1. Build the project
Terminal
pnpm build
  1. Run the CLI with your package
Terminal
pnpn dev tmp/test-project
  1. Select your package during the prompt phase
Terminal
? Select packages to install:
  My Package
  1. Verify the generated files match your expectations

Best Practices

  • Modular Design: Keep your package focused on a single responsibility
  • Consistent Naming: Follow the naming conventions of existing packages
  • Clear Prompts: Make your configuration prompts clear and provide sensible defaults
  • Error Handling: Add proper error handling in your builder
  • Documentation: Add README files with clear instructions for your package
  • Next Steps: Add next steps to the builder to guide the user on what to do next

Next Steps

Now that you understand how to create new packages, you can: