使用 Deno 如何将 Node.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 unchangedif (destPath.endsWith(".deno.ts")) return Deno.writeTextFile(destPath, file);// if file ends with '.node.ts', skip fileif (destPath.endsWith(".node.ts")) return;// parse the source file using the typescript Compiler APIconst 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));复制代码


推荐阅读