Open source guide to front-end projects

This article explains the detailed process of building an engineered front-end library and automatically publishing to Github and NPM with Github Actions.

The sample

Popular open source projects like Vue and React have a lot of configuration files. What do they do? Their Commit and Release records are so formal, are they based on some convention?

Without further ado, picture above first!

The red icon is associated with the engineering configuration, Linter, Tests, Github Actions, etc., covering the entire process of development, testing, and release.

Related Configuration list

  • Eslint
  • Prettier
  • Commitlint
  • Husky
  • Jest
  • GitHub Actions
  • Semantic Release

Let’s start by creating a TypeScript project and go through all the engineered configurations step by step, explaining what each configuration means and the easy holes.

Initialize the

To avoid compatibility problems, you are advised to upgrade Node to the latest long-supported version.

Start by creating a repO on Github, pull it down and initialize it with NPM init-Y. Then create the SRC folder and write index.ts.

After package.json is generated, I need to add the following configuration items:

   "main": "index.js",
+ "type": "module",
   "scripts": {
     "test": "echo \"Error: no test specified\" && exit 1"
   },
+ "publishConfig": {
+ "access": "public"
+}
Copy the code

We defined the project as an ESM specification, and the front-end community is gradually migrating to the ESM standard. Starting with Node V12.0.0, as long as “type”: “module” is set, Node treats the entire project as an ESM specification, and we can write the bare import/export directly.

Restricted indicates the access level of the current project to NPM. It has restricted and public options. Restricted indicates that we are publishing private packages to NPM (for a fee). The access level is restricted by default and marked public because we are an open source project.

configuration

After creating the project, we start installing the engineered dependencies, and since we are a TypeScript project, we need to install TypeScript dependencies as well.

Typescript

Install TypeScript and use TSC naming to generate tsconfig.json.

npm i typescript -D
npx tsc --init
Copy the code

Then we need to add and modify the tsconfig.json configuration items as follows:

{
  "compilerOptions": {
    /* Basic Options */
    "baseUrl": ".".// The module resolves the root path, which defaults to the directory where tsconfig.json is located
    "rootDir": "src".// Compile and parse the root path, which defaults to the directory where tsconfig.json is located
    "target": "ESNEXT".// Specify the output version of ECMAScript, es5 by default
    "module": "ESNext".// Specify the output module specification, default is Commonjs
    "lib": ["ESNext"."DOM"].// Compile the API that needs to be included. Default is the default value of target
    "outDir": "dist".// Compile the output folder path, default is the same directory as the source file
    "sourceMap": true.// Enable sourceMap, default is false
    "declaration": true.// Generate.d.ts files, default is false
    "declarationDir": "dist/types".// Output directory of.d.ts files. The default directory is outDir
    /* Strict Type-Checking Options */
    "strict": true.// Enable all strict type checking options, default to true
    "esModuleInterop": true.// Implement interoperability between CommonJS and ES modules by creating a namespace for imported content. Default is true
    "skipLibCheck": true.// Skip type checking for imported third-party lib declaration files. Default is true
    "forceConsistentCasingInFileNames": true.// Enforces consistent case in file names, defaults to true
    "moduleResolution": "Node".// Specify which module resolution strategy to use. The default is Classic
  },
  "include": ["src"] By default, all. Ts,.d.ts,.tsx files in the current directory except exclude are included
} 
Copy the code

More detailed configuration reference: www.typescriptlang.org/tsconfig

Note that if your project involves the WebWorker API, you need to add it to the lib field

"lib": ["ESNext"."DOM"."WebWorker"].Copy the code

We then add the compiled file path to package.json and compile commands to scripts.

- "main": "index.js",
+ "main": "dist/index.js",
+ "types": "dist/types/index.d.ts"
   "type": "module",
- "scripts": {
- "test": "echo \"Error: no test specified\" && exit 1"
-},
+ "scripts": {
+ "dev": "tsc --watch",
+ "build": "npm run clean && tsc",
+ "clean": "rm -rf dist"
+},
   "publishConfig": {
     "access": "public"
   }
Copy the code

Types compiler to generate the type of the file is specified configuration items, if compilerOptions declarationDir specifies the dist, is the source and the which s, at the same level as the types can be omitted.

