diff --git a/extension-api/ExtensionAPI.d.ts b/extension-api/ExtensionAPI.d.ts index e6b5d078c0..c3ad1353f8 100644 --- a/extension-api/ExtensionAPI.d.ts +++ b/extension-api/ExtensionAPI.d.ts @@ -25,6 +25,7 @@ export namespace Chrome { * there must be no "straddling" (i.e. partially overlapping ranges). */ setFunctionRangesForScript(ranges: NamedFunctionRange[]): Promise; + attachSourceMapURL(sourceMapURL: string): Promise; } export interface InspectedWindow { diff --git a/front_end/models/extensions/ExtensionAPI.ts b/front_end/models/extensions/ExtensionAPI.ts index 30eab72027..0f0b35b87d 100644 --- a/front_end/models/extensions/ExtensionAPI.ts +++ b/front_end/models/extensions/ExtensionAPI.ts @@ -82,6 +82,7 @@ export namespace PrivateAPI { ShowPanel = 'showPanel', Unsubscribe = 'unsubscribe', UpdateButton = 'updateButton', + AttachSourceMapToResource = 'attachSourceMapToResource', RegisterLanguageExtensionPlugin = 'registerLanguageExtensionPlugin', GetWasmLinearMemory = 'getWasmLinearMemory', GetWasmLocal = 'getWasmLocal', @@ -255,6 +256,11 @@ export namespace PrivateAPI { command: Commands.GetResourceContent; url: string; } + interface AttachSourceMapToResourceRequest { + command: Commands.AttachSourceMapToResource; + contentUrl: string; + sourceMapURL: string; + } interface SetResourceContentRequest { command: Commands.SetResourceContent; url: string; @@ -314,8 +320,9 @@ export namespace PrivateAPI { SetSidebarHeightRequest|SetSidebarContentRequest|SetSidebarPageRequest|OpenResourceRequest| SetOpenResourceHandlerRequest|SetThemeChangeHandlerRequest|ReloadRequest|EvaluateOnInspectedPageRequest| GetRequestContentRequest|GetResourceContentRequest|SetResourceContentRequest|SetFunctionRangesForScriptRequest| - ForwardKeyboardEventRequest|GetHARRequest|GetPageResourcesRequest|GetWasmLinearMemoryRequest|GetWasmLocalRequest| - GetWasmGlobalRequest|GetWasmOpRequest|ShowNetworkPanelRequest|ReportResourceLoadRequest; + AttachSourceMapToResourceRequest|ForwardKeyboardEventRequest|GetHARRequest|GetPageResourcesRequest| + GetWasmLinearMemoryRequest|GetWasmLocalRequest|GetWasmGlobalRequest|GetWasmOpRequest|ShowNetworkPanelRequest| + ReportResourceLoadRequest; export type ExtensionServerRequestMessage = PrivateAPI.ServerRequests&{requestId?: number}; interface AddRawModuleRequest { @@ -1331,7 +1338,7 @@ self.injectedExtensionAPI = function( } (ResourceImpl.prototype as - Pick) = { + Pick) = { get url(): string { return (this as APIImpl.Resource)._url; }, @@ -1380,6 +1387,25 @@ self.injectedExtensionAPI = function( } })); }, + + attachSourceMapURL: function(this: APIImpl.Resource, sourceMapURL: string): Promise { + return new Promise( + (resolve, reject) => extensionServer.sendRequest( + {command: PrivateAPI.Commands.AttachSourceMapToResource, contentUrl: this._url, sourceMapURL}, + (response: unknown) => { + const result = response as { + code: string, + description: string, + details: unknown[], + isError?: boolean, + }; + if (result.isError) { + reject(new Error(result.description)); + } else { + resolve(); + } + })); + }, }; function getTabId(): string { diff --git a/front_end/models/extensions/ExtensionServer.test.ts b/front_end/models/extensions/ExtensionServer.test.ts index 9c22db22b4..93147af42c 100644 --- a/front_end/models/extensions/ExtensionServer.test.ts +++ b/front_end/models/extensions/ExtensionServer.test.ts @@ -12,7 +12,9 @@ import { describeWithDevtoolsExtension, getExtensionOrigin, } from '../../testing/ExtensionHelpers.js'; +import {MockProtocolBackend} from '../../testing/MockScopeChain.js'; import {addChildFrame, FRAME_URL, getMainFrame} from '../../testing/ResourceTreeHelpers.js'; +import {encodeSourceMap} from '../../testing/SourceMapEncoder.js'; import * as UI from '../../ui/legacy/legacy.js'; import * as Bindings from '../bindings/bindings.js'; import * as Extensions from '../extensions/extensions.js'; @@ -959,3 +961,72 @@ for (const allowFileAccess of [true, false]) { }); }); } + +describeWithDevtoolsExtension('validate attachSourceMapURL ', {}, context => { + it('correctly attaches a source map to a registered script', async () => { + const sourceRoot = 'http://example.com'; + const scriptName = 'script.ts'; + const scriptInfo = { + url: urlString`${sourceRoot}/script.js`, + content: 'function f(x) { console.log(x); } function ignore(y){ console.log(y); }', + }; + const sourceMap = encodeSourceMap( + [ + `0:9 => ${scriptName}:0:1`, + `1:0 => ${scriptName}:4:0`, + `1:2 => ${scriptName}:4:2`, + `2:0 => ${scriptName}:2:0`, + ], + sourceRoot); + + const sourceMapString = { + version: 3, + names: ['f', 'console', 'log', 'ignore'], + sources: [scriptInfo.url], + mappings: sourceMap.mappings, + file: `${scriptInfo.url}.map`, + }; + + const target = createTarget({type: SDK.Target.Type.FRAME}); + const targetManager = target.targetManager(); + const workspace = Workspace.Workspace.WorkspaceImpl.instance(); + const resourceMapping = new Bindings.ResourceMapping.ResourceMapping(targetManager, workspace); + const debuggerWorkspaceBinding = Bindings.DebuggerWorkspaceBinding.DebuggerWorkspaceBinding.instance( + {forceNew: false, resourceMapping, targetManager}); + const backend = new MockProtocolBackend(); + Bindings.IgnoreListManager.IgnoreListManager.instance({forceNew: false, debuggerWorkspaceBinding}); + + // Before any script is registered, there shouldn't be any uiSourceCodes. + assert.isNull(Workspace.Workspace.WorkspaceImpl.instance().uiSourceCodeForURL(scriptInfo.url)); + + // Create promise to await the uiSourceCode given the url and its target. + const uiSourceCodePromise = debuggerWorkspaceBinding.waitForUISourceCodeAdded(scriptInfo.url, target); + + // Register the script. + const currentScript = await backend.addScript(target, scriptInfo, null); + + // Await the promise for sourceCode to be added. + await uiSourceCodePromise; + + assert.exists(context.chrome.devtools); + + const resources = await new Promise(r => { + context.chrome.devtools?.inspectedWindow.getResources(r); + }); + + // Validate that resource is registered. + assert.isTrue(resources && resources.length > 0); + + // Script should not have a source map url attached yet. + assert.notExists(currentScript.sourceMapURL); + + // Call attachSourceMapURL with encoded source map as a dataURL + const scriptResource = resources.find(item => item.url === scriptInfo.url.toString()); + const encodedSourceMap = `data:text/plain;base64,${btoa(JSON.stringify(sourceMapString))}`; + + await scriptResource?.attachSourceMapURL(encodedSourceMap); + + // Validate that the script has the sourcemap dataURL attached. + assert.deepEqual(currentScript.sourceMapURL, encodedSourceMap); + }); +}); diff --git a/front_end/models/extensions/ExtensionServer.ts b/front_end/models/extensions/ExtensionServer.ts index b75bae766d..e6b2c149b1 100644 --- a/front_end/models/extensions/ExtensionServer.ts +++ b/front_end/models/extensions/ExtensionServer.ts @@ -195,6 +195,7 @@ export class ExtensionServer extends Common.ObjectWrapper.ObjectWrapper 0) { + for (const script of scriptFiles) { + const resourceFile = debuggerBindingsInstance.scriptFile( + contentProvider as Workspace.UISourceCode.UISourceCode, script.debuggerModel); + resourceFile?.addSourceMapURL(message.sourceMapURL as Platform.DevToolsPath.UrlString); + } + } + + return this.status.OK(); + } + private onSetResourceContent(message: PrivateAPI.ExtensionServerRequestMessage, port: MessagePort): Record|undefined { if (message.command !== PrivateAPI.Commands.SetResourceContent) { return this.status.E_BADARG('command', `expected ${PrivateAPI.Commands.SetResourceContent}`);