chrome-devtools-frontend 1.0.1706026 → 1.0.1708524

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (86) hide show
  1. package/.agents/skills/code-review/SKILL.md +121 -0
  2. package/front_end/core/sdk/DOMModel.ts +13 -6
  3. package/front_end/core/sdk/NetworkRequest.ts +43 -7
  4. package/front_end/core/sdk/SourceMap.ts +4 -13
  5. package/front_end/core/sdk/SourceMapScopesInfo.ts +134 -86
  6. package/front_end/core/text_utils/ContentData.ts +18 -1
  7. package/front_end/entrypoints/heap_snapshot_worker/ContextAnalyzer.ts +102 -4
  8. package/front_end/generated/InspectorBackendCommands.ts +7 -1
  9. package/front_end/generated/SupportedCSSProperties.ts +11 -2
  10. package/front_end/generated/protocol-mapping.d.ts +31 -0
  11. package/front_end/generated/protocol-proxy-api.d.ts +32 -0
  12. package/front_end/generated/protocol.ts +11 -0
  13. package/front_end/models/ai_assistance/AiAgent2.ts +1 -0
  14. package/front_end/models/ai_assistance/data_formatters/LighthouseFormatter.ts +26 -3
  15. package/front_end/models/ai_assistance/data_formatters/NetworkRequestFormatter.ts +15 -16
  16. package/front_end/models/ai_assistance/skills/Skill.ts +1 -2
  17. package/front_end/models/ai_assistance/skills/SkillRegistry.ts +2 -0
  18. package/front_end/models/ai_assistance/skills/lighthouse.md +33 -0
  19. package/front_end/models/ai_assistance/tools/GetLighthouseAudits.ts +6 -6
  20. package/front_end/models/ai_assistance/tools/RunLighthouse.ts +8 -5
  21. package/front_end/models/bindings/CompilerScriptMapping.ts +102 -45
  22. package/front_end/models/bindings/DebuggerLanguagePlugins.ts +25 -33
  23. package/front_end/models/bindings/DebuggerWorkspaceBinding.ts +33 -39
  24. package/front_end/models/bindings/StylesSourceMapping.ts +3 -0
  25. package/front_end/models/extensions/ExtensionEndpoint.ts +27 -6
  26. package/front_end/models/extensions/LanguageExtensionEndpoint.ts +106 -21
  27. package/front_end/models/extensions/RecorderExtensionEndpoint.ts +4 -2
  28. package/front_end/models/javascript_metadata/NativeFunctions.ts +6 -2
  29. package/front_end/models/persistence/NetworkPersistenceManager.ts +4 -3
  30. package/front_end/models/source_map_scopes/NamesResolver.ts +0 -8
  31. package/front_end/models/stack_trace/StackTraceImpl.ts +138 -47
  32. package/front_end/models/stack_trace/StackTraceModel.ts +117 -84
  33. package/front_end/models/stack_trace/Trie.ts +27 -0
  34. package/front_end/panels/accessibility/ARIAAttributesView.ts +6 -6
  35. package/front_end/panels/accessibility/AccessibilityAnnouncementRecordingView.ts +2 -2
  36. package/front_end/panels/accessibility/AccessibilityNodeView.ts +6 -9
  37. package/front_end/panels/accessibility/AccessibilitySidebarView.ts +83 -69
  38. package/front_end/panels/accessibility/AccessibilitySubPane.ts +24 -9
  39. package/front_end/panels/accessibility/SourceOrderView.ts +7 -3
  40. package/front_end/panels/accessibility/accessibilitySidebarView.css +5 -3
  41. package/front_end/panels/application/serviceWorkersView.css +1 -1
  42. package/front_end/panels/common/CommentThreadWidget.ts +30 -1
  43. package/front_end/panels/common/CommentsOverlayWidget.ts +42 -38
  44. package/front_end/panels/common/common-meta.ts +21 -9
  45. package/front_end/panels/console/ConsoleInsightTeaser.ts +25 -19
  46. package/front_end/panels/console/ConsoleViewMessage.ts +51 -21
  47. package/front_end/panels/console/console-meta.ts +1 -5
  48. package/front_end/panels/elements/DOMTreeWidget.ts +207 -586
  49. package/front_end/panels/elements/ElementsPanel.ts +1 -5
  50. package/front_end/panels/elements/StylePropertyTreeElement.ts +100 -5
  51. package/front_end/panels/lighthouse/LighthouseController.ts +9 -4
  52. package/front_end/panels/network/NetworkConfigView.ts +231 -187
  53. package/front_end/panels/network/RequestPayloadView.ts +22 -14
  54. package/front_end/panels/profiler/HeapDetachedElementsDataGrid.ts +0 -1
  55. package/front_end/panels/recorder/RecordingView.ts +42 -47
  56. package/front_end/panels/recorder/StepEditor.ts +1 -0
  57. package/front_end/panels/recorder/recordingView.css +4 -4
  58. package/front_end/panels/sources/CSSPlugin.ts +11 -0
  59. package/front_end/panels/sources/CoveragePlugin.ts +34 -23
  60. package/front_end/panels/sources/NavigatorView.ts +97 -52
  61. package/front_end/panels/sources/Plugin.ts +15 -1
  62. package/front_end/panels/sources/SourcesView.ts +47 -80
  63. package/front_end/panels/sources/TabbedEditorContainer.ts +27 -2
  64. package/front_end/panels/sources/UISourceCodeFrame.ts +11 -2
  65. package/front_end/panels/sources/navigatorTree.css +2 -2
  66. package/front_end/third_party/chromium/README.chromium +1 -1
  67. package/front_end/ui/comments/CommentAnchorResolver.ts +17 -0
  68. package/front_end/ui/components/suggestion_input/SuggestionInput.ts +10 -1
  69. package/front_end/ui/components/text_editor/AiCodeCompletionTeaser.ts +1 -1
  70. package/front_end/ui/components/text_editor/AiCodeGenerationTeaser.ts +2 -2
  71. package/front_end/ui/components/text_editor/aiCodeCompletionTeaser.css +0 -11
  72. package/front_end/ui/components/text_editor/aiCodeGenerationTeaser.css +0 -11
  73. package/front_end/ui/legacy/SoftDropDown.ts +17 -4
  74. package/front_end/ui/legacy/StackPane.ts +141 -0
  75. package/front_end/ui/legacy/TextPrompt.ts +37 -7
  76. package/front_end/ui/legacy/Toolbar.ts +7 -5
  77. package/front_end/ui/legacy/Widget.ts +3 -3
  78. package/front_end/ui/legacy/components/inline_editor/PositionAreaEditor.ts +85 -0
  79. package/front_end/ui/legacy/components/inline_editor/positionAreaEditor.css +61 -0
  80. package/front_end/ui/legacy/components/utils/Linkifier.ts +21 -45
  81. package/front_end/ui/legacy/inspectorCommon.css +11 -0
  82. package/front_end/ui/legacy/legacy.ts +2 -0
  83. package/front_end/ui/legacy/softContextMenu.css +2 -0
  84. package/front_end/ui/settings/ConsoleSettings.ts +7 -0
  85. package/front_end/ui/visual_logging/KnownContextValues.ts +10 -0
  86. package/package.json +1 -1
