chrome-devtools-frontend 1.0.967728 → 1.0.969882

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 (70) hide show
  1. package/config/gni/devtools_grd_files.gni +84 -0
  2. package/front_end/core/host/UserMetrics.ts +2 -1
  3. package/front_end/core/i18n/locales/en-US.json +24 -0
  4. package/front_end/core/i18n/locales/en-XL.json +24 -0
  5. package/front_end/core/root/Runtime.ts +1 -0
  6. package/front_end/core/sdk/CSSMatchedStyles.ts +1 -1
  7. package/front_end/core/sdk/ChildTargetManager.ts +2 -2
  8. package/front_end/core/sdk/Connections.ts +6 -1
  9. package/front_end/core/sdk/NetworkManager.ts +4 -3
  10. package/front_end/devtools_compatibility.js +0 -36
  11. package/front_end/entrypoints/lighthouse_worker/LighthouseService.ts +84 -13
  12. package/front_end/entrypoints/main/MainImpl.ts +4 -0
  13. package/front_end/models/issues_manager/FederatedAuthRequestIssue.ts +265 -0
  14. package/front_end/models/issues_manager/IssuesManager.ts +5 -0
  15. package/front_end/models/issues_manager/descriptions/federatedAuthRequestAccountsHttpNotFound.md +1 -0
  16. package/front_end/models/issues_manager/descriptions/federatedAuthRequestAccountsInvalidResponse.md +1 -0
  17. package/front_end/models/issues_manager/descriptions/federatedAuthRequestAccountsNoResponse.md +1 -0
  18. package/front_end/models/issues_manager/descriptions/federatedAuthRequestApprovalDeclined.md +1 -0
  19. package/front_end/models/issues_manager/descriptions/federatedAuthRequestCanceled.md +1 -0
  20. package/front_end/models/issues_manager/descriptions/federatedAuthRequestClientIdMetadataHttpNotFound.md +1 -0
  21. package/front_end/models/issues_manager/descriptions/federatedAuthRequestClientIdMetadataInvalidResponse.md +1 -0
  22. package/front_end/models/issues_manager/descriptions/federatedAuthRequestClientIdMetadataNoResponse.md +1 -0
  23. package/front_end/models/issues_manager/descriptions/federatedAuthRequestErrorFetchingSignin.md +1 -0
  24. package/front_end/models/issues_manager/descriptions/federatedAuthRequestErrorIdToken.md +1 -0
  25. package/front_end/models/issues_manager/descriptions/federatedAuthRequestIdTokenHttpNotFound.md +1 -0
  26. package/front_end/models/issues_manager/descriptions/federatedAuthRequestIdTokenInvalidRequest.md +1 -0
  27. package/front_end/models/issues_manager/descriptions/federatedAuthRequestIdTokenInvalidResponse.md +1 -0
  28. package/front_end/models/issues_manager/descriptions/federatedAuthRequestIdTokenNoResponse.md +1 -0
  29. package/front_end/models/issues_manager/descriptions/federatedAuthRequestInvalidSigninResponse.md +1 -0
  30. package/front_end/models/issues_manager/descriptions/federatedAuthRequestTooManyRequests.md +1 -0
  31. package/front_end/models/issues_manager/descriptions/federatedAuthRequestWellKnownHttpNotFound.md +1 -0
  32. package/front_end/models/issues_manager/descriptions/federatedAuthRequestWellKnownInvalidResponse.md +1 -0
  33. package/front_end/models/issues_manager/descriptions/federatedAuthRequestWellKnownNoResponse.md +1 -0
  34. package/front_end/models/persistence/NetworkPersistenceManager.ts +205 -41
  35. package/front_end/models/timeline_model/TimelineFrameModel.ts +21 -7
  36. package/front_end/panels/application/InterestGroupStorageView.ts +21 -7
  37. package/front_end/panels/application/components/ReportsGrid.ts +19 -4
  38. package/front_end/panels/console/ConsoleViewMessage.ts +3 -5
  39. package/front_end/panels/console/ErrorStackParser.ts +5 -3
  40. package/front_end/panels/lighthouse/LighthouseController.ts +25 -1
  41. package/front_end/panels/lighthouse/LighthouseProtocolService.ts +37 -5
  42. package/front_end/panels/lighthouse/LighthouseReportRenderer.ts +6 -3
  43. package/front_end/panels/lighthouse/LighthouseStartView.ts +1 -0
  44. package/front_end/panels/lighthouse/LighthouseStatusView.ts +5 -5
  45. package/front_end/panels/sources/CSSPlugin.ts +2 -0
  46. package/front_end/panels/sources/DebuggerPlugin.ts +1 -1
  47. package/front_end/panels/sources/ScopeChainSidebarPane.ts +31 -0
  48. package/front_end/panels/sources/SourceMapNamesResolver.ts +15 -9
  49. package/front_end/panels/timeline/TimelineFlameChartDataProvider.ts +75 -3
  50. package/front_end/services/puppeteer/PuppeteerConnection.ts +107 -0
  51. package/front_end/services/puppeteer/puppeteer.ts +9 -0
  52. package/front_end/third_party/codemirror.next/README.chromium +10 -0
  53. package/front_end/third_party/codemirror.next/chunk/codemirror.js +1 -1
  54. package/front_end/third_party/codemirror.next/chunk/cpp.js +2 -1
  55. package/front_end/third_party/codemirror.next/chunk/markdown.js +2 -2
  56. package/front_end/third_party/codemirror.next/chunk/python.js +2 -1
  57. package/front_end/third_party/codemirror.next/codemirror.next.d.ts +574 -553
  58. package/front_end/third_party/codemirror.next/package.json +11 -11
  59. package/front_end/ui/components/markdown_view/MarkdownLinksMap.ts +2 -2
  60. package/front_end/ui/legacy/components/object_ui/ObjectPopoverHelper.ts +13 -34
  61. package/front_end/ui/legacy/components/object_ui/ObjectPropertiesSection.ts +1 -1
  62. package/front_end/ui/legacy/components/object_ui/objectPopover.css +0 -25
  63. package/front_end/ui/legacy/components/perf_ui/TimelineOverviewPane.ts +2 -2
  64. package/front_end/ui/legacy/components/source_frame/SourceFrame.ts +4 -2
  65. package/package.json +2 -2
  66. package/scripts/build/devtools_plugin.js +32 -1
  67. package/scripts/build/esbuild.js +1 -24
  68. package/scripts/build/tests/plugins_test.js +60 -1
  69. package/scripts/devtools_paths.js +1 -29
  70. package/scripts/hosted_mode/server.js +5 -0
