Skip to content

Scaffolder - Increasing dev velocity and standardizing file conventions.

Notifications You must be signed in to change notification settings

galElmalah/scaffolder

Repository files navigation

Scaffolder logo

Scaffolder

npm version GalElmalah lerna vscode extension

Copy pasting is prone to mistakes.
Keeping your project file structure consistent is annoying.
Sharing templates is too damn complicated!
This is where Scaffolder come in.

For a brief introduction and motivation for this tool, read this.

check out the vscode extension


TOC

Getting started

Setup

Install scaffolder globally

npm i -g scaffolder-cli

this will make the scaff command available globally, you can now type scaff i in the terminal, to enter the cli in interactive mode.

You can also use npx for example npx scaffolder-cli i will start scaffolder in interactive mode.

Usage

Create a templates folder in your project root directory

The templates folder should be named scaffolder and should contain folders where each folder represents a different template and inside of that folder, there is the template structure you wish to create.

The templates available are the templates defined in the scaffolder folder.

If you have more scaffolder folders in the current file system hierarchy then all of them will be included with precedence to the nearest scaffolder folder.
For example:
In our current project root

scaffolder
├── component
│   ├── index.js
│   ├── {{componentName}}.js
│   └── {{componentName}}.spec.js
└── index
    └── index.js

In our desktop

scaffolder
├── component
│   ├── index.js
│   ├── {{lol}}.js
│   └── {{wattt}}.spec.js
└── coolFile
    └── coolFile.sh

From the above structure, we will have three commands component (from the project scaffolder), index (from the project scaffolder) and coolFile (from the desktop scaffolder).
Lets look at the content of {{componentName}}.js and {{componentName}}.spec.js. {{componentName}}.js from the current project scaffolder folder.

import React from 'react'

export const {{componentName}} = (props) => {
  return (
    <div>
      Such a cool component
    </div>
  )
}

{{componentName}}.spec.js

import React from 'react';
import { mount } from 'enzyme';
import { {{componentName}} } from './{{componentName}}';

describe('{{componentName}}', () => {
  it('should have a div', () => {
    const wrapper = mount(
      <{{componentName}} />
    );
   expect(wrapper.find('div').exists()).toBeTruthy()
  });
});

Now let's run the following command somewhere in our project

scaff create component componentName=CoolAFComponent --folder MyCoolComp

A new folder will be created under our current working directory, let's look at what we got.

MyCoolComp
├── CoolAFComponent.js
├── CoolAFComponent.spec.js
└── index.js

CoolAFComponent.js

import React from "react";

export const CoolAFComponent = (props) => {
  return <div>Such a cool component</div>;
};

CoolAFComponent.spec.js

import React from "react";
import { mount } from "enzyme";
import { CoolAFComponent } from "./CoolAFComponent";

describe("CoolAFComponent", () => {
  it("should have a div", () => {
    const wrapper = mount(<CoolAFComponent />);
    expect(wrapper.find("div").exists()).toBeTruthy();
  });
});

This could also be achieved using the interactive mode!

How cool is this, right?
As you can see our params got injected to the right places and we created our template with little effort.
Hooray!! ✨ 🎆 🎇 ✨

API

interactive, i

Run Scaffolder in interactive mode, meaning, it will prompt the user to choose a template and a value for each parameter.

This command is the most recommended one as it simplifies the process for the user a lot.

options:

  • --from-github
    Passing this flag will cause a prompt to appear, asking the user to enter a github repository (https/ssh) and consume the templates defined on that repository.
    More info about sharing templates.

  • --entry-point <absolutePath>
    Generate the template to a specified location.

  • --path-prefix <relativePath>
    Path that will be appended the the location the template is generated into.

    If the path does not exists Scaffolder will automatically create it and notify the user what paths were created for him.

    For example: <entry-point>/<path-prefix>/<template-will-be-created-here>

  • --template <templateName>
    Start the interactive mode with a preselected template.

  • --values <commaSeparatedParametersValue>
    Predefine values for specific parameters param1=val1,param2=val2...

create <templateName>

<templateName>: One of the templates defined in the scaffolder folder.

