mirror of
https://github.com/microsoft/vscode.git
synced 2026-07-10 16:58:55 -05:00
api: merge module interceptors for esm and node
This commit is contained in:
@@ -0,0 +1,50 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import * as assert from 'assert';
|
||||
import 'mocha';
|
||||
import * as vscode from 'vscode';
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
|
||||
suite('vscode API - Module Interception', () => {
|
||||
|
||||
test('import and require(vscode) return the same API instance in ESM', async function () {
|
||||
// This file CANNOT be written to the OS temp directory.
|
||||
// The VS Code API module interceptor looks up the extension by associating
|
||||
// the parent URL via path containment. If the file is placed outside the
|
||||
// extension's directory, the interceptor will fail to provide the 'vscode' module.
|
||||
const testFile = path.join(__dirname, 'esm-test.mjs');
|
||||
try {
|
||||
try {
|
||||
fs.writeFileSync(testFile, `
|
||||
// THIS IS A TEMPORARY FILE CREATED BY VSCODE-API-TESTS (module.test.ts)
|
||||
// IT SHOULD BE AUTO-DELETED. IF YOU SEE THIS, IT IS SAFE TO REMOVE.
|
||||
import * as vscode1 from 'vscode';
|
||||
import { createRequire } from 'node:module';
|
||||
import * as assert from 'assert';
|
||||
|
||||
export function runTest() {
|
||||
const vscode2 = createRequire(import.meta.url)('vscode');
|
||||
assert.ok(Object.keys(vscode1).length > 0);
|
||||
for (const key of Object.keys(vscode1)) {
|
||||
assert.strictEqual(vscode1[key], vscode2[key], "Mismatch at " + key);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
`);
|
||||
} catch (err) {
|
||||
this.skip();
|
||||
}
|
||||
|
||||
const asyncImport = new Function('url', 'return import(url)');
|
||||
const m = await asyncImport(vscode.Uri.file(testFile).toString(true));
|
||||
assert.strictEqual(m.runTest(), true);
|
||||
} finally {
|
||||
try { fs.unlinkSync(testFile); } catch (err) { /* ignore */ }
|
||||
}
|
||||
});
|
||||
|
||||
});
|
||||
@@ -23,12 +23,23 @@ 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 } from '../../../base/common/lifecycle.js';
|
||||
|
||||
import { DisposableStore, toDisposable } from '../../../base/common/lifecycle.js';
|
||||
const require = nodeModule.createRequire(import.meta.url);
|
||||
|
||||
class NodeModuleRequireInterceptor extends RequireInterceptor {
|
||||
|
||||
private static _createDataUri(scriptContent: string): string {
|
||||
return `data:text/javascript;base64,${Buffer.from(scriptContent).toString('base64')}`;
|
||||
}
|
||||
|
||||
private static _vscodeImportFnName = `_VSCODE_IMPORT_VSCODE_API`;
|
||||
|
||||
private readonly _store = new DisposableStore();
|
||||
|
||||
dispose(): void {
|
||||
this._store.dispose();
|
||||
}
|
||||
|
||||
protected _installInterceptor(): void {
|
||||
const that = this;
|
||||
const node_module = require('module');
|
||||
@@ -72,29 +83,12 @@ class NodeModuleRequireInterceptor extends RequireInterceptor {
|
||||
}
|
||||
return request;
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
class NodeModuleInterceptor extends RequireInterceptor {
|
||||
|
||||
private static _createDataUri(scriptContent: string): string {
|
||||
return `data:text/javascript;base64,${Buffer.from(scriptContent).toString('base64')}`;
|
||||
}
|
||||
|
||||
private static _vscodeImportFnName = `_VSCODE_IMPORT_VSCODE_API`;
|
||||
|
||||
private readonly _store = new DisposableStore();
|
||||
|
||||
dispose(): void {
|
||||
this._store.dispose();
|
||||
}
|
||||
|
||||
protected override _installInterceptor(): void {
|
||||
const apiInstances = new BidirectionalMap<typeof vscode, string>();
|
||||
const apiImportDataUrl = new Map<string, string>();
|
||||
|
||||
// define a global function that can be used to get API instances given a random key
|
||||
Object.defineProperty(globalThis, NodeModuleInterceptor._vscodeImportFnName, {
|
||||
Object.defineProperty(globalThis, NodeModuleRequireInterceptor._vscodeImportFnName, {
|
||||
enumerable: false,
|
||||
configurable: false,
|
||||
writable: false,
|
||||
@@ -127,18 +121,17 @@ class NodeModuleInterceptor 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.${NodeModuleInterceptor._vscodeImportFnName}('${key}');\n\n${Object.keys(apiInstance).map((name => `export const ${name} = _vscodeInstance['${name}'];`)).join('\n')}`;
|
||||
scriptDataUrlSrc = NodeModuleInterceptor._createDataUri(jsCode);
|
||||
const jsCode = `const _vscodeInstance = globalThis.${NodeModuleRequireInterceptor._vscodeImportFnName}('${key}');\n\n${Object.keys(apiInstance).map((name => `export const ${name} = _vscodeInstance['${name}'];`)).join('\n')}`;
|
||||
scriptDataUrlSrc = NodeModuleRequireInterceptor._createDataUri(jsCode);
|
||||
apiImportDataUrl.set(key, scriptDataUrlSrc);
|
||||
}
|
||||
return scriptDataUrlSrc;
|
||||
};
|
||||
nodeModule.registerHooks({
|
||||
const hooks = 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,
|
||||
@@ -146,6 +139,7 @@ class NodeModuleInterceptor extends RequireInterceptor {
|
||||
};
|
||||
},
|
||||
});
|
||||
this._store.add(toDisposable(() => hooks.deregister()));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -174,15 +168,9 @@ export class ExtHostExtensionService extends AbstractExtHostExtensionService {
|
||||
|
||||
// Module loading tricks based on `module._load`.
|
||||
// `module._load` intercepts `require(...)`.
|
||||
await this._instaService.createInstance(NodeModuleRequireInterceptor, extensionApiFactory, { mine: this._myRegistry, all: this._globalRegistry })
|
||||
.install();
|
||||
|
||||
// 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 }))
|
||||
await this._store.add(this._instaService.createInstance(NodeModuleRequireInterceptor, extensionApiFactory, { mine: this._myRegistry, all: this._globalRegistry }))
|
||||
.install();
|
||||
|
||||
performance.mark('code/extHost/didInitAPI');
|
||||
|
||||
Reference in New Issue
Block a user