Dnt 為何火?Deno 運行時的生死之局

2024年2月6日 22点热度 0人点赞

傢好,很高興又見面了,我是"高級前端‬進階‬",由我帶著大傢一起關註前端前沿、深入前端底層技術,大傢一起進步,也歡迎大傢關註、點贊、收藏、轉發!您的支持是我不斷創作的動力。

高級前端‬進階

前言

阿特伍德定律(Atwood's Law)指出,任何可以用 JavaScript 編寫的應用程序最終都將用 Javascript 編寫。 這個預言是在 Node.js 誕生前兩年提出的,結果證明也是非常正確的。 在瀏覽器上下文之外運行 JavaScript 環境的到來,鼓勵開發者開始用純 JS 編寫服務器、CLI,甚至機器學習算法模型。

Any application that can be written in JS will eventually be written in JavaScript!

盡管 Node.js 是在服務器上運行 JavaScript 的事實標準,但並不是每個人都對它非常滿意,尤其是它的作者!所以才有了其他的 JS Runtime,比如:Deno、Bun 等替代方案。以下是已發佈文章的傳送門,更多關於 JavaScript 運行時的文章可以查看我在頭條創建的《前端運行時》合集,後續會持續更新合集內容,歡迎大傢保持關註。

最近在社區閑逛,無意中遇到了一個新的工具,即 Deno 生態的 Dnt。關於前端運行時的發展我最近也比較關註,所以決定一探究竟弄明白 Dnt 到底是什麼,為什麼Deno生態會主推它。話不多說,直接開始!

1.什麼是 Dnt

Dnt (全稱為:Deno to Node Transform) 使用 Deno 模塊作為輸入創建一個 npm 包以在 Node.js 運行時中使用。

在 Dnt 的轉換管道中經歷了以下幾個步驟:

  • 將 Deno 代碼轉換為 Node/canonical TypeScript,包括 deno test 中找到的文件。主要涉及:重寫模塊說明符;為任何 Deno 命名空間或指定的其他全局名稱用法註入墊片;將 Skypack 和 esm.sh 說明符重寫為裸說明符,並將這些依賴項包含在 package.json 中;當遠程模塊無法解析為 npm 包時,它會下載它們並重寫說明符以使其成為本地模塊;允許將任何說明符映射到 npm 包。
  • 類型檢查輸出
  • 輸出 ESM、CommonJS 和 TypeScript 聲明文件以及 package.json 文件。
  • 通過調用所有 Deno.test 調用的測試運行器在 Node.js 中運行最終輸出

目前 Dnt 在 Github 上通過 MIT 協議開源,有超過700 的 star,是 Deno 和 Node.js 生態不容或缺的粘合劑。

2.如何使用 Dnt

首先需要創建一個構建腳本:

// ex. scripts/build_npm.ts
import { build, emptyDir } from "https://deno.land/x/dnt/mod.ts";
await emptyDir("./npm");
await build({
  entryPoints: ["./mod.ts"],
  outDir: "./npm",
  shims: {
    // see JS docs for overview and more options
    deno: true,
  },
  package: {
    // package.json properties
    name: "your-package",
    version: Deno.args[0],
    description: "Your package.",
    license: "MIT",
    repository: {
      type: "git",
      url: "git https://github.com/username/repo.git",
    },
    bugs: {
      url: "https://github.com/username/repo/issues",
    },
  },
  postBuild() {
    // steps to run after building and before running the tests
    Deno.copyFileSync("LICENSE", "npm/LICENSE");
    Deno.copyFileSync("README.md", "npm/README.md");
  },
});

如果需要,請忽略帶有源代碼管理的輸出目錄(例如,將 npm/ 添加到 .gitignore)。接著運行 deno run 和 npm publish:

// run script
deno run -A scripts/build_npm.ts 0.1.0
//go to output directory and publish
cd npm
npm publish

下面的輸出為示例構建日志:

[dnt] Transforming...
[dnt] Running npm install...
[dnt] Building project...
[dnt] Type checking ESM...
[dnt] Emitting ESM package...
[dnt] Emitting script package...
[dnt] Running tests...
> test
> node test_runner.js
Running tests in ./script/mod.test.js...
test escapeWithinString ... ok
test escapeChar ... ok
Running tests in ./esm/mod.test.js...
test escapeWithinString ... ok
test escapeChar ... ok
[dnt] Complete!

2. Dnt 高級用法

禁用類型檢查、測試、聲明輸出或 CommonJS/UMD 輸出

使用以下選項禁用其中任何一個,默認情況下啟用:

await build({
  // ...etc...
  typeCheck: false,
  test: false,
  declaration: false,
  scriptModule: false,
});

類型檢查 ESM 和腳本輸出

默認情況下,出於性能原因,隻會對 ESM 輸出進行類型檢查。也就是說,建議通過將 typeCheck 設置為“both”來對 ESM 和腳本 (CJS/UMD) 輸出進行類型檢查:

await build({
  // ...etc...
  typeCheck: "both",
});

忽略特定類型檢查錯誤

有時可能會收到一個無用的 TypeScript 錯誤,而想忽略它。這可以通過使用 filterDiagnostic 選項來實現:

await build({
  // ...etc...
  filterDiagnostic(diagnostic) {
    if (diagnostic.file?.fileName.endsWith("fmt/colors.ts")) {
      return false; // ignore all diagnostics in this file
    }
    // etc... more checks here
    return true;
  },
});

這對於忽略遠程依賴項中的類型檢查錯誤特別有用。

Top Level Await

Top Level Await 在 CommonJS/UMD 中不起作用,如果使用 Top Level Await 並且正在輸出 CommonJS/UMD 代碼,dnt 將出錯。 如果想輸出一個 CommonJS/UMD 包,那麼將不得不重組代碼以不使用任何 Top Level Await。 否則,將 scriptModule 構建選項設置為 false:

await build({
  // ...etc...
  scriptModule: false,
});

Shims(墊片)

dnt 將自動 shim 構建選項中指定的全局變量。例如,如果指定以下構建選項:

await build({
  // ...etc...
  shims: {
    deno: true,
  },
});

然後寫了下面的聲明:

Deno.readTextFileSync(...);

dnt 將在輸出中創建一個 shim 文件,重新導出 @deno/shim-deno npm shim 包,並將 Deno 全局更改為用作此對象的屬性。

import * as dntShim from "./_dnt.shims.js";
dntShim.Deno.readTextFileSync(...);

如果希望 shim 僅在測試代碼中用作開發依賴項,請為該選項指定“dev”。例如,要僅將 Deno 命名空間用於開發,並在分佈式代碼中使用 setTimeout 和 setInterval 瀏覽器/Deno 兼容墊片,可以這樣做:

await build({
  // ...etc...
  shims: {
    deno: "dev",
    timers: true,
  },
});

npm 包映射的說明符

在大多數情況下,dnt 不會知道 npm 包可用於實際依賴項之一,並且會下載遠程模塊以包含在包中。 但在某些情況下,可能存在 npm 包,而想改用它。 這可以通過為 npm 包映射提供說明符來完成。

await build({
  // ...etc...
  mappings: {
    "https://deno.land/x/[email protected]/mod.ts": {
      name: "code-block-writer",
      version: "^11.0.0",
      // optionally specify if this should be a peer dependency
      peerDependency: false,
    },
  },
});

此時會完成以下兩件事情:

  • 將所有“https://deno.land/x/[email protected]/mod.ts”說明符更改為“code-block-writer”
  • 為“code-block-writer”添加一個 package.json 依賴:“^11.0.0”。

請註意,如果指定了一個映射並且在代碼中找不到它,dnt 將出錯。 這樣做是為了防止遠程說明符的版本發生碰撞並且映射未更新的情況。

另外一種情況,假設一個名為 example 的 npm 包在 sub_path.js 有一個子路徑,開發者想將
https://deno.land/x/[email protected]/sub_path.ts 映射到那個子路徑。要指定它,可以執行以下操作:

await build({
  // ...etc...
  mappings: {
    "https://deno.land/x/[email protected]/sub_path.ts": {
      name: "example",
      version: "^0.1.0",
      subPath: "sub_path.js", // note this
    },
  },
});

這相當於將如下代碼:

import * as mod from "https://deno.land/x/[email protected]/sub_path.ts";

修改為:

import * as mod from "example/sub_path.js";

同時自動添加"example": "^0.1.0"依賴。

多個入口點

為此,請按照如下方式指定多個入口點(例如,一個入口點位於 .,另一個入口點位於 ./internal):

await build({
  entryPoints: [
    "mod.ts",
    {
      name: "./internal",
      path: "internal.ts",
    },
  ],
  // ...etc...
});

這將創建一個 package.json 並將這些作為導出:

{
  "name": "your-package",
  // etc...
  "main": "./script/mod.js",
  "module": "./esm/mod.js",
  "types": "./types/mod.d.ts",
  "exports": {
    ".": {
      "import": {
        "types": "./types/mod.d.ts",
        "default": "./esm/mod.js"
      },
      "require": {
        "types": "./types/mod.d.ts",
        "default": "./script/mod.js"
      }
    },
    "./internal": {
      "import": {
        "types": "./types/internal.d.ts",
        "default": "./esm/internal.js"
      },
      "require": {
        "types": "./types/internal.d.ts",
        "default": "./script/internal.js"
      }
    }
  }
}

現在可以導入這些入口點,例如: import _ as main from "your-package" 和 import _ as internal from "your-package/internal";。

Node.js v14 及以下版本

dnt 能夠通過在構建選項中指定 { compilerOption: { target: ... }} 值來定位舊版本的 Node(有關目標映射到 Node 版本的信息,請參閱 Node Target Mapping)。 一個問題是某些墊片可能無法在舊版本的 Node.js 中工作。

如果想要針對 Node v14 及以下版本,建議使用 Deno.test-only shim(如上所述),然後使用“映射”功能編寫 Node-only 文件,可以在其中處理差異。 或者,查看對 shim 庫的更改是否可以使其在舊版本的 Node.js 上運行。 不幸的是,某些功能不可能或不可行。

對於可能對打包器有用的 Deno 到規范 TypeScript 的轉換,請使用以下命令:

// docs: https://doc.deno.land/https/deno.land/x/dnt/transform.ts
import { transform } from "https://deno.land/x/dnt/transform.ts";
const outputResult = await transform({
  entryPoints: ["./mod.ts"],
  testEntryPoints: ["./mod.test.ts"],
  shims: [],
  testShims: [],
  // mappings: {}, // optional specifier mappings
});

3.本文總結

本文主要和大傢介紹 Dnt ,即使用 Deno 模塊作為輸入創建一個 npm 包以在 Node.js 運行時中使用。相信通過本文的閱讀,大傢對 Dnt 會有一個初步的了解。

其實,從個人觀點來看,Deno 創建的初衷主要在於其安全性,而 Node.js 持續發展了很長一段的時間,讓大多數開發者從 Node.js 切換到 Deno 生態絕非一蹴而就,而 Dnt 在這裡就扮演了一個中間角色,讓大傢能同時享受 Deno 和 Node.js 的雙重優勢。因此,從這一點來看 Deno 運行時應該也是早早意識到了這一點,所以采用了 Dnt 的橫空出世

因為篇幅有限,關於 Dnt 的更多用法和特性文章並沒有過多展開,如果有興趣,可以在我的主頁繼續閱讀,同時文末的參考資料提供了大量優秀文檔以供學習。最後,歡迎大傢點贊、評論、轉發、收藏,您的支持是我不斷創作的動力。

參考資料

https://github.com/denoland/dnt

https://dev.to/dajiaji/write-once-run-anywhere-with-deno-and-dnt-f29#dnt