Skip to content

Latest commit

 

History

History
580 lines (465 loc) · 11.6 KB

create-template.md

File metadata and controls

580 lines (465 loc) · 11.6 KB

Writing Custom Templates from Scratch

English | 简体中文

In reading this section, you'll learn how to create and distribute your own template.

Template structure

└── my-template
    ├── template ··················· Template source files directory (Required, Can be configured with other names)
    │   ├── lib ···················· Any directory (Recurse all subdirectories)
    │   │   ├── {name}.js ·········· Any file name with interpolate (Auto rename by answers)
    │   │   └── logo.png ··········· Any file without interpolate (Auto skip binary file)
    │   └── package.json ··········· Any file contents with interpolate (Auto render interpolate by answers)
    ├── index.js ··················· Entry point (Optional, Template configuration file)
    ├── package.json ··············· Package info (Optional)
    └── README.md ·················· README (Optional)

Generate a template from a template

We built a template to help users get started with their own template.

$ caz template my-template

Feel free to use it to bootstrap your own template once you understand the below concepts.

Configuration

A template repo may have a configuration file for the template which can be either a index.js or main field defined in package.json.

It must export an object:

module.exports = {
  // your config...
}

Type: Template

The configuration file can contain the following fields:

name

The name of the template.

  • Type: string
module.exports = {
  name: 'my-template'
}

version

The version of the template.

  • Type: string
module.exports = {
  version: '0.1.0'
}

source

Template source files directory.

  • Type: string
  • Default: 'template'
module.exports = {
  source: 'template'
}

metadata

The metadata you can use in the template files.

  • Type: Record<string, unknown>
module.exports = {
  metadata: {
    bio: 'my template generated',
    year: new Date().getFullYear()
  }
}

Upon metadata definition, they can be used in template files as follows:

<%= bio %>
// => 'my template generated'
<%= year %>
// => 2022 (current year)

prompts

Interactive prompts, use prompts, please refer to prompts docs.

  • Type: PromptObject | PromptObject[]
  • Default: '{ name: 'name', type: 'text', message: 'Project name' }'
module.exports = {
  prompts: [
    { name: 'name', type: 'text', message: 'Project name' },
    { name: 'version', type: 'text', message: 'Project version' },
    { name: 'sass', type: 'confirm', message: 'Use sass preprocessor?', initial: true }
  ]
}

The following keys automatically assign initial values (from other config or system info):

  • name - destination path basename, fallback: path.basename(dest)
  • version - npm init config, fallback: '0.1.0'
  • author - npm or git name config
  • email - npm or git email config
  • url - npm or git url config

The following keys automatically assign default validater:

Upon prompts answers, they can be used in template files as follows:

<%= name %>
// => User input text

<%= version %>
// => User input text

<% if (sass) { %>
// use sass preprocessor
<% } %>

filters

Filter files that you want to output.

  • Type: Record<string, (answers: Answers) => boolean>
module.exports = {
  prompts: [
    { name: 'sass', type: 'confirm', message: 'Use sass preprocessor?', initial: true }
  ],
  filters: {
    '*/*.scss': answers => answers.sass,
    '*/*.css': answers => !answers.sass
  }
}

helpers

Custom template engine helpers.

  • Type: Record<string, any>
  • Default: { _: require('lodash') }
module.exports = {
  helpers: {
    upper: input => input.toUpperCase()
  }
}

Upon registration, they can be used in template files as follows:

<%= upper('zce') %>
// => 'ZCE'

// lodash is always
<%= _.camelCase('wow caz') %>
// => 'wowCaz'

install

Auto install dependencies after generation.

  • Type: false | 'npm' | 'yarn' | 'pnpm'
  • Default: According generated files contains package.json
module.exports = {
  // run `yarn install` after files emit.
  install: 'yarn'
}

init

Auto init git repository after generation.

  • Type: boolean
  • Default: According generated files contains .gitignore
module.exports = {
  // run `git init && git add && git commit` after files emit.
  init: true
}

setup

Template setup hook, execute after template loaded & inquire completed.

  • Type: (ctx: Context) => Promise<void>
  • Ref: Context
module.exports = {
  setup: async ctx => {
    // You can get the following data in context
    const {
      template,
      project,
      options,
      dest,
      src,
      config,
      answers // inquire answers
    } = ctx
    console.log('template setup', ctx)
  }
}

Examples:

Package manager choose.

module.exports = {
  // ...
  prompts: [
    {
      name: 'install',
      type: 'confirm',
      message: 'Install dependencies',
      initial: true
    },
    {
      name: 'pm',
      type: prev => prev ? 'select' : null,
      message: 'Package manager',
      hint: ' ',
      choices: [
        { title: 'npm', value: 'npm' },
        { title: 'yarn', value: 'yarn' }
      ]
    }
  ],
  setup: async ctx => {
    // Execute install according to user's choice.
    ctx.config.install = ctx.answers.install && ctx.answers.pm
  }
}

Dynamic setting template files directory.

