Skip to content

Main Builder

The MainBuilder class is the core orchestrator of AI Scaffolding's project generation process. It's responsible for creating the project structure, handling package generation, and executing various build tasks in a coordinated sequence.

Overview

The MainBuilder is instantiated with the project configuration collected during the prompt phase and used to execute the entire build process.

import { MainBuilder } from './builders/MainBuilder.js';
 
// Create a new MainBuilder with the collected config
const builder = new MainBuilder(config); 
 
// Execute the build process
await builder.build(); 

Class Structure

export class MainBuilder {
  config: ProjectConfig;           // Project configuration
  tasks: Listr;                    // Task list for execution
  packagesPath: string;            // Path to packages directory
  templatesPath: string;           // Path to templates directory
  nextSteps: string[] = [];        // Next steps to display after build
  
  constructor(config: ProjectConfig) { /* ... */ }
  
  // Main public method
  public async build() { /* ... */ }
  
  // Private implementation methods
  private _createProjectStructure() { /* ... */ }
  private _buildPackages() { /* ... */ }
  private _createCursorrules() { /* ... */ }
  private _installDependencies() { /* ... */ }
  private _formatAll() { /* ... */ }
  private _initGitRepository() { /* ... */ }
  // More helper methods...
}

Key Responsibilities

1. Build Orchestration

The build() method orchestrates the entire process in a specific order:

public async build() {
  const startTime = Date.now();
 
  // Orchestrate tasks
  this._createProjectStructure(); 
  this._buildPackages(); 
  this._createCursorrules(); 
  this._installDependencies(); 
  this._formatAll(); 
  this._initGitRepository(); 
 
  // Run tasks
  await this.tasks.run(); 
 
  // Log completion and next steps
  const endTime = Date.now();
  const duration = ((endTime - startTime) / 1000).toFixed(2);
  console.log(green(`\n✨ Project created successfully! Built in ${duration} seconds`));
  this._nextSteps();
}

2. Project Structure Creation

The MainBuilder creates the basic project structure:

private _createProjectStructure() {
  this.tasks.add({
    title: magenta('Ensure project directories'),
    task: async () => {
      await fs.ensureDir(this.config.projectPath);
      await fs.ensureDir(this.packagesPath);
    }
  });
 
  // Add tasks for project files, README, package.json, etc.
}

3. Package Building

The MainBuilder delegates to package-specific builders:

private _buildPackages() {
  for (const pkg of Object.keys(this.config.packages)) {
    const Builder = Packages[pkg as keyof ProjectConfigV2['packages']].builder
    if (Builder) {
      this.tasks.add({
        title: magenta(`Create ${pkg} package`),
        task: async (_, task) => {
          const builder = new Builder(this.config);
          return builder.build(task, this.nextSteps);
        }
      })
    }
  }
}

4. Cursor Rules Generation

The MainBuilder creates AI assistant rules for Cursor:

private async _createCursorrules() {
  // Collects and merges .cursorrules files from each package
  // and processes them with Handlebars
}

5. Dependency Installation

private _installDependencies() {
  this.tasks.add({
    title: magenta('Install dependencies'),
    skip: this.config.skipInstall,
    task: async (_, task) => task.newListr([{
      title: `Run ${this.config.packageManager} install`,
      task: async () => {
        const cmd = execa.command(`${this.config.packageManager} install`, {
          cwd: this.config.projectPath,
        })
        // Execute the command
      }
    }])
  })
}

6. Code Formatting

private _formatAll() {
  this.tasks.add({
    title: magenta('Format code'),
    skip: this.config.skipInstall || this.config.global.isNpm,
    task: async (_, task) => task.newListr([
      {
        title: 'Format all packages',
        task: async () => {
          await execSync(`${this.config.packageManager} format:all`, { cwd: this.config.projectPath });
        }
      },
      // More formatting tasks
    ])
  })
}

7. Git Repository Setup

private _initGitRepository() {
  this.tasks.add({
    title: magenta('Setup Git repository'),
    task: async (_, task) => task.newListr([
      {
        title: 'Initialize repository',
        task: async () => {
          execSync('git init -b main', { cwd: this.config.projectPath });
        }
      },
      // More git tasks
    ])
  })
}

Template Processing Methods

The MainBuilder includes several methods for processing Handlebars templates:

Write Raw File

private async _writeRawFile(
  file: string,
  extraParams: Record<string, unknown> = {},
  srcPath: string
): Promise<void> {
  const template = await this._compile(file, srcPath);
  const data = template({
    ...this.config,
    ...extraParams
  });
 
  return fs.writeFile(
    path.join(this.config.projectPath, path.basename(file)),
    data
  );
}

Write Template File

private async _writeFile(
  file: string,
  extraParams: Record<string, unknown> = {},
  isJson?: boolean,
  parseFn?: (data: string) => string
): Promise<void> {
  const template = await this._compile(`${file}.hbs`);
  const data = template({
    ...this.config,
    ...extraParams
  });
 
  // Write file based on type (JSON or plain text)
}

Copy Folder

private async _copyFolder(folder: string, targetFolder?: string) {
  return fs.copy(
    path.join(this.templatesPath, folder),
    path.join(this.config.projectPath, targetFolder || folder)
  );
}

Compile Template

private async _compile(templateName: string, srcPath: string = this.templatesPath): Promise<TemplateDelegate> {
  const templatePath = path.join(srcPath, templateName);
  const templateContent = await fs.readFile(templatePath, 'utf-8');
  return Handlebars.compile(templateContent);
}

Helper Methods

Package Manager Command Generator

private _getPackageManagerCmd(): string {
  const packageManagerCmd = {
    [PackageManager.NPM]: 'npm run -w',
    [PackageManager.YARN]: 'yarn workspace',
    [PackageManager.PNPM]: 'pnpm --filter'
  }[this.config.packageManager];
 
  if (!packageManagerCmd) {
    throw new Error('Unsupported package manager');
  }
 
  return packageManagerCmd;
}

Next Steps Display

private _nextSteps(): void {
  console.log(blue(`\nNext steps:\n`));
  this.nextSteps.forEach((step, i) => console.log(`  ${i + 1}. ${step}`));
  console.log(green('\nHappy coding! 🚀\n'));
}

Usage in Custom Packages

When creating custom packages, you typically won't interact with the MainBuilder directly. Instead, your package's builder will be invoked by the MainBuilder during the _buildPackages() phase.

However, understanding the MainBuilder is important for:

  1. Understanding the overall project generation flow
  2. Knowing what files and directories will already be created
  3. Seeing how template processing works at the project level

Best Practices

  1. Don't Modify MainBuilder Directly: Instead of modifying the MainBuilder class, create a custom Package Builder
  2. Leverage Next Steps: Add custom next steps to guide users after project creation
  3. Follow Task Structure: Use similar task structure in your own builders for consistency

Related Documentation