不发呆的希希 發表於 2022-11-30 22:27:00

如何发布一个 TypeScript 编写的 npm 包

<h2 id="前言">前言</h2>
<p>在这篇文章中,我们将使用TypeScript和Jest从头开始构建和发布一个NPM包。</p>
<p>我们将初始化一个项目,设置TypeScript,用Jest编写测试,并将其发布到NPM。</p>
<h2 id="项目">项目</h2>
<p>我们的库称为<code>digx</code>。它允许从嵌套对象中根据路径找出值,类似于<code>lodash</code>中的<code>get</code>函数。</p>
<p>比如说:</p>
<pre><code class="language-jsx">const source = { my: { nested: } }
digx(source, "my.nested") //=&gt; 2
</code></pre>
<p>就本文而言,只要它是简洁的和可测试的,它做什么并不那么重要。</p>
<p>npm包可以在这里找到。GitHub仓库地址在这里。</p>
<h2 id="初始化项目">初始化项目</h2>
<p>让我们从创建空目录并初始化它开始。</p>
<pre><code class="language-shell">mkdir digx
cd digx
npm init --yes
</code></pre>
<p><code>npm init --yes</code>命令将为你创建<code>package.json</code>文件,并填充一些默认值。</p>
<p>让我们也在同一文件夹中设置一个<code>git</code>仓库。</p>
<pre><code class="language-shell">git init
echo "node_modules" &gt;&gt; .gitignore
echo "dist" &gt;&gt; .gitignore
git add .
git commit -m "initial"
</code></pre>
<h2 id="构建库">构建库</h2>
<p>这里会用到TypeScript,我们来安装它。</p>
<pre><code>npm i -D typescript
</code></pre>
<p>使用下面的配置创建<code>tsconfig.json</code>文件:</p>
<pre><code class="language-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
}
}
</code></pre>
<p>最重要的设置是这些:</p>
<ol>
<li>库的主文件会位于<code>src</code>文件夹下,因此需要这么设置<code>"files": ["src/index.ts"]</code>。</li>
<li><code>"target": "es2015"</code> 确保我们的库支持现代平台,并且不会携带不必要的垫片。</li>
<li><code>"module": "es2015"</code>。我们的模块将是一个标准的<code>ES</code>模块(默认是<code>CommonJS</code>)。<code>ES</code>模式在现代浏览器下没有任何问题;甚至Node从13版本开始就支持<code>ES</code>模式。</li>
<li><code>"declaration": true</code> - 因为我们想要自动生成<code>d.ts</code>声明文件。我们的TypeScript用户将需要这些声明文件。</li>
</ol>
<p>其他大部分选项只是各种可选的TypeScript检查,我更喜欢开启这些检查。</p>
<p>打开<code>package.json</code>,更新<code>scripts</code>的内容:</p>
<pre><code class="language-json">"scripts": {
"build": "tsc"
}
</code></pre>
<p>现在我们可以用<code>npm run build</code>来运行构建...这样会失败的,因为我们还没有任何可以构建的代码。</p>
<p>我们从另一端开始。</p>
<h2 id="添加测试">添加测试</h2>
<p>作为一名负责任的开发,我们将从测试开始。我们将使用<code>jest</code>,因为它简单且好用。</p>
<pre><code class="language-shell">npm i -D jest @types/jest ts-jest
</code></pre>
<p><code>ts-jest</code>包是Jest理解TypeScript所需要的。另一个选择是使用<code>babel</code>,这将需要更多的配置和额外的模块。我们就保持简洁,采用<code>ts-jest</code>。</p>
<p>使用如下命令初始化<code>jest</code>配置文件:</p>
<pre><code class="language-shell">./node_modules/.bin/jest --init
</code></pre>
<p>一路狂按回车键就行,默认值就很好。</p>
<p>这会使用一些默认选项创建<code>jest.config.js</code>文件,并添加<code>"test": "jest"</code>脚本到<code>package.json</code>中。</p>
<p>打开<code>jest.config.js</code>,找到以<code>preset</code>开始的行,并更新为:</p>
<pre><code class="language-jsx">{
// ...
preset: "ts-jest",
// ...
}
</code></pre>
<p>最后,创建<code>src</code>目录,以及测试文件<code>src/digx.test.ts</code>,填入如下代码:</p>
<pre><code class="language-jsx">import dg from "./index";

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

test("works with a shallow array", () =&gt; {
expect(dg(, "")).toBe(3);
});

test("works with a shallow array when shouldThrow is true", () =&gt; {
expect(dg(, "", true)).toBe(3);
});

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

test("returns undefined when source is null", () =&gt; {
expect(dg(null, "param.test")).toBeUndefined();
});

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

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

