日日操夜夜添-日日操影院-日日草夜夜操-日日干干-精品一区二区三区波多野结衣-精品一区二区三区高清免费不卡

公告:魔扣目錄網為廣大站長提供免費收錄網站服務,提交前請做好本站友鏈:【 網站目錄:http://www.ylptlb.cn 】, 免友鏈快審服務(50元/站),

點擊這里在線咨詢客服
新站提交
  • 網站:51998
  • 待審:31
  • 小程序:12
  • 文章:1030137
  • 會員:747

原文:
www.edgedb.com/blog/how-we…

作者:James Clarke

發布時間:MAY 26, 2022

文章首發于知乎
zhuanlan.zhihu.com/p/524296632 著作權歸作者所有。商業轉載請聯系作者獲得授權,非商業轉載請注明出處。

Deno 是新出的 JAVAScript 的運行時,默認支持 TypeScript 而不需要編譯。 Deno 的創作者 Ryan Dahl,同樣也是 Node.js 的創作者,解決了很多 Node 的基礎設計問題和安全漏洞,更好的支持了 ESM 和 TypeScript。

在 EdgeDB 中,我們開發了 Node.js 下的 EdgeDB client 庫 ,并且在 NPM 中發布。然而 Deno 有完全不同的另一套依賴管理機制,imports URL 地址 來引入 在 deno.land/x 上發布的包。我們想找到一條簡單的路 “Denoify” 代碼庫,可以將現有的 Node.js 包 生成 Deno 可以使用的包。這樣可以減輕維護的復雜度。

Node.js vs Deno

Node.js 和 Deno 的運行時有幾個關鍵的區別,我們在調整使用 Node.js 編寫的庫時必須考慮到:

  1. TypeScript 支持:Deno 可以直接執行 TypeScript 文件,Node.js 只能運行 JavaScript 代碼。
  2. 模塊解析:默認 Node.js 支持 CommonJS 規范的模塊引入,使用 require/module.exports 語法。同時 Node.js 有一個復雜的依賴解析算法, 當 從 node_modules 中加載像 react 這樣的模塊時,對于 react 導出的內容會自動加后綴,比如說增加 .js 或 .json,如果 import 是目錄的話,將會直接查找目錄下的 index.js 文件。Deno 極大簡化了這一過程。Deno 支持 ESM 模塊規范,支持 import /export 語法。同時 TypeScript 同樣支持這一語法。所有的引入如果不是一個相對路徑包含顯示文件擴展名就是一個 URL 路徑。這表明 Deno 不需要 node_modules 文件或是如 npm 或是 yarn 之類的包管理工具。外部包通過 URL 路徑直接導入,如 deno.land/x 或 GitHub。
  3. 標準庫:Node.js 有一套內置的標準模塊,如 fs path crypto http 等。這些模塊可以直接通過 require(’fs’) 導入。Deno 的標準庫是通過 URL deno.land/std/ 導入。兩個標準庫的功能也不同,Deno 放棄了一些舊的或過時的 Node.js api,引入了一個新的標準庫(受 Go 的啟發),統一支持現代 JavaScript 特性,如 Promises (而許多 Node.js api 仍然依賴于舊的回調風格)。
  4. 內置的全局變量:Deno 將核心 API 都封裝在 Deno 變量下,除了這之外 就沒有其他暴露的全局變量,沒有 Node.js 里的 Buffer 和 process 變量。

因此,我們如何才能解決這些差異,并讓我們的 Node.js 庫盡可能輕松運行在 Deno ?讓我們逐一分析這些變化。

TypeScript 和模塊語法

幸運的是,我們不需要太擔心將 CommonJS require/module 語法轉換為 ESM import/export。我們完全用 TypeScript 編寫 edgedb-js,它已經使用了 ESM 語法。在編譯過程中,tsc 利用CommonJS 語法將我們的文件轉換成普通的 JavaScript 文件。Node.js 可以直接使用編譯后的文件。

這篇文章的其余部分將討論如何將 TypeScript 源文件修改為 Deno 可以直接使用的格式。

依賴

幸運的是 edgedb-js 沒有任何第三方依賴,所以我們不必擔心任何外部庫的 Deno 兼容性。然而,我們需要將所有從 Node.js 標準庫中導入的文件(例如 path, fs 等)替換為 Deno 等價文件。