@@ -0,0 +1,121 @@
1
+ ---
2
+ name: code-review
3
+ description: Generic checklist for reviewing DevTools CLs (your own before upload, or someone else's on Gerrit). Covers test correctness (tautological/vacuous tests, cleanup, leaks), code clarity, CL hygiene, and points to the specialized skills to consult for imports, UI, testing, strings, models, and verification. Use when asked to review a CL, a diff, a patch, or to self-review before `git cl upload`.
4
+ ---
5
+
6
+ # Code Review
7
+
8
+ This checklist comes from recurring reviewer feedback on landed DevTools CLs.
9
+ Use it for self-review before upload and for reviewing other people's CLs.
10
+
11
+ ## 1. Gather context
12
+
13
+ - **Local change:** `git diff origin/main...HEAD` (or `git show HEAD` for a
14
+ single-commit branch). To learn about the branch and upload workflow, see the
15
+ `devtools-version-control` skill.
16
+ - **Gerrit CL:** strip the leading `)]}'` line from each response to get JSON:
17
+ - Diff: `git cl diff` (for the current branch), or fetch the patch set.
18
+ - Inline comments:
19
+ `curl -s https://chromium-review.googlesource.com/changes/devtools%2Fdevtools-frontend~<CL_NUMBER>/comments | tail -n +2`
20
+ - Messages:
21
+ `.../changes/devtools%2Fdevtools-frontend~<CL_NUMBER>/messages`
22
+ - Read the CL description first. Then check that the diff does what the
23
+ description says, and nothing else.
24
+
25
+ ## 2. Consult the specialized skills
26
+
27
+ Load the skill that matches what the diff touches, and apply its rules:
28
+
29
+ | If the diff touches… | Use skill |
30
+ | :--- | :--- |
31
+ | Any `import` statement, or a new cross-module dependency | `devtools-imports` |
32
+ | `UI.Widget`, lit-html views, components, CSS | `devtools-ui-widgets` |
33
+ | Migration of legacy imperative DOM to widgets or Lit | `ui-eng-vision-orchestrator` (and its sub-skills) |
34
+ | New or changed tests: choosing unit, API, or E2E | `devtools-testing-guidance` |
35
+ | Tests that use `describeWithEnvironment` or `describeWithMockConnection`, or foundation modules | `foundation-test-migration` |
36
+ | Ported Chromium web tests (`web_tests/http/tests/devtools`) | `migrate-chromium-test` |
37
+ | Flaky or disabled tests, or `it.skip` / `it.skipOnPlatforms` | `fix-tests` |
38
+ | `UIStrings` / user-facing text | `devtools-ux-writing-refactor` |
39
+ | Rendering user-controlled strings (URLs, names, console text) | `devtools-unicode-escaping` |
40
+ | `front_end/models/*`, `BUILD.gn`, `devtools_grd_files.gni`, entrypoints | `devtools-model-management` |
41
+ | Merging modules or consolidating `BUILD.gn` | `merging-devtools-module` |
42
+ | `Settings` registrations and descriptors | `devtools-setting-migration` |
43
+ | Stack traces, source maps, `DebuggerWorkspaceBinding` | `devtools-source-maps` |
44
+ | Building, running tests, or lint | `devtools-verification` |
45
+
46
+ ## 3. Test correctness (the most common review feedback)
47
+
48
+ - **Tautological tests:** A test must exercise real production code. If a
49
+ mock does the thing the test asserts (for example, a mocked `DOM.undo` that
50
+ calls `removeSection()` directly, followed by an assertion that the section
51
+ was removed), the test checks the mock and nothing else. Mock the *boundary*
52
+ instead (for example, have the CDP stub emit `CSSModel.Events.StyleSheetChanged`)
53
+ and let production code react to it.
54
+ - **Vacuous tests:** Look for local promises or data that are never connected
55
+ to DevTools logic, or stubs (like `initialize`) that skip the code path under
56
+ test. These tests pass because nothing happens.
57
+ - **Assertions that check the setup:** For example,
58
+ `assert.isNull(pane.node())` right after `pane.setNodeForTest(null)`. Each
59
+ assertion should verify the behavior that the test name describes.
60
+ - **Test lives where it belongs:** A test in `Foo.test.ts` should exercise
61
+ `Foo`. Move logic-only tests to the model's test file (for example,
62
+ `CSSMatchedStyles.test.ts`). Otherwise, render the UI and assert on the DOM.
63
+ - **Test covers the scenario it claims:** For example, an "iframe event
64
+ listeners" test must select a node *inside* the iframe. It is not enough
65
+ that it passes on `<body>` of the main frame.
66
+ - **Right suite:** Ask whether an E2E test could be a unit test, or whether the
67
+ E2E coverage is intentional for a user story. See `devtools-testing-guidance`.
68
+ - **E2E selectors match real UI:** Labels and `aria-label`s must match the
69
+ actual strings, for example "Show user agent shadow DOM" and not "User agent
70
+ shadow DOM". Check that the setting being toggled actually affects the
71
+ scenario.
72
+ - **Fail fast:** Throw or assert when a required value is missing. Don't let
73
+ the test time out later.
74
+
75
+ ## 4. Test hygiene and cleanup
76
+
77
+ - Pair every `beforeEach` singleton creation with a cleanup in `afterEach`
78
+ (for example, `CSSWorkspaceBinding.removeInstance()` or
79
+ `WorkspaceImpl.removeInstance()`) so that state does not leak between tests.
80
+ - Restore fake timers in `try { … } finally { clock.restore(); }`, or use the
81
+ sandbox or cleanup that the test runner provides. Keep the scope of fake
82
+ timers small.
83
+ - Don't stub the same method twice without calling `restore()` first. Sinon
84
+ throws `Attempted to wrap … which is already wrapped` when the object is the
85
+ same instance.
86
+ - Remove guards that were copied from helpers for no reason (for example,
87
+ `if (!stub.called) …` in a test that stubs the method only once).
88
+
89
+ ## 5. Code clarity
90
+
91
+ - Don't take detours to reach a value. If a module is imported, call
92
+ `TextUtils.TextRange.TextRange.fromObject(...)` directly instead of going
93
+ through `rule.style.range.constructor`.
94
+ - Look for dead code, leftover debug code, commented-out blocks, and unrelated
95
+ formatting churn.
96
+ - Check that the change stays in scope. Flag unrelated refactors that should be
97
+ split into a separate CL.
98
+ - Check for lifecycle issues: event listeners and observers that are added
99
+ without being removed, and async work that continues after a widget is
100
+ detached.
101
+
102
+ ## 6. CL hygiene
103
+
104
+ - The description explains *why*, including external dependencies (for
105
+ example, "V8 will stop emitting X; this must land first").
106
+ - Include a `Bug:` or `Fixed:` trailer, or `Bug: None`. Keep lines under 72
107
+ characters.
108
+ - For migrations and stacked series, list the legacy tests or files covered,
109
+ and number the series (for example, `(13/16)`).
110
+ - Disabled tests reference a bug.
111
+ - Presubmit, lint, and format pass. See `devtools-verification`.
112
+
113
+ ## 7. Writing review comments
114
+
115
+ - Be specific: cite file and line, explain the concrete failure mode (such as
116
+ a leak, a hang, or "passes vacuously"), and suggest a fix. Include a code
117
+ snippet when it helps.
118
+ - Separate blocking issues from nits and questions (for example, "Should we
119
+ move this to a unit test?").
120
+ - When a reviewer's concern is not an actual bug, reply with the rationale.
121
+ Don't make a silent change.
@@ -1139,7 +1139,7 @@ export class DOMNode extends Common.ObjectWrapper.ObjectWrapper<DOMNodeEventType
1139
1139
  }
