A scaffolding creation tool.
- 🗝 Simple and easy to use with clean design
- 🛠️ Template-based project generation
- ⚙️ Interactive CLI configuration
- 📦 Support for multiple templates
- 🧩 EJS template rendering
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
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();
}
- 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
// 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 %>
// 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();
}
// 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();
}
// 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,
};
}
});
}
When publishing npm packages, .gitignore
and .npmignore
files are ignored by default. The conventional approach is:
- Rename
.gitignore
and.npmignore
to_gitignore
and_npmignore
- 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('_', '.'),
}));
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;
}
/**
* 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;
};
/**
* 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;
};
/**
* 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;
};
/**
* 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;
};
/**
* 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;
class ExitError extends Error {
exitCode: number;
constructor(message: string);
}
Triggered before creation
Triggered when creation starts
creator.on('written', (fileMeta: FileMeta, data: CreatorData<T>, override?: OverrideWrite) => unknown)
Triggered after file is written
Triggered when creation ends
Intercept file writing. For example:
- If
ssr
is configured, generatesrc/client.ts
andsrc/server.ts
- Otherwise
- If source file is
client.ts
, rename toindex.ts
- If source file is
server.ts
, don't generate
- If source file is
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
}
})
/**
* 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;
MIT