diff --git a/source-map-support.d.ts b/source-map-support.d.ts index 701e917..8fdedbc 100755 --- a/source-map-support.d.ts +++ b/source-map-support.d.ts @@ -31,6 +31,16 @@ export interface Options { overrideRetrieveSourceMap?: boolean | undefined; retrieveFile?(path: string): string; retrieveSourceMap?(source: string): UrlAndMap | null; + /** + * Set false to disable redirection of require / import `source-map-support` to `@cspotcode/source-map-support` + */ + redirectConflictingLibrary?: boolean; + /** + * Callback will be called every time we redirect due to `redirectConflictingLibrary` + * This allows consumers to log helpful warnings if they choose. + * @param parent NodeJS.Module which made the require() or require.resolve() call + */ + onConflictingLibraryRedirect?: (request: string, parent: any, isMain: boolean, redirectedRequest: string) => void; } export interface Position { diff --git a/source-map-support.js b/source-map-support.js index 908f08a..268ccab 100644 --- a/source-map-support.js +++ b/source-map-support.js @@ -77,6 +77,11 @@ var sharedData = initializeSharedData({ errorPrepareStackTraceHook: undefined, /** @type {HookState} */ processEmitHook: undefined, + /** @type {HookState} */ + moduleResolveFilenameHook: undefined, + + /** @type {Array<(request: string, parent: any, isMain: boolean, redirectedRequest: string) => void>} */ + onConflictingLibraryRedirectArr: [], // If true, the caches are reset before a stack trace formatting operation emptyCacheBetweenOperations: false, @@ -633,6 +638,47 @@ exports.install = function(options) { } } + // Use dynamicRequire to avoid including in browser bundles + var Module = dynamicRequire(module, 'module'); + + // Redirect subsequent imports of "source-map-support" + // to this package + const {redirectConflictingLibrary = true, onConflictingLibraryRedirect} = options; + if(redirectConflictingLibrary) { + if (!sharedData.moduleResolveFilenameHook) { + const originalValue = Module._resolveFilename; + const moduleResolveFilenameHook = sharedData.moduleResolveFilenameHook = { + enabled: true, + originalValue, + installedValue: undefined, + } + Module._resolveFilename = sharedData.moduleResolveFilenameHook.installedValue = function (request, parent, isMain, options) { + if (moduleResolveFilenameHook.enabled) { + // Match all source-map-support entrypoints: source-map-support, source-map-support/register + let requestRedirect; + if (request === 'source-map-support') { + requestRedirect = './'; + } else if (request === 'source-map-support/register') { + requestRedirect = './register'; + } + + if (requestRedirect !== undefined) { + const newRequest = require.resolve(requestRedirect); + for (const cb of sharedData.onConflictingLibraryRedirectArr) { + cb(request, parent, isMain, options, newRequest); + } + request = newRequest; + } + } + + return originalValue.call(this, request, parent, isMain, options); + } + } + if (onConflictingLibraryRedirect) { + sharedData.onConflictingLibraryRedirectArr.push(onConflictingLibraryRedirect); + } + } + // Allow sources to be found by methods other than reading the files // directly from disk. if (options.retrieveFile) { @@ -655,8 +701,6 @@ exports.install = function(options) { // Support runtime transpilers that include inline source maps if (options.hookRequire && !isInBrowser()) { - // Use dynamicRequire to avoid including in browser bundles - var Module = dynamicRequire(module, 'module'); var $compile = Module.prototype._compile; if (!$compile.__sourceMapSupport) { @@ -738,6 +782,17 @@ exports.uninstall = function() { } sharedData.errorPrepareStackTraceHook = undefined; } + if (sharedData.moduleResolveFilenameHook) { + // Disable behavior + sharedData.moduleResolveFilenameHook.enabled = false; + // If possible, remove our hook function. May not be possible if subsequent third-party hooks have wrapped around us. + var Module = dynamicRequire(module, 'module'); + if(Module._resolveFilename === sharedData.moduleResolveFilenameHook.installedValue) { + Module._resolveFilename = sharedData.moduleResolveFilenameHook.originalValue; + } + sharedData.moduleResolveFilenameHook = undefined; + } + sharedData.onConflictingLibraryRedirectArr.length = 0; } exports.resetRetrieveHandlers = function() { diff --git a/test.js b/test.js index 5891e28..f57cf89 100644 --- a/test.js +++ b/test.js @@ -1,8 +1,10 @@ // Note: some tests rely on side-effects from prior tests. // You may not get meaningful results running a subset of tests. +const Module = require('module'); const priorErrorPrepareStackTrace = Error.prepareStackTrace; const priorProcessEmit = process.emit; +const priorResolveFilename = Module._resolveFilename; const underTest = require('./source-map-support'); var SourceMapGenerator = require('source-map').SourceMapGenerator; var child_process = require('child_process'); @@ -704,16 +706,52 @@ it('supports multiple instances', function(done) { ]); }); +describe('redirects require() of "source-map-support" to this module', function() { + it('redirects', function() { + assert.strictEqual(require.resolve('source-map-support'), require.resolve('.')); + assert.strictEqual(require.resolve('source-map-support/register'), require.resolve('./register')); + assert.strictEqual(require('source-map-support'), require('.')); + }); + + it('emits notifications', function() { + let onConflictingLibraryRedirectCalls = []; + let onConflictingLibraryRedirectCalls2 = []; + underTest.install({ + onConflictingLibraryRedirect(request, parent, isMain, redirectedRequest) { + onConflictingLibraryRedirectCalls.push([...arguments]); + } + }); + underTest.install({ + onConflictingLibraryRedirect(request, parent, isMain, redirectedRequest) { + onConflictingLibraryRedirectCalls2.push([...arguments]); + } + }); + require.resolve('source-map-support'); + assert.strictEqual(onConflictingLibraryRedirectCalls.length, 1); + assert.strictEqual(onConflictingLibraryRedirectCalls2.length, 1); + for(const args of [onConflictingLibraryRedirectCalls[0], onConflictingLibraryRedirectCalls2[0]]) { + const [request, parent, isMain, options, redirectedRequest] = args; + assert.strictEqual(request, 'source-map-support'); + assert.strictEqual(parent, module); + assert.strictEqual(isMain, false); + assert.strictEqual(options, undefined); + assert.strictEqual(redirectedRequest, require.resolve('.')); + } + }); +}); + describe('uninstall', function() { this.beforeEach(function() { underTest.uninstall(); process.emit = priorProcessEmit; Error.prepareStackTrace = priorErrorPrepareStackTrace; + Module._resolveFilename = priorResolveFilename; }); it('uninstall removes hooks and source-mapping behavior', function() { assert.strictEqual(Error.prepareStackTrace, priorErrorPrepareStackTrace); assert.strictEqual(process.emit, priorProcessEmit); + assert.strictEqual(Module._resolveFilename, priorResolveFilename); normalThrowWithoutSourceMapSupportInstalled(); }); @@ -773,4 +811,20 @@ describe('uninstall', function() { process.emit('foo'); assert(peInvocations >= 1); }); + + it('uninstall preserves third-party module._resolveFilename hooks installed after us', function() { + installSms(); + const wrappedResolveFilename = Module._resolveFilename; + let peInvocations = 0; + function thirdPartyModuleResolveFilename() { + peInvocations++; + return wrappedResolveFilename.apply(this, arguments); + } + Module._resolveFilename = thirdPartyModuleResolveFilename; + underTest.uninstall(); + assert.strictEqual(Module._resolveFilename, thirdPartyModuleResolveFilename); + normalThrowWithoutSourceMapSupportInstalled(); + Module._resolveFilename('repl'); + assert(peInvocations >= 1); + }); });