您当前的位置:首页 > 电脑百科 > 程序开发 > 编程百科

如何将 Node.js 库转换为 Deno 库(使用 Deno)

时间:2022-06-06 14:03:01  来源:  作者:Hugo.S

原文:
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 编译脚本,跨工作流。



Tags:Node.js   点击:()  评论:()
声明:本站部分内容及图片来自互联网,转载是出于传递更多信息之目的,内容观点仅代表作者本人,不构成投资建议。投资者据此操作,风险自担。如有任何标注错误或版权侵犯请与我们联系,我们将及时更正、删除。
▌相关推荐
理解 Node.js 中的事件循环
你已经使用 Node.js 一段时间了,构建了一些应用程序,尝试了不同的模块,甚至对异步编程感到很舒适。但是有些事情一直在困扰着你&mdash;&mdash;事件循环(Event Loop)。如果你像我...【详细内容】
2024-01-05  Search: Node.js  点击:(124)  评论:(0)  加入收藏
让Node.js加速你的网络应用开发之旅
Node.js 是一个基于Chrome V8 引擎的JavaScript运行时,用于构建高性能、可伸缩的网络应用。它的出现在很大程度上改变了后端开发的方式,并成为了现代Web开发中不可或缺的一部...【详细内容】
2023-12-13  Search: Node.js  点击:(188)  评论:(0)  加入收藏
告别Node.js版本困扰,轻松切换开发环境!
最近,在下载别人的开源项目进行学习的时候,总是因为 Node 环境问题导致没法依赖无法正常安装,因为人家开源项目限制了一定要高于某个版本,差点劝退了我。Node版本限制因为平时偶...【详细内容】
2023-11-08  Search: Node.js  点击:(277)  评论:(0)  加入收藏
19 种常见的 JavaScript 和 Node.js 错误
译者 | 刘汪洋审校 | 重楼速度、性能和响应性在 Web 开发中起着至关重要的作用,尤其是在使用 JavaScript 和 Node.js 开发时尤为重要。如果一个网站响应缓慢或界面卡顿,就会让...【详细内容】
2023-11-03  Search: Node.js  点击:(195)  评论:(0)  加入收藏
通过这个技术,浏览器可以运行Node.js、Rust、Python、PHP、C++、Java代码了!
近日,WebContainers 发布重要更新,WASI(WebAssembly 系统接口)已全面集成到 WebContainers 中。这是一个重要里程碑,它扩大了可以使用浏览器执行的操作,是 Web 开发的全新范例,允许...【详细内容】
2023-10-13  Search: Node.js  点击:(276)  评论:(0)  加入收藏
为什么Node.js 是后端开发的规则改变者
作者丨P. Rehan编译丨诺亚“Node.js有危险了!”“任何能够自救的开发人员都应该尽快迁移到另一个后端环境!”JavaScript的仇恨者说。不用理会这些言论。Node.js将继续存在,并...【详细内容】
2023-09-09  Search: Node.js  点击:(251)  评论:(0)  加入收藏
Node.js 终于原生支持 .env 文件了!
近日,Node.js 团队核心成员 Ulises Gasc&oacute;n 在社交平台表示,Node.js 20.6 版本将原生支持 .env 文件,该版本计划于 8 月 28 日发布。下面就来看看 .env 文件是什么,有什么...【详细内容】
2023-08-29  Search: Node.js  点击:(385)  评论:(0)  加入收藏
将 Node.js 应用程序容器化的七种方法
本文列出了七种容器化 node.js 应用程序的方法,让我们简要地看一下它们。在过去的五年里,Node.js 一直是严肃程序员的最爱。最大吞吐量的 JavaScript 运行时环境是一个免费的...【详细内容】
2023-03-24  Search: Node.js  点击:(262)  评论:(0)  加入收藏
十个优质的基于Node.js的CMS 内容管理平台
内容管理系统 (「CMS」) 使没有强大技术背景的人也能够轻松发布内容。我们可以使用 「CMS」 来管理我们的内容和交付。市面上有不同类型的 「CMS」,它们执行不同的目的并具有...【详细内容】
2023-03-03  Search: Node.js  点击:(285)  评论:(0)  加入收藏
Node.js 是如何跑起来的
本文为来自 字节跳动-国际化电商-S 项目团队 成员的文章,已授权 ELab 发布。一个 TCP 连接的案例​TCP 服务端​const net = require(&#39;net&#39;);const server = new net...【详细内容】
2023-03-03  Search: Node.js  点击:(169)  评论:(0)  加入收藏
▌简易百科推荐
Meta如何将缓存一致性提高到99.99999999%
介绍缓存是一种强大的技术,广泛应用于计算机系统的各个方面,从硬件缓存到操作系统、网络浏览器,尤其是后端开发。对于Meta这样的公司来说,缓存尤为重要,因为它有助于减少延迟、扩...【详细内容】
2024-04-15    dbaplus社群  Tags:Meta   点击:(3)  评论:(0)  加入收藏
SELECT COUNT(*) 会造成全表扫描?回去等通知吧
前言SELECT COUNT(*)会不会导致全表扫描引起慢查询呢?SELECT COUNT(*) FROM SomeTable网上有一种说法,针对无 where_clause 的 COUNT(*),MySQL 是有优化的,优化器会选择成本最小...【详细内容】
2024-04-11  dbaplus社群    Tags:SELECT   点击:(3)  评论:(0)  加入收藏
10年架构师感悟:从问题出发,而非技术
这些感悟并非来自于具体的技术实现,而是关于我在架构设计和实施过程中所体会到的一些软性经验和领悟。我希望通过这些分享,能够激发大家对于架构设计和技术实践的思考,帮助大家...【详细内容】
2024-04-11  dbaplus社群    Tags:架构师   点击:(2)  评论:(0)  加入收藏
Netflix 是如何管理 2.38 亿会员的
作者 | Surabhi Diwan译者 | 明知山策划 | TinaNetflix 高级软件工程师 Surabhi Diwan 在 2023 年旧金山 QCon 大会上发表了题为管理 Netflix 的 2.38 亿会员 的演讲。她在...【详细内容】
2024-04-08    InfoQ  Tags:Netflix   点击:(5)  评论:(0)  加入收藏
即将过时的 5 种软件开发技能!
作者 | Eran Yahav编译 | 言征出品 | 51CTO技术栈(微信号:blog51cto) 时至今日,AI编码工具已经进化到足够强大了吗?这未必好回答,但从2023 年 Stack Overflow 上的调查数据来看,44%...【详细内容】
2024-04-03    51CTO  Tags:软件开发   点击:(9)  评论:(0)  加入收藏
跳转链接代码怎么写?
在网页开发中,跳转链接是一项常见的功能。然而,对于非技术人员来说,编写跳转链接代码可能会显得有些困难。不用担心!我们可以借助外链平台来简化操作,即使没有编程经验,也能轻松实...【详细内容】
2024-03-27  蓝色天纪    Tags:跳转链接   点击:(16)  评论:(0)  加入收藏
中台亡了,问题到底出在哪里?
曾几何时,中台一度被当做“变革灵药”,嫁接在“前台作战单元”和“后台资源部门”之间,实现企业各业务线的“打通”和全域业务能力集成,提高开发和服务效率。但在中台如火如荼之...【详细内容】
2024-03-27  dbaplus社群    Tags:中台   点击:(14)  评论:(0)  加入收藏
员工写了个比删库更可怕的Bug!
想必大家都听说过删库跑路吧,我之前一直把它当一个段子来看。可万万没想到,就在昨天,我们公司的某位员工,竟然写了一个比删库更可怕的 Bug!给大家分享一下(不是公开处刑),希望朋友们...【详细内容】
2024-03-26  dbaplus社群    Tags:Bug   点击:(9)  评论:(0)  加入收藏
我们一起聊聊什么是正向代理和反向代理
从字面意思上看,代理就是代替处理的意思,一个对象有能力代替另一个对象处理某一件事。代理,这个词在我们的日常生活中也不陌生,比如在购物、旅游等场景中,我们经常会委托别人代替...【详细内容】
2024-03-26  萤火架构  微信公众号  Tags:正向代理   点击:(15)  评论:(0)  加入收藏
看一遍就理解:IO模型详解
前言大家好,我是程序员田螺。今天我们一起来学习IO模型。在本文开始前呢,先问问大家几个问题哈~什么是IO呢?什么是阻塞非阻塞IO?什么是同步异步IO?什么是IO多路复用?select/epoll...【详细内容】
2024-03-26  捡田螺的小男孩  微信公众号  Tags:IO模型   点击:(10)  评论:(0)  加入收藏
站内最新
站内热门
站内头条