0.9.0 • Published 6 years ago

project-templator v0.9.0

Weekly downloads
1
License
MIT
Repository
github
Last release
6 years ago

Project templator

Transform (or generate) a project based on a directory of template files and configurations.

This is an extension of project-template by @aiham that is now highly customizable and designed for composition.

Can be used in combination with sao and similar libs to great effect :)

Requirements

  • Node JS v6+

Install

yarn add project-template

Example

yarn run example

See example/index.js

Usage

const { projectTemplate, projectTemplates } = require('project-template');

projectTemplate({
  templatePath: '/path/to/templates',
  buildPath: '/path/to/build',
  params: {
    'path/to/file.txt': {
      firstParam: 'First param value',
      secondParam: 'Second param value',
    },
    'path/to/different/file.txt': {
      anotherParam: 'Another param value',
    },
  },
})
.then(files => console.log('Done', files))
.catch(err => console.error('Error', err));


projectTemplates({
  src: {
    templatePath: '/path/to/templates/src',
    buildPath: '/path/to/build/lib'
    // ... more src template options
  },
  test: {
    templatePath: '/path/to/templates/test',
    buildPath: '/path/to/build/tests'
    // ... more test template options
  }
}, {
  // generic templating options used as defaults
})

Advanced example

Say we have a template with the following structure. We have variants for the source files, ie. :

  • .ts when using TypeScript
  • .js when using CommonJS
  • .mjs when using ES6 modules and ES2015+ javascript
/template
  /src
    index.ts.ect
    index.js.ect
    index.mjs.ect
    /helpers
      index.ts.ect
      index.js.ect
      index.mjs.ect
  /test
    my-ava.test.js
  /__tests__
    my-jest.test.js

We would like the following project build output:

/my/target
  /src
    index.ts
    /helpers
      index.ts
  /__tests__
    my-jest.test.js
