UNPKG

chrome-devtools-frontend

Version:
547 lines (482 loc) • 24.1 kB
// Copyright 2022 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. import * as Platform from '../../core/platform/platform.js'; import * as SDK from '../../core/sdk/sdk.js'; import type * as Protocol from '../../generated/protocol.js'; import {createTarget} from '../../testing/EnvironmentHelpers.js'; import {describeWithMockConnection} from '../../testing/MockConnection.js'; import {MockProtocolBackend} from '../../testing/MockScopeChain.js'; import {encodeSourceMap} from '../../testing/SourceMapEncoder.js'; import * as TextUtils from '../text_utils/text_utils.js'; import * as Workspace from '../workspace/workspace.js'; import * as Bindings from './bindings.js'; const {urlString} = Platform.DevToolsPath; describeWithMockConnection('CompilerScriptMapping', () => { let backend: MockProtocolBackend; let debuggerWorkspaceBinding: Bindings.DebuggerWorkspaceBinding.DebuggerWorkspaceBinding; let workspace: Workspace.Workspace.WorkspaceImpl; beforeEach(() => { const targetManager = SDK.TargetManager.TargetManager.instance(); workspace = Workspace.Workspace.WorkspaceImpl.instance({forceNew: true}); const resourceMapping = new Bindings.ResourceMapping.ResourceMapping(targetManager, workspace); debuggerWorkspaceBinding = Bindings.DebuggerWorkspaceBinding.DebuggerWorkspaceBinding.instance( {forceNew: true, resourceMapping, targetManager}); backend = new MockProtocolBackend(); Bindings.IgnoreListManager.IgnoreListManager.instance({forceNew: true, debuggerWorkspaceBinding}); }); const waitForUISourceCodeAdded = (url: string, target: SDK.Target.Target): Promise<Workspace.UISourceCode.UISourceCode> => debuggerWorkspaceBinding.waitForUISourceCodeAdded(urlString`${url}`, target); const waitForUISourceCodeRemoved = (uiSourceCode: Workspace.UISourceCode.UISourceCode): Promise<void> => new Promise(resolve => { const {eventType, listener} = workspace.addEventListener(Workspace.Workspace.Events.UISourceCodeRemoved, event => { if (event.data === uiSourceCode) { workspace.removeEventListener(eventType, listener); resolve(); } }); }); it('creates UISourceCodes with the correct content type', async () => { const target = createTarget(); const sourceRoot = 'http://example.com'; const sources = ['foo.js', 'bar.ts', 'baz.jsx']; const scriptInfo = {url: `${sourceRoot}/bundle.js`, content: '1;\n'}; const sourceMapInfo = {url: `${scriptInfo.url}.map`, content: {version: 3, mappings: '', sourceRoot, sources}}; await Promise.all([ ...sources.map(name => waitForUISourceCodeAdded(`${sourceRoot}/${name}`, target).then(uiSourceCode => { assert.isTrue(uiSourceCode.contentType().isFromSourceMap()); assert.isTrue(uiSourceCode.contentType().isScript()); })), backend.addScript(target, scriptInfo, sourceMapInfo), ]); }); it('removes webpack hashes from display names', async () => { const target = createTarget(); const sourceRoot = 'http://example.com'; const sources = ['foo.js?a1b2', 'two%20words.ts?c3d4', '?e5f6']; const scriptInfo = {url: `${sourceRoot}/bundle.js`, content: '1;\n'}; const sourceMapInfo = {url: `${scriptInfo.url}.map`, content: {version: 3, mappings: '', sourceRoot, sources}}; const namesPromise = Promise.all( sources.map( name => waitForUISourceCodeAdded(`${sourceRoot}/${name}`, target).then(uiSourceCode => uiSourceCode.name())), ); await backend.addScript(target, scriptInfo, sourceMapInfo); assert.deepEqual(await namesPromise, ['foo.js', 'two words.ts', '?e5f6']); }); it('creates UISourceCodes with the correct media type', async () => { const target = createTarget(); const sourceRoot = 'http://example.com'; const scriptInfo = { url: `${sourceRoot}/bundle.js`, content: 'foo();\nbar();\nbaz();\n', }; const sourceMapInfo = { url: `${scriptInfo.url}.map`, content: encodeSourceMap(['0:0 => foo.js:0:0', '1:0 => bar.ts:0:0', '2:0 => baz.jsx:0:0'], sourceRoot), }; const [fooUISourceCode, barUISourceCode, bazUISourceCode] = await Promise.all([ waitForUISourceCodeAdded(`${sourceRoot}/foo.js`, target), waitForUISourceCodeAdded(`${sourceRoot}/bar.ts`, target), waitForUISourceCodeAdded(`${sourceRoot}/baz.jsx`, target), backend.addScript(target, scriptInfo, sourceMapInfo), ]); assert.strictEqual(fooUISourceCode.mimeType(), 'text/javascript'); assert.strictEqual(barUISourceCode.mimeType(), 'text/typescript'); assert.strictEqual(bazUISourceCode.mimeType(), 'text/jsx'); }); it('creates UISourceCodes with the correct content and metadata', async () => { const target = createTarget(); const sourceRoot = 'http://example.com'; const sourceContent = 'const x = 1; console.log(x)'; const scriptInfo = { url: `${sourceRoot}/script.min.js`, content: 'console.log(1);', }; const sourceMapInfo = { url: `${scriptInfo.url}.map`, content: {version: 1, mappings: '', sources: ['script.js'], sourcesContent: [sourceContent], sourceRoot}, }; const [uiSourceCode] = await Promise.all([ waitForUISourceCodeAdded(`${sourceRoot}/script.js`, target), backend.addScript(target, scriptInfo, sourceMapInfo), ]); const metadata = await uiSourceCode.requestMetadata(); assert.strictEqual(metadata?.contentSize, sourceContent.length); const content = await uiSourceCode.requestContentData(); assert.instanceOf(content, TextUtils.ContentData.ContentData); assert.strictEqual(content.text, sourceContent); }); it('creates separate UISourceCodes for separate targets', async () => { // Create a main target and a worker child target. const mainTarget = createTarget({ id: 'main' as Protocol.Target.TargetID, type: SDK.Target.Type.FRAME, }); const workerTarget = createTarget({ id: 'worker' as Protocol.Target.TargetID, type: SDK.Target.Type.ServiceWorker, parentTarget: mainTarget, }); const sourceRoot = 'http://example.com'; const scriptInfo = { url: `${sourceRoot}/script.min.js`, content: 'console.log(1);', }; const sourceMapInfo = { url: `${scriptInfo.url}.map`, content: encodeSourceMap(['0:0 => script.js:0:0'], sourceRoot), }; // Register the same script for both targets, and wait until the `CompilerScriptMapping` // adds a UISourceCode for the `script.js` that is listed in the source map for each of // the two targets. const [mainUISourceCode, mainScript, workerUISourceCode, workerScript] = await Promise.all([ waitForUISourceCodeAdded(`${sourceRoot}/script.js`, mainTarget), backend.addScript(mainTarget, scriptInfo, sourceMapInfo), waitForUISourceCodeAdded(`${sourceRoot}/script.js`, workerTarget), backend.addScript(workerTarget, scriptInfo, sourceMapInfo), ]); assert.notStrictEqual(mainUISourceCode, workerUISourceCode); for (const {script, uiSourceCode} of [{script: mainScript, uiSourceCode: mainUISourceCode}, {script: workerScript, uiSourceCode: workerUISourceCode}]) { const rawLocations = await debuggerWorkspaceBinding.uiLocationToRawLocations(uiSourceCode, 0, 0); assert.lengthOf(rawLocations, 1); const [rawLocation] = rawLocations; assert.strictEqual(rawLocation.script(), script); const uiLocation = await debuggerWorkspaceBinding.rawLocationToUILocation(rawLocation); assert.strictEqual(uiLocation!.uiSourceCode, uiSourceCode); } }); it('creates separate UISourceCodes for content scripts', async () => { // By default content scripts are ignore listed, which will prevent processing the // source map. We need to disable that option. Bindings.IgnoreListManager.IgnoreListManager.instance().unIgnoreListContentScripts(); const target = createTarget(); const sourceRoot = 'http://example.com'; const scriptInfo = { url: `${sourceRoot}/script.min.js`, content: 'console.log(1);', }; const sourceMapInfo = { url: `${scriptInfo.url}.map`, content: encodeSourceMap(['0:0 => script.js:0:0'], sourceRoot), }; // Register `script.min.js` as regular script first. const regularScriptInfo = {...scriptInfo, isContentScript: false}; const [regularUISourceCode, regularScript] = await Promise.all([ waitForUISourceCodeAdded(`${sourceRoot}/script.js`, target), backend.addScript(target, regularScriptInfo, sourceMapInfo), ]); // Now register the same `script.min.js` as content script. const contentScriptInfo = {...scriptInfo, isContentScript: true}; const [contentUISourceCode, contentScript] = await Promise.all([ waitForUISourceCodeAdded(`${sourceRoot}/script.js`, target), backend.addScript(target, contentScriptInfo, sourceMapInfo), ]); assert.notStrictEqual(regularUISourceCode, contentUISourceCode); for (const {script, uiSourceCode} of [{script: regularScript, uiSourceCode: regularUISourceCode}, {script: contentScript, uiSourceCode: contentUISourceCode}]) { const rawLocations = await debuggerWorkspaceBinding.uiLocationToRawLocations(uiSourceCode, 0, 0); assert.lengthOf(rawLocations, 1); const [rawLocation] = rawLocations; assert.strictEqual(rawLocation.script(), script); const uiLocation = await debuggerWorkspaceBinding.rawLocationToUILocation(rawLocation); assert.strictEqual(uiLocation!.uiSourceCode, uiSourceCode); } }); it('correctly marks known 3rdparty UISourceCodes', async () => { const target = createTarget(); const sourceRoot = 'http://example.com'; const scriptInfo = { url: `${sourceRoot}/bundle.js`, content: '1;\n', }; const sourceMapInfo = { url: `${scriptInfo.url}.map`, content: { version: 3, mappings: '', sourceRoot, sources: ['app.ts', 'lib.ts'], ignoreList: [1], }, }; await Promise.all([ waitForUISourceCodeAdded(`${sourceRoot}/app.ts`, target).then(uiSourceCode => { assert.isFalse(uiSourceCode.isKnownThirdParty(), '`app.ts` is not a known 3rdparty script'); }), waitForUISourceCodeAdded(`${sourceRoot}/lib.ts`, target).then(uiSourceCode => { assert.isTrue(uiSourceCode.isKnownThirdParty(), '`lib.ts` is a known 3rdparty script'); }), backend.addScript(target, scriptInfo, sourceMapInfo), ]); }); it('correctly maps to inline <script>s with `//# sourceURL` annotations', async () => { const target = createTarget(); const sourceRoot = 'http://example.com'; const scriptInfo = { url: `${sourceRoot}/test.out.js`, content: 'function f(x) {\n console.log(x);\n}\n', startLine: 4, startOffset: 12, hasSourceURL: true, }; const sourceMapInfo = { url: `${scriptInfo.url}.map`, content: encodeSourceMap( [ '0:0 => test.ts:0:0', '1:0 => test.ts:1:0', '1:2 => test.ts:1:2', '2:0 => test.ts:2:0', ], sourceRoot), }; const [uiSourceCode, script] = await Promise.all([ waitForUISourceCodeAdded(`${sourceRoot}/test.ts`, target), backend.addScript(target, scriptInfo, sourceMapInfo), ]); const rawLocations = await debuggerWorkspaceBinding.uiLocationToRawLocations(uiSourceCode, 1, 2); assert.lengthOf(rawLocations, 1); const [rawLocation] = rawLocations; assert.strictEqual(rawLocation.script(), script); assert.strictEqual(rawLocation.lineNumber, 1); assert.strictEqual(rawLocation.columnNumber, 2); const uiLocation = await debuggerWorkspaceBinding.rawLocationToUILocation(rawLocation); assert.strictEqual(uiLocation!.uiSourceCode, uiSourceCode); assert.strictEqual(uiLocation!.lineNumber, 1); assert.strictEqual(uiLocation!.columnNumber, 2); }); it('correctly removes UISourceCodes when detaching a sourcemap', async () => { const target = createTarget(); const sourceRoot = 'http://example.com'; const scriptInfo = { url: `${sourceRoot}/test.out.js`, content: '1\n2\n', }; const sourceMapInfo = { url: `${scriptInfo.url}.map`, content: encodeSourceMap( [ '0:0 => a.ts:0:0', '1:0 => b.ts:1:0', ], sourceRoot), }; const [, , script] = await Promise.all([ waitForUISourceCodeAdded(`${sourceRoot}/a.ts`, target), waitForUISourceCodeAdded(`${sourceRoot}/b.ts`, target), backend.addScript(target, scriptInfo, sourceMapInfo), ]); script.debuggerModel.sourceMapManager().detachSourceMap(script); assert.isNull( workspace.uiSourceCodeForURL(urlString`${`${sourceRoot}/a.ts`}`), '`a.ts` should not be around anymore'); assert.isNull( workspace.uiSourceCodeForURL(urlString`${`${sourceRoot}/b.ts`}`), '`b.ts` should not be around anymore'); }); it('correctly reports source-mapped lines', async () => { const target = createTarget(); const sourceRoot = 'http://example.com'; const scriptInfo = { url: `${sourceRoot}/test.out.js`, content: 'function f(x) {\n console.log(x);\n}\n', }; const sourceMapInfo = { url: `${scriptInfo.url}.map`, content: encodeSourceMap( [ '0:9 => test.ts:0:1', '1:0 => test.ts:4:0', '1:2 => test.ts:4:2', '2:0 => test.ts:2:0', ], sourceRoot), }; const [uiSourceCode] = await Promise.all([ waitForUISourceCodeAdded(`${sourceRoot}/test.ts`, target), backend.addScript(target, scriptInfo, sourceMapInfo), ]); const mappedLines = await debuggerWorkspaceBinding.getMappedLines(uiSourceCode); assert.deepEqual(mappedLines, new Set([0, 2, 4])); }); describe('supports modern Web development workflows', () => { it('supports webpack code splitting', async () => { // This is basically the "Shared code with webpack entry point code-splitting" scenario // outlined in http://go/devtools-source-identities, where two routes (`route1.ts` and // `route2.ts`) share some common code (`shared.ts`), and webpack is configured to spit // out a dedicated bundle for each route (`route1.js` and `route2.js`). The demo can be // found at https://devtools-source-identities.glitch.me/webpack-code-split/ for further // reference. const target = createTarget(); const sourceRoot = 'webpack:///src'; // Load the script and source map for the first route. const route1ScriptInfo = { url: 'http://example.com/route1.js', content: 'function f(x){}\nf(1)', }; const route1SourceMapInfo = { url: `${route1ScriptInfo.url}.map`, content: encodeSourceMap(['0:0 => shared.ts:0:0', '1:0 => route1.ts:0:0'], sourceRoot), }; const [route1UISourceCode, firstSharedUISourceCode, route1Script] = await Promise.all([ waitForUISourceCodeAdded(`${sourceRoot}/route1.ts`, target), waitForUISourceCodeAdded(`${sourceRoot}/shared.ts`, target), backend.addScript(target, route1ScriptInfo, route1SourceMapInfo), ]); // Both `route1.ts` and `shared.ts` are referred to only by `route1.js` at this point. assert.deepEqual(await debuggerWorkspaceBinding.uiLocationToRawLocations(route1UISourceCode, 0), [ route1Script.debuggerModel.createRawLocation(route1Script, 1, 0), ]); assert.deepEqual(await debuggerWorkspaceBinding.uiLocationToRawLocations(firstSharedUISourceCode, 0), [ route1Script.debuggerModel.createRawLocation(route1Script, 0, 0), ]); // Load the script and source map for the second route. At this point a new `shared.ts` should // appear, replacing the original `shared.ts` UISourceCode. const route2ScriptInfo = { url: 'http://example.com/route2.js', content: 'function f(x){}\nf(2)', }; const route2SourceMapInfo = { url: `${route2ScriptInfo.url}.map`, content: encodeSourceMap(['0:0 => shared.ts:0:0', '1:0 => route2.ts:0:0'], sourceRoot), }; const [route2UISourceCode, secondSharedUISourceCode, route2Script] = await Promise.all([ waitForUISourceCodeAdded(`${sourceRoot}/route2.ts`, target), waitForUISourceCodeAdded(`${sourceRoot}/shared.ts`, target), backend.addScript(target, route2ScriptInfo, route2SourceMapInfo), waitForUISourceCodeRemoved(firstSharedUISourceCode), ]); // Now `route1.ts` is provided exclusively by `route1.js`... const route1UILocation = route1UISourceCode.uiLocation(0, 0); const route1Locations = await debuggerWorkspaceBinding.uiLocationToRawLocations( route1UILocation.uiSourceCode, route1UILocation.lineNumber, route1UILocation.columnNumber); assert.lengthOf(route1Locations, 1); const [route1Location] = route1Locations; assert.strictEqual(route1Location.script(), route1Script); assert.deepEqual(await debuggerWorkspaceBinding.rawLocationToUILocation(route1Location), route1UILocation); // ...and `route2.ts` is provided exclusively by `route2.js`... const route2UILocation = route2UISourceCode.uiLocation(0, 0); const route2Locations = await debuggerWorkspaceBinding.uiLocationToRawLocations( route2UILocation.uiSourceCode, route2UILocation.lineNumber, route2UILocation.columnNumber); assert.lengthOf(route2Locations, 1); const [route2Location] = route2Locations; assert.strictEqual(route2Location.script(), route2Script); assert.deepEqual(await debuggerWorkspaceBinding.rawLocationToUILocation(route2Location), route2UILocation); // ...but `shared.ts` is provided by both `route1.js` and `route2.js`. const sharedUILocation = secondSharedUISourceCode.uiLocation(0, 0); const sharedLocations = await debuggerWorkspaceBinding.uiLocationToRawLocations( sharedUILocation.uiSourceCode, sharedUILocation.lineNumber, sharedUILocation.columnNumber); assert.sameMembers(sharedLocations.map(location => location.script()), [route1Script, route2Script]); for (const location of sharedLocations) { assert.deepEqual(await debuggerWorkspaceBinding.rawLocationToUILocation(location), sharedUILocation); } }); it('supports webpack hot module replacement', async () => { // This simulates the webpack HMR machinery, where originally a `bundle.js` is served, // which includes embedded authored code for `lib.js` and `app.js`, both of which map // to `bundle.js`. Later an update script is sent that replaces `app.js` with a newer // version, while sending the same authored code for `lib.js` (presumably because the // devserver figured the file might have changed). Now the initial `app.js` should be // removed and `bundle.js` will have un-mapped locations for the `app.js` part. The // new `app.js` will point to the update script. `lib.js` remains unchanged. // // This is a generalization of https://crbug.com/1403362 and http://crbug.com/1403432, // which both present special cases of the general stale mapping problem. const target = createTarget(); const sourceRoot = 'webpack:///src'; // Load the original bundle. const originalScriptInfo = { url: 'http://example.com/bundle.js', content: 'const f = console.log;\nf("Hello from the original bundle");', }; const originalSourceMapInfo = { url: `${originalScriptInfo.url}.map`, content: encodeSourceMap( [ '0:0 => lib.js:0:0', 'lib.js: const f = console.log;', '1:0 => app.js:0:0', 'app.js: f("Hello from the original bundle")', ], sourceRoot), }; const [originalLibUISourceCode, originalAppUISourceCode, originalScript] = await Promise.all([ waitForUISourceCodeAdded(`${sourceRoot}/lib.js`, target), waitForUISourceCodeAdded(`${sourceRoot}/app.js`, target), backend.addScript(target, originalScriptInfo, originalSourceMapInfo), ]); // Initially the original `bundle.js` maps to the original `app.js` and `lib.js`. assert.deepEqual( await debuggerWorkspaceBinding.rawLocationToUILocation( originalScript.debuggerModel.createRawLocation(originalScript, 0, 0)), originalLibUISourceCode.uiLocation(0, 0)); assert.deepEqual( await debuggerWorkspaceBinding.rawLocationToUILocation( originalScript.debuggerModel.createRawLocation(originalScript, 1, 0)), originalAppUISourceCode.uiLocation(0, 0)); // Inject the HMR update script. const updateScriptInfo = { url: 'http://example.com/hot.update.1234.js', content: 'f("Hello from the update");', }; const updateSourceMapInfo = { url: `${updateScriptInfo.url}.map`, content: encodeSourceMap( [ '0:0 => app.js:0:0', 'lib.js: const f = console.log;', 'app.js: f("Hello from the update")', ], sourceRoot), }; const [updateAppUISourceCode, , updateScript] = await Promise.all([ waitForUISourceCodeAdded(`${sourceRoot}/app.js`, target), // The original `app.js` should disappear as part of the HMR update. waitForUISourceCodeRemoved(originalAppUISourceCode), backend.addScript(target, updateScriptInfo, updateSourceMapInfo), ]); // Now we have a new `app.js`... assert.notStrictEqual(updateAppUISourceCode, originalAppUISourceCode); assert.isEmpty(await debuggerWorkspaceBinding.uiLocationToRawLocations(originalAppUISourceCode, 0, 0)); assert.deepEqual(await debuggerWorkspaceBinding.uiLocationToRawLocations(updateAppUISourceCode, 0, 0), [ updateScript.debuggerModel.createRawLocation(updateScript, 0, 0), ]); // ...and the `app.js` mapping of the `bundle.js` is now gone... const {uiSourceCode} = (await debuggerWorkspaceBinding.rawLocationToUILocation( originalScript.debuggerModel.createRawLocation(originalScript, 1, 0)))!; assert.notStrictEqual(uiSourceCode, originalAppUISourceCode); assert.notStrictEqual(uiSourceCode, updateAppUISourceCode); // ...while the `lib.js` mapping of `bundle.js` is still intact (because it // was the same content). assert.deepEqual( await debuggerWorkspaceBinding.rawLocationToUILocation( originalScript.debuggerModel.createRawLocation(originalScript, 0, 0)), originalLibUISourceCode.uiLocation(0, 0)); }); }); it('assumes UTF-8 encoding for source files embedded in source maps', async () => { const target = createTarget(); const sourceRoot = 'http://example.com'; const sourceContent = 'console.log("Ahoj světe!");'; const scriptInfo = { url: `${sourceRoot}/script.min.js`, content: sourceContent, }; const sourceMapInfo = { url: `${scriptInfo.url}.map`, content: {version: 3, mappings: '', sources: ['script.js'], sourcesContent: [sourceContent], sourceRoot}, }; const [uiSourceCode] = await Promise.all([ waitForUISourceCodeAdded(`${sourceRoot}/script.js`, target), backend.addScript(target, scriptInfo, sourceMapInfo), ]); const metadata = await uiSourceCode.requestMetadata(); assert.notStrictEqual(metadata?.contentSize, sourceContent.length); const sourceUTF8 = new TextEncoder().encode(sourceContent); assert.strictEqual(metadata?.contentSize, sourceUTF8.length); }); });