chrome-devtools-frontend 1.0.969345 → 1.0.970539

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 (60) hide show
  1. package/config/gni/devtools_grd_files.gni +64 -0
  2. package/front_end/core/common/ParsedURL.ts +25 -2
  3. package/front_end/core/i18n/locales/en-US.json +18 -3
  4. package/front_end/core/i18n/locales/en-XL.json +18 -3
  5. package/front_end/core/sdk/ChildTargetManager.ts +2 -2
  6. package/front_end/core/sdk/Connections.ts +6 -1
  7. package/front_end/core/sdk/DebuggerModel.ts +4 -0
  8. package/front_end/core/sdk/NetworkManager.ts +4 -3
  9. package/front_end/entrypoints/lighthouse_worker/LighthouseService.ts +84 -13
  10. package/front_end/models/persistence/Automapping.ts +2 -32
  11. package/front_end/models/persistence/FileSystemWorkspaceBinding.ts +9 -7
  12. package/front_end/models/persistence/IsolatedFileSystem.ts +20 -14
  13. package/front_end/models/persistence/NetworkPersistenceManager.ts +213 -45
  14. package/front_end/models/persistence/PlatformFileSystem.ts +3 -2
  15. package/front_end/models/timeline_model/TimelineFrameModel.ts +21 -7
  16. package/front_end/models/workspace/UISourceCode.ts +11 -14
  17. package/front_end/models/workspace/WorkspaceImpl.ts +5 -1
  18. package/front_end/panels/animation/animationTimeline.css +0 -3
  19. package/front_end/panels/application/components/ReportsGrid.ts +19 -4
  20. package/front_end/panels/application/components/trustTokensViewDeleteButton.css +0 -1
  21. package/front_end/panels/console/consolePinPane.css +0 -17
  22. package/front_end/panels/css_overview/cssOverviewCompletedView.css +0 -1
  23. package/front_end/panels/elements/components/adornerSettingsPane.css +0 -1
  24. package/front_end/panels/elements/components/computedStyleTrace.css +1 -1
  25. package/front_end/panels/elements/components/elementsBreadcrumbs.css +0 -1
  26. package/front_end/panels/elements/computedStyleWidgetTree.css +2 -2
  27. package/front_end/panels/elements/elementsTreeOutline.css +0 -2
  28. package/front_end/panels/emulation/deviceModeView.css +0 -1
  29. package/front_end/panels/event_listeners/eventListenersView.css +0 -1
  30. package/front_end/panels/issues/components/hideIssuesMenu.css +0 -1
  31. package/front_end/panels/lighthouse/LighthouseController.ts +25 -1
  32. package/front_end/panels/lighthouse/LighthouseProtocolService.ts +37 -5
  33. package/front_end/panels/lighthouse/LighthouseStartView.ts +1 -0
  34. package/front_end/panels/lighthouse/LighthouseStatusView.ts +5 -5
  35. package/front_end/panels/media/playerListView.css +0 -1
  36. package/front_end/panels/network/networkLogView.css +0 -4
  37. package/front_end/panels/network/requestPayloadTree.css +0 -2
  38. package/front_end/panels/network/signedExchangeInfoTree.css +0 -1
  39. package/front_end/panels/settings/emulation/components/userAgentClientHintsForm.css +0 -4
  40. package/front_end/panels/settings/emulation/devicesSettingsTab.css +0 -1
  41. package/front_end/panels/snippets/ScriptSnippetFileSystem.ts +4 -4
  42. package/front_end/panels/snippets/SnippetsQuickOpen.ts +1 -1
  43. package/front_end/panels/sources/TabbedEditorContainer.ts +9 -0
  44. package/front_end/panels/sources/watchExpressionsSidebarPane.css +0 -1
  45. package/front_end/panels/timeline/TimelineFlameChartDataProvider.ts +75 -3
  46. package/front_end/panels/webauthn/webauthnPane.css +0 -12
  47. package/front_end/services/puppeteer/PuppeteerConnection.ts +107 -0
  48. package/front_end/services/puppeteer/puppeteer.ts +9 -0
  49. package/front_end/third_party/codemirror/package/addon/fold/foldgutter.css +1 -5
  50. package/front_end/ui/components/adorners/adorner.css +0 -4
  51. package/front_end/ui/components/buttons/button.css +0 -4
  52. package/front_end/ui/components/data_grid/dataGrid.css +0 -4
  53. package/front_end/ui/components/icon_button/iconButton.css +0 -1
  54. package/front_end/ui/components/markdown_view/MarkdownLinksMap.ts +2 -2
  55. package/front_end/ui/legacy/TabbedPane.ts +1 -1
  56. package/front_end/ui/legacy/tabbedPane.css +0 -4
  57. package/front_end/ui/legacy/textButton.css +0 -1
  58. package/front_end/ui/legacy/toolbar.css +0 -1
  59. package/package.json +2 -2
  60. package/scripts/hosted_mode/server.js +5 -0
