| // Copyright 2022 The Chromium Authors |
| // 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 Root from '../../core/root/root.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, waitForAllSourceMapsProcessed} from '../../testing/SourceMapEncoder.js'; |
| import {protocolCallFrame, stringifyFrame} from '../../testing/StackTraceHelpers.js'; |
| import * as ScopesCodec from '../../third_party/source-map-scopes-codec/source-map-scopes-codec.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); |
| const ignoreListManager = Workspace.IgnoreListManager.IgnoreListManager.instance({forceNew: true}); |
| debuggerWorkspaceBinding = Bindings.DebuggerWorkspaceBinding.DebuggerWorkspaceBinding.instance({ |
| forceNew: true, |
| resourceMapping, |
| targetManager, |
| ignoreListManager, |
| workspace, |
| }); |
| backend = new MockProtocolBackend(); |
| }); |
| |
| afterEach(async () => { |
| await waitForAllSourceMapsProcessed(); |
| }); |
| |
| 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. |
| Workspace.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); |
| }); |
| |
| describe('translateRawFramesStep', () => { |
| it('returns false for builtin frames', async () => { |
| const target = createTarget(); |
| const compilerScriptMapping = new Bindings.CompilerScriptMapping.CompilerScriptMapping( |
| target.model(SDK.DebuggerModel.DebuggerModel)!, workspace, debuggerWorkspaceBinding); |
| |
| assert.isFalse(await compilerScriptMapping.translateRawFramesStep( |
| [{lineNumber: -1, columnNumber: -1, functionName: 'Array.map'}], [])); |
| }); |
| |
| it('translates a single frame using "proposal scopes" information', async () => { |
| Root.Runtime.experiments.enableForTest(Root.Runtime.ExperimentName.USE_SOURCE_MAP_SCOPES); |
| |
| const target = createTarget(); |
| const compilerScriptMapping = new Bindings.CompilerScriptMapping.CompilerScriptMapping( |
| target.model(SDK.DebuggerModel.DebuggerModel)!, workspace, debuggerWorkspaceBinding); |
| const sourceMap = encodeSourceMap([ |
| '0:0 => index.ts:0:0', |
| '0:21 => index.ts:2:11', |
| ]); |
| ScopesCodec.encode( |
| new ScopesCodec.ScopeInfoBuilder() |
| .startScope(1, 0, {isStackFrame: true, name: 'foo', key: 'fn'}) |
| .endScope(3, 1) |
| .startRange(0, 10, {isStackFrame: true, scopeKey: 'fn'}) |
| .endRange(0, 23) |
| .build(), |
| sourceMap as ScopesCodec.SourceMapJson); |
| |
| const uiSourceCodePromise = waitForUISourceCodeAdded('http://example.com/index.ts', target); |
| const script = |
| await backend.addScript(target, {url: 'http://example.com/index.js', content: 'function f(){debugger;}'}, { |
| url: 'http://example.com/index.js.map', |
| content: sourceMap, |
| }); |
| |
| const translatedFrames: |
| Parameters<Bindings.CompilerScriptMapping.CompilerScriptMapping['translateRawFramesStep']>[1] = []; |
| assert.isTrue(await compilerScriptMapping.translateRawFramesStep( |
| [{ |
| scriptId: script.scriptId, |
| url: script.sourceURL, |
| lineNumber: 0, |
| columnNumber: 21, |
| functionName: 'f', |
| }], |
| translatedFrames)); |
| assert.deepEqual(translatedFrames, [[{ |
| line: 2, |
| column: 11, |
| name: 'foo', |
| uiSourceCode: await uiSourceCodePromise, |
| url: undefined, |
| }]]); |
| |
| Root.Runtime.experiments.disableForTest(Root.Runtime.ExperimentName.USE_SOURCE_MAP_SCOPES); |
| }); |
| |
| it('translates a single frame using "fallback" scope information (created from AST and mappigns)', async () => { |
| const target = createTarget(); |
| const compilerScriptMapping = new Bindings.CompilerScriptMapping.CompilerScriptMapping( |
| target.model(SDK.DebuggerModel.DebuggerModel)!, workspace, debuggerWorkspaceBinding); |
| const sourceMap = encodeSourceMap([ |
| '0:0 => index.ts:0:0', |
| '0:10 => index.ts:1:10@foo', |
| '0:21 => index.ts:2:11', |
| ]); |
| |
| const uiSourceCodePromise = waitForUISourceCodeAdded('http://example.com/index.ts', target); |
| const script = |
| await backend.addScript(target, {url: 'http://example.com/index.js', content: 'function f(){debugger;}'}, { |
| url: 'http://example.com/index.js.map', |
| content: sourceMap, |
| }); |
| |
| const translatedFrames: |
| Parameters<Bindings.CompilerScriptMapping.CompilerScriptMapping['translateRawFramesStep']>[1] = []; |
| assert.isTrue(await compilerScriptMapping.translateRawFramesStep( |
| [{ |
| scriptId: script.scriptId, |
| url: script.sourceURL, |
| lineNumber: 0, |
| columnNumber: 21, |
| functionName: 'f', |
| }], |
| translatedFrames)); |
| assert.deepEqual(translatedFrames, [[{ |
| line: 2, |
| column: 11, |
| name: 'foo', |
| uiSourceCode: await uiSourceCodePromise, |
| url: undefined, |
| }]]); |
| }); |
| |
| it('expands inlined frames and populates UISourceCode', async () => { |
| Root.Runtime.experiments.enableForTest(Root.Runtime.ExperimentName.USE_SOURCE_MAP_SCOPES); |
| |
| const target = createTarget(); |
| const compilerScriptMapping = new Bindings.CompilerScriptMapping.CompilerScriptMapping( |
| target.model(SDK.DebuggerModel.DebuggerModel)!, workspace, debuggerWorkspaceBinding); |
| // |
| // orig. code gen. code |
| // 10 20 10 20 |
| // 012345678901234567890 012345678901234567890 |
| // |
| // 0: function inner() { print('hello') |
| // 1: print('hello'); |
| // 2: } |
| // 3: |
| // 4: function outer() { |
| // 5: if (true) { |
| // 6: inner(); |
| // 7: } |
| // 8: } |
| // 9: |
| // 10: outer(); |
| |
| const builder = new ScopesCodec.ScopeInfoBuilder(); |
| builder.startScope(0, 0, {kind: 'global', key: 'global'}) |
| .startScope(0, 14, {kind: 'function', name: 'inner', key: 'inner', isStackFrame: true}) |
| .endScope(2, 1) |
| .startScope(4, 14, {kind: 'function', name: 'outer', key: 'outer', isStackFrame: true}) |
| .startScope(5, 12, {kind: 'block', key: 'block'}) |
| .endScope(7, 3) |
| .endScope(8, 1) |
| .endScope(11, 0); |
| |
| builder.startRange(0, 0, {scopeKey: 'global'}) |
| .startRange(0, 0, {scopeKey: 'outer', callSite: {sourceIndex: 0, line: 10, column: 5}}) |
| .startRange(0, 0, {scopeKey: 'block'}) |
| .startRange(0, 0, {scopeKey: 'inner', callSite: {sourceIndex: 0, line: 6, column: 9}}) |
| .endRange(0, 14) |
| .endRange(0, 14) |
| .endRange(0, 14) |
| .endRange(1, 0); |
| |
| const sourceMap = |
| ScopesCodec.encode(builder.build(), encodeSourceMap(['0:5 => index.ts:1:7']) as ScopesCodec.SourceMapJson); |
| const script = |
| await backend.addScript(target, {url: 'http://example.com/index.js', content: 'print(\'hello\')'}, { |
| url: 'http://example.com/index.js.map', |
| content: sourceMap as SDK.SourceMap.SourceMapV3, |
| }); |
| |
| const translatedFrames: |
| Parameters<Bindings.CompilerScriptMapping.CompilerScriptMapping['translateRawFramesStep']>[1] = []; |
| assert.isTrue(await compilerScriptMapping.translateRawFramesStep( |
| [protocolCallFrame(`${script.sourceURL}:${script.scriptId}::0:5`)], translatedFrames)); |
| |
| assert.deepEqual(translatedFrames[0].map(stringifyFrame), [ |
| 'at inner (index.ts:1:7)', |
| 'at outer (index.ts:6:9)', |
| 'at <anonymous> (index.ts:10:5)', |
| ]); |
| |
| const uiSourceCode = compilerScriptMapping.uiSourceCodeForURL(urlString`http://example.com/index.ts`, false); |
| assert.exists(uiSourceCode); |
| assert.strictEqual(translatedFrames[0][0].uiSourceCode, uiSourceCode); |
| assert.strictEqual(translatedFrames[0][1].uiSourceCode, uiSourceCode); |
| assert.strictEqual(translatedFrames[0][2].uiSourceCode, uiSourceCode); |
| |
| Root.Runtime.experiments.disableForTest(Root.Runtime.ExperimentName.USE_SOURCE_MAP_SCOPES); |
| }); |
| }); |
| }); |