Summary: Using require(esm) breaks the mechanism for intercepting the vscode module. This appears to be a bug in Node.js module loader. I believe a workaround should be implemented in vscode until this Node.js bug is resolved.
Does this issue occur when all extensions are disabled?: Yes
- VS Code Version: 1.107.1
- OS Version: macOS 26.1
Steps to Reproduce:
- Clone https://github.com/mizdra/repro-vscode-test-hang
cd repro-vscode-test-hang && npm i & npm start
Description
Suppose we have the following extension test file. This test file is an ESM and is imported via require(esm) from test/runner.cjs.
// test/index.test.js
import assert from 'node:assert/strict';
import * as vscode from 'vscode';
assert.equal(1, 2);
// test/runner.cjs
exports.run = async function run() {
require('./index.test.js');
}
Executing test/runner.cjs by @vscode/test-electron causes the application to hang.
Expected behavior
The test will run until completion.
$ npm start
> repro-vscode-test-hang@1.0.0 start
> node test/runTest.js
✔ Validated version: 1.107.1
✔ Found existing install in /Users/mizdra/src/github.com/mizdra/repro-vscode-test-hang/.vscode-test/vscode-darwin-arm64-1.107.1
[main 2025-12-28T16:08:16.301Z] update#setState disabled
[main 2025-12-28T16:08:16.302Z] update#ctor - updates are disabled by the environment
ChatSessionStore: Migrating 0 chat sessions from storage service to file system
Started local extension host with pid 33448.
MCP Registry configured: https://api.mcp.github.com
Settings Sync: Account status changed from uninitialized to unavailable
AssertionError [ERR_ASSERTION]: Expected values to be strictly equal:
1 !== 2
at file:///Users/mizdra/src/github.com/mizdra/repro-vscode-test-hang/test/index.test.js:6:8
at ModuleJob.run (node:internal/modules/esm/module_job:343:25)
at async onImport.tracePromise.__proto__ (node:internal/modules/esm/loader:665:26)
at async Object.run (/Users/mizdra/src/github.com/mizdra/repro-vscode-test-hang/test/runner.cjs:6:3)
[main 2025-12-28T16:08:17.527Z] Extension host with pid 33448 exited with code: 0, signal: unknown.
Exit code: 1
TestRunFailedError: Test run failed with code 1
at ChildProcess.onProcessClosed (/Users/mizdra/src/github.com/mizdra/repro-vscode-test-hang/node_modules/@vscode/test-electron/out/runTest.js:110:24)
at ChildProcess.emit (node:events:508:28)
at ChildProcess._handle.onexit (node:internal/child_process:293:12) {
code: 1,
signal: undefined
}
Failed to run tests
Actual behavior
The test hangs during execution and does not complete.
$ npm start
> repro-vscode-test-hang@1.0.0 start
> node test/runTest.js
✔ Validated version: 1.107.1
✔ Found existing install in /Users/mizdra/src/github.com/mizdra/repro-vscode-test-hang/.vscode-test/vscode-darwin-arm64-1.107.1
[main 2025-12-28T16:09:16.678Z] update#setState disabled
[main 2025-12-28T16:09:16.680Z] update#ctor - updates are disabled by the environment
ChatSessionStore: Migrating 0 chat sessions from storage service to file system
Started local extension host with pid 36009.
MCP Registry configured: https://api.mcp.github.com
Settings Sync: Account status changed from uninitialized to unavailable
(...hangs here...)
Description
VS Code has a mechanism to intercept the vscode module imported from an extension. This provides a dedicated Extension API for each extension.
Below, module._load is used to intercept require('vscode'):
|
node_module._load = function load(request: string, parent: { filename: string }, isMain: boolean) { |
|
request = applyAlternatives(request); |
|
if (!that._factories.has(request)) { |
|
return originalLoad.apply(this, arguments); |
|
} |
|
return that._factories.get(request)!.load( |
|
request, |
|
URI.file(realpathSync(parent.filename)), |
|
request => originalLoad.apply(this, [request, parent, isMain]) |
|
); |
|
}; |
Here, a hook registered with module.register is used to intercept import * as vscode from 'vscode':
|
export const resolve = async (specifier, context, nextResolve) => { |
|
if (specifier !== 'vscode' || !context.parentURL) { |
|
return nextResolve(specifier, context); |
|
} |
|
const otherUrl = await lookup(context.parentURL); |
|
return { |
|
url: otherUrl, |
|
shortCircuit: true, |
|
}; |
|
};`; |
The hook operates in the following steps:
- Send
parentURL (the URL of the module attempting to import the vscode module) to the main thread
|
port.postMessage({ id: myId, url, }); |
- On the main thread, generate the appropriate
vscode module code for each parentURL, convert it to a Data URI, and send it to the hook thread
|
port1LayerCheckerWorkaround.onmessage = (e: { data: Message }) => { |
|
|
|
// Get the vscode-module factory - which is the same logic that's also used by |
|
// the CommonJS require interceptor |
|
if (!apiModuleFactory) { |
|
apiModuleFactory = this._factories.get('vscode'); |
|
assertType(apiModuleFactory); |
|
} |
|
|
|
const { id, url } = e.data; |
|
const uri = URI.parse(url); |
|
|
|
// Get or create the API instance. The interface is per extension and extensions are |
|
// looked up by the uri (e.data.url) and path containment. |
|
const apiInstance = apiModuleFactory.load('_not_used', uri, () => { throw new Error('CANNOT LOAD MODULE from here.'); }); |
|
let key = apiInstances.get(apiInstance); |
|
if (!key) { |
|
key = generateUuid(); |
|
apiInstances.set(apiInstance, key); |
|
} |
|
|
|
// Create and cache a data-url which is the import script for the API instance |
|
let scriptDataUrlSrc = apiImportDataUrl.get(key); |
|
if (!scriptDataUrlSrc) { |
|
const jsCode = `const _vscodeInstance = globalThis.${NodeModuleESMInterceptor._vscodeImportFnName}('${key}');\n\n${Object.keys(apiInstance).map((name => `export const ${name} = _vscodeInstance['${name}'];`)).join('\n')}`; |
|
scriptDataUrlSrc = NodeModuleESMInterceptor._createDataUri(jsCode); |
|
apiImportDataUrl.set(key, scriptDataUrlSrc); |
|
} |
|
|
|
port1.postMessage({ |
|
id, |
|
url: scriptDataUrlSrc |
|
}); |
|
}; |
- The hook receives the Data URI and returns it as the result of the
vscode module's resolve.
|
port.onmessage = (event) => { |
|
const { id, url } = event.data; |
|
pendingRequests.get(id)?.(url); |
|
const otherUrl = await lookup(context.parentURL); |
|
return { |
|
url: otherUrl, |
|
shortCircuit: true, |
|
}; |
Data transfer between threads is performed using MessageChannel. Since data receiving is asynchronous, the resolve hook is implemented as an asynchronous function.
Incidentally, the asynchronous resolve hook appears to cause race conditions. This results in the following symptoms:
console.log executed from the hook may not output to stdout
- In Node.js 22.21.1, the issue only reproduces when used with
require(esm).
- In Node.js 25.2.1, the issue reproduces with both
require(esm) and import(esm).
- Messages sent from the loader hook thread may not be received on the main thread
- In Node.js 22.21.1, the issue only reproduces when used with
require(esm).
- In Node.js 25.2.1, the issue reproduces with both
require(esm) and import(esm).
This resembles nodejs/node#60380 fixed in Node.js 24.12.0 and 25.2.0. However, this issue still reproduces in Node.js 25.2.1, suggesting it may be a different bug. The reproduction code is below.
Due to this bug, importing the vscode module in VS Code never completes, causing the application to hang.
VS Code is affected by this Node.js bug. When you run import * as vscode from 'vscode', execution stops at step 2. As a result, the application hangs.
How to Fix
There are several ways to address this issue.
1. Fix the bug in Node.js
One approach is to report the bug reproduced in https://github.com/mizdra/repro-vscode-test-hang-simple to the Node.js team and have them fix it. I plan to report this to the Node.js team soon. However, it may take some time before the bug is fixed.
Even after a fixed version of Node.js is released, it may take additional time for that version to be bundled with Electron and VS Code. Therefore, I believe a temporary workaround should be implemented on the VS Code side.
2. Implement a workaround in VS Code
It appears that using module.registerHooks can avoid the problem. With this approach, you can avoid using MessageChannel and implement the hook synchronously.
I also tried using Atomics.wait to convert asynchronous code into synchronous code. However, this did not resolve the issue. For some reason, the onmessage event on the main thread did not fire. This suggests that executing postMessage from the resolve hook is unreliable.
Workaround
Use import(esm) instead of require(esm)
The bug in https://github.com/mizdra/repro-vscode-test-hang-simple can be avoided in Node.js 22.21.1 by using import(esm). Since VS Code 1.107.1 uses Node.js 22.21.1, you can work around the issue as follows:
// test/runner.cjs
exports.run = async function run() {
- require('./index.test.js');
+ await import('./index.test.js');
}
However, in Node.js 25.2.1, this workaround does not work even if you use import(esm). If a future version of VS Code uses Node.js 25.2.1, this workaround may no longer be effective.
Use require('vscode') instead of import * as vscode from 'vscode'
Using require(‘vscode’) will be intercepted by module._load. This avoids the issue because it is not affected by the Node.js bug.
// test/index.test.js
import assert from 'node:assert/strict';
-import * as vscode from 'vscode';
+import { createRequire } from 'node:module';
+const require = createRequire(import.meta.url);
+const vscode = require('vscode');
assert.equal(1, 2);
Additional Information
@vscode/test-cli uses mocha to run test files. Mocha imports test files via require(esm) (ref: mochajs/mocha#5366). Therefore, users of @vscode/test-cli are affected by this issue.
It appears other users have also encountered this problem.
Summary: Using
require(esm)breaks the mechanism for intercepting thevscodemodule. This appears to be a bug in Node.js module loader. I believe a workaround should be implemented in vscode until this Node.js bug is resolved.Does this issue occur when all extensions are disabled?: Yes
Steps to Reproduce:
cd repro-vscode-test-hang && npm i & npm startDescription
Suppose we have the following extension test file. This test file is an ESM and is imported via
require(esm)fromtest/runner.cjs.Executing
test/runner.cjsby@vscode/test-electroncauses the application to hang.Expected behavior
The test will run until completion.
Actual behavior
The test hangs during execution and does not complete.
Description
VS Code has a mechanism to intercept the
vscodemodule imported from an extension. This provides a dedicated Extension API for each extension.Below,
module._loadis used to interceptrequire('vscode'):vscode/src/vs/workbench/api/node/extHostExtensionService.ts
Lines 36 to 46 in dcce2aa
Here, a hook registered with
module.registeris used to interceptimport * as vscode from 'vscode':vscode/src/vs/workbench/api/node/extHostExtensionService.ts
Lines 104 to 113 in dcce2aa
The hook operates in the following steps:
parentURL(the URL of the module attempting to import thevscodemodule) to the main threadvscode/src/vs/workbench/api/node/extHostExtensionService.ts
Line 100 in 74c4ecd
vscodemodule code for each parentURL, convert it to a Data URI, and send it to the hook threadvscode/src/vs/workbench/api/node/extHostExtensionService.ts
Lines 148 to 181 in 74c4ecd
vscodemodule's resolve.vscode/src/vs/workbench/api/node/extHostExtensionService.ts
Lines 91 to 93 in 74c4ecd
vscode/src/vs/workbench/api/node/extHostExtensionService.ts
Lines 108 to 112 in 74c4ecd
Data transfer between threads is performed using
MessageChannel. Since data receiving is asynchronous, theresolvehook is implemented as an asynchronous function.Incidentally, the asynchronous
resolvehook appears to cause race conditions. This results in the following symptoms:console.logexecuted from the hook may not output to stdoutrequire(esm).require(esm)andimport(esm).require(esm).require(esm)andimport(esm).This resembles nodejs/node#60380 fixed in Node.js 24.12.0 and 25.2.0. However, this issue still reproduces in Node.js 25.2.1, suggesting it may be a different bug. The reproduction code is below.
Due to this bug, importing the
vscodemodule in VS Code never completes, causing the application to hang.VS Code is affected by this Node.js bug. When you run
import * as vscode from 'vscode', execution stops at step 2. As a result, the application hangs.How to Fix
There are several ways to address this issue.
1. Fix the bug in Node.js
One approach is to report the bug reproduced in https://github.com/mizdra/repro-vscode-test-hang-simple to the Node.js team and have them fix it. I plan to report this to the Node.js team soon. However, it may take some time before the bug is fixed.
Even after a fixed version of Node.js is released, it may take additional time for that version to be bundled with Electron and VS Code. Therefore, I believe a temporary workaround should be implemented on the VS Code side.
2. Implement a workaround in VS Code
It appears that using
module.registerHookscan avoid the problem. With this approach, you can avoid usingMessageChanneland implement the hook synchronously.module.registerHooksto synchronously execute the module resolve hook mizdra/vscode#2I also tried using
Atomics.waitto convert asynchronous code into synchronous code. However, this did not resolve the issue. For some reason, theonmessageevent on the main thread did not fire. This suggests that executingpostMessagefrom theresolvehook is unreliable.Atomics.waitto synchronously execute the module resolve hook mizdra/vscode#1Workaround
Use
import(esm)instead ofrequire(esm)The bug in https://github.com/mizdra/repro-vscode-test-hang-simple can be avoided in Node.js 22.21.1 by using
import(esm). Since VS Code 1.107.1 uses Node.js 22.21.1, you can work around the issue as follows:// test/runner.cjs exports.run = async function run() { - require('./index.test.js'); + await import('./index.test.js'); }However, in Node.js 25.2.1, this workaround does not work even if you use
import(esm). If a future version of VS Code uses Node.js 25.2.1, this workaround may no longer be effective.Use
require('vscode')instead ofimport * as vscode from 'vscode'Using
require(‘vscode’)will be intercepted bymodule._load. This avoids the issue because it is not affected by the Node.js bug.Additional Information
@vscode/test-cliuses mocha to run test files. Mocha imports test files viarequire(esm)(ref: mochajs/mocha#5366). Therefore, users of@vscode/test-cliare affected by this issue.It appears other users have also encountered this problem.