@@ -0,0 +1,265 @@
1
+ // Copyright 2022 The Chromium Authors. All rights reserved.
2
+ // Use of this source code is governed by a BSD-style license that can be
3
+ // found in the LICENSE file.
4
+
5
+ import * as i18n from '../../core/i18n/i18n.js';
6
+ import type * as SDK from '../../core/sdk/sdk.js';
7
+ import * as Protocol from '../../generated/protocol.js';
8
+
9
+ import {Issue, IssueCategory, IssueKind} from './Issue.js';
10
+ import type {MarkdownIssueDescription, LazyMarkdownIssueDescription} from './MarkdownIssueDescription.js';
11
+ import {resolveLazyDescription} from './MarkdownIssueDescription.js';
12
+
13
+ const UIStrings = {
14
+ /**
15
+ *@description Title for Client Hint specification url link
16
+ */
17
+ fedCm: 'Federated Credential Management API',
18
+ };
19
+ const str_ = i18n.i18n.registerUIStrings('models/issues_manager/FederatedAuthRequestIssue.ts', UIStrings);
20
+ const i18nLazyString = i18n.i18n.getLazilyComputedLocalizedString.bind(undefined, str_);
21
+
22
+ export class FederatedAuthRequestIssue extends Issue {
23
+ readonly #issueDetails: Protocol.Audits.FederatedAuthRequestIssueDetails;
24
+
25
+ constructor(
26
+ issueDetails: Protocol.Audits.FederatedAuthRequestIssueDetails, issuesModel: SDK.IssuesModel.IssuesModel) {
27
+ super(
28
+ {
29
+ code: Protocol.Audits.InspectorIssueCode.FederatedAuthRequestIssue,
30
+ umaCode: [
31
+ Protocol.Audits.InspectorIssueCode.FederatedAuthRequestIssue,
32
+ issueDetails.federatedAuthRequestIssueReason,
33
+ ].join('::'),
34
+ },
35
+ issuesModel);
36
+ this.#issueDetails = issueDetails;
37
+ }
38
+
39
+ getCategory(): IssueCategory {
40
+ return IssueCategory.Other;
41
+ }
42
+
43
+ details(): Protocol.Audits.FederatedAuthRequestIssueDetails {
44
+ return this.#issueDetails;
45
+ }
46
+
47
+ getDescription(): MarkdownIssueDescription|null {
48
+ const description = issueDescriptions.get(this.#issueDetails.federatedAuthRequestIssueReason);
49
+ if (!description) {
50
+ return null;
51
+ }
52
+ return resolveLazyDescription(description);
53
+ }
54
+
55
+ primaryKey(): string {
56
+ return JSON.stringify(this.#issueDetails);
57
+ }
58
+
59
+ getKind(): IssueKind {
60
+ return IssueKind.PageError;
61
+ }
62
+
63
+ static fromInspectorIssue(issuesModel: SDK.IssuesModel.IssuesModel, inspectorIssue: Protocol.Audits.InspectorIssue):
64
+ FederatedAuthRequestIssue[] {
65
+ const details = inspectorIssue.details.federatedAuthRequestIssueDetails;
66
+ if (!details) {
67
+ console.warn('Federated auth request issue without details received.');
68
+ return [];
69
+ }
70
+ return [new FederatedAuthRequestIssue(details, issuesModel)];
71
+ }
72
+ }
73
+
74
+ const issueDescriptions: Map<Protocol.Audits.FederatedAuthRequestIssueReason, LazyMarkdownIssueDescription> = new Map([
75
+ [
76
+ Protocol.Audits.FederatedAuthRequestIssueReason.ApprovalDeclined,
77
+ {
78
+ file: 'federatedAuthRequestApprovalDeclined.md',
79
+ links: [{
80
+ link: 'https://fedidcg.github.io/FedCM/',
81
+ linkTitle: i18nLazyString(UIStrings.fedCm),
82
+ }],
83
+ },
84
+ ],
85
+ [
86
+ Protocol.Audits.FederatedAuthRequestIssueReason.TooManyRequests,
87
+ {
88
+ file: 'federatedAuthRequestTooManyRequests.md',
89
+ links: [{
90
+ link: 'https://fedidcg.github.io/FedCM/',
91
+ linkTitle: i18nLazyString(UIStrings.fedCm),
92
+ }],
93
+ },
94
+ ],
95
+ [
96
+ Protocol.Audits.FederatedAuthRequestIssueReason.WellKnownHttpNotFound,
97
+ {
98
+ file: 'federatedAuthRequestWellKnownHttpNotFound.md',
99
+ links: [{
100
+ link: 'https://fedidcg.github.io/FedCM/',
101
+ linkTitle: i18nLazyString(UIStrings.fedCm),
102
+ }],
103
+ },
104
+ ],
105
+ [
106
+ Protocol.Audits.FederatedAuthRequestIssueReason.WellKnownNoResponse,
107
+ {
108
+ file: 'federatedAuthRequestWellKnownNoResponse.md',
109
+ links: [{
110
+ link: 'https://fedidcg.github.io/FedCM/',
111
+ linkTitle: i18nLazyString(UIStrings.fedCm),
112
+ }],
113
+ },
114
+ ],
115
+ [
116
+ Protocol.Audits.FederatedAuthRequestIssueReason.WellKnownInvalidResponse,
117
+ {
118
+ file: 'federatedAuthRequestWellKnownInvalidResponse.md',
119
+ links: [{
120
+ link: 'https://fedidcg.github.io/FedCM/',
121
+ linkTitle: i18nLazyString(UIStrings.fedCm),
122
+ }],
123
+ },
124
+ ],
125
+ [
126
+ Protocol.Audits.FederatedAuthRequestIssueReason.ClientIdMetadataHttpNotFound,
127
+ {
128
+ file: 'federatedAuthRequestClientIdMetadataHttpNotFound.md',
129
+ links: [{
130
+ link: 'https://fedidcg.github.io/FedCM/',
131
+ linkTitle: i18nLazyString(UIStrings.fedCm),
132
+ }],
133
+ },
134
+ ],
135
+ [
136
+ Protocol.Audits.FederatedAuthRequestIssueReason.ClientIdMetadataNoResponse,
137
+ {
138
+ file: 'federatedAuthRequestClientIdMetadataNoResponse.md',
139
+ links: [{
140
+ link: 'https://fedidcg.github.io/FedCM/',
141
+ linkTitle: i18nLazyString(UIStrings.fedCm),
142
+ }],
143
+ },
144
+ ],
145
+ [
146
+ Protocol.Audits.FederatedAuthRequestIssueReason.ClientIdMetadataInvalidResponse,
147
+ {
148
+ file: 'federatedAuthRequestClientIdMetadataInvalidResponse.md',
149
+ links: [{
150
+ link: 'https://fedidcg.github.io/FedCM/',
151
+ linkTitle: i18nLazyString(UIStrings.fedCm),
152
+ }],
153
+ },
154
+ ],
155
+ [
156
+ Protocol.Audits.FederatedAuthRequestIssueReason.ErrorFetchingSignin,
157
+ {
158
+ file: 'federatedAuthRequestErrorFetchingSignin.md',
159
+ links: [{
160
+ link: 'https://fedidcg.github.io/FedCM/',
161
+ linkTitle: i18nLazyString(UIStrings.fedCm),
162
+ }],
163
+ },
164
+ ],
165
+ [
166
+ Protocol.Audits.FederatedAuthRequestIssueReason.InvalidSigninResponse,
167
+ {
168
+ file: 'federatedAuthRequestInvalidSigninResponse.md',
169
+ links: [{
170
+ link: 'https://fedidcg.github.io/FedCM/',
171
+ linkTitle: i18nLazyString(UIStrings.fedCm),
172
+ }],
173
+ },
174
+ ],
175
+ [
176
+ Protocol.Audits.FederatedAuthRequestIssueReason.AccountsHttpNotFound,
177
+ {
178
+ file: 'federatedAuthRequestAccountsHttpNotFound.md',
179
+ links: [{
180
+ link: 'https://fedidcg.github.io/FedCM/',
181
+ linkTitle: i18nLazyString(UIStrings.fedCm),
182
+ }],
183
+ },
184
+ ],
185
+ [
186
+ Protocol.Audits.FederatedAuthRequestIssueReason.AccountsNoResponse,
187
+ {
188
+ file: 'federatedAuthRequestAccountsNoResponse.md',
189
+ links: [{
190
+ link: 'https://fedidcg.github.io/FedCM/',
191
+ linkTitle: i18nLazyString(UIStrings.fedCm),
192
+ }],
193
+ },
194
+ ],
195
+ [
196
+ Protocol.Audits.FederatedAuthRequestIssueReason.AccountsInvalidResponse,
197
+ {
198
+ file: 'federatedAuthRequestAccountsInvalidResponse.md',
199
+ links: [{
200
+ link: 'https://fedidcg.github.io/FedCM/',
201
+ linkTitle: i18nLazyString(UIStrings.fedCm),
202
+ }],
203
+ },
204
+ ],
205
+ [
206
+ Protocol.Audits.FederatedAuthRequestIssueReason.IdTokenHttpNotFound,
207
+ {
208
+ file: 'federatedAuthRequestIdTokenHttpNotFound.md',
209
+ links: [{
210
+ link: 'https://fedidcg.github.io/FedCM/',
211
+ linkTitle: i18nLazyString(UIStrings.fedCm),
212
+ }],
213
+ },
214
+ ],
215
+ [
216
+ Protocol.Audits.FederatedAuthRequestIssueReason.IdTokenNoResponse,
217
+ {
218
+ file: 'federatedAuthRequestIdTokenNoResponse.md',
219
+ links: [{
220
+ link: 'https://fedidcg.github.io/FedCM/',
221
+ linkTitle: i18nLazyString(UIStrings.fedCm),
222
+ }],
223
+ },
224
+ ],
225
+ [
226
+ Protocol.Audits.FederatedAuthRequestIssueReason.IdTokenInvalidResponse,
227
+ {
228
+ file: 'federatedAuthRequestIdTokenInvalidResponse.md',
229
+ links: [{
230
+ link: 'https://fedidcg.github.io/FedCM/',
231
+ linkTitle: i18nLazyString(UIStrings.fedCm),
232
+ }],
233
+ },
234
+ ],
235
+ [
236
+ Protocol.Audits.FederatedAuthRequestIssueReason.IdTokenInvalidRequest,
237
+ {
238
+ file: 'federatedAuthRequestIdTokenInvalidRequest.md',
239
+ links: [{
240
+ link: 'https://fedidcg.github.io/FedCM/',
241
+ linkTitle: i18nLazyString(UIStrings.fedCm),
242
+ }],
243
+ },
244
+ ],
245
+ [
246
+ Protocol.Audits.FederatedAuthRequestIssueReason.ErrorIdToken,
247
+ {
248
+ file: 'federatedAuthRequestErrorIdToken.md',
249
+ links: [{
250
+ link: 'https://fedidcg.github.io/FedCM/',
251
+ linkTitle: i18nLazyString(UIStrings.fedCm),
252
+ }],
253
+ },
254
+ ],
255
+ [
256
+ Protocol.Audits.FederatedAuthRequestIssueReason.Canceled,
257
+ {
258
+ file: 'federatedAuthRequestCanceled.md',
259
+ links: [{
260
+ link: 'https://fedidcg.github.io/FedCM/',
261
+ linkTitle: i18nLazyString(UIStrings.fedCm),
262
+ }],
263
+ },
264
+ ],
265
+ ]);
@@ -13,6 +13,7 @@ import {ContentSecurityPolicyIssue} from './ContentSecurityPolicyIssue.js';
13
13
  import {CorsIssue} from './CorsIssue.js';
14
14
  import {CrossOriginEmbedderPolicyIssue, isCrossOriginEmbedderPolicyIssue} from './CrossOriginEmbedderPolicyIssue.js';
15
15
  import {DeprecationIssue} from './DeprecationIssue.js';
16
+ import {FederatedAuthRequestIssue} from './FederatedAuthRequestIssue.js';
16
17
  import {GenericIssue} from './GenericIssue.js';
17
18
  import {HeavyAdIssue} from './HeavyAdIssue.js';
18
19
  import type {Issue, IssueKind} from './Issue.js';
@@ -104,6 +105,10 @@ const issueCodeHandlers = new Map<
104
105
  Protocol.Audits.InspectorIssueCode.ClientHintIssue,
105
106
  ClientHintIssue.fromInspectorIssue,
106
107
  ],
108
+ [
109
+ Protocol.Audits.InspectorIssueCode.FederatedAuthRequestIssue,
110
+ FederatedAuthRequestIssue.fromInspectorIssue,
111
+ ],
107
112
  ]);
