diff --git a/src/vs/workbench/api/common/extHostRequireInterceptor.ts b/src/vs/workbench/api/common/extHostRequireInterceptor.ts index 85ee4635675..9cc572c7e7c 100644 --- a/src/vs/workbench/api/common/extHostRequireInterceptor.ts +++ b/src/vs/workbench/api/common/extHostRequireInterceptor.ts @@ -19,16 +19,20 @@ import { IInstantiationService } from 'vs/platform/instantiation/common/instanti import { IExtHostExtensionService } from 'vs/workbench/api/common/extHostExtensionService'; import { platform } from 'vs/base/common/process'; import { ILogService } from 'vs/platform/log/common/log'; +import { escapeRegExpCharacters } from 'vs/base/common/strings'; interface LoadFunction { (request: string): any; } -interface INodeModuleFactory { +interface IAlternativeModuleProvider { + alternativeModuleName(name: string): string | undefined; +} + +interface INodeModuleFactory extends Partial { readonly nodeModuleName: string | string[]; load(request: string, parent: URI, original: LoadFunction): any; - alternativeModuleName?(name: string): string | undefined; } export abstract class RequireInterceptor { @@ -60,6 +64,7 @@ export abstract class RequireInterceptor { this.register(new VSCodeNodeModuleFactory(this._apiFactory, extensionPaths, this._extensionRegistry, configProvider, this._logService)); this.register(this._instaService.createInstance(KeytarNodeModuleFactory, extensionPaths)); + this.register(this._instaService.createInstance(NodeModuleAliasingModuleFactory)); if (this._initData.remote.isRemote) { this.register(this._instaService.createInstance(OpenNodeModuleFactory, extensionPaths, this._initData.environment.appUriScheme)); } @@ -67,14 +72,17 @@ export abstract class RequireInterceptor { protected abstract _installInterceptor(): void; - public register(interceptor: INodeModuleFactory): void { - if (Array.isArray(interceptor.nodeModuleName)) { - for (let moduleName of interceptor.nodeModuleName) { - this._factories.set(moduleName, interceptor); + public register(interceptor: INodeModuleFactory | IAlternativeModuleProvider): void { + if ('nodeModuleName' in interceptor) { + if (Array.isArray(interceptor.nodeModuleName)) { + for (let moduleName of interceptor.nodeModuleName) { + this._factories.set(moduleName, interceptor); + } + } else { + this._factories.set(interceptor.nodeModuleName, interceptor); } - } else { - this._factories.set(interceptor.nodeModuleName, interceptor); } + if (typeof interceptor.alternativeModuleName === 'function') { this._alternatives.push((moduleName) => { return interceptor.alternativeModuleName!(moduleName); @@ -83,6 +91,60 @@ export abstract class RequireInterceptor { } } +//#region --- module renames + +class NodeModuleAliasingModuleFactory implements IAlternativeModuleProvider { + /** + * Map of aliased internal node_modules, used to allow for modules to be + * renamed without breaking extensions. In the form "original -> new name". + */ + private static readonly aliased: ReadonlyMap = new Map([ + ['vscode-ripgrep', '@vscode/ripgrep'], + ]); + + private readonly re?: RegExp; + + constructor(@IExtHostInitDataService initData: IExtHostInitDataService) { + if (initData.environment.appRoot && NodeModuleAliasingModuleFactory.aliased.size) { + const root = escapeRegExpCharacters(this.forceForwardSlashes(initData.environment.appRoot.fsPath)); + // decompose ${appRoot}/node_modules/foo/bin to ['${appRoot}/node_modules/', 'foo', '/bin'], + // and likewise the more complex form ${appRoot}/node_modules.asar.unpacked/@vcode/foo/bin + // to ['${appRoot}/node_modules.asar.unpacked/',' @vscode/foo', '/bin']. + const npmIdChrs = `[a-z0-9_.-]`; + const npmModuleName = `@${npmIdChrs}+\\/${npmIdChrs}+|${npmIdChrs}+`; + const moduleFolders = 'node_modules|node_modules\\.asar(?:\\.unpacked)?'; + this.re = new RegExp(`^(${root}/${moduleFolders}\\/)(${npmModuleName})(.*)$`, 'i'); + } + } + + public alternativeModuleName(name: string): string | undefined { + if (!this.re) { + return; + } + + const result = this.re.exec(this.forceForwardSlashes(name)); + if (!result) { + return; + } + + const [, prefix, moduleName, suffix] = result; + const dealiased = NodeModuleAliasingModuleFactory.aliased.get(moduleName); + if (dealiased === undefined) { + return; + } + + console.warn(`${moduleName} as been renamed to ${dealiased}, please update your imports`); + + return prefix + dealiased + suffix; + } + + private forceForwardSlashes(str: string) { + return str.replace(/\\/g, '/'); + } +} + +//#endregion + //#region --- vscode-module class VSCodeNodeModuleFactory implements INodeModuleFactory { diff --git a/src/vs/workbench/api/node/extHostExtensionService.ts b/src/vs/workbench/api/node/extHostExtensionService.ts index b237c4e4e63..b9071ad93ba 100644 --- a/src/vs/workbench/api/node/extHostExtensionService.ts +++ b/src/vs/workbench/api/node/extHostExtensionService.ts @@ -23,8 +23,25 @@ class NodeModuleRequireInterceptor extends RequireInterceptor { protected _installInterceptor(): void { const that = this; const node_module = require.__$__nodeRequire('module'); - const original = node_module._load; + const originalLoad = node_module._load; 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]) + ); + }; + + const originalLookup = node_module._resolveLookupPaths; + node_module._resolveLookupPaths = (request: string, parent: unknown) => { + return originalLookup.call(this, applyAlternatives(request), parent); + }; + + const applyAlternatives = (request: string) => { for (let alternativeModuleName of that._alternatives) { let alternative = alternativeModuleName(request); if (alternative) { @@ -32,14 +49,7 @@ class NodeModuleRequireInterceptor extends RequireInterceptor { break; } } - if (!that._factories.has(request)) { - return original.apply(this, arguments); - } - return that._factories.get(request)!.load( - request, - URI.file(realpathSync(parent.filename)), - request => original.apply(this, [request, parent, isMain]) - ); + return request; }; } }