DEV Community

Cover image for Creating a template repository in GitHub
Alex Shulaev
Alex Shulaev

Posted on • Updated on

Creating a template repository in GitHub

I decided to start my new project with the development of a template for GitHub repositories, and in this article, I want to share my experience!

By the way, the whole process that will be discussed later I recorded on video (with all my errors along the way 😄) take a look, suddenly you will like it 😉

Why do I need a template for the GitHub repository?

A template is a very convenient tool if you often start developing new projects and you need a preconfigured starting template with installed dependencies, structure and customized automation processes

Step by step

Create a new GitHub repository. At this stage, this repository is no different from your regular repositories

The example of my new repository

The example of my new repository

Go to your local folder where your project will be located. Create a new git repository (you can immediately execute the commands that GitHub offers after creating the remote repository) and create the package.json file

git init
npm init

npm will ask you some questions and based on your answers will create a basic package.json, but since this file is very important for us, let's take a closer look

name

This is the identifier of your package, which must be unique. Advice from me, check in advance if the name is free at npmjs.com if you are going to publish your package there

version

Shows the current version of the application

description

A brief description of your project

main

The main entry point to your project. This field (and the next two) should indicate the place where your package will be collected (I usually use the dist folder)

modules

Pointer to an ECMAScript Module

types

Type в declaration pointer for TS

files

The list of files that will be included in the build after your package is installed as a dependency. I recommend placing only the files necessary for your package to work, it makes no sense to install all the files that you use during the development process (here I just specify the dist folder)

repository

It's important to specify the place where your code is stored for the convenience of contributors (just copy the link to your GitHub repository here)

author

Just indicate yourself or your team

license

Indicate how other users can use your package. This information also appears in your package when published to npm and to GitHub. GitHub also recommends adding the LICENSE.md file to expand the license. In my case, I choose MIT

keywords

List of keywords by which other people can find your package

bugs

Link to where users report problems in your project. In my case, this is a link to GitHub issues

As a result, I got such a package.json file:

{
  "name": "como-north",
  "version": "1.0.0",
  "description": "GitHub template for starting new projects",
  "main": "./dist/index.js",
  "module": "./dist/index.es.js",
  "types": "./dist/index.d.ts",
  "files": [
    "dist"
  ],
  "repository": {
    "type": "git",
    "url": "https://github.com/Alexandrshy/como-north"
  },
  "author": "Alex Shualev <alexandrshy@gmail.com>",
  "license": "MIT",
  "keywords": [
    "template",
    "javascript",
    "package"
  ],
  "bugs": {
    "url": "https://github.com/Alexandrshy/como-north/issues"
  },
  "homepage": "https://github.com/Alexandrshy/como-north",
}

Do not forget to make commits, if you have already watched my video, I do it constantly 😄

Now let's move on to the tools. I will not dwell on individual libraries for a long time or explain my choice, each tool is variable and can be replaced or completely removed, I just tell you one of the options that suit me

Linters

In my template, I'll use a bunch of ESLint and Prettier. In my opinion, this is the best choice at the moment because of the flexibility of the settings

📦 Prettier

Prettier is a code formatting tool that aims to use predefined rules for the code design. It formats the code automatically and has extensions for modern IDE

Install the package:

npm i prettier -D

Write the configuration:

{
  "singleQuote": true,
  "parser": "typescript",
  "tabWidth": 4,
  "bracketSpacing": false,
  "printWidth": 100,
  "trailingComma": "all"
}

You can also create .prettierignore if you have files that you do not want to format

.github/
.idea/
node_modules/
dist/

📦 ESLint

This tool analyzes the code to help detect problematic patterns that don't comply with the rules and standards. It works for most programming languages and has a large number of ready-made configurations from large companies and extensions for various tasks

Install the package:

npm i eslint eslint-config-prettier eslint-plugin-prettier @typescript-eslint/eslint-plugin @typescript-eslint/parser eslint-plugin-jsdoc -D
  • eslint-config-prettier - is a config that disables rules that conflict with Prettier
  • eslint-plugin-prettier - exposes a "recommended" configuration that configures both eslint-plugin-prettier and eslint-config-prettier in a single step
  • @typescript-eslint/eslint-plugin - an ESLint-specific plugin which, when used in conjunction with @typescript-eslint/parser, allows for TypeScript-specific linting rules to run
  • @typescript-eslint/parser - an ESLint-specific parser which leverages typescript-estree and is designed to be used as a replacement for ESLint's default parser, espree
  • eslint-plugin-jsdoc - JSDoc linting rules for ESLint

