如何釋出一個 TypeScript 編寫的 npm 包

chuckQu 發表於 2022-11-30

前言

在這篇文章中,我們將使用TypeScript和Jest從頭開始構建和釋出一個NPM包。

我們將初始化一個專案,設定TypeScript,用Jest編寫測試,並將其釋出到NPM。

專案

我們的庫稱為digx。它允許從巢狀物件中根據路徑找出值,類似於lodash中的get函式。

比如說:

const source = { my: { nested: [1, 2, 3] } }
digx(source, "my.nested[1]") //=> 2

就本文而言,只要它是簡潔的和可測試的,它做什麼並不那麼重要。

npm包可以在這裡找到。GitHub倉庫地址在這裡

初始化專案

讓我們從建立空目錄並初始化它開始。

mkdir digx
cd digx
npm init --yes

npm init --yes命令將為你建立package.json檔案,並填充一些預設值。

讓我們也在同一資料夾中設定一個git倉庫。

git init
echo "node_modules" >> .gitignore
echo "dist" >> .gitignore
git add .
git commit -m "initial"

構建庫

這裡會用到TypeScript,我們來安裝它。

npm i -D typescript

使用下面的配置建立tsconfig.json檔案:

{
  "files": ["src/index.ts"],
  "compilerOptions": {
    "target": "es2015",
    "module": "es2015",
    "declaration": true,
    "outDir": "./dist",
    "noEmit": false,
    "strict": true,
    "noImplicitAny": true,
    "strictNullChecks": true,
    "strictFunctionTypes": true,
    "strictBindCallApply": true,
    "strictPropertyInitialization": true,
    "noImplicitThis": true,
    "alwaysStrict": true,
    "noUnusedLocals": true,
    "noUnusedParameters": true,
    "noImplicitReturns": true,
    "noFallthroughCasesInSwitch": true,
    "noUncheckedIndexedAccess": true,
    "noImplicitOverride": true,
    "noPropertyAccessFromIndexSignature": true,
    "esModuleInterop": true,
    "forceConsistentCasingInFileNames": true,
    "skipLibCheck": true
  }
}

最重要的設定是這些:

  1. 庫的主檔案會位於src資料夾下,因此需要這麼設定"files": ["src/index.ts"]
  2. "target": "es2015" 確保我們的庫支援現代平臺,並且不會攜帶不必要的墊片。
  3. "module": "es2015"。我們的模組將是一個標準的ES模組(預設是CommonJS)。ES模式在現代瀏覽器下沒有任何問題;甚至Node從13版本開始就支援ES模式。
  4. "declaration": true - 因為我們想要自動生成d.ts宣告檔案。我們的TypeScript使用者將需要這些宣告檔案。

其他大部分選項只是各種可選的TypeScript檢查,我更喜歡開啟這些檢查。

開啟package.json,更新scripts的內容:

"scripts": {
  "build": "tsc"
}

現在我們可以用npm run build來執行構建...這樣會失敗的,因為我們還沒有任何可以構建的程式碼。

我們從另一端開始。

新增測試

作為一名負責任的開發,我們將從測試開始。我們將使用jest,因為它簡單且好用。

npm i -D jest @types/jest ts-jest

ts-jest包是Jest理解TypeScript所需要的。另一個選擇是使用babel,這將需要更多的配置和額外的模組。我們就保持簡潔,採用ts-jest

使用如下命令初始化jest配置檔案:

./node_modules/.bin/jest --init

一路狂按Enter鍵就行,預設值就很好。

這會使用一些預設選項建立jest.config.js檔案,並新增"test": "jest"指令碼到package.json中。

開啟jest.config.js,找到以preset開始的行,並更新為:

{
  // ...
  preset: "ts-jest",
  // ...
}

最後,建立src目錄,以及測試檔案src/digx.test.ts,填入如下程式碼:

import dg from "./index";

test("works with a shallow object", () => {
  expect(dg({ param: 1 }, "param")).toBe(1);
});

test("works with a shallow array", () => {
  expect(dg([1, 2, 3], "[2]")).toBe(3);
});

test("works with a shallow array when shouldThrow is true", () => {
  expect(dg([1, 2, 3], "[2]", true)).toBe(3);
});

test("works with a nested object", () => {
  const source = { param: [{}, { test: "A" }] };
  expect(dg(source, "param[1].test")).toBe("A");
});

test("returns undefined when source is null", () => {
  expect(dg(null, "param[1].test")).toBeUndefined();
});

test("returns undefined when path is wrong", () => {
  expect(dg({ param: [] }, "param[1].test")).toBeUndefined();
});