projectTemplate({
  templatePath: '/path/to/templates',

  // use a custom entry option to set destination file extension for all (.js) files
  populateEntry(entry) {
    entry.destExt = entry.fileExt === 'js' ? entry.opts.srcExt : entry.fileExt
    return entry
  },
  // select template file to use based on srcExt option
  resolve: {
    templateFile({destExt, name}) {
      return path.join(name, destExt)
    }
  },
  // ignore template files in folders that don't match convention of test library used
  ignore({dirName}) {
    return opts.testLib === 'jest' ? dirName.test(/__tests__/) : dirName.test(/test\//)
  },
  // renames any files that have .js extension to .ts in target dest
  // via populateEntry custom destExt option
  destPath({ext, destExt, dirName}) {
    return path.join(opts.rootBuildPath, dirName, name, destExt)
  },
  opts: {
    srcExt: 'ts', // used by resolveTemplateFile via populateEntry
    rootBuildPath: '/path/to/build', // used by buildPath
    testLib: 'ava' // used by ignore
  },
  params: {
    filePath: {
      'src/index.js': {
      }
    },
    name: {
      index: {
        firstParam: 'First param value',
        secondParam: 'Second param value',
      },
    },
    type: {
      folder: {
        test: {
          // ...
        }
      },
      file: {
        src(entry) {
          return {
            // dynamic params based on entry
          }
        }
      },
      entity: {
        service: {
          // ...
        }
      }
    }
  }
})

Notice

  • Directory structure of files in templatePath is by default maintained in buildPath but can easily be customized by supplying a destPath function
  • Files in templatePath without matching template extension are just copied over to destPath as they are without template rendering or transformation
  • If you have two files with the same file path where one is a template and one isn't, the template file will be stripped off its template extension and there will then be a matching filePath conflict. You can either choose to abort and flag the conflict an an error or continue with one of the files taking precedence.

API

projectTemplate(options) -> Promise<Array<String>>

projectTemplates(tmplMap, options)

Composability

This project is designed for easy composition of a templating (or file transformation) solution that suits your needs.

It can be used with a number of other libraries, such as:

Custom template engine

You could integrate your own templating engine...

tree/object transformer

See obj-templator for a new type of templator to generate nested objects or tree structures for JSON, YAML or js files.

options

KeyTypeRequiredDefaultNotes
templatePathStringYes'./templates'Directory path containing template files
templateSrcObjectNo{}Src of templates, can be a remote pointer or local filePath
buildPathString/FunctionYesDirectory path to write generated files to (for destPath)
paramsObjectNo{}Keys are relative paths of template files (with fileExtension stripped). Values are objects of template variables
ignoreFilesString[]No[]File paths to ignore (exclude the fileExtension for template files). Also useful for using template partials
fileExtensionStringNoectFile extension of template files
ignoreFunctionNoundefinedWhether to ignore file
optsObjectNo{}Global options
resolveFunction mapNo{}Map of custom resolver functions (see below)
createFunction mapNo{}Map of custom factory functions
populateEntryFunctionNoundefinedCustom function to populate entry
mapsObjectNo{}Map of maps used internally (see below)
transformFileDataFunctionNoundefinedCustom function to transform data before write to disk
prependWithObjectNo{}Map for what to prepend with for different output types
appendWithObjectNo{}Map for what to append with for different output types
warningOnBooleanNofalseenable warnings
infoOnBooleanNofalseenable info messages
loggerObjectNoconsoleCustom logger to log error and info messages

entry

The entry that is passed on in each step consists of the following:

  • filePath file path of the template (stripped of the template extension) such as src/index.js
  • name name of the file such as index
  • dirName name of directory
  • ext file extension (such as js)
  • type (Object) type of file including folder, file and entity
  • params (Object) the params resolved for this entry to be passed to template for rendering
  • config the full configuration passed to projectTemplate
  • isTemplate if it is a template to be rendered with params

You can use populateEntry option to pass a function to further customize the entry being used to suit your specific needs.

{
  fullTemplatePath: '/Users/guest/projects/templates/src/helpers/service-finder.ts.ect',
  templatePath: '/src/helpers/service-finder.ts.ect',
  filePath: 'src/helpers/service-finder.test.ts',
  name: 'service-finder',
  fileName: 'service-finder.ts',
  fileExt: 'ts',
  dirName: 'src/helpers',
  isTemplate: true,
  type: {
    file: 'test:src', // create your own convention
    folder: 'helpers',
    entity: 'service'
  },
  params: {
    // params resolved for entry
  }
  opts: {
    // from config.opts
  },
  config: {
    // ... all the "global" configs passed
  }
}

Templating libs

We recommend using consolidate, the Template engine consolidation library by @TJ of Express/Node.js fame.

Additional template meta data

Use optional template manifest files (or json/yaml)

  • template.manifest.js
  • template.entry.js
  • template.params.js

Can be used to provide meta info for each template included, such as entity type etc. that can not be determined from the filename alone.

Can include params map with type, required and default for each.

Before rendering, can populate missing params with default values then check if supplied params match requirements.

paramDefinitions: {
  // the name param can be reused in multiple params lists below
  string: {
    type: String,
    validate: 'string',
    default: 'unknown',
  }
  name: {
    default: 'john doe',
    required: true,
    inherit: 'string'
  },
  // ...
}

Add map of params supported, then reference in maps for type: file, entity name or fileName

entryData: {
  filePath: {
    'Readme.md': {
      type: 'main.readme',
      params: [
        'title',
        'description',
        ...
      ],
    'docs/Readme.md': {
      type: 'main.docs.readme',
      // ...
    }
  },
  name: {
    'package': {
      type: 'node.package.management',
      params: [
        'author', // reference params definitions
        ...
      ]
    ]
  },
  entity: {
    service: [
      'name',
      'provider'
    ],
  },
  folder: {
    test: [
      'author'
    ]
  }
}

After creating the initial entry, we use it to lookup into this entryType map:

  • look for matching folder, entity, name, filePath in this sequence.

Deep merge all objects matched into one Object. Use this is defaults.populateEntry strategy!

When rendering, validate each param on params about to be sent to template!

resolve

The resolve option can be used to pass one or more custom resolver functions.

KeyTypeRequiredDefaultNotes
templateFileFunctionNoundefinedResolve template file to use from entry
fileTypeFunctionNoundefinedDetermine file type from entry
folderTypeFunctionNoundefinedDetermine folder type from entry
entityTypeFunctionNoundefinedDetermine entity type from entry
paramsFunctionNoundefinedDetermine params to use for entry
templateRendererFunctionNoundefinedCreate custom template renderer
normalizePathFunctionNoundefinedStrip off template extension from filePath

params

The built-in params resolver will lookup in each of the following entry params containers with the key of the entry itself and merge each object it finds into a params object to be passed to the template.

You can pass in your own resolveParams function as an option to fully control how params are resolved to suit your needs.

params: {
  // static params used as fallback
  'default': {

  },
  filePath: {
    'src/index.js': {
    }
  },
  name: {
    index: {
      firstParam: 'First param value',
      secondParam: 'Second param value',
    },
  },
  type: {
    // dynamic params based on entry
    src(entry) {
      return {
      }
    }
  },
  // also available...
  folder: {
  },
  ext: {
  }

transformFileData(entry)

Will by default try to prepend and append data to the file.

prependWith/appendWith

prependWith and appendWith can contain keys for a type of files or folder that the file is in. If the template being iterated matches a key, the prepend is done on the data

prependWith: {
  src: readFile('./src-notice'),
  test: (entry) => {
    return entry.name === 'index' ? readFile('./test-notice') : null
  }
}

License

MIT