jiti 原理学习
4/20/2024, 4:49:00 PM#nodejs
AI Summary
jiti 是一个直接用来运行 TS 和 ESM 的 node 运行时,它的原理是通过 NodeJS 中的 commonJS 和 VM 模块提供的 runInThisContext 方法来实现的。

什么是 jiti

Runtime Typescript and ESM support for Node.js 一个直接用来运行 TS 和 ESM 的 node 运行时

jiti vs esno

在此之前,学习过 esno & tsx 的 TS 运行原理,esno & tsx 原理初探,大致原理是拓展了 import 和 require 的能力,ESM 通过 node 官方的 lodaer 实现的解析和加载 TS,CJS 通过扩展 Module 的语法,通过修改原型链的方法来扩展。而 jiti 使用了另一种方式来实现类似的逻辑。

NodeJS 中的 commonJS 本质是啥

Note

从文章可知,commonjs 的本质就是一个函数


_10
(function(exports, require, module, **filename, **dirname) {
_10
// Module code actually lives in here
_10
});

我们用到的 exports, require ,module, __filename , __dirname 其实都是 node 在运行的时候帮帮我们注入进来的。

runInThisContext

node VM 模块提供的方法,可以用来直接执行 js 代码,但是作用域就是当前的全局环境,本地的环境是访问不到的,然后将返回的结果返回回来


_10
const vm = require('vm');
_10
let localVar = 'GfG';
_10
const vmresult = vm.runInThisContext('localVar = "Geeks";');
_10
console.log(`vmresult: '${vmresult}', localVar: '${localVar}'`);

vm.runInThisContext() 方法无法访问本地范围,因此 localVar 此处未更改。所以在这里我们知道了 runInThisContext 可以隔离作用域,防止修改 node 原本的运行环境

jiti 的基本原理

Note

以下的内容是基于 jiti 理解的简单实现,只列举了很少的一部分功能,需要学习全部的功能实现请移步到 GitHub 源码观看

jiti 如何使用

Note

仓库给出的代码实例如下


_10
const jiti = require("jiti")(\_\_filename);
_10
jiti("./path/to/file.ts");

直接给出最小实现,然后解释代码的大体执行顺序

index.js

_68
const createRequire = require('create-require');
_68
const { extname, dirname } = require('path');
_68
const { readFileSync } = require('fs');
_68
const babel = require('@babel/core');
_68
const vm = require('vm');
_68
const { Module } = require('module');
_68
const { transform } = require('./transform')
_68
_68
const createJITI = (**filename) => {
_68
const nativeRequire = createRequire(**filename || process.cwd());
_68
_68
// jiti 需要执行的文件名
_68
return function jiti(id, parentModule) {
_68
const filename = nativeRequire.resolve(id);
_68
const ext = extname(filename);
_68
_68
if (ext === '.json') {
_68
const jsonModule = nativeRequire(id);
_68
Object.defineProperty(jsonModule, 'default', { value: jsonModule });
_68
return jsonModule;
_68
}
_68
const source = readFileSync(filename, 'utf8');
_68
return evalModule(source, { filename, id, ext, caches: {} });
_68
_68
function evalModule(source, options = {}) {
_68
const id = options.id;
_68
const filename = options.filename;
_68
const ext = options.ext;
_68
const cache = {};
_68
_68
const isTypescript = ext === '.ts' || ext === '.mts' || ext === '.cts';
_68
const isNativeModule = ext === '.mjs';
_68
const isCommonJS = ext === '.cjs';
_68
const needsTranspile = !isCommonJS && (isTypescript || isNativeModule);
_68
_68
source = transform(source, filename);
_68
const mod = new Module(filename);
_68
mod.filename = filename;
_68
if (parentModule) {
_68
mod.parent = parentModule;
_68
if (
_68
Array.isArray(parentModule.children) &&
_68
!parentModule.children.includes(mod)
_68
) {
_68
parentModule.children.push(mod);
_68
}
_68
}
_68
mod.require = createJITI(filename, mod);
_68
mod.path = dirname(filename);
_68
mod.paths = Module._nodeModulePaths(mod.path);
_68
nativeRequire.cache[filename] = mod;
_68
_68
const compiled = vm.runInThisContext(Module.wrap(source));
_68
compiled(
_68
mod.exports,
_68
mod.require,
_68
mod,
_68
mod.filename,
_68
dirname(mod.filename)
_68
);
_68
mod.loaded = true;
_68
return mod.exports;
_68
}
_68
_68
};
_68
};
_68
_68
module.exports = { createJITI };

transform.js

_13
function transform(code, filename) {
_13
const output = babel.transformSync(code, {
_13
presets: [
_13
'@babel/preset-typescript',
_13
['@babel/preset-env', { modules: 'commonjs' }],
_13
],
_13
plugins: ['@babel/plugin-transform-modules-commonjs'],
_13
filename,
_13
});
_13
return output.code;
_13
}
_13
_13
module.exports = { transform };

  1. 在第 8 行代码中我们创建了一个新的 require 然后直接将 jiti 运行的代码返回回去
  2. 在第 16 行的时候, 我们通过运行时候传入的文件后缀名判断出来了是 JSON 的文件,直接返回回去就可以了
  3. 在 21 行的时候,我们默认所有的文件都需要编译,去走下面的 evalModule 方法
  4. 在 35 行的时候,我们使用了 babel 的方法将所有的文件都编译成 commonjs 的内容
  5. 然后根据当前文件的信息新建一个 mod,更新 mod 的 parent 和 parent 的 children 信息
  6. 在第 52 行的时候 我们用 Module.wrap 来包装一下我们的代码,在上次的 commonjs 转换中,我们将原来的代码都编译成了 commonjs 的格式,里面存在 require 和 exports,我们通过 Module.wrap 来包装原来的代码,将自己实现的 require 穿入进去
  7. 在 52 行的时候,我们可以大致认为我们的 compiled 就是下面的样子

_10
(function (exports, require, module, __filename, __dirname) {
_10
// Module code actually lives in here
_10
});

然后将我们自定义的 exports, require ,module, __filename , __dirname 穿入进去


_10
compiled(mod.exports, mod.require, mod, mod.filename, dirname(mod.filename));

图片

可以发现,最简代码可以运行。

缺点

jiti 是基于 commonjs 来实现的,所以存在 commonjs 的限制 例如直接 引入 HTTP 的模块


_10
import presetUno from 'https://esm.sh/@unocss/[email protected]';

https://github.com/unjs/jiti/issues/161

作者:Madinah