108
113
 
109
114
  /**
@@ -0,0 +1 @@
1
+ # The provider's accounts list endpoint cannot be found.
@@ -0,0 +1 @@
1
+ # Provider's accounts list is invalid.
@@ -0,0 +1 @@
1
+ # The response body is empty when fetching the provider's accounts list.
@@ -0,0 +1 @@
1
+ # User declined the sign-in attempt.
@@ -0,0 +1 @@
1
+ # The request has been aborted.
@@ -0,0 +1 @@
1
+ # The provider's client metadata endpoint cannot be found.
@@ -0,0 +1 @@
1
+ # The response body is empty when fetching the provider's client metadata.
@@ -0,0 +1 @@
1
+ # Error attempting to reach the provider's sign-in endpoint.
@@ -0,0 +1 @@
1
+ # Error retrieving an id token.
@@ -0,0 +1 @@
1
+ # The provider's id token endpoint cannot be found.
@@ -0,0 +1 @@
1
+ # The id token fetching request is invalid.
@@ -0,0 +1 @@
1
+ # The response body is empty when fetching the provider's id token.
@@ -0,0 +1 @@
1
+ # Provider's sign-in response is invalid.
@@ -0,0 +1 @@
1
+ # Only one navigator.credentials.get request may be outstanding at one time.
@@ -0,0 +1 @@
1
+ # The provider's .well-known configuration cannot be found.
@@ -0,0 +1 @@
1
+ # Provider's .well-known configuration is invalid.
@@ -0,0 +1 @@
1
+ # The response body is empty when fetching the provider's .well-known configuration.
@@ -4,6 +4,7 @@
4
4
 
5
5
  import * as Common from '../../core/common/common.js';
6
6
  import * as Platform from '../../core/platform/platform.js';
7
+ import * as Root from '../../core/root/root.js';
7
8
  import * as SDK from '../../core/sdk/sdk.js';
8
9
  import * as Protocol from '../../generated/protocol.js';
9
10
  import * as Workspace from '../workspace/workspace.js';
@@ -31,6 +32,7 @@ export class NetworkPersistenceManager extends Common.ObjectWrapper.ObjectWrappe
31
32
  private activeInternal: boolean;
32
33
  private enabled: boolean;
33
34
  private eventDescriptors: Common.EventTarget.EventDescriptor[];
35
+ #headerOverridesMap: Map<string, HeaderOverrideWithRegex[]> = new Map();
34
36
 
35
37
  private constructor(workspace: Workspace.Workspace.WorkspaceImpl) {
36
38
  super();
@@ -363,29 +365,97 @@ export class NetworkPersistenceManager extends Common.ObjectWrapper.ObjectWrappe
363
365
  }
364
366
  }
365
367
 
366
- private updateInterceptionPatterns(): void {
367
- void this.updateInterceptionThrottler.schedule(innerUpdateInterceptionPatterns.bind(this));
368
-
369
- function innerUpdateInterceptionPatterns(this: NetworkPersistenceManager): Promise<void> {
370
- if (!this.activeInternal || !this.projectInternal) {
371
- return SDK.NetworkManager.MultitargetNetworkManager.instance().setInterceptionHandlerForPatterns(
372
- [], this.interceptionHandlerBound);
368
+ async generateHeaderPatterns(uiSourceCode: Workspace.UISourceCode.UISourceCode):
369
+ Promise<{headerPatterns: Set<string>, path: string, overridesWithRegex: HeaderOverrideWithRegex[]}> {
370
+ const headerPatterns = new Set<string>();
371
+ const content = (await uiSourceCode.requestContent()).content || '';
372
+ let headerOverrides: HeaderOverride[] = [];
373
+ try {
374
+ headerOverrides = JSON.parse(content) as HeaderOverride[];
375
+ if (!headerOverrides.every(isHeaderOverride)) {
376
+ throw 'Type mismatch after parsing';
373
377
  }
374
- const patterns = new Set<string>();
375
- const indexFileName = 'index.html';
376
- for (const uiSourceCode of this.projectInternal.uiSourceCodes()) {
377
- const pattern = this.patternForFileSystemUISourceCode(uiSourceCode);
378
- patterns.add(pattern);
379
- if (pattern.endsWith('/' + indexFileName)) {
380
- patterns.add(pattern.substr(0, pattern.length - indexFileName.length));
381
- }
378
+ } catch (e) {
379
+ console.error('Failed to parse', uiSourceCode.url(), 'for locally overriding headers.');
380
+ return {headerPatterns, path: '', overridesWithRegex: []};
381
+ }
382
+ const relativePath = FileSystemWorkspaceBinding.relativePath(uiSourceCode).join('/');
383
+ const decodedPath = this.decodeLocalPathToUrlPath(relativePath).slice(0, -HEADERS_FILENAME.length);
384
+
385
+ const overridesWithRegex: HeaderOverrideWithRegex[] = [];
386
+ for (const headerOverride of headerOverrides) {
387
+ headerPatterns.add('http?://' + decodedPath + headerOverride.applyTo);
388
+
389
+ // Most servers have the concept of a "directory index", which is a
390
+ // default resource name for a request targeting a "directory", e. g.
391
+ // requesting "example.com/path/" would result in the same response as
392
+ // requesting "example.com/path/index.html". To match this behavior we
393
+ // generate an additional pattern without "index.html" as the longer
394
+ // pattern would not match against a shorter request.
395
+ const {head, tail} = extractDirectoryIndex(headerOverride.applyTo);
396
+ if (tail) {
397
+ headerPatterns.add('http?://' + decodedPath + head);
398
+
399
+ const pattern = escapeRegex(decodedPath + head) + '(' + escapeRegex(tail) + ')?';
400
+ const regex = new RegExp('^https?:\/\/' + pattern + '$');
401
+ overridesWithRegex.push({
402
+ applyToRegex: regex,
403
+ headers: headerOverride.headers,
404
+ });
405
+ } else {
406
+ const regex = new RegExp('^https?:\/\/' + escapeRegex(decodedPath + headerOverride.applyTo) + '$');
407
+ overridesWithRegex.push({
408
+ applyToRegex: regex,
409
+ headers: headerOverride.headers,
410
+ });
382
411
  }
412
+ }
413
+ return {headerPatterns, path: decodedPath, overridesWithRegex};
414
+ }
415
+
416
+ async updateInterceptionPatternsForTests(): Promise<void> {
417
+ await this.#innerUpdateInterceptionPatterns();
418
+ }
419
+
420
+ private updateInterceptionPatterns(): void {
421
+ void this.updateInterceptionThrottler.schedule(this.#innerUpdateInterceptionPatterns.bind(this));
422
+ }
383
423
 
424
+ async #innerUpdateInterceptionPatterns(): Promise<void> {
425
+ this.#headerOverridesMap.clear();
426
+ if (!this.activeInternal || !this.projectInternal) {
384
427
  return SDK.NetworkManager.MultitargetNetworkManager.instance().setInterceptionHandlerForPatterns(
385
- Array.from(patterns).map(
386
- pattern => ({urlPattern: pattern, requestStage: Protocol.Fetch.RequestStage.Response})),
387
- this.interceptionHandlerBound);
428
+ [], this.interceptionHandlerBound);
429
+ }
430
+ let patterns = new Set<string>();
431
+ for (const uiSourceCode of this.projectInternal.uiSourceCodes()) {
432
+ const pattern = this.patternForFileSystemUISourceCode(uiSourceCode);
433
+ if (Root.Runtime.experiments.isEnabled(Root.Runtime.ExperimentName.HEADER_OVERRIDES) &&
434
+ uiSourceCode.name() === HEADERS_FILENAME) {
435
+ const {headerPatterns, path, overridesWithRegex} = await this.generateHeaderPatterns(uiSourceCode);
436
+ if (headerPatterns.size > 0) {
437
+ patterns = new Set([...patterns, ...headerPatterns]);
438
+ this.#headerOverridesMap.set(path, overridesWithRegex);
439
+ }
440
+ } else {
441
+ patterns.add(pattern);
442
+ }
443
+ // Most servers have the concept of a "directory index", which is a
444
+ // default resource name for a request targeting a "directory", e. g.
445
+ // requesting "example.com/path/" would result in the same response as
446
+ // requesting "example.com/path/index.html". To match this behavior we
447
+ // generate an additional pattern without "index.html" as the longer
448
+ // pattern would not match against a shorter request.
449
+ const {head, tail} = extractDirectoryIndex(pattern);
450
+ if (tail) {
451
+ patterns.add(head);
452
+ }
388
453
  }
454
+
455
+ return SDK.NetworkManager.MultitargetNetworkManager.instance().setInterceptionHandlerForPatterns(
456
+ Array.from(patterns).map(
457
+ pattern => ({urlPattern: pattern, requestStage: Protocol.Fetch.RequestStage.Response})),
458
+ this.interceptionHandlerBound);
389
459
  }
390
460
 
391
461
  private async onUISourceCodeRemoved(uiSourceCode: Workspace.UISourceCode.UISourceCode): Promise<void> {
@@ -409,7 +479,7 @@ export class NetworkPersistenceManager extends Common.ObjectWrapper.ObjectWrappe
409
479
  await this.unbind(uiSourceCode);
410
480
  }
411
481
 
412
- private async setProject(project: Workspace.Workspace.Project|null): Promise<void> {
482
+ async setProject(project: Workspace.Workspace.Project|null): Promise<void> {
413
483
  if (project === this.projectInternal) {
414
484
  return;
415
485
  }
@@ -452,6 +522,49 @@ export class NetworkPersistenceManager extends Common.ObjectWrapper.ObjectWrappe
452
522
  }
453
523
  }
454
524
 
525
+ mergeHeaders(baseHeaders: Protocol.Fetch.HeaderEntry[], overrideHeaders: Protocol.Network.Headers):
526
+ Protocol.Fetch.HeaderEntry[] {
527
+ const result: Protocol.Fetch.HeaderEntry[] = [];
528
+ const headerMap = new Map<string, string>();
529
+ for (const header of baseHeaders) {
530
+ headerMap.set(header.name, header.value);
531
+ }
532
+ for (const [headerName, headerValue] of Object.entries(overrideHeaders)) {
533
+ headerMap.set(headerName, headerValue);
534
+ }
535
+ headerMap.forEach((headerValue, headerName) => {
536
+ result.push({name: headerName, value: headerValue});
537
+ });
538
+ return result;
539
+ }
540
+
541
+ #maybeMergeHeadersForPathSegment(path: string, requestUrl: string, headers: Protocol.Fetch.HeaderEntry[]):
542
+ Protocol.Fetch.HeaderEntry[] {
543
+ const headerOverrides = this.#headerOverridesMap.get(path) || [];
544
+ for (const headerOverride of headerOverrides) {
545
+ if (headerOverride.applyToRegex.test(requestUrl)) {
546
+ headers = this.mergeHeaders(headers, headerOverride.headers);
547
+ }
548
+ }
549
+ return headers;
550
+ }
551
+
552
+ handleHeaderInterception(interceptedRequest: SDK.NetworkManager.InterceptedRequest): Protocol.Fetch.HeaderEntry[] {
553
+ let result: Protocol.Fetch.HeaderEntry[] = interceptedRequest.responseHeaders || [];
554
+ const urlSegments = this.encodedPathFromUrl(interceptedRequest.request.url).split('/');
555
+ // Traverse the hierarchy of overrides from the most general to the most
556
+ // specific. Check with empty string first to match overrides applying to
557
+ // all domains.
558
+ // e.g. '', 'www.example.com/', 'www.example.com/path/', ...
559
+ let path = '';
560
+ result = this.#maybeMergeHeadersForPathSegment(path, interceptedRequest.request.url, result);
561
+ for (const segment of urlSegments) {
562
+ path += segment + '/';
563
+ result = this.#maybeMergeHeadersForPathSegment(path, interceptedRequest.request.url, result);
564
+ }
565
+ return result;
566
+ }
567
+
455
568
  private async interceptionHandler(interceptedRequest: SDK.NetworkManager.InterceptedRequest): Promise<void> {
456
569
  const method = interceptedRequest.request.method;
457
570
  if (!this.activeInternal || (method !== 'GET' && method !== 'POST')) {
@@ -460,9 +573,16 @@ export class NetworkPersistenceManager extends Common.ObjectWrapper.ObjectWrappe
460
573
  const proj = this.projectInternal as FileSystem;
461
574
  const path = proj.fileSystemPath() + '/' + this.encodedPathFromUrl(interceptedRequest.request.url);
462
575
  const fileSystemUISourceCode = proj.uiSourceCodeForURL(path);
463
- if (!fileSystemUISourceCode) {
576
+ let responseHeaders: Protocol.Fetch.HeaderEntry[] = [];
577
+ if (Root.Runtime.experiments.isEnabled(Root.Runtime.ExperimentName.HEADER_OVERRIDES)) {
578
+ responseHeaders = this.handleHeaderInterception(interceptedRequest);
579
+ }
580
+ if (!fileSystemUISourceCode && !responseHeaders.length) {
464
581
  return;
465
582
  }
583
+ if (!responseHeaders.length) {
584
+ responseHeaders = interceptedRequest.responseHeaders || [];
585
+ }
466
586
 
467
587
  let mimeType = '';
468
588
  if (interceptedRequest.responseHeaders) {
@@ -477,32 +597,41 @@ export class NetworkPersistenceManager extends Common.ObjectWrapper.ObjectWrappe
477
597
  if (!mimeType) {
478
598
  const expectedResourceType =
479
599
  Common.ResourceType.resourceTypes[interceptedRequest.resourceType] || Common.ResourceType.resourceTypes.Other;
480
- mimeType = fileSystemUISourceCode.mimeType();
600
+ mimeType = fileSystemUISourceCode?.mimeType() || '';
481
601
  if (Common.ResourceType.ResourceType.fromMimeType(mimeType) !== expectedResourceType) {
482
602
  mimeType = expectedResourceType.canonicalMimeType();
483
603
  }
484
604
  }
485
- const project = fileSystemUISourceCode.project() as FileSystem;
486
-
487
- this.originalResponseContentPromises.set(
488
- fileSystemUISourceCode, interceptedRequest.responseBody().then(response => {
489
- if (response.error || response.content === null) {
490
- return null;
491
- }
492
- if (response.encoded) {
493
- const text = atob(response.content);
494
- const data = new Uint8Array(text.length);
495
- for (let i = 0; i < text.length; ++i) {
496
- data[i] = text.charCodeAt(i);
497
- }
498
- return new TextDecoder('utf-8').decode(data);
499
- }
500
- return response.content;
501
- }));
502
605
 
503
- const blob = await project.requestFileBlob(fileSystemUISourceCode);
504
- if (blob) {
505
- void interceptedRequest.continueRequestWithContent(new Blob([blob], {type: mimeType}));
606
+ if (fileSystemUISourceCode) {
607
+ this.originalResponseContentPromises.set(
608
+ fileSystemUISourceCode, interceptedRequest.responseBody().then(response => {
609
+ if (response.error || response.content === null) {
610
+ return null;
611
+ }
612
+ if (response.encoded) {
613
+ const text = atob(response.content);
614
+ const data = new Uint8Array(text.length);
615
+ for (let i = 0; i < text.length; ++i) {
616
+ data[i] = text.charCodeAt(i);
617
+ }
618
+ return new TextDecoder('utf-8').decode(data);
619
+ }
620
+ return response.content;
621
+ }));
622
+
623
+ const project = fileSystemUISourceCode.project() as FileSystem;
624
+ const blob = await project.requestFileBlob(fileSystemUISourceCode);
625
+ if (blob) {
626
+ void interceptedRequest.continueRequestWithContent(
627
+ new Blob([blob], {type: mimeType}), /* encoded */ false, responseHeaders);
628
+ }
629
+ } else {
630
+ const responseBody = await interceptedRequest.responseBody();
631
+ if (!responseBody.error && responseBody.content) {
632
+ void interceptedRequest.continueRequestWithContent(
633
+ new Blob([responseBody.content], {type: mimeType}), /* encoded */ true, responseHeaders);
634
+ }
506
635
  }
