Skip to content

Package Builder

The PackageBuilder is an abstract base class that provides a foundation for all package-specific builders in AI Scaffolding.

Every package that can be selected during project creation needs a corresponding builder class that extends PackageBuilder.

Overview

The PackageBuilder class provides common functionality for:

  • Copying template files to the destination project
  • Processing Handlebars templates
  • Managing package-specific tasks
import { PackageBuilder } from '../../builders/PackageBuilder.js';
 
// Create a custom package builder that extends PackageBuilder
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 {
    // Implementation
  }
}

Class Structure

export abstract class PackageBuilder {
  protected config: ProjectConfigV2;         // Project configuration
  protected tasks: Listr[] = [];             // Task list
  protected packagePath: string;             // Path to package templates
  protected folderName: string;              // Name of the package
  protected packageInstallPath: string;      // Destination path for package
 
  constructor(config: ProjectConfigV2, packagePath: string, folderName: string) { /* ... */ }
 
  // Abstract method to be implemented by subclasses
  abstract build(task: ListrTaskArg, nextSteps: string[]): ListrTaskList;
 
  // File utilities
  protected async copyPackageFiles() { /* ... */ }
  protected async removeEmptyFiles() { /* ... */ }
  protected async removeFiles(files: string[]) { /* ... */ }
 
  // Package utilities
  protected async createPackageJson(extraParams?: Record<string, unknown>) { /* ... */ }
  protected async createReadme(extraParams?: Record<string, unknown>) { /* ... */ }
 
  // Template utilities
  protected async parseFile(file: string, extraParams?: Record<string, unknown>, isJson?: boolean, parseFn?: (data: string) => string) { /* ... */ }
  protected async compile(templateName: string): Promise<TemplateDelegate> { /* ... */ }
  protected async parseAllFiles(options: { exclude?: string[], extraParams?: Record<string, unknown> }) { /* ... */ }
 
  // Helper methods
  protected getPackageManagerCmd(): string { /* ... */ }
}

Key Responsibilities

1. Abstract Build Method

Every subclass must implement the build method which defines the package-specific build process:

abstract build(task: ListrTaskArg, nextSteps: string[]): ListrTaskList;

This method is called by the MainBuilder during project generation to build the specific package.

2. File Utilities

Copy Package Files

Copies all files from the package template directory to the destination project:

protected async copyPackageFiles() {
  await fs.ensureDir(this.packageInstallPath);
  await fs.copy(this.packagePath, this.packageInstallPath);
}

Remove Empty Files

Removes any empty files that might be present in the package:

protected async removeEmptyFiles() {
  const files = await fs.readdir(this.packageInstallPath, {
    recursive: true,
    withFileTypes: true
  });
  for (const file of files) {
    if (file.isDirectory()) continue;
    const fileName = path.join(file.parentPath || file.path, file.name);
    const content = await fs.readFile(fileName, 'utf-8');
    if (content.trim() === '') {
      await fs.remove(fileName);
    }
  }
}

Remove Specific Files

Removes specified files from the package:

protected async removeFiles(files: string[]) {
  await Promise.all(files.map(async (file) => {
    await fs.remove(path.join(this.packageInstallPath, file));
  }));
}

3. Package Utilities

Create Package.json

Processes the package.json file using Handlebars:

protected async createPackageJson(extraParams?: Record<string, unknown>) {
  await this.parseFile(
    'package.json',
    extraParams,
    true,
    (data) => JSON.parse(data)
  );
}

Create README

Processes the README.md file using Handlebars:

protected async createReadme(extraParams?: Record<string, unknown>) {
  await this.parseFile(
    'README.md',
    extraParams,
    false
  );
}

4. Template Utilities

Parse File

Processes a single file with Handlebars:

protected async parseFile(
  file: string,
  extraParams: Record<string, unknown> = {},
  isJson?: boolean,
  parseFn?: (data: string) => string
): Promise<void> {
  const templatePath = path.join(this.packageInstallPath, file);
  const templateContent = await fs.readFile(templatePath, 'utf-8');
 
  // Early return if file doesn't contain any handlebars
  if (!templateContent.includes('{{')) return;
 
  const template = Handlebars.compile(templateContent);
  let data = template({
    ...this.config,
    ...extraParams
  });
 
  // Remove template escape comments
  if (data.includes('/*#')) {
    data = data.split('\n')
      .filter(line => !line.includes('/*#*/'))
      .filter(line => !line.includes('/*#'))
      .filter(line => !line.includes('#*/'))
      .join('\n');
  }
 
  // Write file based on type (JSON or plain text)
}

Compile Template

Compiles a Handlebars template:

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

Parse All Files

Processes all files in the package with Handlebars:

protected async parseAllFiles(options: {
  exclude?: string[]
  extraParams?: Record<string, unknown>
}) {
  const files = await fs.readdir(this.packageInstallPath, {
    recursive: true,
    withFileTypes: true
  });
  for (const file of files) {
    if (file.isDirectory() || options.exclude?.includes(file.name)) continue;
 
    const filePath = path.relative(this.packageInstallPath, file.parentPath || file.path);
    const isJson = file.name.endsWith('.json');
    await this.parseFile(
      path.join(filePath, file.name),
      options.extraParams,
      isJson,
      isJson ? (data) => JSON.parse(data) : undefined
    );
  }
}

5. Helper Methods

Get Package Manager Command

Returns the appropriate command for the selected package manager:

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

Implementing a Custom Package Builder

When creating a custom package for AI Scaffolding, you need to extend the PackageBuilder class:

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'), // Path to your template files
      config.packages.mypackage.name // Package name
    );
  }
 
  public build(task: ListrTaskArg, nextSteps: string[]): ListrTaskList {
    // Add package-specific next steps
    this._addNextSteps(nextSteps);
 
    // Define build tasks
    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']
          });
        }
      },
      // Add more tasks specific to your package
    ]);
  }
 
  private _addNextSteps(nextSteps: string[]) {
    // Add package-specific next steps to guide users
    const pkgCmd = this.config.packageManager;
    const { packages } = this.config;
 
    nextSteps.push(
      white(`Run \`${pkgCmd} ${packages.mypackage.name} start\``)
    );
  }
}

Best Practices

1. Organize Build Tasks

Organize build tasks into logical groups and make them descriptive:

return task.newListr([
  // Group 1: Copy files
  {
    title: 'Copy package files',
    task: this.copyPackageFiles.bind(this)
  },
  // Group 2: Setup configuration
  {
    title: 'Setup configuration',
    task: async (_, subtask) => subtask.newListr([
      {
        title: 'Create .env.example',
        task: async () => { /* ... */ }
      },
      {
        title: 'Configure network settings',
        task: async () => { /* ... */ }
      }
    ])
  }
]);

2. Add Helpful Next Steps

Always add next steps to guide users after project creation:

private _addNextSteps(nextSteps: string[]) {
  nextSteps.push(
    white(`Create .env file based on .env.example`),
    white(`Run \`${pkgCmd} ${packages.mypackage.name} compile\``)
  );
}

3. Handle Conditional Logic

Use the configuration object to conditionally include or exclude features:

if (this.config.packages.mypackage.useFeatureA) {
  // Only include Feature A tasks if selected
  tasks.push({
    title: 'Setup Feature A',
    task: async () => { /* ... */ }
  });
}

4. Process Handlebars Templates

Make sure to process any files containing Handlebars templates:

await this.parseAllFiles({
  exclude: ['README.md', 'package.json'], // Already processed separately
  extraParams: {
    customParam: 'value' // Additional parameters
  }
});

Related Documentation