module.exports = {
  // ...
  prompts: [
    {
      name: 'features',
      type: 'multiselect',
      message: 'Project features',
      instructions: false,
      choices: [
        { title: 'TypeScript', value: 'typescript', selected: true }
        // ....
      ]
    }
  ],
  setup: async ctx => {
    // Dynamic setting template files directory.
    ctx.config.source = ctx.answers.features.includes('typescript')
      ? 'template/typescript'
      : 'template/javascript'
  }
}

Other settings, use your creativity as much as possible...

prepare

Template prepare hook, execute after template files prepare, before rename & render.

  • Type: (ctx: Context) => Promise<void>
  • Ref: Context
module.exports = {
  prepare: async ctx => {
    // You can get the following data in context
    const {
      template,
      project,
      options,
      dest,
      src,
      config,
      answers,
      files // before rename & render
    } = ctx
    console.log('template prepare', ctx)
  }
}

Examples:

Add files to be generated dynamically.

module.exports = {
  prepare: async ctx => {
    ctx.files.push({
      path: 'additional.txt',
      contents: Buffer.from('<%= name %> additional contents')
    })
  }
}

emit

Template emit hook, execute after all files emit to the destination.

  • Type: (ctx: Context) => Promise<void>
  • Ref: Context
module.exports = {
    // You can get the following data in context
  emit: async ctx => {
    const {
      template,
      project,
      options,
      dest,
      src,
      config,
      answers,
      files // after rename & render
    } = ctx
    console.log('template emit')
  }
}

complete

Generate completed callback. if got a string, print it to the console.

  • Type: string or (ctx: Context) => string | Promise<void | string>
  • Default: Log all generated files.
  • Ref: Context

callback

module.exports = {
  complete: async ctx => {
    // ctx => all context
    console.log('  Happy hacking ;)')
  }
}

or string

module.exports = {
  complete: '  Happy hacking ;)'
}

For more examples, please refer to the fixtures.

Core Types

Context

/**
 * Creator context.
 */
interface Context {
  /**
   * Template name.
   * e.g.
   * - offlical short name: `nm`
   * - offlical short name with branch: `nm#master`
   * - custom full name: `zce/nm`
   * - custom full name with branch: `zce/nm#master`
   * - local directory path: `~/templates/nm`
   * - full url: `https://github.com/zce/nm/archive/master.zip`
   */
  readonly template: string
  /**
   * Project name, which is also the project directory.
   */
  readonly project: string
  /**
   * More options.
   */
  readonly options: Options & Record<string, any>
  /**
   * The source directory where the template (absolute).
   */
  src: string
  /**
   * Generated result output destination directory (absolute).
   */
  dest: string
  /**
   * Template config.
   */
  readonly config: Template
  /**
   * Template prompts answers.
   */
  readonly answers: Answers<string>
  /**
   * Template files.
   */
  readonly files: File[]
}

Template

/**
 * Template config.
 */
export interface Template {
  /**
   * Template name.
   */
  name: string
  /**
   * Template version.
   */
  version?: string
  /**
   * Template source dirname.
   */
  source?: string
  /**
   * Template metadata.
   */
  metadata?: Record<string, unknown>
  /**
   * Template prompts.
   */
  prompts?: PromptObject | PromptObject[]
  /**
   * Template file filters.
   */
  filters?: Record<string, (answers: Answers<string>) => boolean>
  /**
   * Template engine helpers.
   */
  helpers?: Record<string, unknown>
  /**
   * Auto install dependencies.
   */
  install?: false | 'npm' | 'yarn' | 'pnpm'
  /**
   * Auto init git repository.
   */
  init?: boolean
  /**
   * Template setup hook, execute after template loaded & inquire completed.
   */
  setup?: (ctx: Context) => Promise<void>
  /**
   * Template prepare hook, execute after template files prepare, before rename & render.
   */
  prepare?: (ctx: Context) => Promise<void>
  /**
   * Template emit hook, execute after all files emit to the destination.
   */
  emit?: (ctx: Context) => Promise<void>
  /**
   * Template all completed.
   */
  complete?: ((ctx: Context) => string | Promise<string> | Promise<void>) | string
}

File

/**
 * File info.
 */
export interface File {
  /**
   * File full path
   */
  path: string
  /**
   * File contents (buffer)
   */
  contents: Buffer
}

Dependencies

Because the template will automatically install its production dependencies before it works, so you can normally use the third-party NPM module in the template configuration file.

e.g.

Install chalk as production dependencies:

$ npm install chalk --save

index.js:

const chalk = require('chalk')

NOTE: Only production dependencies are automatically installed.

Type Annotation

Install caz as devDependencies:

$ npm install caz --save-dev

Then in your template configuration file:

/** @type {import('caz').Template} */
module.exports = {
  // Have type hint and IntelliSense (VSCode)
}

Template Paraphrase

If you want direct output template interpolate, like this:

  • <%= '\<%= name %\>' %> => <%= name %>
  • <%= '${name}' %> => ${name}