All additional packages are optional and depend on your goals. You can also pay attention to eslint-config-airbnb this package provides the developer with the configuration from Airbnb

module.exports = {
    plugins: ['@typescript-eslint', 'prettier', 'jsdoc'],
    extends: [
        'plugin:@typescript-eslint/recommended',
        'prettier/@typescript-eslint',
        'plugin:prettier/recommended',
        'plugin:jsdoc/recommended',
    ],
    rules: {},
    overrides: [
        {
            files: ['src/*/*'],
            rules: {
                'max-lines': 'off',
                'max-nested-callbacks': 'off',
                'max-statements': 'off',
            },
        },
    ],
    settings: {
        node: {
            extensions: ['.ts', '.json'],
        },
    },
};

Add scripts for linter:

"prettier": "prettier '**/*.{js,ts}' --ignore-path ./.prettierignore",
"lint": "eslint '*/**/*.{js,ts}'",

📦 TypeScript

JavaScript is a dynamically typed language, meaning the compiler doesn't know what type of variable you are using until the variable is initialized. Such things can cause difficulties and errors in your projects. However, TypeScript helps solve these problems. I use TypeScript in all my projects regardless of their size. I believe that early catching errors is very important and it is better to foresee the structure of your functions in advance than spending time catching bugs later

npm i typescript -D

The tsconfig.json file specifies the root files and the compiler options required to compile the project.

{
    "compilerOptions": {
        "outDir": "dist",
        "module": "es2015",
        "target": "es6",
        "lib": [
            "es5",
            "es6",
            "es7",
            "es2017",
            "dom"
        ],
        "sourceMap": true,
        "moduleResolution": "node",
        "baseUrl": "src",
        "skipLibCheck": true,
        "strict": true,
        "declaration": true
    },
    "include": [
        "src",
        "typeScript"
    ],
    "exclude": [
        "node_modules",
    ]
}

You can find all available options here

Add some more scripts:

"types": "tsc --noEmit",
"finish": "npm run lint && npm run types"

The finish script we need when working on workflows

Now we can create the src/index.ts

export const union = (a: Array<string>, b: Array<string>): Array<string> => [...a, ...b];

And now we can run finish script

npm run finish

If everything is done correctly we will not get any error

📦 Babel

We'll add Babel to the template for the correct operation of our code in older versions of browsers

npm i @babel/core @babel/preset-env @babel/preset-typescript -D

Add configuration file

module.exports = {
    presets: [
        ['@babel/preset-env', {targets: {node: 'current'}, modules: false, loose: true}],
        '@babel/preset-typescript',
    ],
};

Need to pay attention to

targets

Describes the environments you support/target for your project. You need to specify a minimum environment required for your users

modules

Enable transformation of ES6 module syntax to another module type

loose

Enable "loose" transformations for any plugins in this preset that allow them

We will not create a separate script for running babel since we'll use babel through the plugin in rollup

📦 Rollup.js

Rollup is a module bundler for JavaScript. Now the Rollup community is very active, and I often see new projects that use Rollup for building. Its main advantage is its easy configuration. Let's add Rollup to the project and write a config file

npm i rollup rollup-plugin-terser rollup-plugin-typescript2 @rollup/plugin-babel @rollup/plugin-commonjs @rollup/plugin-node-resolve -D

As you can see, in addition to the main package, we install many extensions, let's say a few words about each:

  • rollup-plugin-terser - plugin to minimize the generated package
  • rollup-plugin-typescript2 - plugin for typescript with compiler errors
  • @rollup/plugin-babel - plugin for seamless integration between Rollup and Babel
  • @rollup/plugin-commonjs - plugin to convert CommonJS modules to ES6, so they can be included in a Rollup bundle
  • @rollup/plugin-node-resolve - plugin which locates modules using the Node resolution algorithm, for using third party modules in node_modules

And now the config file itself

