Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Appearance settings

tinyplex/tinycreate

Open more actions menu

Repository files navigation

TinyCreate

A flexible CLI framework for scaffolding projects with templates.

Overview

TinyCreate is a powerful templating engine and CLI framework designed for creating project generators. It combines interactive prompts, Handlebars templates, and smart post-processing to generate complete projects from templates with conditional logic, automatic imports, and code formatting.

Features

  • Interactive Prompts - Built-in support for text, select, and confirm questions
  • Smart Templates - Handlebars with custom helpers for common patterns
  • Automatic Import Management - Colocate imports with conditional logic
  • Intelligent Transpilation - Convert TypeScript to JavaScript on demand
  • Code Formatting - Built-in Prettier support with import organization
  • File Inclusion System - Compose templates from reusable components
  • Non-Interactive Mode - Full CLI argument support for CI/CD
  • Package Manager Detection - Automatically uses npm, yarn, pnpm, or bun

Installation

npm install tinycreate

Quick Start

import {createCLI} from 'tinycreate';
import {dirname, join} from 'path';
import {fileURLToPath} from 'url';

const __dirname = dirname(fileURLToPath(import.meta.url));

const config = {
  welcomeMessage: '🎉 Welcome to My Generator!\n',

  questions: [
    {
      type: 'text',
      name: 'projectName',
      message: 'Project name:',
      initial: 'my-app',
    },
    {
      type: 'select',
      name: 'language',
      message: 'Language:',
      choices: [
        {title: 'TypeScript', value: 'typescript'},
        {title: 'JavaScript', value: 'javascript'},
      ],
    },
  ],

  createContext: (answers) => ({
    projectName: answers.projectName,
    isTypescript: answers.language === 'typescript',
    ext: answers.language === 'typescript' ? 'ts' : 'js',
  }),

  getFiles: (context) => [
    {
      template: 'templates/App.tsx.hbs',
      output: `src/App.${context.ext}`,
      prettier: true,
      transpile: context.isTypescript === false,
    },
  ],

  templateRoot: __dirname,

  onSuccess: (projectName) => {
    console.log(`✅ Created ${projectName}!`);
    console.log(`\nNext steps:`);
    console.log(`  cd ${projectName}`);
    console.log(`  npm install`);
    console.log(`  npm run dev`);
  },
};

await createCLI(config);

Custom Handlebars Helpers

TinyCreate extends Handlebars with powerful custom helpers designed for code generation:

{{addImport}}

Automatically manage imports at the top of files, keeping them colocated with conditional logic:

{{#if isReact}}
  {{addImport "import React from 'react';"}}
{{/if}}
{{#if isTypescript}}
  {{addImport "import type {FC} from 'react';"}}
{{/if}}

export const Component = () => { return
<div>Hello</div>; };

Output (when both conditions are true):

import React from 'react';
import type {FC} from 'react';

export const Component = () => {
  return <div>Hello</div>;
};

All imports added with {{addImport}} are automatically:

  • Deduplicated
  • Moved to the top of the file
  • Preserved in their original order

{{#list}}...{{/list}}

Generate comma-separated lists with automatic formatting:

const dependencies = {
{{#list}}
  "react": "^18.0.0"
  {{#if includeRouter}}
    "react-router": "^6.0.0"
  {{/if}}
  "lodash": "^4.17.21"
{{/list}}
};

Output (when includeRouter is true):

const dependencies = {
  react: '^18.0.0',
  'react-router': '^6.0.0',
  lodash: '^4.17.21',
};

The {{#list}} helper:

  • Automatically adds commas between items
  • Removes trailing comma from last item
  • Handles conditional items gracefully
  • Filters out empty lines and comments

{{eq}}

Compare values in conditional blocks:

{{#if (eq framework 'react')}}
  import React from 'react';
{{/if}}

{{includeFile}}

Compose templates from other templates and track file dependencies:

{{includeFile
  template='components/Button.tsx.hbs'
  output='src/Button.tsx'
  prettier=true
}}
import {Button} from './Button'; export const App = () => (
<Button>Click me</Button>
);

This directive:

  • Signals that Button.tsx should be generated
  • Allows the parent file to import it
  • Processes the included template with the same context
  • Supports prettier and transpile options per file

API Reference

createCLI(config, options)

Main entry point for creating a CLI generator.

Config Options:

interface ProjectConfig {
  // Optional welcome message shown before prompts
  welcomeMessage?: string;

  // Array of question objects (text, select, or confirm)
  questions: Question[];

  // Transform user answers into template context
  createContext: (answers: Record<string, unknown>) => TemplateContext;

  // Return array of files to generate
  getFiles: (context: TemplateContext) => FileConfig[] | Promise<FileConfig[]>;

  // Optional: Process included files before rendering
  processIncludedFile?: (
    file: FileConfig,
    context: TemplateContext,
  ) => FileConfig;

  // Root directory containing templates
  templateRoot: string;

  // Optional: Create custom directories
  createDirectories?: (
    targetDir: string,
    context: TemplateContext,
  ) => Promise<void>;

  // Optional: Custom install command (default: auto-detected package manager)
  installCommand?: string;

  // Optional: Custom dev command
  devCommand?: string;

  // Optional: Success callback after project creation
  onSuccess?: (
    projectName: string,
    context: TemplateContext,
  ) => void | Promise<void>;
}

CLI Options:

interface CLIOptions {
  // Enable non-interactive mode for CI/CD
  nonInteractive?: boolean;

  // Custom CLI arguments (defaults to process.argv.slice(2))
  args?: string[];
}

Question Types

interface Question {
  // Question type (or function returning type based on previous answers)
  type:
    | 'text'
    | 'select'
    | 'confirm'
    | ((
        prev: unknown,
        answers: Record<string, unknown>,
      ) => 'text' | 'select' | 'confirm' | null);

  // Answer key name
  name: string;

  // Question prompt
  message: string;

  // Default value
  initial?: string | number | boolean;

  // Choices for 'select' type
  choices?: Array<{title: string; value: string | boolean}>;

  // Validation function for 'text' type
  validate?: (value: string) => boolean | string;
}

FileConfig

interface FileConfig {
  // Path to Handlebars template (relative to templateRoot)
  template: string;

  // Output path (supports template variables like {{projectName}})
  output: string;

  // Enable Prettier formatting (default: false)
  prettier?: boolean;

  // Enable TypeScript to JavaScript transpilation (default: false)
  transpile?: boolean;

  // Internal: processed content (set by engine)
  processedContent?: string;
}

Post-Processing

import {postProcessFile, postProcessProject} from 'tinycreate';

// Process a single file
const {filePath, content} = await postProcessFile('src/App.tsx', fileContent, {
  prettier: true,
  transpileToJS: true,
});

// Process multiple files
const processedFiles = await postProcessProject('/path/to/project', filesMap, {
  prettier: true,
  transpileToJS: false,
});

Non-Interactive Mode

TinyCreate supports non-interactive mode for use in CI/CD or automated scripts:

node cli.js --non-interactive \
  --projectName my-app \
  --language typescript \
  --framework react

Any question without a provided argument will use its initial value.

Advanced Patterns

Conditional Questions

Questions can be conditional based on previous answers:

{
  type: (prev, answers) =>
    answers.language === 'typescript' ? 'confirm' : null,
  name: 'strictMode',
  message: 'Enable strict mode?',
  initial: true,
}

If the function returns null, the question is skipped.

Dynamic File Generation

Generate files based on context:

getFiles: (context) => {
  const files = [{template: 'App.tsx.hbs', output: 'src/App.tsx'}];

  if (context.includeTests) {
    files.push({
      template: 'App.test.tsx.hbs',
      output: 'src/App.test.tsx',
    });
  }

  return files;
};

Custom Directory Structure

Create custom directories before file generation:

createDirectories: async (targetDir, context) => {
  const {mkdir} = await import('fs/promises');

  await mkdir(join(targetDir, 'src', 'components'), {recursive: true});
  await mkdir(join(targetDir, 'public'), {recursive: true});

  if (context.includeServer) {
    await mkdir(join(targetDir, 'server'), {recursive: true});
  }
};

Processing Included Files

Modify included files before rendering:

processIncludedFile: (file, context) => {
  // Force all component files to be formatted
  if (file.output.includes('components/')) {
    return {...file, prettier: true};
  }
  return file;
};

Examples

See the create-tinybase project for a complete real-world example that uses TinyCreate to generate TinyBase applications with multiple frameworks, languages, and configurations.

Testing

npm test

TinyCreate includes comprehensive tests covering:

  • Template engine functionality
  • Import management
  • List formatting
  • File inclusion
  • Post-processing
  • TypeScript transpilation

Requirements

  • Node.js >= 18.0.0

Dependencies

  • handlebars - Template rendering
  • prompts - Interactive CLI prompts
  • esbuild - Fast TypeScript transpilation
  • prettier - Code formatting

License

MIT License - see LICENSE file for details.

About

A flexible CLI framework for scaffolding projects with templates.

Topics

Resources

License

Contributing

Stars

Watchers

Forks

Contributors

Morty Proxy This is a proxified and sanitized view of the page, visit original site.