diff --git a/src/vs/workbench/api/node/extHostExtensionService.ts b/src/vs/workbench/api/node/extHostExtensionService.ts index 92cf46521041b..dac895f46bb03 100644 --- a/src/vs/workbench/api/node/extHostExtensionService.ts +++ b/src/vs/workbench/api/node/extHostExtensionService.ts @@ -23,7 +23,7 @@ import nodeModule from 'node:module'; import { assertType } from '../../../base/common/types.js'; import { generateUuid } from '../../../base/common/uuid.js'; import { BidirectionalMap } from '../../../base/common/map.js'; -import { DisposableStore, toDisposable } from '../../../base/common/lifecycle.js'; +import { DisposableStore } from '../../../base/common/lifecycle.js'; const require = nodeModule.createRequire(import.meta.url); @@ -75,43 +75,12 @@ class NodeModuleRequireInterceptor extends RequireInterceptor { } } -class NodeModuleESMInterceptor extends RequireInterceptor { +class NodeModuleInterceptor extends RequireInterceptor { private static _createDataUri(scriptContent: string): string { return `data:text/javascript;base64,${Buffer.from(scriptContent).toString('base64')}`; } - // This string is a script that runs in the loader thread of NodeJS. - private static _loaderScript = ` - let lookup; - export const initialize = async (context) => { - let requestIds = 0; - const { port } = context; - const pendingRequests = new Map(); - port.onmessage = (event) => { - const { id, url } = event.data; - pendingRequests.get(id)?.(url); - }; - lookup = url => { - // debugger; - const myId = requestIds++; - return new Promise((resolve) => { - pendingRequests.set(myId, resolve); - port.postMessage({ id: myId, url, }); - }); - }; - }; - 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, - }; - };`; - private static _vscodeImportFnName = `_VSCODE_IMPORT_VSCODE_API`; private readonly _store = new DisposableStore(); @@ -121,14 +90,11 @@ class NodeModuleESMInterceptor extends RequireInterceptor { } protected override _installInterceptor(): void { - - type Message = { id: string; url: string }; - const apiInstances = new BidirectionalMap(); const apiImportDataUrl = new Map(); // define a global function that can be used to get API instances given a random key - Object.defineProperty(globalThis, NodeModuleESMInterceptor._vscodeImportFnName, { + Object.defineProperty(globalThis, NodeModuleInterceptor._vscodeImportFnName, { enumerable: false, configurable: false, writable: false, @@ -137,16 +103,9 @@ class NodeModuleESMInterceptor extends RequireInterceptor { } }); - const { port1, port2 } = new MessageChannel(); - let apiModuleFactory: INodeModuleFactory | undefined; - // this is a workaround for the fact that the layer checker does not understand - // that onmessage is NodeJS API here - const port1LayerCheckerWorkaround: any = port1; - - port1LayerCheckerWorkaround.onmessage = (e: { data: Message }) => { - + const lookup = (url: string): string => { // Get the vscode-module factory - which is the same logic that's also used by // the CommonJS require interceptor if (!apiModuleFactory) { @@ -154,7 +113,6 @@ class NodeModuleESMInterceptor extends RequireInterceptor { 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 @@ -169,27 +127,25 @@ class NodeModuleESMInterceptor extends RequireInterceptor { // 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); + const jsCode = `const _vscodeInstance = globalThis.${NodeModuleInterceptor._vscodeImportFnName}('${key}');\n\n${Object.keys(apiInstance).map((name => `export const ${name} = _vscodeInstance['${name}'];`)).join('\n')}`; + scriptDataUrlSrc = NodeModuleInterceptor._createDataUri(jsCode); apiImportDataUrl.set(key, scriptDataUrlSrc); } - - port1.postMessage({ - id, - url: scriptDataUrlSrc - }); + return scriptDataUrlSrc; }; - - nodeModule.register(NodeModuleESMInterceptor._createDataUri(NodeModuleESMInterceptor._loaderScript), { - parentURL: import.meta.url, - data: { port: port2 }, - transferList: [port2], + nodeModule.registerHooks({ + resolve: (specifier, context, nextResolve) => { + if (specifier !== 'vscode' || !context.parentURL) { + return nextResolve(specifier, context); + } + console.log('ESM resolve', specifier, context); + const otherUrl = lookup(context.parentURL); + return { + url: otherUrl, + shortCircuit: true, + }; + }, }); - - this._store.add(toDisposable(() => { - port1.close(); - port2.close(); - })); } } @@ -216,12 +172,17 @@ export class ExtHostExtensionService extends AbstractExtHostExtensionService { // Register local file system shortcut this._instaService.createInstance(ExtHostDiskFileSystemProvider); - // Module loading tricks + // Module loading tricks based on `module._load`. + // `module._load` intercepts `require(...)`. await this._instaService.createInstance(NodeModuleRequireInterceptor, extensionApiFactory, { mine: this._myRegistry, all: this._globalRegistry }) .install(); - // ESM loading tricks - await this._store.add(this._instaService.createInstance(NodeModuleESMInterceptor, extensionApiFactory, { mine: this._myRegistry, all: this._globalRegistry })) + // Module loading tricks based on `module.registerHooks`. + // `module.registerHooks` is a generic interceptor that intercepts `require(...)`, `import ...`, and `import(...)`. + // However, at this time, `NodeModuleInterceptor` only intercepts `import 'vscode'` and `import('vscode')`. + // This can also intercept `require('vscode')`, but interception by `NodeModuleRequireInterceptor` takes precedence. + // In the future, we can consider migrating all interception logic to `NodeModuleInterceptor` and removing `NodeModuleRequireInterceptor`. + await this._store.add(this._instaService.createInstance(NodeModuleInterceptor, extensionApiFactory, { mine: this._myRegistry, all: this._globalRegistry })) .install(); performance.mark('code/extHost/didInitAPI');