To verify that the configuration takes effect, write to index.ts

const calc = (a: number, b: number) = > {
  return a - b
}
console.log(calc(1024.28))
Copy the code

Execute in the console

npm run build && node dist/index.js
Copy the code

Types /index.d.ts, index.js, index.js.map are generated in the dist directory and 996 is printed.

Eslint & Prettier

Code specifications can’t live without linters, so putting the two together, Prettier uses “Prettier to solve code formatting problems, Linters code quality problems,” reads a quote from Prettier’s website. While ESLint also has formatting, Prettier is more powerful.

Prettier-vs code and ESlint-VS code are installed in most editors where there is prettier-VS code and eslint-VS code. If you configure only one of the two because they format part of the code differently, the code will be formatted by the two plugins separately. There are dozens of ways to solve the prettier+ esLint conflict online, even Posting entire rules lists.

Prettier and ESLint integrate prettier and ESLint integrate prettier and ESLint integrate prettier and ESLint integrate prettier and ESLint integrate prettier and ESLint integrate prettier

Eslint

Install ESLint first, then use esLint’s command-line tools to generate the basic configuration.

npm i eslint -D
npx eslint --init
Copy the code

After executing the above command, a few options are presented, and we choose the configuration that fits our project in turn.

Note that ESLint here recommends three dominant community specifications, Airbnb, Standard, and Google. For personal preference, I chose the Standard specification without a semicolon.

The resulting.eslintrc.cjs file should look like this

module.exports = {
  env: {
    browser: true.es2021: true.node: true
  },
  extends: [
    'standard'].parser: '@typescript-eslint/parser'.parserOptions: {
    ecmaVersion: 12.sourceType: 'module'
  },
  plugins: [
    '@typescript-eslint'].rules: {}}Copy the code

Now, some of you might be wondering, why is the generated configuration file named.eslintrc.cjs instead of.eslintrc.js?

Since we defined the project as ESM, eslit –init will automatically recognize type and generate a compatible configuration file name, and if we change it back to the.js end, running ESLint will report an error. The problem is that ESLint uses the require() syntax internally to read configurations.

The same applies to configuring other functions, such as Prettier, Commitlint, etc., where no configuration file ends in XX.js and instead use.xxrc,.xxrc.json,.xxrc.yml, where the library supports only.xxrc,.xxrc.json,.xxrC.yml.

To verify whether the configuration takes effect, modify index.ts

  const calc = (a: number, b: number) => {
    return a - b
  }
- console.log(calc(1024, 28))
+ // console.log(calc(1024, 28))
Copy the code

Add lint command to package.json

  "scripts": {
    "dev": "tsc --watch",
    "build": "npm run clean && tsc",
+ "lint": "eslint src --ext .js,.ts --cache --fix",
    "clean": "rm -rf dist"
  },
Copy the code

Then execute Lint on the console and ESLint will display an error message indicating validation is in effect.

npm run lint
# 1:7  error  'calc' is assigned a value but never used  no-unused-vars
Copy the code

As a Typescript project, we also need to add the TypeScrip extension configuration provided by the Standard specification (the same goes for other specifications)

Install eslint – config – standard – with – typescript

npm i eslint-config-standard-with-typescript -D
Copy the code

Add modify.eslintrc.cjs

    module.exports = {
      env: {
        browser: true,
        es2021: true,
        node: true
      },
- extends: ['standard']
+ extends: ['standard', 'eslint-config-standard-with-typescript'],
      parser: '@typescript-eslint/parser',
      parserOptions: {
        ecmaVersion: 12,
        sourceType: 'module',
+ project: './tsconfig.json'
      },
      plugins: ['@typescript-eslint'],
      rules: {}
    }

Copy the code

Verify that the configuration takes effect

When you run Lint on the console, ESLint will display two error messages indicating that validation is in effect.

npm run lint
# 1:7  error  'calc' is assigned a value but never used  no-unused-vars
# 1:14 error  Missing return type on function 
Copy the code

Prettier

Prettier is now integrated into ESLint as recommended.

Install prettier and initialize the configuration file

npm i prettier -D
echo {}> .prettierrc.json
Copy the code

Next, add the configuration to.prettierrc.json, where you only need to add the parts that conflict with your chosen specification.

