Skip to content

Latest commit

 

History

History
548 lines (477 loc) · 12.1 KB

README-en.md

File metadata and controls

548 lines (477 loc) · 12.1 KB

➡️ 中文

create-creator

code-review dependency-review Codacy Badge Codacy Badge npm version

A scaffolding creation tool.

Features

  • 🗝 Simple and easy to use with clean design
  • 🛠️ Template-based project generation
  • ⚙️ Interactive CLI configuration
  • 📦 Support for multiple templates
  • 🧩 EJS template rendering

Installation & Usage

npm create creator my-creator

Need to install the following packages:
create-creator@2.0.0
Ok to proceed? (y)

> npx
> create-creator my-creator

┌   create-creator@2.0.0
│
●  Create a creator - npm create creator
│
▲  The project directory is: /path/to/my-creator
│
◇  Select node version
│  v22.x
│
◇  Select npm registry
│  npm official
│
◇  Select code linter
│  biome
│
◆  Git repository initialized
│
◆  The project has been created successfully!
│
◆  cd my-creator to start your coding journey
│
└  🎉🎉🎉

my-creator
├── .editorconfig
├── .gitignore
├── .npmrc
├── .nvmrc
├── README.md
├── bin
│   └── index.cjs
├── biome.jsonc
├── commitlint.config.mjs
├── lefthook.yml
├── package.json
├── src
│   ├── const.ts
│   ├── dts
│   │   ├── global.d.ts
│   │   └── types.d.ts
│   └── index.ts
├── templates
│   └── default
│       └── README.md.ejs
├── test
│   └── sample.test.ts
├── tsconfig.json
└── vite.config.mts

Open src/index.ts to customize creation logic

import { Creator } from 'create-creator';

export async function createCLI() {
  const creator = new Creator({
    projectPath: process.argv[2],
    templatesRoot: path.join(__dirname, '../templates'),
  });

  // create method won't throw errors, no need to catch
  await creator.create();
}

Open templates to write template files

  • templates is the root directory for templates
  • templates/default is a specific template directory, can be any name
  • If there are multiple directories under templates, users can choose during project creation

Examples

Extend custom data

// src/index.ts
export async function createCLI() {
  const creator = new Creator({
    // ... other options
    async extendData({ prompts }) {
      // Add custom data
      return {
        timestamp: Date.now(),
        author: 'Your Name'
      };
    }
  });
  await creator.create();
}
// templates/default/README.md.ejs
# <%= ctx.projectName %>

Created by: <%= author %>
Created at: <%= timestamp %>

Render different template files based on conditions

// src/index.ts
export async function createCLI() {
  const creator = new Creator({
    // ... other options
  });

  // Don't generate eslint related files if eslint is not selected
  creator.writeIntercept(['eslint*', '.eslint*'], (meta, data) => ({
    disableWrite: data.codeLinter !== 'eslint',
  }));

  // Don't generate biome related files if biome is not selected
  creator.writeIntercept(['biome*'], (meta, data) => ({
    disableWrite: data.codeLinter !== 'biome',
  }));

  await creator.create();
}

Print related logs

// src/index.ts
export async function createCLI() {
  const creator = new Creator({
    // ... other options
    onWritten(meta, data) {
      console.log(`Created file: ${meta.targetPath}`);
    }
  });

  creator.on('before', ({prompts}) => {
    prompts.log.info('Output some banner information');
  });

  creator.on('start', ({prompts}) => {
    prompts.log.info('Starting new project creation');
  });

  creator.on('written', (meta, data, override) => {
    data.ctx.prompts.log.info(`File written: ${meta.targetPath}`);
  });

  creator.on('end', ({prompts}, meta) => {
    prompts.log.info('Creation successful');
  });

  await creator.create();
}

Custom CLI selection interaction

// src/index.ts
import { promptSafe } from 'create-creator';

export async function createCLI() {
  const creator = new Creator({
    // ... other options
    async extendData({ prompts }) {
      const tabSize = await promptSafe(prompts.select({
        message: 'Select your preferred tab size',
        choices: [
          {
            value: 2,
            label: '2 spaces'
          },
          {
            value: 4,
            label: '4 spaces'
          }
        ]
      }))
      // Add custom data
      return {
        // type is number
        tabSize,
      };
    }
  });
}

Special dot files

When publishing npm packages, .gitignore and .npmignore files are ignored by default. The conventional approach is:

  1. Rename .gitignore and .npmignore to _gitignore and _npmignore
  2. Add custom interceptors for special handling
// Rename _gitignore and _npmignore files in any directory to .gitignore and .npmignore
creator.writeIntercept(['**/_gitignore', '**/_npmignore'], (meta) => ({
  targetFileName: meta.targetFileName.replace('_', '.'),
}));

API

Creator Class

class Creator<T extends Record<string, unknown>> {
  constructor(options: CreatorOptions<T>);

  /**
   * Start project creation
   */
  create(): Promise<void>;

  /**
   * Intercept file writing
   * @param paths File path patterns to intercept
   * @param interceptor Interceptor function
   */
  writeIntercept(
    paths: string | string[],
    interceptor: WriteInterceptor
  ): void;

  /**
   * Register event listeners
   * @param event Event name
   * @param listener Listener function
   */
  on(event: 'before' | 'start' | 'written' | 'end', listener: (...args: any[]) => void): void;
}

CreatorOptions

/**
 * Configuration options for the creator
 * @template T - Type of custom data to extend with
 */
