blob: d0e0b3799fbbf3e8b9700188cc2ce5876da1e04d [file] [log] [blame]
// 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);
});
});
});