507
636
  }
508
637
  }
@@ -512,6 +641,8 @@ const RESERVED_FILENAMES = new Set<string>([
512
641
  'com8', 'com9', 'lpt1', 'lpt2', 'lpt3', 'lpt4', 'lpt5', 'lpt6', 'lpt7', 'lpt8', 'lpt9',
513
642
  ]);
514
643
 
644
+ const HEADERS_FILENAME = '.headers';
645
+
515
646
  // TODO(crbug.com/1167717): Make this a const enum again
516
647
  // eslint-disable-next-line rulesdir/const_enum
517
648
  export enum Events {
@@ -521,3 +652,36 @@ export enum Events {
521
652
  export type EventTypes = {
522
653
  [Events.ProjectChanged]: Workspace.Workspace.Project|null,
523
654
  };
655
+
656
+ interface HeaderOverride {
657
+ applyTo: string;
658
+ headers: Protocol.Network.Headers;
659
+ }
660
+
661
+ interface HeaderOverrideWithRegex {
662
+ applyToRegex: RegExp;
663
+ headers: Protocol.Network.Headers;
664
+ }
665
+
666
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
667
+ function isHeaderOverride(arg: any): arg is HeaderOverride {
668
+ if (!(arg && arg.applyTo && typeof (arg.applyTo === 'string') && arg.headers && Object.keys(arg.headers).length)) {
669
+ return false;
670
+ }
671
+ return Object.values(arg.headers).every(value => typeof value === 'string');
672
+ }
673
+
674
+ export function escapeRegex(pattern: string): string {
675
+ return Platform.StringUtilities.escapeCharacters(pattern, '[]{}()\\.^$+|-,?').replaceAll('*', '.*');
676
+ }
677
+
678
+ export function extractDirectoryIndex(pattern: string): {head: string, tail?: string} {
679
+ const lastSlash = pattern.lastIndexOf('/');
680
+ const tail = lastSlash >= 0 ? pattern.slice(lastSlash + 1) : pattern;
681
+ const head = lastSlash >= 0 ? pattern.slice(0, lastSlash + 1) : '';
682
+ const regex = new RegExp('^' + escapeRegex(tail) + '$');
683
+ if (regex.test('index.html') || regex.test('index.htm') || regex.test('index.php')) {
684
+ return {head, tail};
685
+ }
686
+ return {head: pattern};
687
+ }