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
- Main Builder - The orchestrator that calls package builders
- Package Architecture - How to create a new package
- Handlebars Templates - Using Handlebars in AI Scaffolding