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.
- package/config/gni/devtools_grd_files.gni +64 -0
- package/front_end/core/common/ParsedURL.ts +25 -2
- package/front_end/core/i18n/locales/en-US.json +18 -3
- package/front_end/core/i18n/locales/en-XL.json +18 -3
- package/front_end/core/sdk/ChildTargetManager.ts +2 -2
- package/front_end/core/sdk/Connections.ts +6 -1
- package/front_end/core/sdk/DebuggerModel.ts +4 -0
- package/front_end/core/sdk/NetworkManager.ts +4 -3
- package/front_end/entrypoints/lighthouse_worker/LighthouseService.ts +84 -13
- package/front_end/models/persistence/Automapping.ts +2 -32
- package/front_end/models/persistence/FileSystemWorkspaceBinding.ts +9 -7
- package/front_end/models/persistence/IsolatedFileSystem.ts +20 -14
- package/front_end/models/persistence/NetworkPersistenceManager.ts +213 -45
- package/front_end/models/persistence/PlatformFileSystem.ts +3 -2
- package/front_end/models/timeline_model/TimelineFrameModel.ts +21 -7
- package/front_end/models/workspace/UISourceCode.ts +11 -14
- package/front_end/models/workspace/WorkspaceImpl.ts +5 -1
- package/front_end/panels/animation/animationTimeline.css +0 -3
- package/front_end/panels/application/components/ReportsGrid.ts +19 -4
- package/front_end/panels/application/components/trustTokensViewDeleteButton.css +0 -1
- package/front_end/panels/console/consolePinPane.css +0 -17
- package/front_end/panels/css_overview/cssOverviewCompletedView.css +0 -1
- package/front_end/panels/elements/components/adornerSettingsPane.css +0 -1
- package/front_end/panels/elements/components/computedStyleTrace.css +1 -1
- package/front_end/panels/elements/components/elementsBreadcrumbs.css +0 -1
- package/front_end/panels/elements/computedStyleWidgetTree.css +2 -2
- package/front_end/panels/elements/elementsTreeOutline.css +0 -2
- package/front_end/panels/emulation/deviceModeView.css +0 -1
- package/front_end/panels/event_listeners/eventListenersView.css +0 -1
- package/front_end/panels/issues/components/hideIssuesMenu.css +0 -1
- package/front_end/panels/lighthouse/LighthouseController.ts +25 -1
- package/front_end/panels/lighthouse/LighthouseProtocolService.ts +37 -5
- package/front_end/panels/lighthouse/LighthouseStartView.ts +1 -0
- package/front_end/panels/lighthouse/LighthouseStatusView.ts +5 -5
- package/front_end/panels/media/playerListView.css +0 -1
- package/front_end/panels/network/networkLogView.css +0 -4
- package/front_end/panels/network/requestPayloadTree.css +0 -2
- package/front_end/panels/network/signedExchangeInfoTree.css +0 -1
- package/front_end/panels/settings/emulation/components/userAgentClientHintsForm.css +0 -4
- package/front_end/panels/settings/emulation/devicesSettingsTab.css +0 -1
- package/front_end/panels/snippets/ScriptSnippetFileSystem.ts +4 -4
- package/front_end/panels/snippets/SnippetsQuickOpen.ts +1 -1
- package/front_end/panels/sources/TabbedEditorContainer.ts +9 -0
- package/front_end/panels/sources/watchExpressionsSidebarPane.css +0 -1
- package/front_end/panels/timeline/TimelineFlameChartDataProvider.ts +75 -3
- package/front_end/panels/webauthn/webauthnPane.css +0 -12
- package/front_end/services/puppeteer/PuppeteerConnection.ts +107 -0
- package/front_end/services/puppeteer/puppeteer.ts +9 -0
- package/front_end/third_party/codemirror/package/addon/fold/foldgutter.css +1 -5
- package/front_end/ui/components/adorners/adorner.css +0 -4
- package/front_end/ui/components/buttons/button.css +0 -4
- package/front_end/ui/components/data_grid/dataGrid.css +0 -4
- package/front_end/ui/components/icon_button/iconButton.css +0 -1
- package/front_end/ui/components/markdown_view/MarkdownLinksMap.ts +2 -2
- package/front_end/ui/legacy/TabbedPane.ts +1 -1
- package/front_end/ui/legacy/tabbedPane.css +0 -4
- package/front_end/ui/legacy/textButton.css +0 -1
- package/front_end/ui/legacy/toolbar.css +0 -1
- package/package.json +2 -2
- 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<
|
81
|
-
private readonly initialGitFoldersInternal: Set<
|
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():
|
146
|
+
initialFilePaths(): Platform.DevToolsPath.EncodedPathString[] {
|
147
147
|
return [...this.initialFilePathsInternal];
|
148
148
|
}
|
149
149
|
|
150
|
-
initialGitFolders():
|
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(
|
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(
|
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
|
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(
|
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(
|
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(
|
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
|
-
|
367
|
-
|
368
|
-
|
369
|
-
|
370
|
-
|
371
|
-
|
372
|
-
|
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
|
-
|
375
|
-
|
376
|
-
|
377
|
-
|
378
|
-
|
379
|
-
|
380
|
-
|
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
|
-
|
386
|
-
|
387
|
-
|
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
|
-
|
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 =
|
578
|
+
const path = this.fileUrlFromNetworkUrl(interceptedRequest.request.url as Platform.DevToolsPath.UrlString);
|
462
579
|
const fileSystemUISourceCode = proj.uiSourceCodeForURL(path);
|
463
|
-
|
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
|
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
|
-
|
504
|
-
|
505
|
-
|
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():
|
30
|
+
initialFilePaths(): Platform.DevToolsPath.EncodedPathString[] {
|
30
31
|
return [];
|
31
32
|
}
|
32
33
|
|
33
|
-
initialGitFolders():
|
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
|
-
|
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
|
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
|
-
|
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
|
-
|
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() ?
|
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,
|