Adding attachSourceMapURL to Resource in ExtensionAPI

This CL adds the APIs required for attaching a sourcemap to a resource via Extension APIs.

This CL considers that the External Developer will subscribe to some of
the existing Devtools Extension API (chrome.runtime.onConnect,
port.OnMessage & inspectedWindow.onResourceAdded)

This “subscribed“ extension, will be notified every time a resource is
loaded. This includes images and any other resources

This CL introduces:
 - APIs shape and implementation (attachSourceMapURL)
 - ExtensionServer implementation
 - ExtensionServer tests

Full spec can be found here (Implementation):
https://docs.google.com/document/d/1x9_MTMVr213ApruMK6ryc4JpPHahvOBlExP6sHkMv8Y/edit?pli=1

Bug: 364508694
Change-Id: I35a53b58fae1ba41b733bc4bcb8ed6b8022dad54
Reviewed-on: https://chromium-review.googlesource.com/c/devtools/devtools-frontend/+/6166854
Reviewed-by: Danil Somsikov <dsv@chromium.org>
Commit-Queue: Vidal Diazleal <vidorteg@microsoft.com>
Reviewed-by: Philip Pfaffe <pfaffe@chromium.org>
This commit is contained in:
Vidal Guillermo Diazleal Ortega
2025-02-07 09:55:56 -08:00
committed by Devtools-frontend LUCI CQ
parent f271e0d644
commit dc49e2d266
4 changed files with 130 additions and 3 deletions
+1
View File
@@ -25,6 +25,7 @@ export namespace Chrome {
* there must be no "straddling" (i.e. partially overlapping ranges).
*/
setFunctionRangesForScript(ranges: NamedFunctionRange[]): Promise<void>;
attachSourceMapURL(sourceMapURL: string): Promise<void>;
}
export interface InspectedWindow {
+29 -3
View File
@@ -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<APIImpl.Resource, 'url'|'type'|'getContent'|'setContent'|'setFunctionRangesForScript'>) = {
Pick<APIImpl.Resource, 'url'|'type'|'getContent'|'setContent'|'setFunctionRangesForScript'|'attachSourceMapURL'>) = {
get url(): string {
return (this as APIImpl.Resource)._url;
},
@@ -1380,6 +1387,25 @@ self.injectedExtensionAPI = function(
}
}));
},
attachSourceMapURL: function(this: APIImpl.Resource, sourceMapURL: string): Promise<void> {
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 {
@@ -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<Chrome.DevTools.Resource[]>(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);
});
});
@@ -195,6 +195,7 @@ export class ExtensionServer extends Common.ObjectWrapper.ObjectWrapper<EventTyp
this.registerHandler(PrivateAPI.Commands.SetOpenResourceHandler, this.onSetOpenResourceHandler.bind(this));
this.registerHandler(PrivateAPI.Commands.SetThemeChangeHandler, this.onSetThemeChangeHandler.bind(this));
this.registerHandler(PrivateAPI.Commands.SetResourceContent, this.onSetResourceContent.bind(this));
this.registerHandler(PrivateAPI.Commands.AttachSourceMapToResource, this.onAttachSourceMapToResource.bind(this));
this.registerHandler(PrivateAPI.Commands.SetSidebarHeight, this.onSetSidebarHeight.bind(this));
this.registerHandler(PrivateAPI.Commands.SetSidebarContent, this.onSetSidebarContent.bind(this));
this.registerHandler(PrivateAPI.Commands.SetSidebarPage, this.onSetSidebarPage.bind(this));
@@ -998,6 +999,34 @@ export class ExtensionServer extends Common.ObjectWrapper.ObjectWrapper<EventTyp
return undefined;
}
private onAttachSourceMapToResource(message: PrivateAPI.ExtensionServerRequestMessage): Record|undefined {
if (message.command !== PrivateAPI.Commands.AttachSourceMapToResource) {
return this.status.E_BADARG('command', `expected ${PrivateAPI.Commands.GetResourceContent}`);
}
if (!message.sourceMapURL) {
return this.status.E_FAILED('Expected a source map URL but got null');
}
const url = message.contentUrl as Platform.DevToolsPath.UrlString;
const contentProvider = Workspace.Workspace.WorkspaceImpl.instance().uiSourceCodeForURL(url);
if (!contentProvider) {
return this.status.E_NOTFOUND(url);
}
const debuggerBindingsInstance = Bindings.DebuggerWorkspaceBinding.DebuggerWorkspaceBinding.instance();
const scriptFiles = debuggerBindingsInstance.scriptsForUISourceCode(contentProvider);
if (scriptFiles.length > 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}`);