export type CreatorOptions<T> = {
  /**
   * Current working directory (default: process.cwd())
   */
  cwd?: string;
  /**
   * Path to project directory
   */
  projectPath?: string;
  /**
   * Root directory containing templates
   */
  templatesRoot: string;
  /**
   * Convert creation context to template options
   * @param context - The creation context containing information about the current process
   * @returns Array of template options or promise resolving to array of template options
   */
  toTemplateOptions?: (context: CreatorContext) => TemplateOption[] | Promise<TemplateOption[]>;
  /**
   * Extend template data with custom properties
   */
  extendData?: (context: CreatorContext) => T | Promise<T>;
  /**
   * Check for updates
   */
  checkUpdate?: CheckPkgUpdate & { version: string };
  /**
   * Check Node.js version
   */
  checkNodeVersion?: number;
};

FileMeta

/**
 * Metadata about files being processed
 */
export type FileMeta = {
  /**
   * Whether file uses EJS templating
   */
  isEjsFile: boolean;
  /**
   * Whether file uses underscore prefix
   */
  isUnderscoreFile: boolean;
  /**
   * Whether file uses dot prefix
   */
  isDotFile: boolean;

  /**
   * Root directory of source files
   */
  sourceRoot: string;
  /**
   * Name of source file
   */
  sourceFileName: string;
  /**
   * Relative path to source file
   */
  sourcePath: string;
  /**
   * Full path to source file
   */
  sourceFile: string;
  /**
   * Root directory of target files
   */
  targetRoot: string;
  /**
   * Name of target file
   */
  targetFileName: string;
  /**
   * Relative path to target file
   */
  targetPath: string;
  /**
   * Full path to target file
   */
  targetFile: string;
};

OverrideWrite

/**
 * Options to override default file writing behavior
 */
export type OverrideWrite = {
  /**
   * Whether to disable EJS rendering for EJS files
   */
  disableRenderEjs?: boolean;

  /**
   * Specify target file name
   */
  targetFileName?: string;

  /**
   * Whether to disable file writing
   * When true, other configurations will be ignored
   */
  disableWrite?: boolean;
};

CreatorContext

/**
 * Context object containing information about the current creation process
 */
export type CreatorContext = {
  /**
   * Current working directory
   */
  cwd: string;
  /**
   * Root directory containing templates
   */
  templatesRoot: string;
  /**
   * Path to selected template directory
   */
  templateRoot: string;
  /**
   * Names of selected template directories
   */
  templateNames: string[];
  /**
   * Name of selected template
   */
  templateName: string;
  /**
   * Root directory of project being created
   */
  projectRoot: string;
  /**
   * Relative path to project directory
   */
  projectPath: string;
  /**
   * Name of project being created
   */
  projectName: string;
  /**
   * Current write mode (overwrite/clean/cancel)
   */
  writeMode: WriteMode;
};

CreatorData<T>

/**
 * Complete template data type combining built-in and custom data
 * @template T - Type of custom data to extend with
 */
export type CreatorData<T> = {
  /**
   * The creation context
   */
  ctx: CreatorContext;
} & T;

ExitError Class

class ExitError extends Error {
  exitCode: number;
  constructor(message: string);
}

Events

creator.on('before', (context: CreatorContext) => unknown)

Triggered before creation

creator.on('start', (context: CreatorContext) => unknown)

Triggered when creation starts

creator.on('written', (fileMeta: FileMeta, data: CreatorData<T>, override?: OverrideWrite) => unknown)

Triggered after file is written

creator.on('end', (context: CreatorContext) => unknown)

Triggered when creation ends

Interceptors

writeIntercept(paths: string | string[], interceptor: WriteInterceptor)

Intercept file writing. For example:

  • If ssr is configured, generate src/client.ts and src/server.ts
  • Otherwise
    • If source file is client.ts, rename to index.ts
    • If source file is server.ts, don't generate
creator.writeIntercept(['*/src/client.ts', '*/src/server.ts'], (fileMeta, data) => {
  if (data.ssr) return {};

  return fileMeta.sourceFileName === 'client.ts'
  // client.ts -> index.ts
  ? {
    targetFileName: 'index.ts'
  }
  // Don't write server.ts
  : {
    disableWrite: true
  }
})

Utility Methods

/**
 * Safely execute prompts operations
 */
function promptSafe<T>(promise: Promise<T | symbol>): Promise<T | symbol>;

/**
 * Initialize Git repository
 */
function initGitRepo(cwd: string): Promise<void>;

/**
 * Check Node.js version
 */
function checkNodeVersion(version: number): Promise<boolean>;

/**
 * Check for updates
 */
function checkUpdate(pkgName: string, currentVersion: string): Promise<boolean>;

/**
 * Select Node.js version
 */
function selectNodeVersion(versions?: number[]): Promise<number>;

/**
 * Select npm registry
 */
function selectNpmRegistry(registries?: string[]): Promise<string>;

/**
 * Select code linter
 */
function selectCodeLinter(linters?: string[]): Promise<string>;

/**
 * Select file write mode
 */
function selectWriteMode(cwd: string, ignoreNames?: string[]): Promise<WriteMode>;

/**
 * Execute shell command
 */
function execCommand(
  command: string,
  options?: ExecOptions
): Promise<[Error | null, { stderr: string; stdout: string; exitCode: number }]>;

/**
 * @see https://www.npmjs.com/package/@clack/prompts
 */
export const prompts = Prompts;

/**
 * @see https://www.npmjs.com/package/picocolors
 */
export const colors = Colors;

License

MIT