test("works tranparently with Sets and Maps", () =&gt; {
const source = new Map([
    ["param", new Set()],
    ["innerSet", new Set(])])],
]);
expect(dg(source, "innerSet.innerKey")).toBe("value");
});
</code></pre>
<p>这些单元测试让我们对正在构建的东西有一个直观的了解。</p>
<p>我们的模块导出一个单一函数,<code>digx</code>。它接收任意对象,字符串参数<code>path</code>,以及可选参数<code>shouldThrow</code>,该参数使得提供的路径在源对象的嵌套结构中不被允许时,抛出一个异常。</p>
<p>嵌套结构可以是对象和数组,也可以是Map和Set。</p>
<p>使用<code>npm t</code>运行测试,当然,不出意外会失败。</p>
<p>现在打开<code>src/index.ts</code>文件,并写入下面内容:</p>
<pre><code class="language-jsx">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.test.field`
* @param {boolean} - 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.test" =&gt; ["param", 3, "test"]
const parts = splitPath(path);

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

    if (isNum(el)) {
      // array getter
      const arrIndex = parseInt(el);
      if (acc instanceof Set) {
      return Array.from(acc);
      } else {
      return acc;
      }
    } else {
      // object getter
      if (acc instanceof Map) {
      return acc.get(el);
      } else {
      return acc;
      }
    }
}, 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) =&gt; !!x)
);
}
</code></pre>
<p>这个实现可以更好,但对我们来说重要的是,现在测试通过了。自己用<code>npm t</code>试试吧。</p>
<p>现在,如果运行<code>npm run build</code>,可以看到<code>dist</code>目录下会有两个文件,<code>index.js</code>和<code>index.d.ts</code>。</p>
<p>接下来就来发布吧。</p>
<h2 id="发布">发布</h2>
<p>如果你还没有在npm上注册,就先注册。</p>
<p>注册成功后,通过你的终端用<code>npm login</code>登录。</p>
<p>我们离发布我们的新包只有一步之遥。不过,还有几件事情需要处理。</p>
<p>首先,确保我们的<code>package.json</code>中拥有正确的元数据。</p>
<ol>
<li>确保<code>main</code>属性设置为打包的文件<code>"main": "dist/index.js"</code>。</li>
<li>为TypeScript用户添加<code>"types": "dist/index.d.ts"</code>。</li>
<li>因为我们的库会作为ES Module被使用,因此需要指定<code>"type": "module"</code>。</li>
<li><code>name</code>和<code>description</code>也应填写。</li>
</ol>
<p>接着,我们应该处理好我们希望发布的文件。我不觉得要发布任何配置文件,也不觉得要发布源文件和测试文件。</p>
<p>我们可以做的一件事是使用<code>.npmignore</code>,列出所有我们不想发布的文件。我更希望有一个"白名单",所以让我们使用<code>package.json</code>中的<code>files</code>字段来指定我们想要包含的文件。</p>
<pre><code class="language-json">{
// ...
"files": ["dist", "LICENSE", "README.md", "package.json"],
// ...
}
</code></pre>
<p>终于,我们已经准备好发包了。</p>
<p>运行以下命令:</p>
<pre><code class="language-shell">npm publish --dry-run
</code></pre>
<p>并确保只包括所需的文件。当一切准备就绪时,就可以运行:</p>
<pre><code class="language-shell">npm publish
</code></pre>
<h2 id="测试一下">测试一下</h2>
<p>让我们创建一个全新的项目并安装我们的模块。</p>
<pre><code class="language-shell">npm install --save digx
</code></pre>
<p>现在,让我们写一个简单的程序来测试它。</p>
<pre><code class="language-jsx">import dg from "digx"

console.log(dg({ test: }, "test"))
</code></pre>
<p>结果非常棒!</p>
<p><img src="https://p1-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/fdfd87e864634bbb9eacc696c72507ff~tplv-k3u1fbpfcp-watermark.image?"></p>
<p>然后运行<code>node index.js</code>,你会看到屏幕上打印<code>1</code>。</p>
<h2 id="总结">总结</h2>
<p>我们从头开始创建并发布了一个简单的npm包。</p>
<p>我们的库提供了一个ESM模块,TypeScript的类型,使用<code>jest</code>覆盖测试用例。</p>
<p>你可能会认为,这其实一点都不难,的确如此。</p>
<p>以上就是本文的所有内容,如果对你有所帮助,欢迎收藏、点赞、转发~</p><br><br>
来源:https://www.cnblogs.com/chuckQu/p/16939993.html
頁: [1]
查看完整版本: 如何发布一个 TypeScript 编写的 npm 包