如何發布一個 TypeScript 編寫的 npm 包
- 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
就本文而言,只要它是簡潔的和可測試的,它做什麼並不那麼重要。
初始化項目
讓我們從創建空目錄並初始化它開始。
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
}
}
最重要的設置是這些:
- 庫的主文件會位於
src
文件夾下,因此需要這麼設置"files": ["src/index.ts"]
。 "target": "es2015"
確保我們的庫支援現代平台,並且不會攜帶不必要的墊片。"module": "es2015"
。我們的模組將是一個標準的ES
模組(默認是CommonJS
)。ES
模式在現代瀏覽器下沒有任何問題;甚至Node從13版本開始就支援ES
模式。"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
一路狂按回車鍵就行,默認值就很好。
這會使用一些默認選項創建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.js
和index.d.ts
。
接下來就來發布吧。
發布
如果你還沒有在npm上註冊,就先註冊。
註冊成功後,通過你的終端用npm login
登錄。
我們離發布我們的新包只有一步之遙。不過,還有幾件事情需要處理。
首先,確保我們的package.json
中擁有正確的元數據。
- 確保
main
屬性設置為打包的文件"main": "dist/index.js"
。 - 為TypeScript用戶添加
"types": "dist/index.d.ts"
。 - 因為我們的庫會作為ES Module被使用,因此需要指定
"type": "module"
。 name
和description
也應填寫。
接著,我們應該處理好我們希望發布的文件。我不覺得要發布任何配置文件,也不覺得要發布源文件和測試文件。
我們可以做的一件事是使用.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]"))
結果非常棒!
然後運行node index.js
,你會看到螢幕上列印1
。
總結
我們從頭開始創建並發布了一個簡單的npm包。
我們的庫提供了一個ESM模組,TypeScript的類型,使用jest
覆蓋測試用例。
你可能會認為,這其實一點都不難,的確如此。
以上就是本文的所有內容,如果對你有所幫助,歡迎收藏、點贊、轉發~