??注意:如果你的程序依賴于外部的包,需要到 deno.land/x 中檢查是否有 Deno 的版本。如果有繼續向下閱讀。如果沒有你需要和庫作者一起努力,將包改為 Deno 的版本。

由于 Deno 標準庫提供了 Node.js 兼容模塊,這個任務變得更加簡單。這個模塊在 Deno 的標準庫上提供了一個包裝器,它試圖盡可能忠實地遵守 Node 的 API。

- import * as crypto from "crypto";
+ import * as crypto from "<https://deno.land/std@0.114.0/node/crypto.ts>";
復制代碼

為了簡化流程,我們將所有引入的 Node.js API 包裝到 adapter.node.ts 文件中,然后重新導出

// adapter.node.ts
import * as path from "path";
import * as util from "util";
import * as crypto from "crypto";

export {path, net, crypto};
復制代碼

同樣 我們在 Deno 中使用相同的方式 adapter.deno.ts

// adapter.deno.ts
import * as crypto from "<https://deno.land/std@0.114.0/node/crypto.ts>";
import path from "<https://deno.land/std@0.114.0/node/path.ts>";
import util from "<https://deno.land/std@0.114.0/node/util.ts>";

export {path, util, crypto};
復制代碼

當我們需要 Node.js 特定的功能時,我們直接從 adapter.node.ts 導入。通過這種方式,我們可以通過簡單地將所有 adapter.node.ts 導入重寫為 adapter.deno.ts 來使 edgedb-js 兼容 deno。只要這些文件重新導出相同的功能,一切都應該按預期工作。

實際上,我們如何重寫這些導入呢?我們需要編寫一個簡單的 codemod 腳本。為了讓它更有詩意,我們將使用 Deno 本身來編寫這個腳本。

寫 Denoify 腳本

首先我們列舉下腳本需要實現的功能:

  • 將 Node.js 式 import 轉換為更具體的 Deno 式引入。具體是將引用文件都增加 .ts 后綴,給引用目錄都增加 /index.ts 文件。
  • 將 adapter.node 文件中的引用都轉換到 adapter.deno.ts
  • 將 Node.js 全局變量 如 process 和 Buffer 注入到 Deno-ified code。雖然我們可以簡單地從適配器導出這些變量,但我們必須重構 Node.js 文件以顯式地導入它們。為了簡化,我們將檢測在哪里使用了 Node.js 全局變量,并在需要時注入一個導入。
  • 將 src 目錄重命名為 _src,表示它是 edgedb-js 的內部文件,不應該直接導入
  • 將 main 目錄下的 src/index.node.ts 文件都移動到項目根目錄,并重命名為 mod.ts。注意:這里的 index.node.ts 并不表明這是 node 格式的,只是為了區分 index.browser.ts

創建一系列文件

首先,我們需要計算源文件的列表。

import {walk} from "<https://deno.land/std@0.114.0/fs/mod.ts>";

const sourceDir = "./src";
for await (const entry of walk(sourceDir, {includeDirs: false})) {
  // iterate through all files
}
復制代碼

??注意:我們這里使用的是 Deno 的 std/fs,而不是 Node 的 std/node/fs。

讓我們聲明一組重寫規則,并初始化一個 Map,它將從源文件路徑映射到重寫的目標路徑。

  const sourceDir = "./src";