1140
1140
 
1141
1141
  isCustomElement(): boolean {
1142
- if (this.nodeType() !== NodeType.ELEMENT_NODE || this.isXMLNode()) {
1142
+ if (this.nodeType() !== NodeType.ELEMENT_NODE || this.isXMLNode() || Boolean(this.pseudoType())) {
1143
1143
  return false;
1144
1144
  }
1145
1145
  const localName = this.localName() || this.nodeName().toLowerCase();
@@ -1600,11 +1600,18 @@ export class DOMDocument extends DOMNode {
1600
1600
  this.#frameId = frameId ?? null;
1601
1601
 
1602
1602
  const resourceTreeModel = this.domModel().target().model(ResourceTreeModel);
1603
- const frame = this.#frameId ? resourceTreeModel?.frameForId(this.#frameId) : resourceTreeModel?.mainFrame;
1604
- // In production, DOMDocument should always resolve its security origin from an
1605
- // associated frame, but falls back to SecurityOrigin.create() as a last resort
1606
- // for test environments where no frame exists.
1607
- this.#securityOrigin = frame?.securityOrigin() ?? SecurityOrigin.create(this.#documentURL);
1603
+ const frame = this.#frameId ? resourceTreeModel?.frameForId(this.#frameId) : null;
1604
+ if (frame) {
1605
+ this.#securityOrigin = frame.securityOrigin();
1606
+ } else if (resourceTreeModel?.mainFrame) {
1607
+ // If the target has an active frame tree, a DOMDocument without a matching
1608
+ // frame is a detached document and must be isolated with an opaque origin.
1609
+ this.#securityOrigin = SecurityOrigin.createUniqueOpaque();
1610
+ } else {
1611
+ // TODO(b/567434846): Migrate synthetic unit tests to attach frames so this
1612
+ // fallback to parsing documentURL can be removed.
1613
+ this.#securityOrigin = SecurityOrigin.create(this.#documentURL);
1614
+ }
1608
1615
  }
1609
1616
 
1610
1617
  get documentURL(): Platform.DevToolsPath.UrlString {
@@ -179,6 +179,7 @@ export class NetworkRequest extends Common.ObjectWrapper.ObjectWrapper<EventType
179
179
  #blockedReason: Protocol.Network.BlockedReason|undefined = undefined;
180
180
  #renderBlockingBehavior?: Protocol.Network.RenderBlockingBehavior;
181
181
  #initiatorSecurityOrigin?: SecurityOrigin;
182
+ #requestURLSecurityOrigin?: SecurityOrigin;
182
183
  #corsErrorStatus: Protocol.Network.CorsErrorStatus|undefined = undefined;
183
184
  statusCode = 0;
184
185
  statusText = '';
@@ -193,6 +194,7 @@ export class NetworkRequest extends Common.ObjectWrapper.ObjectWrapper<EventType
193
194
  #resourceType: Common.ResourceType.ResourceType = Common.ResourceType.resourceTypes.Other;
194
195
  #contentData: Promise<TextUtils.ContentData.ContentDataOrError>|null = null;
195
196
  #streamingContentData: Promise<TextUtils.StreamingContentData.StreamingContentDataOrError>|null = null;
197
+ #resolvedStreamingContentData: TextUtils.StreamingContentData.StreamingContentData|null = null;
196
198
  readonly #frames: WebSocketFrame[] = [];
197
199
  #responseHeaderValues: Record<string, string|undefined> = {};
198
200
  #responseHeadersText = '';
@@ -402,10 +404,17 @@ export class NetworkRequest extends Common.ObjectWrapper.ObjectWrapper<EventType
402
404
  * (`imported-har://${authority}`) to ensure recorded network traffic never collides with
403
405
  * live web origins.
404
406
  *
407
+ * The result is cached, so repeated calls return the same instance until the URL or the
408
+ * imported HAR flag changes. This keeps opaque origins (such as `data:` URLs) same-origin
409
+ * with themselves.
410
+ *
405
411
  * @see {@link initiatorSecurityOrigin} to obtain the origin of the document that initiated the request.
406
412
  */
407
413
  requestURLSecurityOrigin(): SecurityOrigin {
408
- return this.#resolveSecurityOrigin(this.#url);
414
+ if (!this.#requestURLSecurityOrigin) {
415
+ this.#requestURLSecurityOrigin = this.#resolveSecurityOrigin(this.#url);
416
+ }
417
+ return this.#requestURLSecurityOrigin;
409
418
  }
410
419
 
411
420
  /**
@@ -422,6 +431,9 @@ export class NetworkRequest extends Common.ObjectWrapper.ObjectWrapper<EventType
422
431
  * For imported HAR files, the origin is mapped to an isolated virtual domain
423
432
  * (`imported-har://${authority}`) matching the imported initiating document.
424
433
  *
434
+ * The result is cached, so repeated calls return the same instance until the imported HAR
435
+ * flag changes. This keeps opaque origins same-origin with themselves.
436
+ *
425
437
  * @see {@link requestURLSecurityOrigin} to obtain the origin of the target resource URL being requested.
426
438
  */
427
439
  initiatorSecurityOrigin(): SecurityOrigin {
@@ -462,6 +474,7 @@ export class NetworkRequest extends Common.ObjectWrapper.ObjectWrapper<EventType
462
474
  this.#parsedQueryParameters = undefined;
463
475
  this.#name = undefined;
464
476
  this.#path = undefined;
477
+ this.#requestURLSecurityOrigin = undefined;
465
478
  }
466
479
 
467
480
  get documentURL(): Platform.DevToolsPath.UrlString {
@@ -1207,7 +1220,12 @@ export class NetworkRequest extends Common.ObjectWrapper.ObjectWrapper<EventType
1207
1220
  }
1208
1221
 
1209
1222
  setIsImportedHar(isImportedHar: boolean): void {
1223
+ if (this.#isImportedHar === isImportedHar) {
1224
+ return;
1225
+ }
1210
1226
  this.#isImportedHar = isImportedHar;
1227
+ this.#requestURLSecurityOrigin = undefined;
1228
+ this.#initiatorSecurityOrigin = undefined;
1211
1229
  }
1212
1230
 
1213
1231
  setEarlyHintsHeaders(headers: NameValue[]): void {
@@ -1488,9 +1506,11 @@ export class NetworkRequest extends Common.ObjectWrapper.ObjectWrapper<EventType
1488
1506
  }
1489
1507
  // Note that this is save: "streamResponseBody()" always creates base64-based ContentData and
1490
1508
  // for "contentData()" we'll never call "addChunk".
1491
- return TextUtils.StreamingContentData.StreamingContentData.from(
1509
+ const streamingContentData = TextUtils.StreamingContentData.StreamingContentData.from(
1492
1510
  contentData,
1493
1511
  );
1512
+ this.#resolvedStreamingContentData = streamingContentData;
1513
+ return streamingContentData;
1494
1514
  });
1495
1515
 
1496
1516
  return this.#streamingContentData;
@@ -1510,6 +1530,18 @@ export class NetworkRequest extends Common.ObjectWrapper.ObjectWrapper<EventType
1510
1530
  isRegex: boolean,
1511
1531
  ): Promise<TextUtils.ContentProvider.SearchMatch[]> {
1512
1532
  if (!this.#contentDataProvider) {
1533
+ const cachedContentData = this.finished && !this.failed ?
1534
+ (await this.#contentData ?? this.#resolvedStreamingContentData?.content()) :
1535
+ undefined;
1536
+ if (cachedContentData && !TextUtils.ContentData.ContentData.isError(cachedContentData) &&
1537
+ cachedContentData.isTextContent) {
1538
+ return TextUtils.TextUtils.performSearchInContentData(
1539
+ cachedContentData,
1540
+ query,
1541
+ caseSensitive,
1542
+ isRegex,
1543
+ );
1544
+ }
1513
1545
  return await NetworkManager.searchInRequest(
1514
1546
  this,
1515
1547
  query,
@@ -1940,11 +1972,15 @@ export class NetworkRequest extends Common.ObjectWrapper.ObjectWrapper<EventType
1940
1972
  }
1941
1973
  this.endTime = timestamp;
1942
1974
  if (data) {
1943
- void this.#streamingContentData?.then(contentData => {
1944
- if (!TextUtils.StreamingContentData.isError(contentData)) {
1945
- contentData.addChunk(data);
1946
- }
1947
- });
1975
+ if (this.#resolvedStreamingContentData) {
1976
+ this.#resolvedStreamingContentData.addChunk(data);
1977
+ } else {
1978
+ void this.#streamingContentData?.then(contentData => {
1979
+ if (!TextUtils.StreamingContentData.isError(contentData)) {
1980
+ contentData.addChunk(data);
1981
+ }
1982
+ });
1983
+ }
1948
1984
  }