@@ -77,8 +77,8 @@ export class IsolatedFileSystem extends PlatformFileSystem {
77
77
  private readonly excludedFoldersSetting: Common.Settings.Setting<{[path: string]: string[]}>;
78
78
  private excludedFoldersInternal: Set<string>;
79
79
  private readonly excludedEmbedderFolders: string[];
80
- private readonly initialFilePathsInternal: Set<string>;
81
- private readonly initialGitFoldersInternal: Set<string>;
80
+ private readonly initialFilePathsInternal: Set<Platform.DevToolsPath.EncodedPathString>;
81
+ private readonly initialGitFoldersInternal: Set<Platform.DevToolsPath.EncodedPathString>;
82
82
  private readonly fileLocks: Map<string, Promise<void>>;
83
83
 
84
84
  constructor(
@@ -129,7 +129,7 @@ export class IsolatedFileSystem extends PlatformFileSystem {
129
129
  const promise = new Promise<Metadata|null>(f => {
130
130
  fulfill = f;
131
131
  });
132
- this.domFileSystem.root.getFile(path, undefined, fileEntryLoaded, errorHandler);
132
+ this.domFileSystem.root.getFile(decodeURIComponent(path), undefined, fileEntryLoaded, errorHandler);
133
133
  return promise;
134
134
 
135
135
  function fileEntryLoaded(entry: FileEntry): void {
@@ -143,11 +143,11 @@ export class IsolatedFileSystem extends PlatformFileSystem {
143
143
  }
144
144
  }
145
145
 
146
- initialFilePaths(): string[] {
146
+ initialFilePaths(): Platform.DevToolsPath.EncodedPathString[] {
147
147
  return [...this.initialFilePathsInternal];
148
148
  }
149
149
 
150
- initialGitFolders(): string[] {
150
+ initialGitFolders(): Platform.DevToolsPath.EncodedPathString[] {
151
151
  return [...this.initialGitFoldersInternal];
152
152
  }
153
153
 
@@ -168,12 +168,14 @@ export class IsolatedFileSystem extends PlatformFileSystem {
168
168
  if (this.isFileExcluded(entry.fullPath)) {
169
169
  continue;
170
170
  }
171
- this.initialFilePathsInternal.add(entry.fullPath.substr(1));
171
+ this.initialFilePathsInternal.add(Common.ParsedURL.ParsedURL.rawPathToEncodedPathString(
172
+ entry.fullPath.substr(1) as Platform.DevToolsPath.RawPathString));
172
173
  } else {
173
174
  if (entry.fullPath.endsWith('/.git')) {
174
175
  const lastSlash = entry.fullPath.lastIndexOf('/');
175
176
  const parentFolder = entry.fullPath.substring(1, lastSlash);
176
- this.initialGitFoldersInternal.add(parentFolder);
177
+ this.initialGitFoldersInternal.add(Common.ParsedURL.ParsedURL.rawPathToEncodedPathString(
178
+ parentFolder as Platform.DevToolsPath.RawPathString));
177
179
  }
178
180
  if (this.isFileExcluded(entry.fullPath + '/')) {
179
181
  this.excludedEmbedderFolders.push(Common.ParsedURL.ParsedURL.urlToRawPathString(
@@ -221,7 +223,7 @@ export class IsolatedFileSystem extends PlatformFileSystem {
221
223
  }
222
224
 
223
225
  async createFile(path: string, name: string|null): Promise<string|null> {
224
- const dirEntry = await this.createFoldersIfNotExist(path);
226
+ const dirEntry = await this.createFoldersIfNotExist(decodeURIComponent(path));
225
227
  if (!dirEntry) {
226
228
  return null;
227
229
  }
@@ -230,7 +232,8 @@ export class IsolatedFileSystem extends PlatformFileSystem {
230
232
  if (!fileEntry) {
231
233
  return null;
232
234
  }
233
- return fileEntry.fullPath.substr(1);
235
+ return Common.ParsedURL.ParsedURL.rawPathToEncodedPathString(
236
+ fileEntry.fullPath.substr(1) as Platform.DevToolsPath.RawPathString);
234
237
 
235
238
  function createFileCandidate(
236
239
  this: IsolatedFileSystem, name: string, newFileIndex?: number): Promise<FileEntry|null> {
@@ -256,7 +259,8 @@ export class IsolatedFileSystem extends PlatformFileSystem {
256
259
  const promise = new Promise<boolean>(resolve => {
257
260
  resolveCallback = resolve;
258
261
  });
259
- this.domFileSystem.root.getFile(path, undefined, fileEntryLoaded.bind(this), errorHandler.bind(this));
262
+ this.domFileSystem.root.getFile(
263
+ decodeURIComponent(path), undefined, fileEntryLoaded.bind(this), errorHandler.bind(this));
260
264
  return promise;
261
265
 
262
266
  function fileEntryLoaded(this: IsolatedFileSystem, fileEntry: FileEntry): void {
@@ -279,7 +283,7 @@ export class IsolatedFileSystem extends PlatformFileSystem {
279
283
 
280
284
  requestFileBlob(path: string): Promise<Blob|null> {
281
285
  return new Promise(resolve => {
282
- this.domFileSystem.root.getFile(path, undefined, entry => {
286
+ this.domFileSystem.root.getFile(decodeURIComponent(path), undefined, entry => {
283
287
  entry.file(resolve, errorHandler.bind(this));
284
288
  }, errorHandler.bind(this));
285
289
 
@@ -347,7 +351,8 @@ export class IsolatedFileSystem extends PlatformFileSystem {
347
351
  // @ts-ignore TODO(crbug.com/1172300) Properly type this after jsdoc to ts migration
348
352
  callback = x;
349
353
  });
350
- this.domFileSystem.root.getFile(path, {create: true}, fileEntryLoaded.bind(this), errorHandler.bind(this));
354
+ this.domFileSystem.root.getFile(
355
+ decodeURIComponent(path), {create: true}, fileEntryLoaded.bind(this), errorHandler.bind(this));
351
356
  return promise;
352
357
  };
353
358
 
@@ -391,7 +396,8 @@ export class IsolatedFileSystem extends PlatformFileSystem {
391
396
  let fileEntry: FileEntry;
392
397
  let dirEntry: DirectoryEntry;
393
398
 
394
- this.domFileSystem.root.getFile(path, undefined, fileEntryLoaded.bind(this), errorHandler.bind(this));
399
+ this.domFileSystem.root.getFile(
400
+ decodeURIComponent(path), undefined, fileEntryLoaded.bind(this), errorHandler.bind(this));
395
401
 
396
402
  function fileEntryLoaded(this: IsolatedFileSystem, entry: FileEntry): void {
397
403
  if (entry.name === newName) {
@@ -458,7 +464,7 @@ export class IsolatedFileSystem extends PlatformFileSystem {
458
464
  }
459
465
 
460
466
  private requestEntries(path: string, callback: (arg0: Array<FileEntry>) => void): void {
461
- this.domFileSystem.root.getDirectory(path, undefined, innerCallback.bind(this), errorHandler);
467
+ this.domFileSystem.root.getDirectory(decodeURIComponent(path), undefined, innerCallback.bind(this), errorHandler);
462
468
 
463
469
  function innerCallback(this: IsolatedFileSystem, dirEntry: DirectoryEntry): void {
464
470
  this.readDirectory(dirEntry, callback);
@@ -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();
@@ -246,6 +248,11 @@ export class NetworkPersistenceManager extends Common.ObjectWrapper.ObjectWrappe
246
248
  }
247
249
  }
248
250
 
251
+ private fileUrlFromNetworkUrl(url: Platform.DevToolsPath.UrlString): Platform.DevToolsPath.UrlString {
252
+ return (this.projectInternal as FileSystem).fileSystemPath() + '/' + this.encodedPathFromUrl(url) as
253
+ Platform.DevToolsPath.UrlString;
254
+ }
255
+
249
256
  private decodeLocalPathToUrlPath(path: string): string {
250
257
  try {
251
258
  return unescape(path);
@@ -339,12 +346,11 @@ export class NetworkPersistenceManager extends Common.ObjectWrapper.ObjectWrappe
339
346
  !this.canHandleNetworkUISourceCode(uiSourceCode)) {
340
347
  return;
341
348
  }
342
- const url = Common.ParsedURL.ParsedURL.urlWithoutHash(uiSourceCode.url());
349
+ const url = Common.ParsedURL.ParsedURL.urlWithoutHash(uiSourceCode.url()) as Platform.DevToolsPath.UrlString;
343
350
  this.networkUISourceCodeForEncodedPath.set(this.encodedPathFromUrl(url), uiSourceCode);
344
351
 
345
352
  const project = this.projectInternal as FileSystem;
346
- const fileSystemUISourceCode =
347
- project.uiSourceCodeForURL(project.fileSystemPath() + '/' + this.encodedPathFromUrl(url));
353
+ const fileSystemUISourceCode = project.uiSourceCodeForURL(this.fileUrlFromNetworkUrl(url));
348
354
  if (fileSystemUISourceCode) {
349
355
  await this.bind(uiSourceCode, fileSystemUISourceCode);
350
356
  }
@@ -363,29 +369,97 @@ export class NetworkPersistenceManager extends Common.ObjectWrapper.ObjectWrappe
363
369
  }
364
370
  }
365
371
 
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);
372
+ async generateHeaderPatterns(uiSourceCode: Workspace.UISourceCode.UISourceCode):
373
+ Promise<{headerPatterns: Set<string>, path: string, overridesWithRegex: HeaderOverrideWithRegex[]}> {
374
+ const headerPatterns = new Set<string>();
375
+ const content = (await uiSourceCode.requestContent()).content || '';
376
+ let headerOverrides: HeaderOverride[] = [];
377
+ try {
378
+ headerOverrides = JSON.parse(content) as HeaderOverride[];
379
+ if (!headerOverrides.every(isHeaderOverride)) {
380
+ throw 'Type mismatch after parsing';
373
381
  }
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
- }
382
+ } catch (e) {
383
+ console.error('Failed to parse', uiSourceCode.url(), 'for locally overriding headers.');
384
+ return {headerPatterns, path: '', overridesWithRegex: []};
385
+ }
386
+ const relativePath = FileSystemWorkspaceBinding.relativePath(uiSourceCode).join('/');
387
+ const decodedPath = this.decodeLocalPathToUrlPath(relativePath).slice(0, -HEADERS_FILENAME.length);
388
+
389
+ const overridesWithRegex: HeaderOverrideWithRegex[] = [];
390
+ for (const headerOverride of headerOverrides) {
391
+ headerPatterns.add('http?://' + decodedPath + headerOverride.applyTo);
392
+
393
+ // Most servers have the concept of a "directory index", which is a
394
+ // default resource name for a request targeting a "directory", e. g.
395
+ // requesting "example.com/path/" would result in the same response as
396
+ // requesting "example.com/path/index.html". To match this behavior we
397
+ // generate an additional pattern without "index.html" as the longer
398
+ // pattern would not match against a shorter request.
399
+ const {head, tail} = extractDirectoryIndex(headerOverride.applyTo);
400
+ if (tail) {
401
+ headerPatterns.add('http?://' + decodedPath + head);
402
+
403
+ const pattern = escapeRegex(decodedPath + head) + '(' + escapeRegex(tail) + ')?';
404
+ const regex = new RegExp('^https?:\/\/' + pattern + '$');
405
+ overridesWithRegex.push({
406
+ applyToRegex: regex,
407
+ headers: headerOverride.headers,
408
+ });
409
+ } else {
410
+ const regex = new RegExp('^https?:\/\/' + escapeRegex(decodedPath + headerOverride.applyTo) + '$');
411
+ overridesWithRegex.push({
412
+ applyToRegex: regex,
413
+ headers: headerOverride.headers,
414
+ });
382
415
  }
416
+ }
417
+ return {headerPatterns, path: decodedPath, overridesWithRegex};
418
+ }
419
+
420
+ async updateInterceptionPatternsForTests(): Promise<void> {
421
+ await this.#innerUpdateInterceptionPatterns();
422
+ }
423
+
424
+ private updateInterceptionPatterns(): void {
425
+ void this.updateInterceptionThrottler.schedule(this.#innerUpdateInterceptionPatterns.bind(this));
426
+ }
383
427
 
428
+ async #innerUpdateInterceptionPatterns(): Promise<void> {
429
+ this.#headerOverridesMap.clear();
430
+ if (!this.activeInternal || !this.projectInternal) {
384
431
  return SDK.NetworkManager.MultitargetNetworkManager.instance().setInterceptionHandlerForPatterns(
385
- Array.from(patterns).map(
386
- pattern => ({urlPattern: pattern, requestStage: Protocol.Fetch.RequestStage.Response})),
387
- this.interceptionHandlerBound);
432
+ [], this.interceptionHandlerBound);
433
+ }
434
+ let patterns = new Set<string>();
435
+ for (const uiSourceCode of this.projectInternal.uiSourceCodes()) {
436
+ const pattern = this.patternForFileSystemUISourceCode(uiSourceCode);
437
+ if (Root.Runtime.experiments.isEnabled(Root.Runtime.ExperimentName.HEADER_OVERRIDES) &&
438
+ uiSourceCode.name() === HEADERS_FILENAME) {
439
+ const {headerPatterns, path, overridesWithRegex} = await this.generateHeaderPatterns(uiSourceCode);
440
+ if (headerPatterns.size > 0) {
441
+ patterns = new Set([...patterns, ...headerPatterns]);
442
+ this.#headerOverridesMap.set(path, overridesWithRegex);
443
+ }
444
+ } else {
445
+ patterns.add(pattern);
446
+ }
447
+ // Most servers have the concept of a "directory index", which is a
448
+ // default resource name for a request targeting a "directory", e. g.
449
+ // requesting "example.com/path/" would result in the same response as
450
+ // requesting "example.com/path/index.html". To match this behavior we
451
+ // generate an additional pattern without "index.html" as the longer
452
+ // pattern would not match against a shorter request.
453
+ const {head, tail} = extractDirectoryIndex(pattern);
454
+ if (tail) {
455
+ patterns.add(head);
456
+ }
388
457
  }
458
+
459
+ return SDK.NetworkManager.MultitargetNetworkManager.instance().setInterceptionHandlerForPatterns(
460
+ Array.from(patterns).map(
461
+ pattern => ({urlPattern: pattern, requestStage: Protocol.Fetch.RequestStage.Response})),
462
+ this.interceptionHandlerBound);
389
463
  }
390
464
 
391
465
  private async onUISourceCodeRemoved(uiSourceCode: Workspace.UISourceCode.UISourceCode): Promise<void> {
@@ -409,7 +483,7 @@ export class NetworkPersistenceManager extends Common.ObjectWrapper.ObjectWrappe
409
483
  await this.unbind(uiSourceCode);
410
484
  }
411
485
 
412
- private async setProject(project: Workspace.Workspace.Project|null): Promise<void> {
486
+ async setProject(project: Workspace.Workspace.Project|null): Promise<void> {
413
487
  if (project === this.projectInternal) {
414
488
  return;
415
489
  }
@@ -452,17 +526,67 @@ export class NetworkPersistenceManager extends Common.ObjectWrapper.ObjectWrappe
452
526
  }
453
527
  }
454
528
 
529
+ mergeHeaders(baseHeaders: Protocol.Fetch.HeaderEntry[], overrideHeaders: Protocol.Network.Headers):
530
+ Protocol.Fetch.HeaderEntry[] {
531
+ const result: Protocol.Fetch.HeaderEntry[] = [];
532
+ const headerMap = new Map<string, string>();
533
+ for (const header of baseHeaders) {
534
+ headerMap.set(header.name, header.value);
535
+ }
536
+ for (const [headerName, headerValue] of Object.entries(overrideHeaders)) {
537
+ headerMap.set(headerName, headerValue);
538
+ }
539
+ headerMap.forEach((headerValue, headerName) => {
540
+ result.push({name: headerName, value: headerValue});
541
+ });
542
+ return result;
543
+ }
544
+
545
+ #maybeMergeHeadersForPathSegment(path: string, requestUrl: string, headers: Protocol.Fetch.HeaderEntry[]):
546
+ Protocol.Fetch.HeaderEntry[] {
547
+ const headerOverrides = this.#headerOverridesMap.get(path) || [];
548
+ for (const headerOverride of headerOverrides) {
549
+ if (headerOverride.applyToRegex.test(requestUrl)) {
550
+ headers = this.mergeHeaders(headers, headerOverride.headers);
551
+ }
552
+ }
553
+ return headers;
554
+ }
555
+
556
+ handleHeaderInterception(interceptedRequest: SDK.NetworkManager.InterceptedRequest): Protocol.Fetch.HeaderEntry[] {
557
+ let result: Protocol.Fetch.HeaderEntry[] = interceptedRequest.responseHeaders || [];
558
+ const urlSegments = this.encodedPathFromUrl(interceptedRequest.request.url).split('/');
559
+ // Traverse the hierarchy of overrides from the most general to the most
560
+ // specific. Check with empty string first to match overrides applying to
561
+ // all domains.
562
+ // e.g. '', 'www.example.com/', 'www.example.com/path/', ...
563
+ let path = '';
564
+ result = this.#maybeMergeHeadersForPathSegment(path, interceptedRequest.request.url, result);
565
+ for (const segment of urlSegments) {
566
+ path += segment + '/';
567
+ result = this.#maybeMergeHeadersForPathSegment(path, interceptedRequest.request.url, result);
568
+ }
569
+ return result;
570
+ }
571
+
455
572
  private async interceptionHandler(interceptedRequest: SDK.NetworkManager.InterceptedRequest): Promise<void> {
456
573
  const method = interceptedRequest.request.method;
457
574
  if (!this.activeInternal || (method !== 'GET' && method !== 'POST')) {
458
575
  return;
459
576
  }
460
577
  const proj = this.projectInternal as FileSystem;
461
- const path = proj.fileSystemPath() + '/' + this.encodedPathFromUrl(interceptedRequest.request.url);
578
+ const path = this.fileUrlFromNetworkUrl(interceptedRequest.request.url as Platform.DevToolsPath.UrlString);
462
579
  const fileSystemUISourceCode = proj.uiSourceCodeForURL(path);
463
- if (!fileSystemUISourceCode) {
580
+ let responseHeaders: Protocol.Fetch.HeaderEntry[] = [];
581
+ if (Root.Runtime.experiments.isEnabled(Root.Runtime.ExperimentName.HEADER_OVERRIDES)) {
582
+ responseHeaders = this.handleHeaderInterception(interceptedRequest);
583
+ }
584
+ if (!fileSystemUISourceCode && !responseHeaders.length) {
464
585
  return;
465
586
  }
587
+ if (!responseHeaders.length) {
588
+ responseHeaders = interceptedRequest.responseHeaders || [];
589
+ }
466
590
 
467
591
  let mimeType = '';
468
592
  if (interceptedRequest.responseHeaders) {
@@ -477,32 +601,41 @@ export class NetworkPersistenceManager extends Common.ObjectWrapper.ObjectWrappe
477
601
  if (!mimeType) {
478
602
  const expectedResourceType =
479
603
  Common.ResourceType.resourceTypes[interceptedRequest.resourceType] || Common.ResourceType.resourceTypes.Other;
480
- mimeType = fileSystemUISourceCode.mimeType();
604
+ mimeType = fileSystemUISourceCode?.mimeType() || '';
481
605
  if (Common.ResourceType.ResourceType.fromMimeType(mimeType) !== expectedResourceType) {
482
606
  mimeType = expectedResourceType.canonicalMimeType();
483
607
  }
484
608
  }
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
609
 
503
- const blob = await project.requestFileBlob(fileSystemUISourceCode);
504
- if (blob) {
505
- void interceptedRequest.continueRequestWithContent(new Blob([blob], {type: mimeType}));
610
+ if (fileSystemUISourceCode) {
611
+ this.originalResponseContentPromises.set(
612
+ fileSystemUISourceCode, interceptedRequest.responseBody().then(response => {
613
+ if (response.error || response.content === null) {
614
+ return null;
615
+ }
616
+ if (response.encoded) {
617
+ const text = atob(response.content);
618
+ const data = new Uint8Array(text.length);
619
+ for (let i = 0; i < text.length; ++i) {
620
+ data[i] = text.charCodeAt(i);
621
+ }
622
+ return new TextDecoder('utf-8').decode(data);
623
+ }
624
+ return response.content;
625
+ }));
626
+
627
+ const project = fileSystemUISourceCode.project() as FileSystem;
628
+ const blob = await project.requestFileBlob(fileSystemUISourceCode);
629
+ if (blob) {
630
+ void interceptedRequest.continueRequestWithContent(
631
+ new Blob([blob], {type: mimeType}), /* encoded */ false, responseHeaders);
632
+ }
633
+ } else {
634
+ const responseBody = await interceptedRequest.responseBody();
635
+ if (!responseBody.error && responseBody.content) {
636
+ void interceptedRequest.continueRequestWithContent(
637
+ new Blob([responseBody.content], {type: mimeType}), /* encoded */ true, responseHeaders);
638
+ }
506
639
  }
507
640
  }
508
641
  }
@@ -512,6 +645,8 @@ const RESERVED_FILENAMES = new Set<string>([
512
645
  'com8', 'com9', 'lpt1', 'lpt2', 'lpt3', 'lpt4', 'lpt5', 'lpt6', 'lpt7', 'lpt8', 'lpt9',
513
646
  ]);
514
647
 
648
+ const HEADERS_FILENAME = '.headers';
649
+
515
650
  // TODO(crbug.com/1167717): Make this a const enum again
516
651
  // eslint-disable-next-line rulesdir/const_enum
517
652
  export enum Events {
@@ -521,3 +656,36 @@ export enum Events {
521
656
  export type EventTypes = {
522
657
  [Events.ProjectChanged]: Workspace.Workspace.Project|null,
523
658
  };
659
+
660
+ interface HeaderOverride {
661
+ applyTo: string;
662
+ headers: Protocol.Network.Headers;
663
+ }
664
+
665
+ interface HeaderOverrideWithRegex {
666
+ applyToRegex: RegExp;
667
+ headers: Protocol.Network.Headers;
668
+ }
669
+
670
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
671
+ function isHeaderOverride(arg: any): arg is HeaderOverride {
672
+ if (!(arg && arg.applyTo && typeof (arg.applyTo === 'string') && arg.headers && Object.keys(arg.headers).length)) {
673
+ return false;
674
+ }
675
+ return Object.values(arg.headers).every(value => typeof value === 'string');
676
+ }
677
+
678
+ export function escapeRegex(pattern: string): string {
679
+ return Platform.StringUtilities.escapeCharacters(pattern, '[]{}()\\.^$+|-,?').replaceAll('*', '.*');
680
+ }
681
+
682
+ export function extractDirectoryIndex(pattern: string): {head: string, tail?: string} {
683
+ const lastSlash = pattern.lastIndexOf('/');
684
+ const tail = lastSlash >= 0 ? pattern.slice(lastSlash + 1) : pattern;
685
+ const head = lastSlash >= 0 ? pattern.slice(0, lastSlash + 1) : '';
686
+ const regex = new RegExp('^' + escapeRegex(tail) + '$');
687
+ if (regex.test('index.html') || regex.test('index.htm') || regex.test('index.php')) {
688
+ return {head, tail};
689
+ }
690
+ return {head: pattern};
691
+ }
@@ -3,6 +3,7 @@
3
3
  // found in the LICENSE file.
4
4
 
5
5
  import type * as Common from '../../core/common/common.js';
6
+ import type * as Platform from '../../core/platform/platform.js';
6
7
  import * as i18n from '../../core/i18n/i18n.js';
7
8
  import type * as TextUtils from '../text_utils/text_utils.js';
8
9
 
@@ -26,11 +27,11 @@ export class PlatformFileSystem {
26
27
  return Promise.resolve(null);
27
28
  }
28
29
 
29
- initialFilePaths(): string[] {
30
+ initialFilePaths(): Platform.DevToolsPath.EncodedPathString[] {
30
31
  return [];
31
32
  }
32
33
 
33
- initialGitFolders(): string[] {
34
+ initialGitFolders(): Platform.DevToolsPath.EncodedPathString[] {
34
35
  return [];
35
36
  }
36
37
 
@@ -138,10 +138,10 @@ export class TimelineFrameModel {
138
138
  }
139
139
  this.lastBeginFrame = startTime;
140
140
 
141
- this.beginFrameQueue.addFrameIfNotExists(seqId, startTime, false);
141
+ this.beginFrameQueue.addFrameIfNotExists(seqId, startTime, false, false);
142
142
  }
143
143
 
144
- handleDroppedFrame(startTime: number, seqId: number): void {
144
+ handleDroppedFrame(startTime: number, seqId: number, isPartial: boolean): void {
145
145
  if (!this.lastFrame) {
146
146
  this.startFrame(startTime);
147
147
  }
@@ -149,8 +149,9 @@ export class TimelineFrameModel {
149
149
  // This line handles the case where no BeginFrame event is issued for
150
150
  // the dropped frame. In this situation, add a BeginFrame to the queue
151
151
  // as if it actually occurred.
152
- this.beginFrameQueue.addFrameIfNotExists(seqId, startTime, true);
152
+ this.beginFrameQueue.addFrameIfNotExists(seqId, startTime, true, isPartial);
153
153
  this.beginFrameQueue.setDropped(seqId, true);
154
+ this.beginFrameQueue.setPartial(seqId, isPartial);
154
155
  }
155
156
 
156
157
  handleDrawFrame(startTime: number, seqId: number): void {
@@ -187,6 +188,9 @@ export class TimelineFrameModel {
187
188
  if (frame.isDropped) {
188
189
  this.lastFrame.dropped = true;
189
190
  }
191
+ if (frame.isPartial) {
192
+ this.lastFrame.isPartial = true;
193
+ }
190
194
  }
191
195
  }
192
196
  this.mainFrameCommitted = false;
@@ -318,7 +322,7 @@ export class TimelineFrameModel {
318
322
  } else if (event.name === RecordType.NeedsBeginFrameChanged) {
319
323
  this.handleNeedFrameChanged(timestamp, event.args['data'] && event.args['data']['needsBeginFrame']);
320
324
  } else if (event.name === RecordType.DroppedFrame) {
321
- this.handleDroppedFrame(timestamp, event.args['frameSeqId']);
325
+ this.handleDroppedFrame(timestamp, event.args['frameSeqId'], event.args['hasPartialUpdate']);
322
326
  }
323
327
  }
324
328
 
@@ -426,6 +430,7 @@ export class TimelineFrame {
426
430
  cpuTime: number;
427
431
  idle: boolean;
428
432
  dropped: boolean;
433
+ isPartial: boolean;
429
434
  layerTree: TracingFrameLayerTree|null;
430
435
  paints: LayerPaintEvent[];
431
436
  mainFrameId: number|undefined;
@@ -439,6 +444,7 @@ export class TimelineFrame {
439
444
  this.cpuTime = 0;
440
445
  this.idle = false;
441
446
  this.dropped = false;
447
+ this.isPartial = false;
442
448
  this.layerTree = null;
443
449
  this.paints = [];
444
450
  this.mainFrameId = undefined;
@@ -545,10 +551,12 @@ class BeginFrameInfo {
545
551
  seqId: number;
546
552
  startTime: number;
547
553
  isDropped: boolean;
548
- constructor(seqId: number, startTime: number, isDropped: boolean) {
554
+ isPartial: boolean;
555
+ constructor(seqId: number, startTime: number, isDropped: boolean, isPartial: boolean) {
549
556
  this.seqId = seqId;
550
557
  this.startTime = startTime;
551
558
  this.isDropped = isDropped;
559
+ this.isPartial = isPartial;
552
560
  }
553
561
  }
554
562
 
@@ -570,9 +578,9 @@ export class TimelineFrameBeginFrameQueue {
570
578
  }
571
579
 
572
580
  // Add a BeginFrame to the queue, if it does not already exit.
573
- addFrameIfNotExists(seqId: number, startTime: number, isDropped: boolean): void {
581
+ addFrameIfNotExists(seqId: number, startTime: number, isDropped: boolean, isPartial: boolean): void {
574
582
  if (!(seqId in this.mapFrames)) {
575
- this.mapFrames[seqId] = new BeginFrameInfo(seqId, startTime, isDropped);
583
+ this.mapFrames[seqId] = new BeginFrameInfo(seqId, startTime, isDropped, isPartial);
576
584
  this.queueFrames.push(seqId);
577
585
  }
578
586
  }
@@ -584,6 +592,12 @@ export class TimelineFrameBeginFrameQueue {
584
592
  }
585
593
  }
586
594
 
595
+ setPartial(seqId: number, isPartial: boolean): void {
596
+ if (seqId in this.mapFrames) {
597
+ this.mapFrames[seqId].isPartial = isPartial;
598
+ }
599
+ }
600
+
587
601
  processPendingBeginFramesOnDrawFrame(seqId: number): BeginFrameInfo[] {
588
602
  const framesToVisualize: BeginFrameInfo[] = [];
589
603
 
@@ -36,7 +36,7 @@ import * as Platform from '../../core/platform/platform.js';
36
36
  import * as TextUtils from '../text_utils/text_utils.js';
37
37
 
38
38
  import type {Project} from './WorkspaceImpl.js';
39
- import {Events as WorkspaceImplEvents, projectTypes} from './WorkspaceImpl.js';
39
+ import {Events as WorkspaceImplEvents} from './WorkspaceImpl.js';
40
40
 
41
41
  const UIStrings = {
42
42
  /**
@@ -82,9 +82,13 @@ export class UISourceCode extends Common.ObjectWrapper.ObjectWrapper<EventTypes>
82
82
  if (parsedURL) {
83
83
  this.originInternal = parsedURL.securityOrigin();
84
84
  this.parentURLInternal = this.originInternal + parsedURL.folderPathComponents;
85
- this.nameInternal = parsedURL.lastPathComponent;
86
85
  if (parsedURL.queryParams) {
87
- this.nameInternal += '?' + parsedURL.queryParams;
86
+ // in case file name contains query params, it doesn't look like a normal file name anymore
87
+ // so it can as well remain encoded
88
+ this.nameInternal = parsedURL.lastPathComponent + '?' + parsedURL.queryParams;
89
+ } else {
90
+ // file name looks best decoded
91
+ this.nameInternal = decodeURIComponent(parsedURL.lastPathComponent);
88
92
  }
89
93
  } else {
90
94
  this.originInternal = '';
@@ -138,15 +142,7 @@ export class UISourceCode extends Common.ObjectWrapper.ObjectWrapper<EventTypes>
138
142
  if (!this.nameInternal) {
139
143
  return i18nString(UIStrings.index);
140
144
  }
141
- let name: string = this.nameInternal;
142
- try {
143
- if (this.project().type() === projectTypes.FileSystem) {
144
- name = unescape(name);
145
- } else {
146
- name = decodeURI(name);
147
- }
148
- } catch (error) {
149
- }
145
+ const name = this.nameInternal;
150
146
  return skipTrim ? name : Platform.StringUtilities.trimEndWithMaxLength(name, 100);
151
147
  }
152
148
 
@@ -178,10 +174,12 @@ export class UISourceCode extends Common.ObjectWrapper.ObjectWrapper<EventTypes>
178
174
 
179
175
  private updateName(name: string, url: string, contentType?: Common.ResourceType.ResourceType): void {
180
176
  const oldURL = this.urlInternal;
181
- this.urlInternal = this.urlInternal.substring(0, this.urlInternal.length - this.nameInternal.length) + name;
182
177
  this.nameInternal = name;
183
178
  if (url) {
184
179
  this.urlInternal = url;
180
+ } else {
181
+ this.urlInternal = Common.ParsedURL.ParsedURL.relativePathToUrlString(
182
+ name as Platform.DevToolsPath.RawPathString, oldURL as Platform.DevToolsPath.UrlString);
185
183
  }
186
184
  if (contentType) {
187
185
  this.contentTypeInternal = contentType;
@@ -191,7 +189,6 @@ export class UISourceCode extends Common.ObjectWrapper.ObjectWrapper<EventTypes>
191
189
  WorkspaceImplEvents.UISourceCodeRenamed, {oldURL: oldURL, uiSourceCode: this});
192
190
  }
193
191
 
194
- // TODO(crbug.com/1253323): Cast to RawPathString will be removed when migration to branded types is complete.
195
192
  contentURL(): string {
196
193
  return this.url();
197
194
  }
@@ -29,6 +29,7 @@
29
29
  */
30
30
 
31
31
  import * as Common from '../../core/common/common.js';
32
+ import type * as Platform from '../../core/platform/platform.js';
32
33
  import type * as TextUtils from '../text_utils/text_utils.js';
33
34
 
34
35
  import type {UISourceCodeMetadata} from './UISourceCode.js';
@@ -175,7 +176,10 @@ export abstract class ProjectStore implements Project {
175
176
 
176
177
  renameUISourceCode(uiSourceCode: UISourceCode, newName: string): void {
177
178
  const oldPath = uiSourceCode.url();
178
- const newPath = uiSourceCode.parentURL() ? uiSourceCode.parentURL() + '/' + newName : newName;
179
+ const newPath = uiSourceCode.parentURL() ?
180
+ Common.ParsedURL.ParsedURL.urlFromParentUrlAndName(
181
+ uiSourceCode.parentURL() as Platform.DevToolsPath.UrlString, newName) :
182
+ encodeURIComponent(newName);
179
183
  const value = this.uiSourceCodesMap.get(oldPath) as {
180
184
  uiSourceCode: UISourceCode,
181
185
  index: number,