+ const destDir = "./edgedb-deno";
+ const pathRewriteRules = [
+ {match: /^src/index.node.ts$/, replace: "mod.ts"},
+ {match: /^src//, replace: "_src/"},
+];

+ const sourceFilePathMap = new Map<string, string>();

  for await (const entry of walk(sourceDir, {includeDirs: false})) {
+  const sourcePath = entry.path;
+  sourceFilePathMap.set(sourcePath, resolveDestPath(sourcePath));
  }

+ function resolveDestPath(sourcePath: string) {
+   let destPath = sourcePath;
+   // Apply all rewrite rules
+   for (const rule of pathRewriteRules) {
+    destPath = destPath.replace(rule.match, rule.replace);
+  }
+  return join(destDir, destPath);
+}
復制代碼

以上部分比較簡單,下面開始修改源文件。

重寫 imports 和 exports

為了重寫 import 路徑,我們需要知道文件的存放位置。幸運的是 TypeScript 曝露了 編譯 API,我們可以用來解析源文件到 AST,并查找 import 聲明。

我們需要從 typescript 的 NPM 包中 import 編譯 API。幸運的是,Deno 提供了引用 CommonJS 規范的方法,需要在運行時 添加 --unstable 參數

import {createRequire} from "<https://deno.land/std@0.114.0/node/module.ts>";

const require = createRequire(import.meta.url);
const ts = require("typescript");
復制代碼

讓我們遍歷這些文件并依次解析每個文件。

import {walk, ensureDir} from "<https://deno.land/std@0.114.0/fs/mod.ts>";
import {createRequire} from "<https://deno.land/std@0.114.0/node/module.ts>";

const require = createRequire(import.meta.url);
const ts = require("typescript");

for (const [sourcePath, destPath] of sourceFilePathMap) {
  compileFileForDeno(sourcePath, destPath);
}

async function compileFileForDeno(sourcePath: string, destPath: string) {
  const file = await Deno.readTextFile(sourcePath);
  await ensureDir(dirname(destPath));

  // if file ends with '.deno.ts', copy the file unchanged
  if (destPath.endsWith(".deno.ts")) return Deno.writeTextFile(destPath, file);
  // if file ends with '.node.ts', skip file
  if (destPath.endsWith(".node.ts")) return;

  // parse the source file using the typescript Compiler API
  const parsedSource = ts.createSourceFile(
    basename(sourcePath),
    file,
    ts.ScriptTarget.Latest,
    false,
    ts.ScriptKind.TS
  );
}
復制代碼

對于每個已解析的 AST,讓我們遍歷其頂層節點以查找 import 和 export 聲明。我們不需要深入研究,因為 import/export 總是 top-level 語句(除了動態引用 dynamic import(),但我們在 edgedb-js中不使用它們)。

從這些節點中,我們提取源文件中 import/export 路徑的開始和結束偏移量。然后,我們可以通過切片當前內容并插入修改后的路徑來重寫導入。

  const parsedSource = ts.createSourceFile(/*...*/);

+ const rewrittenFile: string[] = [];
+ let cursor = 0;
+ parsedSource.forEachChild((node: any) => {
+  if (
+    (node.kind === ts.SyntaxKind.ImportDeclaration ||
+      node.kind === ts.SyntaxKind.ExportDeclaration) &&
+    node.moduleSpecifier
+  ) {
+    const pos = node.moduleSpecifier.pos + 2;
+    const end = node.moduleSpecifier.end - 1;
+    const importPath = file.slice(pos, end);
+
+    rewrittenFile.push(file.slice(cursor, pos));
+    cursor = end;
+
+    // replace the adapter import with Deno version
+    let resolvedImportPath = resolveImportPath(importPath, sourcePath);
+    if (resolvedImportPath.endsWith("/adapter.node.ts")) {
+      resolvedImportPath = resolvedImportPath.replace(
+        "/adapter.node.ts",
+        "/adapter.deno.ts"
+      );
+    }
+
+    rewrittenFile.push(resolvedImportPath);
  }
});

rewrittenFile.push(file.slice(cursor));
復制代碼

這里的關鍵部分是 resolveImportPath 函數,它實現了將 Node 類型的引入改為 Deno 類型的引入 。首先,它檢查路徑是否對應于磁盤上的實際文件;如果失敗了,它會嘗試添加 .ts 后綴;如果失敗,它嘗試添加 /index.ts;如果失敗,就會拋出一個錯誤。

注入 Node.js 全局變量

最后一步是處理 Node.js 全局變量。首先,我們在項目目錄中創建一個 global .deno.ts 文件。這個文件應該導出包中使用的所有 Node.js 全局變量的兼容版本。

export {Buffer} from "<https://deno.land/std@0.114.0/node/buffer.ts>";
export {process} from "<https://deno.land/std@0.114.0/node/process.ts>";
復制代碼

編譯后的 AST 提供了一組源文件中使用的所有標識符。我們將使用它在任何引用這些全局變量的文件中注入 import 語句。

 const sourceDir = "./src";
 const destDir = "./edgedb-deno";
 const pathRewriteRules = [
   {match: /^src/index.node.ts$/, replace: "mod.ts"},
   {match: /^src//, replace: "_src/"},
 ];
+ const injectImports = {
+ imports: ["Buffer", "process"],
+  from: "src/globals.deno.ts",
+ };

// ...

 const rewrittenFile: string[] = [];
 let cursor = 0;
+ let isFirstNode = true;
  parsedSource.forEachChild((node: any) => {
+  if (isFirstNode) {  // only run once per file
+    isFirstNode = false;
+
+    const neededImports = injectImports.imports.filter((importName) =>
+      parsedSource.identifiers?.has(importName)
+    );
+
+    if (neededImports.length) {
+     const imports = neededImports.join(", ");
+      const importPath = resolveImportPath(
+        relative(dirname(sourcePath), injectImports.from),
+        sourcePath
+      );
+      const importDecl = `import {${imports}} from "${importPath}";nn`;

+      const injectPos = node.getLeadingTriviaWidth?.(parsedSource) ?? node.pos;
+      rewrittenFile.push(file.slice(cursor, injectPos));
+      rewrittenFile.push(importDecl);
       cursor = injectPos;
     }
   }
復制代碼

寫文件

最后,我們準備將重寫的源文件寫入目標目錄中的新主目錄。首先,我們刪除所有現有的內容,然后依次寫入每個文件。

+ try {
+   await Deno.remove(destDir, {recursive: true});
+ } catch {}

 const sourceFilePathMap = new Map<string, string>();
 for (const [sourcePath, destPath] of sourceFilePathMap) {
  // rewrite file
+  await Deno.writeTextFile(destPath, rewrittenFile.join(""));
 }
復制代碼

持續集成

一個常見的模式是為包的 Deno 版本維護一個單獨的自動生成的 repo。在我們的例子中,每當一個新的提交合并到 master 中時,我們就在 GitHub Actions 中生成 edgedb-js 的 Deno 版本。然后,生成的文件被發布到名為 edgedb-deno 的姊妹存儲庫。下面是我們的工作流文件的簡化版本。

# .github/workflows/deno-release.yml
name: Deno Release
on:
  push:
    branches:
      - master
jobs:
  release:
    runs-on: ubuntu-latest
    steps:
      - name: Checkout edgedb-js
        uses: actions/checkout@v2
      - name: Checkout edgedb-deno
        uses: actions/checkout@v2
        with:
          token: ${{ secrets.GITHUB_TOKEN }}
          repository: edgedb/edgedb-deno
          path: edgedb-deno
      - uses: actions/setup-node@v2
      - uses: denoland/setup-deno@v1
      - name: Install deps
        run: yarn install
      - name: Get version from package.json
        id: package-version
        uses: martinbeentjes/npm-get-version-action@v1.1.0
      - name: Write version to file
        run: echo "${{ steps.package-version.outputs.current-version}}" > edgedb-deno/version.txt
      - name: Compile for Deno
        run: deno run --unstable --allow-env --allow-read --allow-write tools/compileForDeno.ts
      - name: Push to edgedb-deno
        run: cd edgedb-deno && git add . -f && git commit -m "Build from $GITHUB_SHA" && git push
復制代碼

edgedb-deno 內部的另一個工作流會創建一個 GitHub 發布,發布一個新版本到 deno.land/x。這留給讀者作為練習,盡管您可以使用我們的工作流作為起點。

總結

這是一個可廣泛應用的模式,用于將現有的 Node.js 模塊轉換為 Deno 模塊。參考 edgedb-js repo獲得完整的 Deno 編譯腳本,跨工作流。

分享到:
標簽:Node js
用戶無頭像

網友整理

注冊時間:

網站:5 個   小程序:0 個  文章:12 篇

  • 51998

    網站

  • 12

    小程序

  • 1030137

    文章

  • 747

    會員

趕快注冊賬號,推廣您的網站吧!
最新入駐小程序

數獨大挑戰2018-06-03

數獨一種數學游戲,玩家需要根據9

答題星2018-06-03

您可以通過答題星輕松地創建試卷

全階人生考試2018-06-03

各種考試題,題庫,初中,高中,大學四六

運動步數有氧達人2018-06-03

記錄運動步數,積累氧氣值。還可偷

每日養生app2018-06-03

每日養生,天天健康

體育訓練成績評定2018-06-03

通用課目體育訓練成績評定