import typescript from 'rollup-plugin-typescript2';
import resolve from '@rollup/plugin-node-resolve';
import commonjs from '@rollup/plugin-commonjs';
import babel from '@rollup/plugin-babel';
import {terser} from 'rollup-plugin-terser';

import pkg from './package.json';

const extensions = ['.js', '.jsx', '.ts', '.tsx'];

export default {
    input: 'src/index.ts',
    output: [
        {
            file: pkg.main,
            format: 'umd',
            name: 'ComoNorth',
        },
        {
            file: pkg.module,
            format: 'es',
        },
    ],
    plugins: [
        typescript({
            rollupCommonJSResolveHack: true,
            clean: true,
        }),
        babel({
            exclude: 'node_modules/**',
            extensions,
        }),
        resolve(),
        commonjs(),
        terser(),
    ],
};

Add new scripts:

"build": "npm run build:clean && npm run build:lib",
"build:clean": "rimraf dist",
"build:lib": "rollup -c",

To understand that we did everything right, let's run the script. As a result, we should not see any errors in the console and a new dist folder should appear in the project

npm run build

🔥 Automation

In your project, you should think not only about the dev build, but also the delivery processes of your package to your users. Each of your changes should be reflected in a file with changes so that other people can follow the development process, your project must be correctly versioned according to your changes and published immediately (in my case in npm). Let's take it in order

Checking the commit message

Since we want to record all changes made to our package, we need to structure our commit messages. For this, we'll use commitlint

npm i @commitlint/cli @commitlint/config-conventional husky -D

Configuration file

{
    "parserPreset": "conventional-changelog-conventionalcommits",
    "rules": {
        "body-leading-blank": [
            1,
            "always"
        ],
        "footer-leading-blank": [
            1,
            "always"
        ],
        "header-max-length": [
            2,
            "always",
            150
        ],
        "scope-case": [
            2,
            "always",
            "lower-case"
        ],
        "subject-case": [
            2,
            "never",
            [
                "sentence-case",
                "start-case",
                "pascal-case",
                "upper-case"
            ]
        ],
        "subject-empty": [
            2,
            "never"
        ],
        "subject-full-stop": [
            2,
            "never",
            "."
        ],
        "type-case": [
            2,
            "always",
            "lower-case"
        ],
        "type-empty": [
            2,
            "never"
        ],
        "type-enum": [
            2,
            "always",
            [
                "chore",
                "ci",
                "docs",
                "feat",
                "fix",
                "refactor",
                "revert",
                "style",
                "test"
            ]
        ]
    }
}

As you may have noticed we also installed the husky package as a dependency. This package is very well described on their page on GitHub: "Husky can prevent bad git commit, git push and more". Required for correct operation husky: node >= 10 and git >= 2.13.0

Add the following code to package.json:

"husky": {
  "hooks": {
    "pre-commit": "lint-staged",
    "commit-msg": "commitlint -E HUSKY_GIT_PARAMS"
  }
},
"lint-staged": {
  "src/**/*.{js,ts}": [
    "npm run lint"
  ]
},

Now, before each attempt to make a git commit, we will run the lint script, and each commit message we will check for compliance with the template. Experiment time, try the following code:

git add .
git commit -m "added commitlint and husky"

And we get an error, but that is what we were waiting for! This means that we cannot make commits with arbitrary commits

Errors

git add .
git commit -m "feat: added commitlint and husky"

But this will work. The feat tag that we used in the commit message is necessary for further versioning our package

GitHub workflows

You can create custom workflows to automate your project's software development life cycle processes. Detailed workflow instructions.

The first process that we will set up is the process of working with pull requests. Typically, this process involves building the project, checking linter, running tests, and so on

First, create a file .github/workflows/pull-requests_check.yml

And add the following

name: Pull-Requests Check

on: [pull_request]

jobs:
  Test:
    runs-on: ubuntu-latest
    steps:
    - uses: actions/checkout@v2
    - uses: actions/setup-node@v1
      with:
        node-version: 12

    - name: Finish
      env:
        GH_TOKEN: ${{ secrets.GH_TOKEN }}
      run: |
        npm i
        npm run finish

