﻿# 發行 Angular Schematics 程式產生器與設定 .npmignore 的注意事項

昨天寫了一個[小工具](https://www.npmjs.com/package/hmr-enabled)，可以自動將現有 Angular CLI 建立的專案，轉換成有 [Hot Module Replacement](https://webpack.js.org/concepts/hot-module-replacement/) (HMR) 功能的版本，只要簡單一個命令 `ng add hmr-enabled` 就可以搞定，非常方便。不過我在發布專案到 npm registry 的過程中，卻意外遺漏了幾個重要的檔案，原來是有檔案被 `.npmignore` 給忽略掉了，到了今天傍晚才修復。我們就立刻來看看這個錯誤是怎樣發生的！

### 建立 Angular Schematics 專案

這邊我就先示範一個簡單的 Schematics 程式產生器設計過程。

1.  安裝開發工具
    
    ```bash
    npm install -g @angular-devkit/schematics-cli
    ```
    
2.  建立專案骨架
    
    ```bash
    schematics blank --name=willh
    ```
    
    如果你想建立一個含有範例程式的專案，也可以考慮用以下命令建立範本：
    
    ```bash
    schematics schematic --name=willh-with-example
    ```
    
    上述兩種語法，也有另外一種寫法，不過比原本的指令會長很多：
    
    ```bash
    schematics @schematics/schematics:blank     --name my-schematics-1
    schematics @schematics/schematics:schematic --name my-schematics-2
    ```
    
3.  安裝 npm 套件
    
    ```bash
    cd willh
    npm install
    ```
    

### 建立第一支程式碼產生器

這邊我來示範一個可以自動產生 `/src/environments/environment.{name}.ts` 檔案的程式產生器，而且只要執行 `ng g willh:env hmr` 就會自動產生 `/src/environments/environment.hmr.ts` 檔案。我們第一版先不用做太複雜，產生固定的內容就好。

1.  檢查 `package.json` 所定義的 `schematics` 設定，預設會指向 `./src/collection.json` 這個檔案。
    
2.  調整 `src/collection.json` 檔案內容如下：
    
    ```json
    {
      "$schema": "../node_modules/@angular-devkit/schematics/collection-schema.json",
      "schematics": {
        "env": {
          "description": "generates environment file",
          "factory": "./env/index#env",
          "schema": "./env/schema.json"
        }
      }
    }
    ```
    
3.  建立 `src/env/schema.json` 檔案，讓程式可以接受 `name` 參數，並且讓 `name` 參數直接接在參數列上，不用特別透過 `--name` 選項進行設定。檔案內容如下：
    
    ```json
    {
      "$schema": "http://json-schema.org/schema",
      "id": "EnvSchema",
      "title": "Env Schematics Schema",
      "type": "object",
      "properties": {
        "name": {
          "type": "string",
          "description": "The name of the environment.",
          "$default": {
            "$source": "argv",
            "index": 0
          }
        }
      }
    }
    ```
    
4.  建立 `src/env/index.ts` 檔案內容如下：
    
    ```typescript
    import {
      Rule, mergeWith, template, apply, url
    } from '@angular-devkit/schematics';
    
    export function env(options: any): Rule {
      return () => {
        return mergeWith(
          apply(url('./files'), [
            template({
              name: options.name
            })
          ])
        );
      };
    }
    ```
    
5.  建立一個 `src/env/files/src/environments/environment.__name__.ts` 檔案，檔名的部分 `__name__` 會在產生檔案的時候，動態置換成 `name` 參數的內容。
    
    ```typescript
    // This file can be replaced during build by using the `fileReplacements` array.
    // `ng build ---prod` replaces `environment.ts` with `environment.prod.ts`.
    // The list of file replacements can be found in `angular.json`.
    
    export const environment = {
      production: false
    };
    
    /*
    * In development mode, for easier debugging, you can ignore zone related error
    * stack frames such as `zone.run`/`zoneDelegate.invokeTask` by importing the
    * below file. Don't forget to comment it out in production mode
    * because it will have a performance impact when errors are thrown
    */
    // import 'zone.js/dist/zone-error';  // Included with Angular CLI.
    ```
    
6.  建置專案
    
    ```bash
    npm run build
    ```
    
7.  本地測試安裝
    
    ```bash
    npm link
    ```
    
    執行 `npm link` 會將目前專案自動連結到 npm 的**全域模組**中。
    
8.  本地測試執行
    
    透過全域的 `schematics` 工具就可以執行這個命令。如下範例命令執行完後，就會自動產生 `/src/environments/environment.ok.ts` 檔案：
    
    ```bash
    schematics willh:env ok
    ```
    
9.  在 Angular CLI 專案中透過 `ng generate` 進行測試
    
    要在特定專案中執行 `ng generate` 的話，必須透過本地 npm 安裝才能使用。你可以直接透過 `npm install <folder>` 直接將開發中的目錄，當成套件安裝到目前的 Angular 開發專案目錄中。
    
    ```bash
    npm install G:\Projects\willh
    ```
    
    安裝好之後，就可以順利的使用 `ng g willh:env ok` 命令來產生檔案。
    

### 準備發行 npm 套件

一切準備就緒的時候，就是魔鬼出現的時刻！

我今天就是發現到，透過 `schematics blank` 產生的專案範本，預設所建立的 `.npmignore` 檔案是有問題的。這份檔案，主要是將 `*.ts` 檔案從發行檔案中排除，只留下 TypeScript 編譯後的 `*.js` 檔。但留下所有 `*.d.ts` 模組定義檔。檔案內容如下：

```bash
# Ignores TypeScript files, but keeps definitions.
*.ts
!*.d.ts
```

這個 [.npmignore](https://docs.npmjs.com/misc/developers#keeping-files-out-of-your-package) 檔案，最主要的目的，就是讓 npm 套件在發布的時候，將特定檔案/目錄排除在發行的檔案清單中。正常來說，排除掉 `*.ts` 與保留 `*.d.ts` 是蠻合理的。但在我們本次建立的 Angular Schematics 專案中，有個 `src/env/files/` 目錄，裡面的檔案預設會原封不動的合併到目的專案中，所以無論如何，這裡面的檔案，都是不能被排除的。

原則上來說，`.npmignore` 的內容格式與 `.gitignore` 完全一致，應該不會太難理解才對。我從發現這個問題後，就做出了幾種不同的嘗試，但是都是不對的，感覺頗地雷，各位要小心！

首先，先教大家怎樣在 npm 發行之前，如何才能預先知道有哪些套件會上傳到 npm registry 之中。這個命令，就是：

```bash
npm pack
```

這個命令執行完後，預設就會將所有要發行的檔案，打包成 `{name}-{version}.tgz` 的壓縮檔，你從壓縮檔案中，就可以得知會發布哪些檔案，不需要等 `npm publish` 之後才進行測試。

以下我列出幾個錯誤的嘗試：

1.  嘗試將 `/src/env/files/` 目錄排除在忽略清單外，避免 `/src/env/files/` 目錄下的 `*.ts` 檔被排除掉了！(**失敗**)
    
    ```bash
    *.ts
    !*.d.ts
    !/src/env/files/
    ```
    
2.  由於設定完全無效，所以調整一下 `/src/env/files/` 的順序到第一順位，但依然無效！(**失敗**)
    
    ```bash
    !/src/env/files/
    *.ts
    !*.d.ts
    ```
    

到最後，我終於理解到一件官網文件沒有寫的細節。那就是 **如果你將特定檔案類型加入到忽略清單裝，如果要透過 `!` 再把檔案加回來，那就必須要寫成針對定檔案進行描述，不能直接透過「特定目錄」進行設定**！

最後，正確的寫法如下：

```bash
*.ts
!*.d.ts
!src/*/files/**/*
```

寫成這樣也可以：

```bash
*.ts
!*.d.ts
!src/**/files/**/*
```

是不是很雷！真的遇到了才會知道啊！

**相關連結**

-   [Keeping files out of your package | npm Documentation](https://docs.npmjs.com/misc/developers#keeping-files-out-of-your-package)
-   [Ignore Files - Git](https://git-scm.com/book/en/v2/Git-Basics-Recording-Changes-to-the-Repository#_ignoring)
-   [Schematics — An Introduction – Angular Blog](https://blog.angular.io/schematics-an-introduction-dc1dfbc2a2b2)
