如何绕过Jest对Node模块加载器的钩子?

huangapple go评论59阅读模式
英文:

How can I bypass Jest's hooking of Node's module loader?

问题

以下代码

```js
if (!globalThis.test) {
  globalThis.test = function(_, f) {
    f();
  }
}

test("Foo", async () => {
  for (let i = 1; i < 10; ++i) {
    await import(`data:text/javascript,console.log(%22In%20import${i}%22)`);
  }
});

在 Node 中直接运行,输出如下:

In import1
In import2
In import3
In import4
In import5
In import6
In import7
In import8
In import9

然而,在 Jest 中运行这段代码需要使用实验性选项 (NODE_OPTIONS=--experimental_vm_modules npx jest test.js)。此外,这甚至不能保证能正常工作,因为存在 https://github.com/nodejs/node/issues/35889 这个问题;许多情况下它会正常运行,但(特别是在一个更复杂的项目中)它会相当可靠地导致段错误。实际上,如果你将循环修改为运行到 100000 而不是 10,它将在 Node 中正常运行,但在 Jest 中在迭代到大约 5000 时会崩溃。

问题在于 Jest 正在钩取 Node 的模块加载,这种方式我不完全理解,以便允许代码转换、模拟等。然而,这也为特定于 ES 模块的此错误打开了大门(原因我也不完全理解)。

是否有任何方法可以绕过钩取,直接使用 Node 的 ESMLoader?在我的测试目的中,我只想导入数据 URL,因此不需要(也不希望)进行模拟或转译处理。```

英文:

The following code:

if (!globalThis.test) {
  globalThis.test = function(_, f) {
    f();
  }
}

test(&quot;Foo&quot;, async () =&gt; {
  for (let i = 1; i &lt; 10; ++i) {
    await import(`data:text/javascript,console.log(%22In%20import${i}%22)`);
  }
});

runs directly in node, with the following output:

In import1
In import2
In import3
In import4
In import5
In import6
In import7
In import8
In import9

However, to run this under Jest requires the use of experimental options. (NODE_OPTIONS=--experimental_vm_modules npx jest test.js) Furthermore, it's not even guaranteed to work because of https://github.com/nodejs/node/issues/35889 ; many times it will work fine, but (especially in a more complicated project) it will segfault pretty reliably. In fact, if you edit the loop to run to 100000 instead of 10, it will run fine in Node but crash in Jest around iteration 5000.

The issue is that Jest is hooking Node's module loading, in a way I don't fully understand, in order to allow for code transformation, mocking, etc. However, this also opens the door to that bug, specifically for ES modules (also for reasons I don't fully understand).

Is there any way I can bypass the hooking and use Node's ESMLoader directly? For the purpose of my test I literally want to import data URLs, so there's no mocking or transpiling funny business needed (or wanted).

答案1

得分: 1

// 为了绕过 Jest 以便导入数据 URI 而不崩溃,我需要以下几个部分:一个自定义 Jest 环境,以及在我的生产代码中添加一个钩子,让我能够覆盖 `import()` 调用。

`jest.config.js`
```js
module.exports = {
  testEnvironment: "./FixJSDOMEnvironment.ts",
  // 其他选项保持不变
}

FixJSDOMEnvironment.ts

import JSDOMEnvironment from "jest-environment-jsdom";

// https://github.com/facebook/jest/blob/v29.4.3/website/versioned_docs/version-29.4/Configuration.md#testenvironment-string
export default class FixJSDOMEnvironment extends JSDOMEnvironment {
  constructor(...args: ConstructorParameters<typeof JSDOMEnvironment>) {
    super(...args);

    // 修复 https://github.com/nodejs/node/issues/35889
    // 添加缺失的 importActual() 函数以镜像 requireActual(),这使我们能够绕过 ESM 错误。
    // 用 eval 包装函数的构造,以防止转译器触及 import() 调用。
    this.global.importActual = eval("url => import(url)");
  }
}

productionCode.ts

// Webpack 喜欢将 import 转化为 require,这在某种程度上类似于 import,但并不完全相同。
// 因此我们使用一个“魔法注释”来禁用它,并将其保留为动态导入。
//
// 然而,我们需要能够在测试中替换此实现。理想情况下它应该没问题,但是 Jest 在使用动态导入时会导致段错误:
// 请参阅 https://github.com/nodejs/node/issues/35889 和 https://github.com/facebook/jest/issues/11438
// import() 不是一个函数,所以它无法被替换。我们需要这个单独的配置对象来提供一个钩子点。
export const config = {
  doImport(url: string): Promise<MyModule> {
    return import(/*webpackIgnore:true*/ url);
  },
};

// 接下来是实际代码,包括对 doImport 的使用

ImportTest.ts

import { config } from "productionCode";

// 关键:我们必须覆盖它,否则我们将得到 Jest 钩子的实现,这将无法在不传递特殊标志给 Node 的情况下工作,
// 甚至在这样做时也往往会崩溃。
config.doImport = importActual;

test("使用 import 的事物", () => {
  // 可能在这里模拟其他函数,以便 productionCode 导入数据 URI
});

<details>
<summary>英文:</summary>

In order to bypass Jest enough to import data URIs without crashing, I needed a couple of pieces: A custom Jest Environment, and a hook in my production code to let me override the `import()` call.

`jest.config.js`
```js
module.exports = {
  testEnvironment: &quot;./FixJSDOMEnvironment.ts&quot;,
  // Other options unchanged
}

FixJSDOMEnvironment.ts

import JSDOMEnvironment from &quot;jest-environment-jsdom&quot;;

// https://github.com/facebook/jest/blob/v29.4.3/website/versioned_docs/version-29.4/Configuration.md#testenvironment-string
export default class FixJSDOMEnvironment extends JSDOMEnvironment {
  constructor(...args: ConstructorParameters&lt;typeof JSDOMEnvironment&gt;) {
    super(...args);

    // FIXME https://github.com/nodejs/node/issues/35889
    // Add missing importActual() function to mirror requireActual(), which lets us work around the ESM bug.
    // Wrap the construction of the function in eval, so that transpilers don&#39;t touch the import() call.
    this.global.importActual = eval(&quot;url =&gt; import(url)&quot;);
  }
}

productionCode.ts

// Webpack likes to turn the import into a require, which sort of
// but not really behaves like import. So we use a &quot;magic comment&quot;
// to disable that and leave it as a dynamic import.
//
// However, we need to be able to replace this implementation in tests. Ideally
// it would be fine, but Jest causes segfaults when using dynamic import: see
// https://github.com/nodejs/node/issues/35889 and
// https://github.com/facebook/jest/issues/11438
// import() is not a function, so it can&#39;t be replaced. We need this separate
// config object to provide a hook point.
export const config = {
  doImport(url: string): Promise&lt;MyModule&gt; {
    return import(/*webpackIgnore:true*/ url);
  },
};

// Real code follows, including use of doImport

ImportTest.ts

import { config } from &quot;productionCode&quot;;

// Critical: We have to overwrite this, otherwise we get Jest&#39;s hooked
// implementation, which will not work without passing special flags to Node,
// and tends to crash even if you do.
config.doImport = importActual;

test(&quot;Thing that uses import&quot;, () =&gt; {
  // Possibly mock other functions here so that productionCode imports a data URI
});

huangapple
  • 本文由 发表于 2023年3月31日 18:03:02
  • 转载请务必保留本文链接:https://go.coder-hub.com/75897222.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定