Web制作向けnpm-scripts 2

更新版
[Web制作向けnpm-scripts 3](https://qiita.com/takeshisakuma/items/34db1aa11a4e25508f66)


### 変更点
- EJS、Stylusは対応しない
- webpackによるJavaScriptのバンドル対応
- node-sassからDart-Sassに変更
- Jest導入
- コミット前のコードのリント、整形

### 補足
- node-sassではglobを使っていたのですが、Dart-Sassではnpm-scriptsでglobを使う方法が不明なので対応していません
- webpackの使用は最小限(JavaScriptのバンドル)に絞って使用するようにしています

## フォルダ構成

```:directory
website
 ├ src
 │　├ pug
 │　├ img
 │　├ scss
 │　└ ts
 ├ package.json
 ├ package-lock.json
 ├ imagemin.js
 ├ jest.config.js
 ├ tsconfig.json
 ├ webpack.config.js
 ├ .browserslistrc
 ├ .eslintrc.json 
 ├ .prettierrc.json　
 ├ .stylelintrc.json
 └ .stylelintignore 
```


## npm-scriptsの準備

```:terminal
npm init -y

npm i -D npm-run-all
npm i -D chokidar-cli
```


## ホットリロード
### Browser-Sync
#### インストール
```:termina
npm i -D browser-sync

//IE11は2.25.0以降エラーになるので、IEでもbrowser-syncを使う場合は下記
npm i -D browser-sync@2.24.7
```

#### npmコマンド追加
```:package.json
"scripts": {
 "start:server": "browser-sync start -s dist -w src/*.html src/css/*.css src/js/*.js"
}
```

#### 動作確認

```:terminal
npm run start:server 
```

ブラウザが立ち上がれば成功です。
まだデータが無いのでエラーで問題ありません。
Ctrl+Cで終了します。



## Pug

### インストール

```:terminal
npm i -D github:pugjs/pug-cli#master
```

### npmコマンド追加

```:package.json
"scripts": {
  "compile:pug": "pug src/pug/ --hierarchy -o dist/ -P",
  "watch:pug": "pug src/pug/ --hierarchy -o dist/ -w -P",
}
```

### 動作確認

/src/pug/配下にPugファイルを設置して、変換してみてください。

[Pugの記法まとめ](https://qiita.com/takeshisakuma/items/fdcf456d8250e6dafc7b)

```:terminal
//1回変換
npm run compile:pug
 
//Pugファイルの変更を監視して変換
npm run watch:pug
```

/src/pug/配下のPugファイルが/dist/配下にHTMLとして変換されれば成功です。


## Sass(Dart-Sass)
SCSSファイルをCSSファイルに変換します。

### インストール

```:terminal
npm i -D sass
``` 



## autoprefixer
CSSのプロパティにベンダープレフィックスを付与します。

### インストール

```:terminal
npm i -D postcss-cli
npm i -D autoprefixer
npm i -D postcss
```


### 対象ブラウザの指定

#### 設定例

```:.browserslistrc
defaults
```

設定は下記URLを参考に適宜変更してください。
https://github.com/browserslist/browserslist

[.browserslistrcで対象ブラウザを指定する](https://qiita.com/takeshisakuma/items/0bc1c39ee976bd1f52d7)

[.browserslistrcに設定した対象ブラウザの確認](https://browserl.ist/)


### 対象ブラウザの確認

```:terminal
npx browserslist
```

### npmコマンド追加

```:pacjage.json
"scripts": {
  "compile:css2cssprefix": "postcss dist/css/style.css -u autoprefixer -o dist/css/style.css",
  "compile:scss2css": "sass src/scss/style.scss:dist/css/style.css --style=expanded --source-map-urls=relative",
  "compile:scss2cssprefix": "run-s compile:scss2css compile:css2cssprefix",
  "watch:scss2cssprefix": "chokidar \"src/scss/\" --command \"npm run compile:scss2cssprefix\" --initial", 
}
```

### 動作確認

/src/scss/配下にSCSSファイルを設置して、変換してみてください。
[Sassの記法(SCSS構文)](https://qiita.com/takeshisakuma/items/1c40c42f61c6e751c0e3)

```:terminal
//SCSSファイルをベンダープレフィックス付きでCSSに変換(1回)
npm run compile:scss2cssprefix

//SCSSファイルをベンダープレフィックス付きでCSSに変換(SCSSファイルを監視して変更される度)
npm run watch:scss2cssprefix
```

/src/scss/配下のSCSSファイルが/dist/css/配下にCSSファイルとして変換されれば成功です。



## TypeScript
JavaScriptへの変換と、webpackによるバンドルを行います。


### インストール

```:terminal
npm i -D typescript

npm i -D webpack
npm i -D webpack-cli
npm i -D ts-loader
```

### npmコマンド追加

```:packege.json
"scripts": {
  "compile:ts": "webpack --mode production",
  "watch:ts": "webpack  --mode development --watch",
}
```

### 設定ファイル(TypeScript)

```:tsconfig.json
{
  "compilerOptions": {
    /* Basic Options */
    // "incremental": true,                   /* Enable incremental compilation */
    "target": "es5",                          /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019' or 'ESNEXT'. */
    "module": "commonjs",                     /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', or 'ESNext'. */
    // "lib": [],                             /* Specify library files to be included in the compilation. */
    // "allowJs": true,                       /* Allow javascript files to be compiled. */
    // "checkJs": true,                       /* Report errors in .js files. */
    // "jsx": "preserve",                     /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */
    // "declaration": true,                   /* Generates corresponding '.d.ts' file. */
    // "declarationMap": true,                /* Generates a sourcemap for each corresponding '.d.ts' file. */
     "sourceMap": true,                     /* Generates corresponding '.map' file. */
    // "outFile": "./",                       /* Concatenate and emit output to single file. */
    // "outDir": "./",                        /* Redirect output structure to the directory. */
    // "rootDir": "./",                       /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */
    // "composite": true,                     /* Enable project compilation */
    // "tsBuildInfoFile": "./",               /* Specify file to store incremental compilation information */
    // "removeComments": true,                /* Do not emit comments to output. */
    // "noEmit": true,                        /* Do not emit outputs. */
    // "importHelpers": true,                 /* Import emit helpers from 'tslib'. */
    // "downlevelIteration": true,            /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */
    // "isolatedModules": true,               /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */
 
    /* Strict Type-Checking Options */
    //"strict": true,                           /* Enable all strict type-checking options. */
    // "noImplicitAny": true,                 /* Raise error on expressions and declarations with an implied 'any' type. */
    // "strictNullChecks": true,              /* Enable strict null checks. */
    // "strictFunctionTypes": true,           /* Enable strict checking of function types. */
    // "strictBindCallApply": true,           /* Enable strict 'bind', 'call', and 'apply' methods on functions. */
    // "strictPropertyInitialization": true,  /* Enable strict checking of property initialization in classes. */
    // "noImplicitThis": true,                /* Raise error on 'this' expressions with an implied 'any' type. */
    // "alwaysStrict": true,                  /* Parse in strict mode and emit "use strict" for each source file. */
 
    /* Additional Checks */
    // "noUnusedLocals": true,                /* Report errors on unused locals. */
    // "noUnusedParameters": true,            /* Report errors on unused parameters. */
    // "noImplicitReturns": true,             /* Report error when not all code paths in function return a value. */
    // "noFallthroughCasesInSwitch": true,    /* Report errors for fallthrough cases in switch statement. */

    /* Module Resolution Options */
    // "moduleResolution": "node",            /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */
    // "baseUrl": "./",                       /* Base directory to resolve non-absolute module names. */
    // "paths": {},                           /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */
    // "rootDirs": [],                        /* List of root folders whose combined content represents the structure of the project at runtime. */
    // "typeRoots": [],                       /* List of folders to include type definitions from. */
    // "types": [],                           /* Type declaration files to be included in compilation. */
    // "allowSyntheticDefaultImports": true,  /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */
     "esModuleInterop": true                   /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */
    // "preserveSymlinks": true,              /* Do not resolve the real path of symlinks. */
    // "allowUmdGlobalAccess": true,          /* Allow accessing UMD globals from modules. */

    /* Source Map Options */
    // "sourceRoot": "",                      /* Specify the location where debugger should locate TypeScript files instead of source locations. */
    // "mapRoot": "",                         /* Specify the location where debugger should locate map files instead of generated locations. */
    // "inlineSourceMap": true,               /* Emit a single file with source maps instead of having a separate file. */
    // "inlineSources": true,                 /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */
 
    /* Experimental Options */
    // "experimentalDecorators": true,        /* Enables experimental support for ES7 decorators. */
    // "emitDecoratorMetadata": true,         /* Enables experimental support for emitting type metadata for decorators. */
  }
}
``` 



### 設定ファイル(webpack)

```:webpack.config.js
const path = require("path");

module.exports = {
    mode: "production",
    entry: "./src/ts/index.ts",

    output: {
        path: path.resolve(__dirname, "dist"),
        filename: "js/main.js",
    },

    devtool: "source-map",

    module: {
        rules: [
            {
                test: /\.ts$/,
                exclude: /node_modules/,
                use: "ts-loader",
            },
        ],
    },

    resolve: {
        extensions: [".ts", ".js", ".json"],
    },
};
``` 


### 動作確認
インポートした他のTypeScriptファイルが1つのJavsScriptファイルにバンドルされるのを確認します。

```:/src/ts/index.ts
import { importTest } from "./sub";

const message = "TypeScriptの変換成功";

console.log(message);
console.log(importTest);
```

```:/src/ts/sub.ts
export const importTest: string = "sub.tsからのimport成功";
```

```:terminal
//TypeScriptファイルをJavsScriptファイルに変換
npm run compile:ts

//TypeScriptファイルをJavsScriptファイルに変換(TypeScriptファイルを監視して変更される度)
npm run watch:ts
```
/src/ts/配下のTypeScriptファイルが/dist/js/配下にJavsScriptファイルとして変換されれば成功です。


## 画像圧縮
### インストール

```:terminal
npm i -D imagemin-cli
npm i -D imagemin-keep-folder
npm i -D imagemin-mozjpeg
npm i -D imagemin-pngquant
npm i -D imagemin-gifsicle
npm i -D imagemin-svgo
```

### npmコマンド追加

```:package.json
"scripts": {
  "watch:img": "chokidar \"src/img/**/*\" --command \"node imagemin.js\" --initial"
}
```

### 設定ファイル

```:imagemin.js
const keepfolder = require('imagemin-keep-folder');
const mozjpeg = require('imagemin-mozjpeg');
const pngquant = require('imagemin-pngquant');
const gifsicle = require('imagemin-gifsicle');
const svgo = require('imagemin-svgo');
 
keepfolder(['src/img/**/*.{jpg,png,gif,svg}'], {
  plugins: [
    mozjpeg({
      quality: 85
    }),
    pngquant({
      quality: [.7, .8]
    }),
    gifsicle(),
    svgo()
  ],
  replaceOutputDir: output => {
    return output.replace(/img\//, '../dist/img/')
  }
});
```

### 動作確認
/src/img/配下に画像を格納します。(jpg、png、gif、svg)

```:terminal
npm run watch:img
```

/src/img/配下の画像が圧縮されて/dist/img/配下にコピーされれば成功です。


## distディレクトリの削除
以前の作業で生成したファイルがdistに残っているとトラブルにつながる恐れがあるため、削除します。

### インストール
```:terminal
npm i rimraf -D
```

### npmコマンド追加

```:package.json
"scripts": {
  "delete:dist": "rimraf dist"
}
```

### 動作確認

```:terminal
npm run delete:dist
```

distディレクトリが削除されれば成功です。



## ファイルのコピー
今回は、sitemap.xml、robots.txt、humans.txt、manifest.json、favicon.ico、apple-touch-icon.pngをコピーするようにします。
あまり変更が入らないファイルのコピーを想定しているので、-w オプションは付けません。

### インストール

```:terminal
npm i -D cpx
```


### npmコマンド追加

```:package.json
"scripts": {
  "copy:sitemapxml": "cpx \"src/sitemap.xml\" dist/",
  "copy:robotstxt": "cpx \"src/robots.txt\" dist/",
  "copy:humanstxt": "cpx \"src/humans.txt\" dist/",
  "copy:manifestjson": "cpx \"src/manifest.json\" dist/",
  "copy:faviconico": "cpx \"src/favicon.ico\" dist/",
  "copy:apple-touch-iconpng": "cpx \"src/apple-touch-icon.png\" dist/"
}
```

### 動作確認

```:terminal
npm run copy:sitemapxml
```

/src/sitemap.xmlが/dist/sitemap.xmlにコピーされれば成功です。




## タスクの連携
これまで作ってきたタスクを連携して、コマンド1つで実行できるようにします。

```:package.json
"scripts": {
  "prewatch:all": "run-s delete:dist copy:sitemapxml copy:robotstxt copy:humanstxt copy:manifestjson copy:faviconico copy:apple-touch-iconpng",
  "watch:all": "run-p watch:pug watch:scss2cssprefix watch:ts watch:img start:server"
}
```

### 動作確認

```:terminal
npm run watch:all
```

HTML、CSS、JavaScriptのコンパイルを並列処理しているので、表示が崩れている場合は完了タイミングが異なっているのが原因と思われますので、リロードしてみてください。








## コード整形


### prettier
#### インストール

```:terminal
npm i -D prettier 
``` 


```:.prettierrc.json
{
  "semi": true,
  "trailingComma": "all",
  "printWidth": 120,
  "tabWidth": 2,
  "useTabs": false,
  "proseWrap": "preserve",
  "endOfLine": "auto"
}
```

設定は下記URLを参考に適宜変更してください。
[.prettierrc](https://qiita.com/takeshisakuma/items/bbb2cd2f1c65de70e363)


### stylelint
#### インストール

```:terminal
npm i -D stylelint
npm i -D stylelint-config-recommended-scss
npm i -D stylelint-config-prettier

npm i -D stylelint-scss
npm i -D stylelint-order
npm i -D stylelint-prettier
```

#### npmコマンド追加

```:package.json
"scripts": {
  "lint:scss": "stylelint \"**/*.scss\"",
  "format:scss": "stylelint --fix \"**/*.scss\""
}
```

#### 設定ファイル

```:.stylelintrc.json
{
  "extends": [
    "stylelint-config-recommended-scss",
    "stylelint-config-prettier"
  ],
  "plugins": [
    "stylelint-order",
    "stylelint-prettier"
  ],
  "rules": {
    "prettier/prettier": true,
    "indentation": 2,
    "declaration-colon-newline-after": null,
    "value-list-comma-newline-after": "never-multi-line",
    "order/properties-alphabetical-order": true
  }
}
``` 


設定は下記URLを参考に適宜変更してください。
[.stylelintrc](https://qiita.com/takeshisakuma/items/a7a3b8cc0ce05422f686)

#### 動作確認
```:terminal
//SCSSファイルのリント
npm run lint:scss

//SCSSファイルの整形
npm run format:scss
```

### Eslint
#### インストール

```:terminal
npm i -D eslint
npm i -D @typescript-eslint/parser
npm i -D eslint-config-prettier
```

#### npmコマンド追加
```:package.json
"scripts": {
 "lint:ts": "eslint src/ts/**/*.ts",
 "format:ts": "eslint src/ts/**/*.ts --fix",
}
```


#### 設定ファイル

|envの設定値|内容|
|---|---|
|"browser": true|無いとdocumentやconsoleなどでエラーが出てしまう|
|"node":true|無いとwebpack.config.jsonのdirname、module、requireでエラーが出てしまう|



```:.eslintrc.json
{
  "parser": "@typescript-eslint/parser",
  "extends": [
    "eslint:recommended",
    "prettier"
  ],
  "parserOptions":  {
    "ecmaVersion":  2020,
    "sourceType":  "module"
  },
  "env": {
    "browser": true,
    "node: true,
  },
  "root": true,
  "rules": {
    "quotes": ["error", "double"], //クォーテーションがシングルのときエラー
    "indent": ["error", 2]
  }
}
``` 

設定は下記URLを参考に適宜変更してください。
https://eslint.org/docs/user-guide/configuring



### 動作確認

```:terminal
//TypeScriptファイルのリント
npm run lint:ts
 
//TypeScriptファイルの整形
npm run format:ts
```

### SCSS、TypeScript一括整形

```:package.json
"scripts": {
 "format:all":"run-p format:scss format:ts"
}
```

#### 動作確認

```:terminal
//TypeScriptファイル、SCSSファイルの整形
npm run format:all
```

## commit前にコード整形

### インストール

```:terminal
npm i -D simple-git-hooks
npm i -D lint-staged
```

### フック追加
|コマンド|内容|
|---|---|
|pre-commit|コミット前に実行|
|pre-push|プッシュ前に実行|



```:package.json
"simple-git-hooks": {
  "pre-commit": "npx lint-staged"
},
 
"lint-staged": {
  "src/ts/*.ts": [
    "prettier --write",
    "eslint --fix --quiet"
  ],
  "src/scss/*.scss": [
    "prettier --write",
    "stylelint --fix --quiet"
  ]
}
``` 


### pre-commit、pre-pushの有効化

```:terminal
npx simple-git-hooks
```

.git/hooks/pre-commit、.git/hooks/pre-pushというファイルが作られます。
git commitを行うと、lint-stagedに指定したコマンドが実行されます。




## Polyfillの追加(旧ブラウザ対応)
IE11など旧ブラウザでTypeScriptのトランスパイルによる構文の書き換えで対応できない機能を追加します。

SCSSの旧ブラウザ対応は.browserslistで、TipeScriptの旧ブラウザ対応はtsconfigのtargetで指定していますが、書き方の変更では対応できない部分について、機能追加を行います。

axiosはPromiseが必要ですが、IE11は非対応です。
試しにaxiosを使ってみます。

### axiosのインストール

```:terminal
npm i -S axios
```


### サンプルコード
```:index.ts
//外部ライブラリインポート確認
import axios from "axios";

//外部ライブラリ動作確認
axios.get("https://jsonplaceholder.typicode.com/users").then((res) => {
  console.log(res);
});
```

IE11でconsoleを開くと、Promiseが無いため作動していません。


### インストール

```:terminal
npm i -S core-js@3 
npm i -S regenerator
```

### インポート
エンドポイントのindex.tsの一番上にPolyfillインポートを追加

```:index.ts
//Polyfill
import "core-js";

//Can't resolve 'regenerator-runtime/runtime'が表示されたとき(async awaitを使う場合など)
//import  regeneratorRuntime  from  "regenerator-runtime" ;
```

### 動作確認
IE11でconsoleを開くと、エラーが消えてJSONが取得されています。

core-jsをインポートしておくと、必要なPolyfillが自動的に読み込まれるようになります。







## テスト
Jestを使えるようにします。

### インストール
```:terminal
npm i -D jest
npm i -D ts-jest
npm i -D @types/jest
```

### npmコマンド追加

```:package.json
"scripts": {
  "test": "jest"
}
```

### 設定ファイル

```:jest.config.js
module.exports = {
  "roots": [
    "<rootDir>/src/ts/"
  ],
  "testMatch": [
    "**/__tests__/**/*.+(ts|tsx|js)",
    "**/?(*.)+(spec|test).+(ts|tsx|js)"
  ],
  "transform": {
    "^.+\\.(ts|tsx)$": "ts-jest"
  },
}
```

### eslintrc.jsonの変更
Eslintでテストコードにエラーが出ないように、jestを追加

```:.eslintrc.json
"env": {
  "browser": true,
  "node":true,
  "jest": true
},
```

### コードのサンプル
#### テスト用サンプルコード

```:src\ts\test_for_jest.ts
export const calc = (x: number, y: number) => {
  return x + y;
}
``` 

#### テストコード
```:src\ts\__tests__\test_for_jest.spec.ts
import { calc } from "../test_for_jest";
 
describe("関数calcの動作確認", () => {
  it("3+4=7になる", () => {
    expect(calc(1, 8)).toBe(9);
  });
});
``` 

### 動作確認
```:terminal
npm run test
```


## 本番公開対応
/dist/配下のCSS、JavaScriptのminifyを行います。

### インストール
```:terinal
npm i -D uglify-es
npm i -D clean-css-cli
```


#### npmコマンド追加
```:package.json
"scripts": {
  "min:css":"cleancss -o dist/css/style.css dist/css/style.css",
  "min:js": "uglifyjs dist/js/main.js -o dist/js/main.js",
  "delete:map":"rimraf dist/**/*.map",
  "prepare:all":"run-p min:js min:css delete:map"
}
```


### 動作確認
```:terminal
npm run prepare:all
```


/dist/配下のファイルをアップロードします


## まとめ
### 使用時準備

```:termina;
git clone https://github.com/takeshisakuma/disorder.git
cd disorder
npm ci
npx simple-git-hooks
```

### Web制作時
```:terminal
npm run watch:all
```

### コード整形時

```:terminal
npm run format:all
```


### テスト

```:terminal
npm run test
```


### 公開
不要ファイル削除、minify

```:terminal
npm run prepare:all
```

dist配下のファイルをそのままアップロードします



## リポジトリ

https://github.com/takeshisakuma/disorder