1949
1985
  }
1950
1986
 
@@ -12,7 +12,7 @@ import {scopeTreeForScript} from './ScopeTreeCache.js';
12
12
  import type {Script} from './Script.js';
13
13
  import {buildOriginalScopes, decodePastaRanges, type NamedFunctionRange} from './SourceMapFunctionRanges.js';
14
14
  import {decodeRangeMappings} from './SourceMapRangeMappings.js';
15
- import {scriptRelativePosition, SourceMapScopesInfo, type TranslatedFrame} from './SourceMapScopesInfo.js';
15
+ import {type RawFrameTranslation, scriptRelativePosition, SourceMapScopesInfo} from './SourceMapScopesInfo.js';
16
16
 
17
17
  /**
18
18
  * Type of the base source map JSON object, which contains the sources and the mappings at the very least, plus
@@ -867,19 +867,10 @@ export class SourceMap {
867
867
  return this.#scopesInfo?.findOriginalFunctionScope(position) ?? null;
868
868
  }
869
869
 
870
- isOutlinedFrame(generatedLine: number, generatedColumn: number): boolean {
870
+ /** See {@link SourceMapScopesInfo.translateRawFrame}. `null` if no scopes information is available. */
871
+ translateRawFrame(generatedLine: number, generatedColumn: number): RawFrameTranslation|null {
871
872
  this.#ensureSourceMapProcessed();
872
- return this.#scopesInfo?.isOutlinedFrame(generatedLine, generatedColumn) ?? false;
873
- }
874
-
875
- hasInlinedFrames(generatedLine: number, generatedColumn: number): boolean {
876
- this.#ensureSourceMapProcessed();
877
- return this.#scopesInfo?.hasInlinedFrames(generatedLine, generatedColumn) ?? false;
878
- }
879
-
880
- translateCallSite(generatedLine: number, generatedColumn: number): TranslatedFrame[] {
881
- this.#ensureSourceMapProcessed();
882
- return this.#scopesInfo?.translateCallSite(generatedLine, generatedColumn) ?? [];
873
+ return this.#scopesInfo?.translateRawFrame(generatedLine, generatedColumn) ?? null;
883
874
  }
