Skip to content

Create Your First Package

This guide will walk you through creating your first custom package for AI Scaffolding.

We'll create a simple NodeJS module that exports a "Hello World" function.

Prerequisites

Before starting, make sure you have:

  1. A basic understanding of TypeScript/JavaScript
  2. The AI Scaffolding codebase cloned and set up
  3. Node.js and pnpm installed

Package Structure Overview

Our simple NodeJS package will have the following structure:

src/packages/nodejs/
├── files/               # Template files
│   ├── package.json     # Package.json template 
│   ├── README.md        # README template
│   └── src/             # Source files
│       ├── index.ts     # Main entry point
│       └── hello.ts     # Hello world module
├── builder.ts           # Package builder logic
└── prompt.ts            # User prompts for configuration

Steps

1 Create the Package Directory Structure

First, let's create the necessary directories for our package:

Terminal
mkdir -p src/packages/nodejs/files/src

2 Create the Prompt Module

Create src/packages/nodejs/prompt.ts to define the user configuration options:

src/packages/nodejs/prompt.ts
import { input, confirm } from '@inquirer/prompts'
import { PackagePrompt } from '../../types.js';
 
export interface NodeJsConfig {
  useTypescript: boolean
  includeTests: boolean
  [key: string]: unknown
}
 
const prompt: PackagePrompt<NodeJsConfig> = async (ctx) => {
  const name = await input({
    message: 'Enter the name of the NodeJS package:',
    default: 'nodejs',
    transformer: (value: string, { isFinal }) => 
      isFinal ? `@${ctx.projectName.toLowerCase()}/${value}` : value
  });
 
  const useTypescript = await confirm({
    message: 'Do you want to use TypeScript?',
    default: true
  });
 
  const includeTests = await confirm({
    message: 'Do you want to include tests?',
    default: true
  });
 
  return {
    name,
    useTypescript,
    includeTests
  }
}
 
export default prompt;

3 Create the Builder Class

Create src/packages/nodejs/builder.ts to define how the package will be built:

src/packages/nodejs/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 NodeJsBuilder extends PackageBuilder {
  constructor(config: ProjectConfigV2) {
    super(
      config,
      path.join(__dirname, 'files'),
      config.packages.nodejs.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[]) {
    const pkgCmd = this.config.packageManager;
    const { packages } = this.config;
 
    nextSteps.push(
      white(`Run \`${pkgCmd} ${packages.nodejs.name} start\``),
      white(`Run \`${pkgCmd} ${packages.nodejs.name} test\`` + 
        (this.config.packages.nodejs.includeTests ? '' : ' (after adding tests)'))
    );
  }
}

4 Create the Template Files

4.1 Package.json Template

Create src/packages/nodejs/files/package.json:

src/packages/nodejs/files/package.json
{
  "name": "{{name}}",
  "version": "0.1.0",
  "private": true,
  "main": "{{#if useTypescript}}dist/index.js{{else}}src/index.js{{/if}}",
  "scripts": {
    "start": "{{#if useTypescript}}ts-node src/index.ts{{else}}node src/index.js{{/if}}",
    "build": "{{#if useTypescript}}tsc{{else}}echo \"No build step needed\"{{/if}}",
    {{#if includeTests}}
    "test": "{{#if useTypescript}}jest{{else}}node --test{{/if}}"
    {{/if}}
  },
  "dependencies": {
  },
  "devDependencies": {
    {{#if useTypescript}}
    "typescript": "^5.0.0",
    "ts-node": "^10.9.1",
    "@types/node": "^18.0.0",
    {{/if}}
    {{#if includeTests}}
    {{#if useTypescript}}
    "jest": "^29.0.0",
    "ts-jest": "^29.0.0",
    "@types/jest": "^29.0.0"
    {{/if}}
    {{/if}}
  }
}

4.2 README Template

Create src/packages/nodejs/files/README.md:

src/packages/nodejs/files/README.md
# {{name}}
 
A simple NodeJS module created with AI Scaffolding.
 
## Features
 
- Simple hello world function
- {{#if useTypescript}}TypeScript support{{else}}JavaScript{{/if}}
- {{#if includeTests}}Includes tests{{else}}Easily extendable{{/if}}
 
## Getting Started
 
```bash
# Install dependencies
{{packageManagerCmd}} {{name}} install
 
# Run the application
{{packageManagerCmd}} {{name}} start
 
{{#if includeTests}}
# Run tests
{{packageManagerCmd}} {{name}} test
{{/if}}
\```
 
## Usage
 
```javascript
const { hello } = require('{{name}}');
 
// Call the hello function
hello('World'); // Returns: "Hello, World!"
\```

3. Hello World Module

Create src/packages/nodejs/files/src/hello.ts:

src/packages/nodejs/files/src/hello.ts
/**
 * A simple hello world function
 * @param name The name to greet
 * @returns A greeting message
 */
export function hello(name: string): string {
  return `Hello, ${name}!`;
}

4.3 Index File

Create src/packages/nodejs/files/src/index.ts:

src/packages/nodejs/files/src/index.ts
/*#{{#if useTypescript}}*/
export { hello } from './hello';
/*#{{else}}*/
const { hello } = require('./hello');
module.exports = { hello };
/*#{{/if}}*/
 
// Log a message when the module is imported
console.log('NodeJS module initialized');

5 Register the Package

Update src/packages/index.ts to include your new package:

src/packages/index.ts
// Add these imports at the top
import nodejsPrompt, { NodeJsConfig } from './nodejs/prompt.js'
import NodeJsBuilder from './nodejs/builder.js'
 
// Add to the Packages object
export const Packages: PackageExport = {
  hardhat: {
    prompt: hardhatPrompt as PackagePrompt<HardhatConfig>,
    builder: HardhatBuilder
  },
  // ... existing packages
  nodejs: {
    prompt: nodejsPrompt as PackagePrompt<NodeJsConfig>,
    builder: NodeJsBuilder
  }
};
 
// Add to packagesPrompt choices
export async function packagesPrompt() {
  const packages = await checkbox<string>({
    message: 'Select packages to install:',
    choices: [
      { name: 'Hardhat', value: 'hardhat' },
      // ... existing choices
      { name: 'NodeJS', value: 'nodejs' },
    ],
    required: true,
    pageSize: 20,
  })
  // ...
}
 
// Export your config type at the end of the file
export { NodeJsConfig } from './nodejs/prompt.js'

6 Update TypeScript Types

Update src/types.ts to include the new package type:

src/types.ts
// Add import at the top
import { NodeJsConfig } from './packages/nodejs/prompt.js'
 
// Update ProjectConfigV2 interface to include the new package
export interface ProjectConfigV2 {
  projectName: string;
  packageManager: PackageManager;
  projectPath: string;
  targetDir: string;
  packages: Record<'hardhat', PackageConfig<HardhatConfig>>
    & Record<'vite', PackageConfig<ViteConfig>>
    & Record<'nextjs', PackageConfig<NextjsConfig>>
    & Record<'nestjs', PackageConfig<NestJsConfig>>
    & Record<'nodejs', PackageConfig<NodeJsConfig>>; 
  global: GlobalConfig;
  skipInstall: boolean;
}

7 Build and Test

Now build the project:

Terminal
pnpm build

Then test your new package:

Terminal
# Run the CLI
pnpm dev tmp/test-project

During setup, select the NodeJS package and configure it according to your preferences.

Understanding What's Happening

When the user selects your NodeJS package during project creation:

  1. The prompt.ts file is executed to collect configuration options (TypeScript, tests, etc.)
  2. The collected options are stored in the project config object
  3. The MainBuilder creates the project structure
  4. The NodeJsBuilder is instantiated with the config
  5. The build method is called to create the NodeJS package
  6. Template files are copied and processed with Handlebars
  7. The code is generated with the user's selected options

Extending the Package

You can extend your package in several ways:

  1. Add more prompts: Collect more configuration options in prompt.ts
  2. Add more templates: Create additional template files in the files directory
  3. Add build steps: Implement additional build tasks in the build method

Advanced Features

For more advanced packages, you might want to:

  • Add dependencies based on user selections
  • Generate different file structures based on configuration
  • Integrate with other packages (e.g., connect to Hardhat if it's selected)
  • Add more sophisticated template logic

Conclusion

Congratulations! You've created your first package for AI Scaffolding.

This simple example demonstrates the core concepts of creating packages and can serve as a foundation for more complex implementations.

Next Steps