After these files get into your GitHub repository and you create a new pull request in the Actions tab, you'll see a window with the result of your process. If everything is green, excellent, you can merge your request!

Pull-Requests Check result

It took me 20 seconds to complete my process, but it all depends on the complexity of your workflow, if you run a large number of tests for your project, it may take several minutes

Now let's create a more complex workflow for automatically publishing the package in npm and recording changes to the new version of the package in CHANGELOG.md

name: Release

on:
  push:
    branches:
      - master

jobs:
  Release:
    name: release
    runs-on: ubuntu-latest
    steps:
      - name: Checkout
        uses: actions/checkout@v2

      - name: Use Node.js ${{ matrix.node-version }}
        uses: actions/setup-node@v1
        with:
          always-auth: true
          node-version: 12
          registry-url: "https://npm.pkg.github.com"
          scope: "@Alexandrshy"

      - name: Install dependencies
        run: npm i

      - name: Build
        run: npm run build

      - name: Semantic Release
        uses: cycjimmy/semantic-release-action@v2
        id: semantic
        with:
          branch: master
          extra_plugins: |
            @semantic-release/git
            @semantic-release/changelog
        env:
          GITHUB_TOKEN: ${{ secrets.GH_TOKEN }}
          NPM_TOKEN: ${{ secrets.NPM_TOKEN }}

      - name: Push updates to branch for major version
        if: steps.semantic.outputs.new_release_published == 'true'
        run: git push https://x-access-token:${GITHUB_TOKEN}@github.com/${GITHUB_REPOSITORY}.git HEAD:refs/heads/v${{steps.semantic.outputs.new_release_major_version}}
        env:
          GITHUB_TOKEN: ${{ secrets.GH_TOKEN }}

Here you should pay attention to the following two things

  1. We used the GitHub Actions cycjimmy/semantic-release-action@v2 which in turn is a wrapper over semantic-release. GitHub Actions has a lot of useful tools for automating various processes, just check out the market place and you will be surprised 🙂
  2. secrets.GH_TOKEN and secrets.NPM_TOKEN GitHub provides a token that you can use to authenticate on behalf of GitHub Actions. These tokens must be generated (for npm and for GitHub) and added to your repository (for example https://github.com/{your-name}/{repository-name}/settings/secrets)

If you didn't make any mistakes, you'll get your package published in npm

Result actions

Completed action after merga in the master

CHANGELOG.md

Automatically filled in CHANGELOG.md based on commit messages

Package in npm

Package was published on npm and will publish new versions after each merge

Now every change that gets into the master branch will start this process and create a new version depending on the tags that you added to your commit message. For example, if you had version 1.0.0 of the package and you made a merge with the commit message: "fix: eslint config" after the workflow is complete, you'll receive a new version of package 1.0.1

Dependency management

To control dependencies, I recommend that you add dependentabot. This bot automatically checks your dependencies and the need to update them

On the site you need to sign in via GitHub. Then give access to those repositories that dependabot should monitor

And in the project itself you need to create a .dependabot/config.yml with this content:


version: 1
update_configs:
  - package_manager: "javascript"
    directory: "/"
    update_schedule: "weekly"
    target_branch: "master"
    commit_message:
      prefix: "fix"
    target_branch: "dependa"
    default_reviewers:
      - Alexandrshy

Example of running validation

Example of running validation

You can configure the auto-merge immediately in the master, but I would not recommend doing this, I decided to put all the updates in a separate branch and then update the master yourself using a single pull request

Dependentabot pull request example

Dependentabot pull request example

Minor improvements

All we have to do is add README.md and LICENSE

README.md is your space for creativity, but don't forget, its main purpose is to show you how to work with your package very briefly. You can create a LICENSE via the GitHub interface. This is convenient, since GitHub has pre-prepared templates.

Creating a LICENSE File

Creating a LICENSE File

To do this, click on the button "Create new file". Enter the file name LICENSE, then click on the button "Choose a license template" and select the license that suits you

Made the project a template for GitHub

Made the project a template for GitHub

Well and most importantly, make the resulting project a template for your future work. To do this, we go to the repository settings and click on the "Template repository" checkbox, that's all!

Work result

Como-north my template that I'll use in my next projects and maybe I'll update it as needed

Video

Links

Top comments (0)