{
  "semi": false.// Whether to use a semicolon
  "singleQuote": true.// Use single quotes instead of double quotes
  "trailingComma": "none" // Use commas when possible for multiple lines
}
Copy the code

IO /docs/en/opt…

Install two dependencies needed to resolve conflicts

  • eslint-config-prettierClosure may be associated withprettierRules of conflict
  • eslint-plugin-prettieruseprettierInstead ofeslintformatting
npm i eslint-config-prettier eslint-plugin-prettier -D
Copy the code

Add.eslintrc.cjs as follows:

  module.exports = {
  env: {
    browser: true,
    es2021: true,
    node: true,
  },
- extends: ['standard', 'eslint-config-standard-with-typescript'],
+ extends: ['standard', 'eslint-config-standard-with-typescript', 'prettier'],
  parser: '@typescript-eslint/parser',
  parserOptions: {
    ecmaVersion: 12,
    sourceType: 'module',
    project: './tsconfig.json',
  },
- plugins: ['@typescript-eslint'],
+ plugins: ['@typescript-eslint', 'prettier'],
- rules: {},
+ rules: {
+ 'prettier/prettier': 'error'
+},
}
Copy the code

Then verify that the configuration takes effect and modify index.ts

- const calc = (a: number, b: number) => {
+ const calc = (a: number, b: number): number => {
    return a - b
  }
- // console.log(calc(1024, 28))
+ console.log(calc(1024, 28))
Copy the code

Prettier then executes Lint on the console, where Prettier and ESLint already behave the same, and if there are no errors, it succeeds.

npm run lint
Copy the code

We have now finished integrating ESLint with Prettier. Editor independent, which means that whatever editor you use and whether or not you have a plugin installed doesn’t affect your code validation.

Husky

Because a project is usually a team effort, we cannot guarantee that everyone will validate Lint once before committing, so we need Git hooks to automate the validation process, otherwise commit is prohibited.

Install Husky and generate the.husky folder

npm i husky -D
npx husky install
Copy the code

We then need to enable Husky automatically each time NPM install is executed

If your NPM version is 7.1.0 or greater

npm set-script prepare "husky install"
Copy the code

Otherwise, add it manually in package.json

 "scripts": {
    "dev": "tsc --watch",
    "build": "npm run clean && tsc",
    "lint": "eslint src --ext .js,.ts --cache --fix",
    "clean": "rm -rf dist",
+ "prepare": "husky install"
  },
Copy the code

Then add a Lint hook

npx husky add .husky/pre-commit "npm run lint"
Copy the code

Write the following to the. Husky /pre-commit file manually:

#! /bin/sh
. "$(dirname "$0")/_/husky.sh"
npm run lint
Copy the code

To test whether the hook works, modify index.ts

  const calc = (a: number, b: number): number => {
    return a - b
  }
- console.log(calc(1024, 28))
+ // console.log(calc(1024, 28))
Copy the code

If configured correctly, Lint will automatically execute with an error message and the commit will fail.

git add .
git commit -m 'test husky'
# 1:7  error  'calc' is assigned a value but never used
Copy the code

Commitlint

Why do you need Commitlint? In addition to the subsequent generation of Changelog files and semantic release, it is also beneficial for other students to analyze your submitted code, so we need to agree on the commit specification.

Install Commitlint

  • @commitlint/cli commitlint command line tool
  • Commitlint/config-Conventional Conventions based on Angular conventions
npm i @commitlint/config-conventional @commitlint/cli -D
Copy the code

Finally, add Commitlint to the hook

npx husky add .husky/commit-msg 'npx --no-install commitlint --edit "$1"'
Copy the code

Create. Commitlintrc and write the configuration

{
  "extends": [
    "@commitlint/config-conventional"]}Copy the code

Note that the configuration file name uses.commitlintrc instead of the default.commitlintrc.js, as described in the Eslint section

To test whether the hook works, modify index.ts to make the code correct

  const calc = (a: number, b: number): void => {
    console.log(a - b)
  }
- // calc(1024, 28)
+ calc(1024, 28)
Copy the code

Commit a commit that does not conform to the specification and the commit will fail

git add .
git commit -m 'add eslint and commitlint'
Copy the code

Change to the correct commit and commit successfully!

git commit -m 'ci: add eslint and commitlint'
Copy the code

The Angular specification says:

  • featNew features:
  • fix: fixing bugs
  • docs: Modify documents, such as README, CHANGELOG, CONTRIBUTE, etc
  • style: Does not change the code logic (only whitespace, indentation, commas, etc.)
  • refactor: Refactoring (neither fixing bugs nor adding functionality)
  • perf: Optimization related, such as improved performance, experience
  • test: Add tests, including unit tests, integration tests, etc
  • build: Changes to build systems or external dependencies
  • ciAutomates process configuration or script modification
  • chore: non-src and test modifications, release versions, etc
  • revert: Restores the previous commit

Jest

The good life starts with 100% test coverage.

Install jest, and the type declaration @types/jest, which requires TS-Node and TS-Jest to execute

The version of TS-Node is temporarily fixed as V9.1.1. A new version of [email protected] will cause jEST error. Please wait for official repair

NPM I jest @types/jest [email protected] ts-jest-dCopy the code

Initialize the configuration file

npx jest --init
Copy the code

Then modify the jest. Config. ts file

   // A preset that is used as a base for Jest's configuration
- // preset: undefined,
+ preset: 'ts-jest'
Copy the code

Add the test command to package.json.

  "scripts": {
    "dev": "tsc --watch",
    "build": "npm run clean && tsc",
    "lint": "eslint src --ext .js,.ts --cache --fix",
    "clean": "rm -rf dist",
    "prepare": "husky install",
+ "test": "jest"
  },
Copy the code

Create test folder __tests__ and test file __tests__/calc.spec.ts

Modified index. Ts

  const calc = (a: number, b: number): number => {
    return a - b
  }
- // console.log(calc(1024, 28))
+ export default calc
Copy the code

Then write the test code in calc.spec.ts

import calc from '.. /src'

test('The calculation result should be 996.'.() = > {
  expect(calc(1024.28)).toBe(996)})Copy the code

Verify that the configuration takes effect

When you execute test on the console, you will see the results with 100% test coverage.

npm run test
Copy the code

Finally we add lint validation to the __tests__ directory as well

Modify the package. The json

  "scripts": {
    "dev": "tsc --watch",
    "build": "npm run clean && tsc",
- "lint": "eslint src --ext .js,.ts --cache --fix",
+ "lint": "eslint src __tests__ --ext .js,.ts --cache --fix",
    "clean": "rm -rf dist",
    "prepare": "husky install",
    "test": "jest"
  },
Copy the code

NPM run Lint will get an error saying that the __tests__ folder is not included in the tsconfig.json include. When we add this to the include, the output dist will contain the test-related files, which is not what we want.

We use the official typescript-ESLint solution as follows:

Create a new tsconfig.eslint.json file and write the following:

{
  "extends": "./tsconfig.json"."include": ["**/*.ts"."**/*.js"]}Copy the code

Change it in.eslintrc.cjs

  parserOptions: {
    ecmaVersion: 12,
    sourceType: 'module',
- project: './tsconfig.json'
+ project: './tsconfig.eslint.json'
  },
Copy the code

Then verify whether the configuration is effective and directly submit the test file we added. If it can be submitted correctly, the configuration is successful.

git add .
git commit -m 'test: add unit test'
Copy the code

Github Actions

We use Github Actions to merge code or push it to the main branch, and dependabot bots to upgrade dependencies automatically trigger testing and release processes.

Create the. Github /workflows folder in the project root directory and create the ci.yml and cd.yml files

In the ci.yml file, write:

name: CI

on:
  push:
    branches:
      - '* *'
  pull_request:
    branches:
      - '* *'
jobs:
  linter:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v2
      - uses: actions/setup-node@v2
        with:
          node-version: 14
      - run: npm ci
      - run: npm run lint
  tests:
    needs: linter
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v2
      - uses: actions/setup-node@v2
        with:
          node-version: 14
      - run: npm ci
      - run: npm run test

Copy the code

Listen for push and pull_request actions on all branches and automate linter and tests tasks.

GithubActions github.com/features/ac…

Then push the code to verify that the configuration works

git add .
git commit -m 'ci: use github actions'
git push
Copy the code

Open the Github page for the current project and click on the Actions menu at the top to see two ongoing tasks, one that will succeed (test) and one that will fail (publish).

The above just realized the code automatic test process, the following implementation of automatic release process.

To do this, you need to register an account (already negligible) on the NPM website and create a package.

Then create GH_TOKEN and NPM_TOKEN (be careful not to include any TOKEN information in the code) :

  • How do I create GITHUB_TOKEN(Selected during creationrepoworkflowPermissions)
  • How to create NPM_TOKEN(Selected during creationAutomationPermissions)

Add the created two tokens to the project’s Actions Secrets:

Github Project home page -> Top Settings menu -> sidebar Secrets

Then change the “name” in package.json, where “name” is the name of the package you created on NPM.

In the cd.yml file, write:

name: CD

on:
  push:
    branches:
      - master
  pull_request:
    branches:
      - master
jobs:
  release:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v2
      - uses: actions/setup-node@v2
        with:
          node-version: 14
      - run: npm ci --ignore-scripts
      - run: npx semantic-release
        env:
          GH_TOKEN: The ${{ secrets.GH_TOKEN }}
          NPM_TOKEN: The ${{ secrets.NPM_TOKEN }}
Copy the code

Github has changed the default branch name of the new project to “Main” due to the “black life is important”, see: Issues. For convenience, it will be referred to as the main branch

So if your main branch name is “main”, the branches above need to be modified to read:

on:
  push:
    branches:
      - main
  pull_request:
    branches:
      - main
Copy the code

Then install the semantic-release dependency using semantic-Release and its plugins:

  • Semantic-release: a core library for semantic publishing
  • @semantic-release/ Changelog: used to automatically generate Changelog.md
  • @seman-release /git: used to commit changes made at publish time back to the remote repository
npm i semantic-release @semantic-release/changelog @semantic-release/git -D
Copy the code

Create a new configuration file in the project root directory. Releaserc and write:

{
  "branches": ["master"]."plugins": [
    "@semantic-release/commit-analyzer"."@semantic-release/release-notes-generator"."@semantic-release/changelog"."@semantic-release/github"."@semantic-release/npm"."@semantic-release/git"]}Copy the code

Similarly, if your main branch name is “main”, the branches above need to be modified to read:

  "branches": ["+ ([0-9])? (.{+([0-9]),x}).x"."main"].Copy the code

Finally, create the Develop branch and commit the work.

git checkout -b develop
git add .
git commit -m 'feat: complete the CI/CD workflow'
git push --set-upstream origin develop
git push
Copy the code

Then merge the Develop branch into the main branch and commit, noting that this lift triggers tests and releases (automatically creating tags and Changelog)

git checkout master
git merge develop
git push
Copy the code

After completing the above steps, open the Github project home page and the NPM project home page to see a Release update record.

Finally, skip back to the Develop branch and create a workflow that automatically updates dependencies.

Create a dependabot.yml file in the.github folder and write:

version: 2
updates:
  # Enable version updates for npm
  - package-ecosystem: 'npm'
    # Look for `package.json` and `lock` files in the `root` directory
    directory: '/'
    # Check the npm registry for updates every day (weekdays)
    schedule:
      interval: 'weekly'
Copy the code

Commit and see if all workflows pass, merge into the main branch, and commit, which does not trigger a release.

git pull origin master
git add .
git commit -m 'ci: add dependabot'
git push 

git checkout master
git merge develop
git push
Copy the code

Two conditions are required to trigger a release:

  1. Only when thepushandpull_requesttoThe main branchThe release is triggered only when the
  2. onlycommitThe prefix forfeat,fix,perfWill publish, otherwise skip.

For more publishing rules, see github.com/semantic-re…

See semantic-release.gitbook.io for how SemanticRelease is used

If you can configure all the above steps correctly and publish successfully, congratulations! You have a fully automated project with automatic dependency updates, tests, releases, and automatic release information generation.

Complete project example: @resreq/ event-Hub

conclusion

This paper does not involve the configuration of component library, Monorepo, Jenkins CI, etc., but it can cover most of the CI/CD process of front-end projects.

Some places are more detailed, even some wordy, but still hope to help you! And flowers! 🎉 🎉 🎉