test("throws an exception when path is wrong and shouldThrow is true", () => {
  expect(() => dg({ param: [] }, "param[1].test", true)).toThrow();
});

test("works tranparently with Sets and Maps", () => {
  const source = new Map([
    ["param", new Set()],
    ["innerSet", new Set([new Map(), new Map([["innerKey", "value"]])])],
  ]);
  expect(dg(source, "innerSet[1].innerKey")).toBe("value");
});

這些單元測試讓我們對正在構建的東西有一個直觀的瞭解。

我們的模組匯出一個單一函式,digx。它接收任意物件,字串引數path,以及可選引數shouldThrow,該引數使得提供的路徑在源物件的巢狀結構中不被允許時,丟擲一個異常。

巢狀結構可以是物件和陣列,也可以是Map和Set。

使用npm t執行測試,當然,不出意外會失敗。

現在開啟src/index.ts檔案,並寫入下面內容:

export default dig;

/**
 * A dig function that takes any object with a nested structure and a path,
 * and returns the value under that path or undefined when no value is found.
 *
 * @param {any}     source - A nested objects.
 * @param {string}  path - A path string, for example `my[1].test.field`
 * @param {boolean} [shouldThrow=false] - Optionally throw an exception when nothing found
 *
 */
function dig(source: any, path: string, shouldThrow: boolean = false) {
  if (source === null || source === undefined) {
    return undefined;
  }

  // split path: "param[3].test" => ["param", 3, "test"]
  const parts = splitPath(path);

  return parts.reduce((acc, el) => {
    if (acc === undefined) {
      if (shouldThrow) {
        throw new Error(`Could not dig the value using path: ${path}`);
      } else {
        return undefined;
      }
    }

    if (isNum(el)) {
      // array getter [3]
      const arrIndex = parseInt(el);
      if (acc instanceof Set) {
        return Array.from(acc)[arrIndex];
      } else {
        return acc[arrIndex];
      }
    } else {
      // object getter
      if (acc instanceof Map) {
        return acc.get(el);
      } else {
        return acc[el];
      }
    }
  }, source);
}

const ALL_DIGITS_REGEX = /^\d+$/;

function isNum(str: string) {
  return str.match(ALL_DIGITS_REGEX);
}

const PATH_SPLIT_REGEX = /\.|\]|\[/;

function splitPath(str: string) {
  return (
    str
      .split(PATH_SPLIT_REGEX)
      // remove empty strings
      .filter((x) => !!x)
  );
}

這個實現可以更好,但對我們來說重要的是,現在測試透過了。自己用npm t試試吧。

現在,如果執行npm run build,可以看到dist目錄下會有兩個檔案,index.jsindex.d.ts

接下來就來發布吧。

釋出

如果你還沒有在npm上註冊,就先註冊

註冊成功後,透過你的終端用npm login登入。

我們離釋出我們的新包只有一步之遙。不過,還有幾件事情需要處理。

首先,確保我們的package.json中擁有正確的後設資料。

  1. 確保main屬性設定為打包的檔案"main": "dist/index.js"
  2. 為TypeScript使用者新增"types": "dist/index.d.ts"
  3. 因為我們的庫會作為ES Module被使用,因此需要指定"type": "module"
  4. namedescription也應填寫。

接著,我們應該處理好我們希望釋出的檔案。我不覺得要釋出任何配置檔案,也不覺得要釋出原始檔和測試檔案。

我們可以做的一件事是使用.npmignore,列出所有我們不想釋出的檔案。我更希望有一個"白名單",所以讓我們使用package.json中的files欄位來指定我們想要包含的檔案。

{
  // ...
  "files": ["dist", "LICENSE", "README.md", "package.json"],
  // ...
}

終於,我們已經準備好發包了。

執行以下命令:

npm publish --dry-run

並確保只包括所需的檔案。當一切準備就緒時,就可以執行:

npm publish

測試一下

讓我們建立一個全新的專案並安裝我們的模組。

npm install --save digx

現在,讓我們寫一個簡單的程式來測試它。

import dg from "digx"

console.log(dg({ test: [1, 2, 3] }, "test[0]"))

結果非常棒!

digx_autocomplete.png

然後執行node index.js,你會看到螢幕上列印1

總結

我們從頭開始建立併發布了一個簡單的npm包。

我們的庫提供了一個ESM模組,TypeScript的型別,使用jest覆蓋測試用例。

你可能會認為,這其實一點都不難,的確如此。

以上就是本文的所有內容,如果對你有所幫助,歡迎收藏、點贊、轉發~