options:

  • --load-from <absolutePath>
    Load the templates from a specific location.

  • --entry-point <absolutePath>
    Generate the template to a specified location.

  • --path-prefix <relativePath>
    Path that will be appended the the location the template is generated into.

    If the path does not exists Scaffolder will automatically create it and notify the user what paths were created for him.

    For example: <entry-point>/<path-prefix>/<template-will-be-created-here>

  • --folder, -f <folderName>
    <folderName>: The name of the folder you want the template to be generated into. If none is supplied the template will be generated to the current working directory.

  • <parameter>=<value>
    <parameter>: One of the parameters for a specific template
    <value>: The value you want the parameter to be replaced with.

save-remote <alias>

<alias>: The alias that will map to your remote templates.

options:

  • --location <githubSrc>
    The location of your remotes, e.g github ssh or https.
    For example: [email protected]:galElmalah/scaffolder-templates-example.git or https://github.com/galElmalah/scaffolder-templates-example.git

delete-remote <alias>

<alias>: The alias you wish to delete.

list, ls

Show the available templates from the current working directory.

show <templateName>

Show a specific template files
options:

  • --show-content
    Also show the full content of the template files.

Sharing templates

Often you find yourself wanting to share a template while not making every consumer of that template to save it on his machine.

In order to address that problem, Scaffolder lets you consume templates from Github repositories that have a scaffolder folder at their root.
For example, you can see this repository which contains 3 templates and a config file.
To generate one of those templates you can run npx scaffolder-cli i --from-github and enter https://github.com/galElmalah/scaffolder-templates-example.git and you'll be promoted to choose one of those templates.

You can now save remote templates repos under aliases!
For example, assuming you have scaffolder-cli installed globaly, you can run scaff save-remote my-remote-templates --location https://github.com/galElmalah/scaffolder-templates-example.git and from now on, you will have a new entry named (remote) my-remote-templates when you run scffolder in interactive mode that will list all of the available templates in that repo.

Any improvement suggestions? go ahead and open an issue!


Scaffolder config file

Scaffolder lets you extend and define all sorts of things via a config file.
the config file should be placed inside the scaffolder folder that the template you are generating is defined in and named scaffolder.config.js.

Through the scaffolder.config.js file you can extend and customize scaffolder in several ways.
Example config file

module.exports = {
  transformers: {
    toLowerCase: (parameterValue, context) => parameterValue.toLowerCase(),
  },
  functions: {
    date: (context) => Date.now(),
  },
  parametersOptions: {
    someParameter: {
      question:
        "this text will be shown to the user in the interactive mode when he will be asked to enter the value for 'someParameter'",
    },
  },
   templatesOptions: {
    someTemplate: {
      hooks: {
        preTemplateGeneration: (context) => {
        // do something before generating a template
        },
        postTemplateGeneration: (context) => {
        // do something after generating a template
        }
      },
      // transformers, functions, parametersOptions can be scoped to specific templates
      transformers: {...},
      functions: {...},
      parametersOptions: {...}
    }
  }
};

transformers, functions, parametersOptions can be scoped to specific templates, and will have precedents over non scoped options.

transformers

Transformers can be used to transform a parameter value.
For example, you can write the following: {{ someParameter | toLowerCase | someOtherTransformer }} and the value that will be injected in your template will be the value after all of the transformations.

  • Transformers can be chained together.
  • Transformers are invoked with the value supplied for that parameter as the first argument and the context object as the second argument.

Default transformers

You can use the following transformers without defining anything in your config file

  1. toLowerCase
  2. toUpperCase
  3. capitalize - capitalize each word in your parameter value
  4. toCamelCase - takes Kebab or snake case and transform them to camelCase format
  5. camelCaseToSnakeCase
  6. camelCaseToKebabCase

functions

functions are very similar to transformations, but they are unary, meaning, they are invoked without any parameter value supplied to them.
For example, you can write the following: {{date()}} and the value returned from are date function (defined in our config file) will be injected to the template.

parametersOptions

parametersOptions is a map from parameters to their options.
For example, lets say we have a parameter named myReactComponentName and we want to show a custom question to the user when he is asked to enter a value for that parameter, we can add the following to our config file:

{
  ...
  ...
  parametersOptions: {
    myReactComponentName: {
      question: "Enter a name for your react component:",
      validation: (value) => value.length > 3 ? true : "The component name must be longer than 3 chars" 
    },
    parameterTwo: {
      question: "Enter a name for your react component:",
      choices: {
        values:["These", "values", "will", "be", "shown", "to the user to choose from" ]
      }
    }
  }
}

parameter options object

property type description
question string The question that will be shown to the user when he will enter a value for the matching parameter
validation (value: string): string | true this function will be invoked to validate the user input.
Return a string if the value is invalid, this string will be shown to the user as an error message. Return true if the value is valid.
choices {values: string[]} Object containing a values property from which the user will be asked to choose a value for that parameter.

context object

property type description
parametersValues Object<string, string> Key value pairs containing each parameter and his associated value in the current template.
templateName string The name of the template being generated.
templateRoot string Absolute path to the template being generated.
targetRoot string Absolute path to the location the template is being generated into.
currentFilePath string The path to the file being created.
type string, one of: "FILE_NAME", "FILE_CONTENT", "FOLDER" The current type being operated upon - file/folder/content.
fileName string The name of the file being operated upon. Available only if the type is "FILE_NAME" or "FILE_CONTENT".
logger Logger A logger instance

logger

property type description
info (message: string): void Output general info.
warning (message: string): void Output warnings, meaning, it will be colored orange.
error (message: string): void Output error, meaning, it will be colored red.

In vscode context, the loggers will pass the message to "scaffolder" output channel prefixed with the log level used.

vscode logs example

[error][context-logger]:: some message
[warning][context-logger]:: some message
[info][context-logger]:: some message

templatesOptions

templatesOptions is a map from templates names to their options.

transformers, functions, parametersOptions can be scoped to specific templates, and will have precedents over non scoped options.

templates options object

property type description
hooks hooks object Hooks are functions to be executed at some point throughout the template
parametersOptions parameter options object
functions functions
transformers transformers

hooks object

If an hook function returns a Promise then it will be awaited and only then the template generation process will continue.

property type description
preAskingQuestions (context): any | Promise<any> Executed before the user is prompted for the template questions.
The context object will include an empty parametersValues object.
preTemplateGeneration (context): any | Promise<any> Executed before the template is generated.
postTemplateGeneration (context): any | Promise<any> Executed after the template is generated.

By default all errors thrown inside of hooks are ignored. To stop execution inside a hook you can use process.exit(1) .


Motivation and goals

Why I wrote Scaffolder?

Working on several big projects, I noticed that there a few time-consuming tasks that keep popping up. One of those tasks is creating folders and filling in all the boilerplate code while keeping the project structure consistent. After realizing that this process needs to be automated, I set out to find a solution and ended up creating my own CLI tool 🌈.

The first thing I had to do is to understand WHY it’s so annoying, and I realized that this happens for two reasons:

  • Creating files and folders can be repetitive, annoying and a waste of time. Especially if some content repeats itself for every new file.
  • Keeping a project file structure consistent is becoming more and more complex as the number of people working on that project increases — each team member has his preference for naming files and exposing functionality.

Why I didn’t use any existing solutions?

First, came Yeoman. I gave yeoman a try but found it too complex. Furthermore, I want the templates to be a part of the project (in some cases), and committed to git alongside the code. Thus, supporting template generation offline and tight coupling between the project and the templates. All of the above seemed too complex or not possible at all with yeoman, so one hour after trying it out I moved on to other prospects.

Second, came boiler, I did not like this one for the same reasons I did not like Yeoman. Also, the fact that it’s not managed with npm is a bit annoying.

Third, came frustration 😞. After trying two of the most popular solutions out there I realized that If I want something tailored to my needs I should just go ahead and write it myself.

Both of these tools are AWESOME but for my needs, they weren’t right.

My goals while writing this tool

  • making this process as easy and seamless as possible.

  • Addressing the general problem. Meaning, it won’t be language-specific e.g only React or Vue templates. I could potentially create templates in any shape, structure, and language I want.

  • Having the ability to create scoped templates. Meaning, creating project-specific templates that can be committed to git with the rest of the code.

  • Having the ability to create “global” templates that will be available from anywhere.

  • Managed with npm.