884
875
  }
885
876
 
@@ -16,13 +16,16 @@ export class SourceMapScopesInfo {
16
16
  readonly #sourceMap: SourceMap;
17
17
  readonly #originalScopes: Array<ScopesCodec.OriginalScope[]|null>;
18
18
  readonly #generatedRanges: ScopesCodec.GeneratedRange[];
19
+ /** Whether the scope information was derived from the AST and mappings (see {@link createFromAst}). */
20
+ readonly #isFromAst: boolean;
19
21
 
20
22
  #cachedVariablesAndBindingsPresent: boolean|null = null;
21
23
 
22
- constructor(sourceMap: SourceMap, scopeInfo: ScopesCodec.ScopeInfo) {
24
+ constructor(sourceMap: SourceMap, scopeInfo: ScopesCodec.ScopeInfo, {isFromAst = false}: {isFromAst?: boolean} = {}) {
23
25
  this.#sourceMap = sourceMap;
24
26
  this.#originalScopes = scopeInfo.scopes;
25
27
  this.#generatedRanges = scopeInfo.ranges;
28
+ this.#isFromAst = isFromAst;
26
29
  }
27
30
 
28
31
  /**
@@ -114,7 +117,8 @@ export class SourceMapScopesInfo {
114
117
  }
115
118
  }
116
119
 
117
- return new SourceMapScopesInfo(sourceMap, {scopes: scopesBySourceUrl, ranges: rootRange ? [rootRange] : []});
120
+ return new SourceMapScopesInfo(sourceMap, {scopes: scopesBySourceUrl, ranges: rootRange ? [rootRange] : []},
121
+ {isFromAst: true});
118
122
 
119
123
  /**
120
124
  * Finds the correct place in the tree to insert the new scope.
@@ -219,39 +223,17 @@ export class SourceMapScopesInfo {
219
223
  }
220
224
  }
221
225
 
222
- /**
223
- * @returns true, iff the function surrounding the provided position is marked as "hidden".
224
- */
225
- isOutlinedFrame(generatedLine: number, generatedColumn: number): boolean {
226
- const rangeChain = this.#findGeneratedRangeChain(generatedLine, generatedColumn);
227
- return this.#isOutlinedFrame(rangeChain);
228
- }
229
-
230
- #isOutlinedFrame(rangeChain: ScopesCodec.GeneratedRange[]): boolean {
231
- for (let i = rangeChain.length - 1; i >= 0; --i) {
232
- if (rangeChain[i].isStackFrame) {
233
- return rangeChain[i].isHidden;
234
- }
226
+ #generatedFrameKind(rangeChain: ScopesCodec.GeneratedRange[]): GeneratedFrameKind {
227
+ const functionRange = rangeChain.findLast(range => range.isStackFrame);
228
+ if (!functionRange) {
229
+ // Top-level code.
230
+ return GeneratedFrameKind.VISIBLE;
235
231
  }
236
- return false;
237
- }
238
-
239
- /**
240
- * @returns true, iff the range surrounding the provided position contains multiple
241
- * inlined original functions.
242
- */
243
- hasInlinedFrames(generatedLine: number, generatedColumn: number): boolean {
244
- const rangeChain = this.#findGeneratedRangeChain(generatedLine, generatedColumn);
245
- for (let i = rangeChain.length - 1; i >= 0; --i) {
246
- if (rangeChain[i].isStackFrame) {
247
- // We stop looking for inlined original functions once we reach the current frame.
248
- return false;
249
- }
250
- if (rangeChain[i].callSite) {
251
- return true;
252
- }
232
+ if (!functionRange.originalScope) {
233
+ // For scope information derived from the AST, we merely failed to map the function.
234
+ return this.#isFromAst ? GeneratedFrameKind.VISIBLE : GeneratedFrameKind.HIDDEN;
253
235
  }
254
- return false;
236
+ return functionRange.isHidden ? GeneratedFrameKind.OUTLINED : GeneratedFrameKind.VISIBLE;
255
237
  }
256
238
 
257
239
  /**
@@ -419,54 +401,53 @@ export class SourceMapScopesInfo {
419
401
  /**
420
402
  * Returns the authored function scope of the function containing the provided generated position.
421
403
  */
422
- findOriginalFunctionScope({line, column}: ScopesCodec.Position):
404
+ findOriginalFunctionScope(position: ScopesCodec.Position):
423
405
  {scope: ScopesCodec.OriginalScope, url?: Platform.DevToolsPath.UrlString}|null {
406
+ const rangeChain = this.#findGeneratedRangeChain(position.line, position.column);
407
+ const functionScope =
408
+ this.#findFunctionScopeInOriginalScopeChain(this.#innerMostOriginalScope(rangeChain, position));
409
+ return functionScope ? {scope: functionScope, url: this.#sourceURLOfScope(functionScope)} : null;
410
+ }
411
+
412
+ /**
413
+ * Returns the inner-most original scope containing the generated `position`. `rangeChain` must be the generated
414
+ * range chain of `position`.
415
+ */
416
+ #innerMostOriginalScope(rangeChain: ScopesCodec.GeneratedRange[],
417
+ position: ScopesCodec.Position): ScopesCodec.OriginalScope|undefined {
424
418
  // There are 2 approaches:
425
419
  // 1) Find the inner-most generated range containing the provided generated position
426
- // and use it's OriginalScope (then walk it outwards until we hit a function).
420
+ // and use its OriginalScope.
427
421
  // 2) Use the mappings to turn the generated position into an original position.
428
422
  // Then find the inner-most original scope containing that original position.
429
- // Then walk it outwards until we hit a function.
430
423
  //
431
424
  // Both approaches should yield the same result (assuming the mappings are spec compliant
432
425
  // w.r.t. generated ranges). But in the case of "pasta" scopes and extension provided
433
426
  // scope info, we only have the OriginalScope parts and mappings without GeneratedRanges.
434
-
435
- let originalInnerMostScope: ScopesCodec.OriginalScope|undefined;
436
-
437
427
  if (this.#generatedRanges.length > 0) {
438
- const rangeChain = this.#findGeneratedRangeChain(line, column);
439
- originalInnerMostScope = rangeChain.at(-1)?.originalScope;
440
- } else {
441
- // No GeneratedRanges. Try to use mappings.
442
- const entry = this.#sourceMap.findEntry(line, column);
443
- if (entry?.sourceIndex === undefined) {
444
- return null;
445
- }
446
- originalInnerMostScope =
447
- this.#findOriginalScopeChain(
448
- {sourceIndex: entry.sourceIndex, line: entry.sourceLineNumber, column: entry.sourceColumnNumber})
449
- .at(-1);
428
+ return rangeChain.at(-1)?.originalScope;
450
429
  }
451
430
 
452
- if (!originalInnerMostScope) {
453
- return null;
454
- }
455
-
456
- const functionScope = this.#findFunctionScopeInOriginalScopeChain(originalInnerMostScope);
457
- if (!functionScope) {
458
- return null;
431
+ // No GeneratedRanges. Try to use mappings.
432
+ const entry = this.#sourceMap.findEntry(position.line, position.column);
433
+ if (entry?.sourceIndex === undefined) {
434
+ return undefined;
459
435
  }
436
+ return this
437
+ .#findOriginalScopeChain(
438
+ {sourceIndex: entry.sourceIndex, line: entry.sourceLineNumber, column: entry.sourceColumnNumber})
439
+ .at(-1);
440
+ }
460
441
 
442
+ /** @returns the URL of the original source that `scope` belongs to. */
443
+ #sourceURLOfScope(scope: ScopesCodec.OriginalScope): Platform.DevToolsPath.UrlString|undefined {
461
444
  // Find the root scope for some given original source, to get the source url.
462
- let rootScope: ScopesCodec.OriginalScope = functionScope;
445
+ let rootScope: ScopesCodec.OriginalScope = scope;
463
446
  while (rootScope.parent) {
464
447
  rootScope = rootScope.parent;
465
448
  }
466
449
  const sourceIndex = this.#originalScopes.findIndex(scopes => scopes?.includes(rootScope));
467
- const url = sourceIndex !== -1 ? this.#sourceMap.sourceURLForSourceIndex(sourceIndex) : undefined;
468
-
469
- return functionScope ? {scope: functionScope, url} : null;
450
+ return sourceIndex !== -1 ? this.#sourceMap.sourceURLForSourceIndex(sourceIndex) : undefined;
470
451
  }
471
452
 
472
453
  /**
@@ -513,33 +494,54 @@ export class SourceMapScopesInfo {
513
494
  }
514
495
 
515
496
  /**
516
- * Returns one or more original stack frames for this single "raw frame" or call-site.
517
- *
518
- * @returns An empty array if no mapping at the call-site was found, or the resulting frames
519
- * in top-to-bottom order in case of inlining.
520
- * @throws If this range is marked "hidden". Outlining needs to be handled externally as
521
- * outlined function segments in stack traces can span across bundles.
497
+ * Translates a single "raw frame" or call-site, including outlined functions. It's the caller's responsibility to
498
+ * merge outlined frames with their caller(s) (see {@link GeneratedFrameKind}).
522
499
  */
523
- translateCallSite(generatedLine: number, generatedColumn: number): TranslatedFrame[] {
500
+ translateRawFrame(generatedLine: number, generatedColumn: number): RawFrameTranslation {
524
501
  const rangeChain = this.#findGeneratedRangeChain(generatedLine, generatedColumn);
525
- if (this.#isOutlinedFrame(rangeChain)) {
526
- throw new Error('SourceMapScopesInfo is unable to translate an outlined function by itself');
502
+ const kind = this.#generatedFrameKind(rangeChain);
503
+ if (kind === GeneratedFrameKind.HIDDEN) {
504
+ return {kind, frames: []};
527
505
  }
506
+ const frame = this.#translateTopFrame(generatedLine, generatedColumn, rangeChain);
507
+ return {kind, frames: frame ? [frame, ...this.#translateInlinedCallers(rangeChain)] : []};
508
+ }
509
+
510
+ /**
511
+ * The top-most frame is translated the same, regardless of whether we have inlined functions: The name is the
512
+ * original function surrounding the generated position, and the location is the mapped generated position.
513
+ *
514
+ * If the generated position has no mapping, the generated ranges may still tell which authored function (or
515
+ * which file, for top-level code) the position belongs to. The frame then has no position.
516
+ */
517
+ #translateTopFrame(generatedLine: number, generatedColumn: number,
518
+ rangeChain: ScopesCodec.GeneratedRange[]): TranslatedFrame|null {
519
+ const position = {line: generatedLine, column: generatedColumn};
520
+ const innerMostScope = this.#innerMostOriginalScope(rangeChain, position);
521
+ const functionScope = this.#findFunctionScopeInOriginalScopeChain(innerMostScope);
522
+ const name = functionScope ? (functionScope.name ?? '') : undefined;
528
523
 
529
524
  const mapping = this.#sourceMap.findEntry(generatedLine, generatedColumn);
530
- if (mapping?.sourceIndex === undefined) {
531
- return [];
525
+ if (mapping?.sourceIndex !== undefined) {
526
+ return {
527
+ line: mapping.sourceLineNumber,
528
+ column: mapping.sourceColumnNumber,
529
+ name,
530
+ url: mapping.sourceURL,
531
+ functionStart: functionScope?.start,
532
+ };
532
533
  }
534
+ if (!innerMostScope) {
535
+ return null;
536
+ }
537
+ return {name, url: this.#sourceURLOfScope(functionScope ?? innerMostScope), functionStart: functionScope?.start};
538
+ }
533
539
 
534
- // The top-most frame is translated the same even if we have inlined functions.
535
- const result: TranslatedFrame[] = [{
536
- line: mapping.sourceLineNumber,
537
- column: mapping.sourceColumnNumber,
538
- name: this.findOriginalFunctionName({line: generatedLine, column: generatedColumn}) ?? undefined,
539
- url: mapping.sourceURL,
540
- }];
541
-
542
- // Walk the range chain inside out until we find a generated function and for each inlined function add a frame.
540
+ /**
541
+ * Walk the range chain inside out until we find a generated function and for each inlined function add a frame.
542
+ */
543
+ #translateInlinedCallers(rangeChain: ScopesCodec.GeneratedRange[]): TranslatedFrame[] {
544
+ const result: TranslatedFrame[] = [];
543
545
  for (let i = rangeChain.length - 1; i >= 0 && !rangeChain[i].isStackFrame; --i) {
544
546
  const range = rangeChain[i];
545
547
  if (!range.callSite) {
@@ -547,11 +549,13 @@ export class SourceMapScopesInfo {
547
549
  }
548
550
 
549
551
  const originalScopeChain = this.#findOriginalScopeChain(range.callSite);
552
+ const functionScope = this.#findFunctionScopeInOriginalScopeChain(originalScopeChain.at(-1));
550
553
  result.push({
551
554
  line: range.callSite.line,
552
555
  column: range.callSite.column,
553
- name: this.#findFunctionNameInOriginalScopeChain(originalScopeChain.at(-1)) ?? undefined,
556
+ name: functionScope ? (functionScope.name ?? '') : undefined,
554
557
  url: this.#sourceMap.sourceURLForSourceIndex(range.callSite.sourceIndex),
558
+ functionStart: functionScope?.start,
555
559
  });
556
560
  }
557
561
 
@@ -559,17 +563,61 @@ export class SourceMapScopesInfo {
559
563
  }
560
564
  }
561
565
 
566
+ /**
567
+ * Describes how the generated function surrounding a generated position shows up in stack traces.
568
+ *
569
+ * Compilers tend to introduce functions (and calls to them) that don't exist in the authored code. The scopes
570
+ * proposal distinguishes two cases:
571
+ *
572
+ * 1) The generated function contains authored code, e.g. a block scope that was turned into a function. The
573
+ * generated range is marked "hidden", but links to the original scope via its definition. We can pause
574
+ * in such a function, but the frame is merged with its caller(s) in stack traces: The outlined code
575
+ * logically belongs to the (authored) function that transitively calls it.
576
+ *
577
+ * 2) The generated function doesn't represent any authored code, e.g. a compiler helper. The generated range
578
+ * has no definition. The scopes spec is being updated to say that such a range can be ignored in stack
579
+ * traces, so we drop these frames.
580
+ */
581
+ export const enum GeneratedFrameKind {
582
+ /** A regular (possibly with inlined functions) generated function, or top-level code. */
583
+ VISIBLE = 'VISIBLE',
584
+ /** A generated function marked as "hidden" that has a definition (case 1). */
585
+ OUTLINED = 'OUTLINED',
586
+ /** A generated function without a definition (case 2). */
587
+ HIDDEN = 'HIDDEN',
588
+ }
589
+
590
+ /** See {@link SourceMapScopesInfo.translateRawFrame}. */
591
+ export interface RawFrameTranslation {
592
+ kind: GeneratedFrameKind;
593
+ /**
594
+ * [top, ...inlinedCallers] in top-to-bottom order. Empty for {@link GeneratedFrameKind.HIDDEN} frames, or if
595
+ * neither the mappings nor the generated ranges know anything about the generated position.
596
+ *
597
+ * For {@link GeneratedFrameKind.OUTLINED} frames, the top frame is named after the authored function the outlined
598
+ * code belongs to.
599
+ */
600
+ frames: TranslatedFrame[];
601
+ }
602
+
562
603
  /**
563
604
  * Represents a stack frame in original terms. It closely aligns with StackTrace.StackTrace.Frame,
564
605
  * but since we can't import that type here we mirror it here somewhat.
565
606
  *
566
- * Equivalent to Pick<StackTrace.StackTrace.Frame, 'line'|'column'|'name'|'url'>.
607
+ * Equivalent to Pick<StackTrace.StackTrace.Frame, 'line'|'column'|'name'|'url'>, except that the position is
608
+ * optional.
567
609
  */
568
610
  export interface TranslatedFrame {
569
- line: number;
570
- column: number;
611
+ /**
612
+ * `line` and `column` are undefined iff the generated position has no mapping. The frame then only identifies
613
+ * the authored function: `name`, `functionStart` and the `url` of the function's source.
614
+ */
615
+ line?: number;
616
+ column?: number;
571
617
  name?: string;
572
618
  url?: Platform.DevToolsPath.UrlString;
619
+ /** Start of the original function scope containing this frame's position. Undefined for top-level code. */
620
+ functionStart?: ScopesCodec.Position;
573
621
  }
574
622
 
575
623
  export function findExpression(range: ScopesCodec.GeneratedRange|undefined, index: number, line = 0,
@@ -19,6 +19,23 @@ const objectUrlRegistry = new FinalizationRegistry<string>(url => {
19
19
  */
20
20
  const MAX_BLOB_SIZE_BYTES = 10 * 1024 * 1024;
21
21
 
22
+ /**
23
+ * Returns `charset` if `TextDecoder` supports it, and 'utf-8' otherwise.
24
+ * The charset usually comes straight from a `Content-Type` header, and
25
+ * `TextDecoder` has no non-throwing way to check a label.
26
+ */
27
+ function supportedCharsetOrUtf8(charset: string|undefined): string {
28
+ if (!charset) {
29
+ return 'utf-8';
30
+ }
31
+ try {
32
+ new TextDecoder(charset);
33
+ return charset;
34
+ } catch {
35
+ return 'utf-8';
36
+ }
37
+ }
38
+
22
39
  /**
23
40
  * This class is a small wrapper around either raw binary or text data.
24
41
  * As the binary data can actually contain textual data, we also store the
@@ -44,7 +61,7 @@ export class ContentData {
44
61
  #imagePreviewUrl?: string;
45
62
 
46
63
  constructor(data: string, isBase64: boolean, mimeType: string, charset?: string) {
47
- this.charset = charset || 'utf-8';
64
+ this.charset = supportedCharsetOrUtf8(charset);
48
65
  if (isBase64) {
49
66
  this.#contentAsBase64 = data;
50
67
  } else {