chrome-devtools-mcp 0.0.2 → 0.2.0
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/README.md +6 -3
- package/build/node_modules/chrome-devtools-frontend/front_end/core/common/Settings.js +3 -32
- package/build/node_modules/chrome-devtools-frontend/front_end/core/i18n/i18n.js +35 -8
- package/build/node_modules/chrome-devtools-frontend/front_end/core/root/Runtime.js +4 -1
- package/build/node_modules/chrome-devtools-frontend/front_end/generated/InspectorBackendCommands.js +4 -4
- package/build/node_modules/chrome-devtools-frontend/front_end/generated/SupportedCSSProperties.js +12 -0
- package/build/node_modules/chrome-devtools-frontend/front_end/models/ai_assistance/data_formatters/PerformanceTraceFormatter.js +366 -0
- package/build/node_modules/chrome-devtools-frontend/front_end/models/ai_assistance/performance/AICallTree.js +366 -0
- package/build/node_modules/chrome-devtools-frontend/front_end/models/ai_assistance/performance/AIContext.js +64 -0
- package/build/node_modules/chrome-devtools-frontend/front_end/models/ai_assistance/performance/AIQueries.js +105 -0
- package/build/node_modules/chrome-devtools-frontend/front_end/models/bindings/CSSWorkspaceBinding.js +243 -0
- package/build/node_modules/chrome-devtools-frontend/front_end/models/bindings/CompilerScriptMapping.js +407 -0
- package/build/node_modules/chrome-devtools-frontend/front_end/models/bindings/ContentProviderBasedProject.js +128 -0
- package/build/node_modules/chrome-devtools-frontend/front_end/models/bindings/DebuggerLanguagePlugins.js +992 -0
- package/build/node_modules/chrome-devtools-frontend/front_end/models/bindings/DebuggerWorkspaceBinding.js +574 -0
- package/build/node_modules/chrome-devtools-frontend/front_end/models/bindings/DefaultScriptMapping.js +112 -0
- package/build/node_modules/chrome-devtools-frontend/front_end/models/bindings/FileUtils.js +186 -0
- package/build/node_modules/chrome-devtools-frontend/front_end/models/bindings/LiveLocation.js +60 -0
- package/build/node_modules/chrome-devtools-frontend/front_end/models/bindings/NetworkProject.js +107 -0
- package/build/node_modules/chrome-devtools-frontend/front_end/models/bindings/PresentationConsoleMessageHelper.js +244 -0
- package/build/node_modules/chrome-devtools-frontend/front_end/models/bindings/ResourceMapping.js +473 -0
- package/build/node_modules/chrome-devtools-frontend/front_end/models/bindings/ResourceScriptMapping.js +399 -0
- package/build/node_modules/chrome-devtools-frontend/front_end/models/bindings/ResourceUtils.js +87 -0
- package/build/node_modules/chrome-devtools-frontend/front_end/models/bindings/SASSSourceMapping.js +181 -0
- package/build/node_modules/chrome-devtools-frontend/front_end/models/bindings/StylesSourceMapping.js +268 -0
- package/build/node_modules/chrome-devtools-frontend/front_end/models/bindings/TempFile.js +55 -0
- package/build/node_modules/chrome-devtools-frontend/front_end/models/bindings/bindings.js +20 -0
- package/build/node_modules/chrome-devtools-frontend/front_end/models/crux-manager/CrUXManager.js +283 -0
- package/build/node_modules/chrome-devtools-frontend/front_end/models/crux-manager/crux-manager.js +4 -0
- package/build/node_modules/chrome-devtools-frontend/front_end/models/emulation/DeviceModeModel.js +775 -0
- package/build/node_modules/chrome-devtools-frontend/front_end/models/emulation/EmulatedDevices.js +1706 -0
- package/build/node_modules/chrome-devtools-frontend/front_end/models/emulation/emulation.js +6 -0
- package/build/node_modules/chrome-devtools-frontend/front_end/models/formatter/FormatterWorkerPool.js +131 -0
- package/build/node_modules/chrome-devtools-frontend/front_end/models/formatter/ScriptFormatter.js +77 -0
- package/build/node_modules/chrome-devtools-frontend/front_end/models/formatter/formatter.js +6 -0
- package/build/node_modules/chrome-devtools-frontend/front_end/models/geometry/GeometryImpl.js +347 -0
- package/build/node_modules/chrome-devtools-frontend/front_end/models/geometry/geometry.js +4 -0
- package/build/node_modules/chrome-devtools-frontend/front_end/models/source_map_scopes/NamesResolver.js +626 -0
- package/build/node_modules/chrome-devtools-frontend/front_end/models/source_map_scopes/ScopeChainModel.js +59 -0
- package/build/node_modules/chrome-devtools-frontend/front_end/models/source_map_scopes/ScopeTreeCache.js +32 -0
- package/build/node_modules/chrome-devtools-frontend/front_end/models/source_map_scopes/source_map_scopes.js +7 -0
- package/build/node_modules/chrome-devtools-frontend/front_end/models/stack_trace/StackTrace.js +4 -0
- package/build/node_modules/chrome-devtools-frontend/front_end/models/stack_trace/StackTraceImpl.js +67 -0
- package/build/node_modules/chrome-devtools-frontend/front_end/models/stack_trace/StackTraceModel.js +97 -0
- package/build/node_modules/chrome-devtools-frontend/front_end/models/stack_trace/Trie.js +113 -0
- package/build/node_modules/chrome-devtools-frontend/front_end/models/stack_trace/stack_trace.js +5 -0
- package/build/node_modules/chrome-devtools-frontend/front_end/models/stack_trace/stack_trace_impl.js +7 -0
- package/build/node_modules/chrome-devtools-frontend/front_end/models/text_utils/TextUtils.js +23 -0
- package/build/node_modules/chrome-devtools-frontend/front_end/models/trace/Processor.js +1 -1
- package/build/node_modules/chrome-devtools-frontend/front_end/models/trace/helpers/Trace.js +1 -1
- package/build/node_modules/chrome-devtools-frontend/front_end/models/trace/insights/DocumentLatency.js +5 -4
- package/build/node_modules/chrome-devtools-frontend/front_end/models/trace_source_maps_resolver/SourceMapsResolver.js +199 -0
- package/build/node_modules/chrome-devtools-frontend/front_end/models/trace_source_maps_resolver/trace_source_maps_resolver.js +4 -0
- package/build/node_modules/chrome-devtools-frontend/front_end/models/workspace/FileManager.js +64 -0
- package/build/node_modules/chrome-devtools-frontend/front_end/models/workspace/IgnoreListManager.js +511 -0
- package/build/node_modules/chrome-devtools-frontend/front_end/models/workspace/SearchConfig.js +113 -0
- package/build/node_modules/chrome-devtools-frontend/front_end/models/workspace/UISourceCode.js +563 -0
- package/build/node_modules/chrome-devtools-frontend/front_end/models/workspace/WorkspaceImpl.js +204 -0
- package/build/node_modules/chrome-devtools-frontend/front_end/models/workspace/workspace.js +9 -0
- package/build/src/McpContext.js +24 -9
- package/build/src/McpResponse.js +3 -3
- package/build/src/browser.js +3 -1
- package/build/src/index.js +1 -1
- package/build/src/tools/input.js +7 -7
- package/build/src/tools/performance.js +29 -2
- package/build/src/tools/screenshot.js +1 -1
- package/build/src/tools/script.js +40 -14
- package/build/src/trace-processing/parse.js +26 -22
- package/package.json +9 -7
package/build/node_modules/chrome-devtools-frontend/front_end/models/bindings/StylesSourceMapping.js
ADDED
|
@@ -0,0 +1,268 @@
|
|
|
1
|
+
// Copyright 2012 The Chromium Authors
|
|
2
|
+
// Use of this source code is governed by a BSD-style license that can be
|
|
3
|
+
// found in the LICENSE file.
|
|
4
|
+
import * as Common from '../../core/common/common.js';
|
|
5
|
+
import * as SDK from '../../core/sdk/sdk.js';
|
|
6
|
+
import * as TextUtils from '../text_utils/text_utils.js';
|
|
7
|
+
import * as Workspace from '../workspace/workspace.js';
|
|
8
|
+
import { ContentProviderBasedProject } from './ContentProviderBasedProject.js';
|
|
9
|
+
import { NetworkProject } from './NetworkProject.js';
|
|
10
|
+
import { metadataForURL } from './ResourceUtils.js';
|
|
11
|
+
const uiSourceCodeToStyleMap = new WeakMap();
|
|
12
|
+
export class StylesSourceMapping {
|
|
13
|
+
#cssModel;
|
|
14
|
+
#project;
|
|
15
|
+
#styleFiles = new Map();
|
|
16
|
+
#eventListeners;
|
|
17
|
+
constructor(cssModel, workspace) {
|
|
18
|
+
this.#cssModel = cssModel;
|
|
19
|
+
const target = this.#cssModel.target();
|
|
20
|
+
this.#project = new ContentProviderBasedProject(workspace, 'css:' + target.id(), Workspace.Workspace.projectTypes.Network, '', false /* isServiceProject */);
|
|
21
|
+
NetworkProject.setTargetForProject(this.#project, target);
|
|
22
|
+
this.#eventListeners = [
|
|
23
|
+
this.#cssModel.addEventListener(SDK.CSSModel.Events.StyleSheetAdded, this.styleSheetAdded, this),
|
|
24
|
+
this.#cssModel.addEventListener(SDK.CSSModel.Events.StyleSheetRemoved, this.styleSheetRemoved, this),
|
|
25
|
+
this.#cssModel.addEventListener(SDK.CSSModel.Events.StyleSheetChanged, this.styleSheetChanged, this),
|
|
26
|
+
];
|
|
27
|
+
}
|
|
28
|
+
addSourceMap(sourceUrl, sourceMapUrl) {
|
|
29
|
+
this.#styleFiles.get(sourceUrl)?.addSourceMap(sourceUrl, sourceMapUrl);
|
|
30
|
+
}
|
|
31
|
+
rawLocationToUILocation(rawLocation) {
|
|
32
|
+
const header = rawLocation.header();
|
|
33
|
+
if (!header || !this.acceptsHeader(header)) {
|
|
34
|
+
return null;
|
|
35
|
+
}
|
|
36
|
+
const styleFile = this.#styleFiles.get(header.resourceURL());
|
|
37
|
+
if (!styleFile) {
|
|
38
|
+
return null;
|
|
39
|
+
}
|
|
40
|
+
let lineNumber = rawLocation.lineNumber;
|
|
41
|
+
let columnNumber = rawLocation.columnNumber;
|
|
42
|
+
if (header.isInline && header.hasSourceURL) {
|
|
43
|
+
lineNumber -= header.lineNumberInSource(0);
|
|
44
|
+
const headerColumnNumber = header.columnNumberInSource(lineNumber, 0);
|
|
45
|
+
if (typeof headerColumnNumber === 'undefined') {
|
|
46
|
+
columnNumber = headerColumnNumber;
|
|
47
|
+
}
|
|
48
|
+
else {
|
|
49
|
+
columnNumber -= headerColumnNumber;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
return styleFile.getUiSourceCode().uiLocation(lineNumber, columnNumber);
|
|
53
|
+
}
|
|
54
|
+
uiLocationToRawLocations(uiLocation) {
|
|
55
|
+
const styleFile = uiSourceCodeToStyleMap.get(uiLocation.uiSourceCode);
|
|
56
|
+
if (!styleFile) {
|
|
57
|
+
return [];
|
|
58
|
+
}
|
|
59
|
+
const rawLocations = [];
|
|
60
|
+
for (const header of styleFile.getHeaders()) {
|
|
61
|
+
let lineNumber = uiLocation.lineNumber;
|
|
62
|
+
let columnNumber = uiLocation.columnNumber;
|
|
63
|
+
if (header.isInline && header.hasSourceURL) {
|
|
64
|
+
// TODO(crbug.com/1153123): Revisit the `#columnNumber || 0` and also preserve `undefined` for source maps?
|
|
65
|
+
columnNumber = header.columnNumberInSource(lineNumber, uiLocation.columnNumber || 0);
|
|
66
|
+
lineNumber = header.lineNumberInSource(lineNumber);
|
|
67
|
+
}
|
|
68
|
+
rawLocations.push(new SDK.CSSModel.CSSLocation(header, lineNumber, columnNumber));
|
|
69
|
+
}
|
|
70
|
+
return rawLocations;
|
|
71
|
+
}
|
|
72
|
+
acceptsHeader(header) {
|
|
73
|
+
if (header.isConstructedByNew()) {
|
|
74
|
+
return false;
|
|
75
|
+
}
|
|
76
|
+
if (header.isInline && !header.hasSourceURL && header.origin !== 'inspector') {
|
|
77
|
+
return false;
|
|
78
|
+
}
|
|
79
|
+
if (!header.resourceURL()) {
|
|
80
|
+
return false;
|
|
81
|
+
}
|
|
82
|
+
return true;
|
|
83
|
+
}
|
|
84
|
+
styleSheetAdded(event) {
|
|
85
|
+
const header = event.data;
|
|
86
|
+
if (!this.acceptsHeader(header)) {
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
const url = header.resourceURL();
|
|
90
|
+
let styleFile = this.#styleFiles.get(url);
|
|
91
|
+
if (!styleFile) {
|
|
92
|
+
styleFile = new StyleFile(this.#cssModel, this.#project, header);
|
|
93
|
+
this.#styleFiles.set(url, styleFile);
|
|
94
|
+
}
|
|
95
|
+
else {
|
|
96
|
+
styleFile.addHeader(header);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
styleSheetRemoved(event) {
|
|
100
|
+
const header = event.data;
|
|
101
|
+
if (!this.acceptsHeader(header)) {
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
const url = header.resourceURL();
|
|
105
|
+
const styleFile = this.#styleFiles.get(url);
|
|
106
|
+
if (styleFile) {
|
|
107
|
+
if (styleFile.getHeaders().size === 1) {
|
|
108
|
+
styleFile.dispose();
|
|
109
|
+
this.#styleFiles.delete(url);
|
|
110
|
+
}
|
|
111
|
+
else {
|
|
112
|
+
styleFile.removeHeader(header);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
styleSheetChanged(event) {
|
|
117
|
+
const header = this.#cssModel.styleSheetHeaderForId(event.data.styleSheetId);
|
|
118
|
+
if (!header || !this.acceptsHeader(header)) {
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
const styleFile = this.#styleFiles.get(header.resourceURL());
|
|
122
|
+
if (styleFile) {
|
|
123
|
+
styleFile.styleSheetChanged(header);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
dispose() {
|
|
127
|
+
for (const styleFile of this.#styleFiles.values()) {
|
|
128
|
+
styleFile.dispose();
|
|
129
|
+
}
|
|
130
|
+
this.#styleFiles.clear();
|
|
131
|
+
Common.EventTarget.removeEventListeners(this.#eventListeners);
|
|
132
|
+
this.#project.removeProject();
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
export class StyleFile {
|
|
136
|
+
#cssModel;
|
|
137
|
+
#project;
|
|
138
|
+
headers;
|
|
139
|
+
uiSourceCode;
|
|
140
|
+
#eventListeners;
|
|
141
|
+
#throttler = new Common.Throttler.Throttler(200);
|
|
142
|
+
#terminated = false;
|
|
143
|
+
#isAddingRevision;
|
|
144
|
+
#isUpdatingHeaders;
|
|
145
|
+
constructor(cssModel, project, header) {
|
|
146
|
+
this.#cssModel = cssModel;
|
|
147
|
+
this.#project = project;
|
|
148
|
+
this.headers = new Set([header]);
|
|
149
|
+
const target = cssModel.target();
|
|
150
|
+
const url = header.resourceURL();
|
|
151
|
+
const metadata = metadataForURL(target, header.frameId, url);
|
|
152
|
+
this.uiSourceCode = this.#project.createUISourceCode(url, header.contentType());
|
|
153
|
+
uiSourceCodeToStyleMap.set(this.uiSourceCode, this);
|
|
154
|
+
NetworkProject.setInitialFrameAttribution(this.uiSourceCode, header.frameId);
|
|
155
|
+
this.#project.addUISourceCodeWithProvider(this.uiSourceCode, this, metadata, 'text/css');
|
|
156
|
+
this.#eventListeners = [
|
|
157
|
+
this.uiSourceCode.addEventListener(Workspace.UISourceCode.Events.WorkingCopyChanged, this.workingCopyChanged, this),
|
|
158
|
+
this.uiSourceCode.addEventListener(Workspace.UISourceCode.Events.WorkingCopyCommitted, this.workingCopyCommitted, this),
|
|
159
|
+
];
|
|
160
|
+
}
|
|
161
|
+
addHeader(header) {
|
|
162
|
+
this.headers.add(header);
|
|
163
|
+
NetworkProject.addFrameAttribution(this.uiSourceCode, header.frameId);
|
|
164
|
+
}
|
|
165
|
+
removeHeader(header) {
|
|
166
|
+
this.headers.delete(header);
|
|
167
|
+
NetworkProject.removeFrameAttribution(this.uiSourceCode, header.frameId);
|
|
168
|
+
}
|
|
169
|
+
styleSheetChanged(header) {
|
|
170
|
+
console.assert(this.headers.has(header));
|
|
171
|
+
if (this.#isUpdatingHeaders || !this.headers.has(header)) {
|
|
172
|
+
return;
|
|
173
|
+
}
|
|
174
|
+
const mirrorContentBound = this.mirrorContent.bind(this, header, true /* majorChange */);
|
|
175
|
+
void this.#throttler.schedule(mirrorContentBound, "Default" /* Common.Throttler.Scheduling.DEFAULT */);
|
|
176
|
+
}
|
|
177
|
+
workingCopyCommitted() {
|
|
178
|
+
if (this.#isAddingRevision) {
|
|
179
|
+
return;
|
|
180
|
+
}
|
|
181
|
+
const mirrorContentBound = this.mirrorContent.bind(this, this.uiSourceCode, true /* majorChange */);
|
|
182
|
+
void this.#throttler.schedule(mirrorContentBound, "AsSoonAsPossible" /* Common.Throttler.Scheduling.AS_SOON_AS_POSSIBLE */);
|
|
183
|
+
}
|
|
184
|
+
workingCopyChanged() {
|
|
185
|
+
if (this.#isAddingRevision) {
|
|
186
|
+
return;
|
|
187
|
+
}
|
|
188
|
+
const mirrorContentBound = this.mirrorContent.bind(this, this.uiSourceCode, false /* majorChange */);
|
|
189
|
+
void this.#throttler.schedule(mirrorContentBound, "Default" /* Common.Throttler.Scheduling.DEFAULT */);
|
|
190
|
+
}
|
|
191
|
+
async mirrorContent(fromProvider, majorChange) {
|
|
192
|
+
if (this.#terminated) {
|
|
193
|
+
this.styleFileSyncedForTest();
|
|
194
|
+
return;
|
|
195
|
+
}
|
|
196
|
+
let newContent = null;
|
|
197
|
+
if (fromProvider === this.uiSourceCode) {
|
|
198
|
+
newContent = this.uiSourceCode.workingCopy();
|
|
199
|
+
}
|
|
200
|
+
else {
|
|
201
|
+
newContent = TextUtils.ContentData.ContentData.textOr(await fromProvider.requestContentData(), null);
|
|
202
|
+
}
|
|
203
|
+
if (newContent === null || this.#terminated) {
|
|
204
|
+
this.styleFileSyncedForTest();
|
|
205
|
+
return;
|
|
206
|
+
}
|
|
207
|
+
if (fromProvider !== this.uiSourceCode) {
|
|
208
|
+
this.#isAddingRevision = true;
|
|
209
|
+
this.uiSourceCode.setWorkingCopy(newContent);
|
|
210
|
+
this.#isAddingRevision = false;
|
|
211
|
+
}
|
|
212
|
+
this.#isUpdatingHeaders = true;
|
|
213
|
+
const promises = [];
|
|
214
|
+
for (const header of this.headers) {
|
|
215
|
+
if (header === fromProvider) {
|
|
216
|
+
continue;
|
|
217
|
+
}
|
|
218
|
+
promises.push(this.#cssModel.setStyleSheetText(header.id, newContent, majorChange));
|
|
219
|
+
}
|
|
220
|
+
// ------ ASYNC ------
|
|
221
|
+
await Promise.all(promises);
|
|
222
|
+
this.#isUpdatingHeaders = false;
|
|
223
|
+
this.styleFileSyncedForTest();
|
|
224
|
+
}
|
|
225
|
+
styleFileSyncedForTest() {
|
|
226
|
+
}
|
|
227
|
+
dispose() {
|
|
228
|
+
if (this.#terminated) {
|
|
229
|
+
return;
|
|
230
|
+
}
|
|
231
|
+
this.#terminated = true;
|
|
232
|
+
this.#project.removeUISourceCode(this.uiSourceCode.url());
|
|
233
|
+
Common.EventTarget.removeEventListeners(this.#eventListeners);
|
|
234
|
+
}
|
|
235
|
+
contentURL() {
|
|
236
|
+
console.assert(this.headers.size > 0);
|
|
237
|
+
return this.#firstHeader().originalContentProvider().contentURL();
|
|
238
|
+
}
|
|
239
|
+
contentType() {
|
|
240
|
+
console.assert(this.headers.size > 0);
|
|
241
|
+
return this.#firstHeader().originalContentProvider().contentType();
|
|
242
|
+
}
|
|
243
|
+
requestContentData() {
|
|
244
|
+
console.assert(this.headers.size > 0);
|
|
245
|
+
return this.#firstHeader().originalContentProvider().requestContentData();
|
|
246
|
+
}
|
|
247
|
+
searchInContent(query, caseSensitive, isRegex) {
|
|
248
|
+
console.assert(this.headers.size > 0);
|
|
249
|
+
return this.#firstHeader().originalContentProvider().searchInContent(query, caseSensitive, isRegex);
|
|
250
|
+
}
|
|
251
|
+
#firstHeader() {
|
|
252
|
+
console.assert(this.headers.size > 0);
|
|
253
|
+
return this.headers.values().next().value;
|
|
254
|
+
}
|
|
255
|
+
getHeaders() {
|
|
256
|
+
return this.headers;
|
|
257
|
+
}
|
|
258
|
+
getUiSourceCode() {
|
|
259
|
+
return this.uiSourceCode;
|
|
260
|
+
}
|
|
261
|
+
addSourceMap(sourceUrl, sourceMapUrl) {
|
|
262
|
+
const sourceMapManager = this.#cssModel.sourceMapManager();
|
|
263
|
+
this.headers.forEach(header => {
|
|
264
|
+
sourceMapManager.detachSourceMap(header);
|
|
265
|
+
sourceMapManager.attachSourceMap(header, sourceUrl, sourceMapUrl);
|
|
266
|
+
});
|
|
267
|
+
}
|
|
268
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
// Copyright 2013 The Chromium Authors
|
|
2
|
+
// Use of this source code is governed by a BSD-style license that can be
|
|
3
|
+
// found in the LICENSE file.
|
|
4
|
+
import * as Common from '../../core/common/common.js';
|
|
5
|
+
import { ChunkedFileReader } from './FileUtils.js';
|
|
6
|
+
export class TempFile {
|
|
7
|
+
#lastBlob;
|
|
8
|
+
constructor() {
|
|
9
|
+
this.#lastBlob = null;
|
|
10
|
+
}
|
|
11
|
+
write(pieces) {
|
|
12
|
+
if (this.#lastBlob) {
|
|
13
|
+
pieces.unshift(this.#lastBlob);
|
|
14
|
+
}
|
|
15
|
+
this.#lastBlob = new Blob(pieces, { type: 'text/plain' });
|
|
16
|
+
}
|
|
17
|
+
read() {
|
|
18
|
+
return this.readRange();
|
|
19
|
+
}
|
|
20
|
+
size() {
|
|
21
|
+
return this.#lastBlob ? this.#lastBlob.size : 0;
|
|
22
|
+
}
|
|
23
|
+
async readRange(startOffset, endOffset) {
|
|
24
|
+
if (!this.#lastBlob) {
|
|
25
|
+
Common.Console.Console.instance().error('Attempt to read a temp file that was never written');
|
|
26
|
+
return '';
|
|
27
|
+
}
|
|
28
|
+
const blob = typeof startOffset === 'number' || typeof endOffset === 'number' ?
|
|
29
|
+
this.#lastBlob.slice(startOffset, endOffset) :
|
|
30
|
+
this.#lastBlob;
|
|
31
|
+
const reader = new FileReader();
|
|
32
|
+
try {
|
|
33
|
+
await new Promise((resolve, reject) => {
|
|
34
|
+
reader.onloadend = resolve;
|
|
35
|
+
reader.onerror = reject;
|
|
36
|
+
reader.readAsText(blob);
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
catch (error) {
|
|
40
|
+
Common.Console.Console.instance().error('Failed to read from temp file: ' + error.message);
|
|
41
|
+
}
|
|
42
|
+
return reader.result;
|
|
43
|
+
}
|
|
44
|
+
async copyToOutputStream(outputStream, progress) {
|
|
45
|
+
if (!this.#lastBlob) {
|
|
46
|
+
void outputStream.close();
|
|
47
|
+
return null;
|
|
48
|
+
}
|
|
49
|
+
const reader = new ChunkedFileReader(this.#lastBlob, 10 * 1000 * 1000, progress);
|
|
50
|
+
return await reader.read(outputStream).then(success => success ? null : reader.error());
|
|
51
|
+
}
|
|
52
|
+
remove() {
|
|
53
|
+
this.#lastBlob = null;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
// Copyright 2019 The Chromium Authors
|
|
2
|
+
// Use of this source code is governed by a BSD-style license that can be
|
|
3
|
+
// found in the LICENSE file.
|
|
4
|
+
import * as CompilerScriptMapping from './CompilerScriptMapping.js';
|
|
5
|
+
import * as ContentProviderBasedProject from './ContentProviderBasedProject.js';
|
|
6
|
+
import * as CSSWorkspaceBinding from './CSSWorkspaceBinding.js';
|
|
7
|
+
import * as DebuggerLanguagePlugins from './DebuggerLanguagePlugins.js';
|
|
8
|
+
import * as DebuggerWorkspaceBinding from './DebuggerWorkspaceBinding.js';
|
|
9
|
+
import * as DefaultScriptMapping from './DefaultScriptMapping.js';
|
|
10
|
+
import * as FileUtils from './FileUtils.js';
|
|
11
|
+
import * as LiveLocation from './LiveLocation.js';
|
|
12
|
+
import * as NetworkProject from './NetworkProject.js';
|
|
13
|
+
import * as PresentationConsoleMessageHelper from './PresentationConsoleMessageHelper.js';
|
|
14
|
+
import * as ResourceMapping from './ResourceMapping.js';
|
|
15
|
+
import * as ResourceScriptMapping from './ResourceScriptMapping.js';
|
|
16
|
+
import * as ResourceUtils from './ResourceUtils.js';
|
|
17
|
+
import * as SASSSourceMapping from './SASSSourceMapping.js';
|
|
18
|
+
import * as StylesSourceMapping from './StylesSourceMapping.js';
|
|
19
|
+
import * as TempFile from './TempFile.js';
|
|
20
|
+
export { CompilerScriptMapping, ContentProviderBasedProject, CSSWorkspaceBinding, DebuggerLanguagePlugins, DebuggerWorkspaceBinding, DefaultScriptMapping, FileUtils, LiveLocation, NetworkProject, PresentationConsoleMessageHelper, ResourceMapping, ResourceScriptMapping, ResourceUtils, SASSSourceMapping, StylesSourceMapping, TempFile, };
|
package/build/node_modules/chrome-devtools-frontend/front_end/models/crux-manager/CrUXManager.js
ADDED
|
@@ -0,0 +1,283 @@
|
|
|
1
|
+
// Copyright 2024 The Chromium Authors
|
|
2
|
+
// Use of this source code is governed by a BSD-style license that can be
|
|
3
|
+
// found in the LICENSE file.
|
|
4
|
+
import * as Common from '../../core/common/common.js';
|
|
5
|
+
import * as i18n from '../../core/i18n/i18n.js';
|
|
6
|
+
import * as Root from '../../core/root/root.js';
|
|
7
|
+
import * as SDK from '../../core/sdk/sdk.js';
|
|
8
|
+
import * as EmulationModel from '../../models/emulation/emulation.js';
|
|
9
|
+
const UIStrings = {
|
|
10
|
+
/**
|
|
11
|
+
* @description Warning message indicating that the user will see real user data for a URL which is different from the URL they are currently looking at.
|
|
12
|
+
*/
|
|
13
|
+
fieldOverrideWarning: 'Field metrics are configured for a different URL than the current page.',
|
|
14
|
+
};
|
|
15
|
+
const str_ = i18n.i18n.registerUIStrings('models/crux-manager/CrUXManager.ts', UIStrings);
|
|
16
|
+
const i18nString = i18n.i18n.getLocalizedString.bind(undefined, str_);
|
|
17
|
+
// This key is expected to be visible in the frontend.
|
|
18
|
+
// b/349721878
|
|
19
|
+
const CRUX_API_KEY = 'AIzaSyCCSOx25vrb5z0tbedCB3_JRzzbVW6Uwgw';
|
|
20
|
+
const DEFAULT_ENDPOINT = `https://chromeuxreport.googleapis.com/v1/records:queryRecord?key=${CRUX_API_KEY}`;
|
|
21
|
+
let cruxManagerInstance;
|
|
22
|
+
// TODO: Potentially support `TABLET`. Tablet field data will always be `null` until then.
|
|
23
|
+
export const DEVICE_SCOPE_LIST = ['ALL', 'DESKTOP', 'PHONE'];
|
|
24
|
+
const pageScopeList = ['origin', 'url'];
|
|
25
|
+
const metrics = [
|
|
26
|
+
'first_contentful_paint',
|
|
27
|
+
'largest_contentful_paint',
|
|
28
|
+
'cumulative_layout_shift',
|
|
29
|
+
'interaction_to_next_paint',
|
|
30
|
+
'round_trip_time',
|
|
31
|
+
'form_factors',
|
|
32
|
+
'largest_contentful_paint_image_time_to_first_byte',
|
|
33
|
+
'largest_contentful_paint_image_resource_load_delay',
|
|
34
|
+
'largest_contentful_paint_image_resource_load_duration',
|
|
35
|
+
'largest_contentful_paint_image_element_render_delay',
|
|
36
|
+
];
|
|
37
|
+
export class CrUXManager extends Common.ObjectWrapper.ObjectWrapper {
|
|
38
|
+
#originCache = new Map();
|
|
39
|
+
#urlCache = new Map();
|
|
40
|
+
#mainDocumentUrl;
|
|
41
|
+
#configSetting;
|
|
42
|
+
#endpoint = DEFAULT_ENDPOINT;
|
|
43
|
+
#pageResult;
|
|
44
|
+
fieldDeviceOption = 'AUTO';
|
|
45
|
+
fieldPageScope = 'url';
|
|
46
|
+
constructor() {
|
|
47
|
+
super();
|
|
48
|
+
/**
|
|
49
|
+
* In an incognito or guest window - which is called an "OffTheRecord"
|
|
50
|
+
* profile in Chromium -, we do not want to persist the user consent and
|
|
51
|
+
* should ask for it every time. This is why we see what window type the
|
|
52
|
+
* user is in before choosing where to look/create this setting. If the
|
|
53
|
+
* user is in OTR, we store it in the session, which uses sessionStorage
|
|
54
|
+
* and is short-lived. If the user is not in OTR, we use global, which is
|
|
55
|
+
* the default behaviour and persists the value to the Chrome profile.
|
|
56
|
+
* This behaviour has been approved by Chrome Privacy as part of the launch
|
|
57
|
+
* review.
|
|
58
|
+
*/
|
|
59
|
+
const useSessionStorage = Root.Runtime.hostConfig.isOffTheRecord === true;
|
|
60
|
+
const storageTypeForConsent = useSessionStorage ? "Session" /* Common.Settings.SettingStorageType.SESSION */ : "Global" /* Common.Settings.SettingStorageType.GLOBAL */;
|
|
61
|
+
this.#configSetting = Common.Settings.Settings.instance().createSetting('field-data', { enabled: false, override: '', originMappings: [], overrideEnabled: false }, storageTypeForConsent);
|
|
62
|
+
this.#configSetting.addChangeListener(() => {
|
|
63
|
+
void this.refresh();
|
|
64
|
+
});
|
|
65
|
+
SDK.TargetManager.TargetManager.instance().addModelListener(SDK.ResourceTreeModel.ResourceTreeModel, SDK.ResourceTreeModel.Events.FrameNavigated, this.#onFrameNavigated, this);
|
|
66
|
+
}
|
|
67
|
+
static instance(opts = { forceNew: null }) {
|
|
68
|
+
const { forceNew } = opts;
|
|
69
|
+
if (!cruxManagerInstance || forceNew) {
|
|
70
|
+
cruxManagerInstance = new CrUXManager();
|
|
71
|
+
}
|
|
72
|
+
return cruxManagerInstance;
|
|
73
|
+
}
|
|
74
|
+
/** The most recent page result from the CrUX service. */
|
|
75
|
+
get pageResult() {
|
|
76
|
+
return this.#pageResult;
|
|
77
|
+
}
|
|
78
|
+
getConfigSetting() {
|
|
79
|
+
return this.#configSetting;
|
|
80
|
+
}
|
|
81
|
+
isEnabled() {
|
|
82
|
+
return this.#configSetting.get().enabled;
|
|
83
|
+
}
|
|
84
|
+
async getFieldDataForPage(pageUrl) {
|
|
85
|
+
const pageResult = {
|
|
86
|
+
'origin-ALL': null,
|
|
87
|
+
'origin-DESKTOP': null,
|
|
88
|
+
'origin-PHONE': null,
|
|
89
|
+
'origin-TABLET': null,
|
|
90
|
+
'url-ALL': null,
|
|
91
|
+
'url-DESKTOP': null,
|
|
92
|
+
'url-PHONE': null,
|
|
93
|
+
'url-TABLET': null,
|
|
94
|
+
warnings: [],
|
|
95
|
+
};
|
|
96
|
+
try {
|
|
97
|
+
const normalizedUrl = this.#normalizeUrl(pageUrl);
|
|
98
|
+
const promises = [];
|
|
99
|
+
for (const pageScope of pageScopeList) {
|
|
100
|
+
for (const deviceScope of DEVICE_SCOPE_LIST) {
|
|
101
|
+
const promise = this.#getScopedData(normalizedUrl, pageScope, deviceScope).then(response => {
|
|
102
|
+
pageResult[`${pageScope}-${deviceScope}`] = response;
|
|
103
|
+
});
|
|
104
|
+
promises.push(promise);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
await Promise.all(promises);
|
|
108
|
+
}
|
|
109
|
+
catch (err) {
|
|
110
|
+
console.error(err);
|
|
111
|
+
}
|
|
112
|
+
finally {
|
|
113
|
+
return pageResult;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
#getMappedUrl(unmappedUrl) {
|
|
117
|
+
try {
|
|
118
|
+
const unmapped = new URL(unmappedUrl);
|
|
119
|
+
const mappings = this.#configSetting.get().originMappings || [];
|
|
120
|
+
const mapping = mappings.find(m => m.developmentOrigin === unmapped.origin);
|
|
121
|
+
if (!mapping) {
|
|
122
|
+
return unmappedUrl;
|
|
123
|
+
}
|
|
124
|
+
const mapped = new URL(mapping.productionOrigin);
|
|
125
|
+
mapped.pathname = unmapped.pathname;
|
|
126
|
+
return mapped.href;
|
|
127
|
+
}
|
|
128
|
+
catch {
|
|
129
|
+
return unmappedUrl;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
async getFieldDataForCurrentPageForTesting() {
|
|
133
|
+
return await this.#getFieldDataForCurrentPage();
|
|
134
|
+
}
|
|
135
|
+
/**
|
|
136
|
+
* In general, this function should use the main document URL
|
|
137
|
+
* (i.e. the URL after all redirects but before SPA navigations)
|
|
138
|
+
*
|
|
139
|
+
* However, we can't detect the main document URL of the current page if it's
|
|
140
|
+
* navigation occurred before DevTools was first opened. This function will fall
|
|
141
|
+
* back to the currently inspected URL (i.e. what is displayed in the omnibox) if
|
|
142
|
+
* the main document URL cannot be found.
|
|
143
|
+
*/
|
|
144
|
+
async #getFieldDataForCurrentPage() {
|
|
145
|
+
const currentUrl = this.#mainDocumentUrl || await this.#getInspectedURL();
|
|
146
|
+
const urlForCrux = this.#configSetting.get().overrideEnabled ? this.#configSetting.get().override || '' :
|
|
147
|
+
this.#getMappedUrl(currentUrl);
|
|
148
|
+
const result = await this.getFieldDataForPage(urlForCrux);
|
|
149
|
+
if (currentUrl !== urlForCrux) {
|
|
150
|
+
result.warnings.push(i18nString(UIStrings.fieldOverrideWarning));
|
|
151
|
+
}
|
|
152
|
+
return result;
|
|
153
|
+
}
|
|
154
|
+
async #getInspectedURL() {
|
|
155
|
+
const targetManager = SDK.TargetManager.TargetManager.instance();
|
|
156
|
+
let inspectedURL = targetManager.inspectedURL();
|
|
157
|
+
if (!inspectedURL) {
|
|
158
|
+
inspectedURL = await new Promise(resolve => {
|
|
159
|
+
function handler(event) {
|
|
160
|
+
const newInspectedURL = event.data.inspectedURL();
|
|
161
|
+
if (newInspectedURL) {
|
|
162
|
+
resolve(newInspectedURL);
|
|
163
|
+
targetManager.removeEventListener("InspectedURLChanged" /* SDK.TargetManager.Events.INSPECTED_URL_CHANGED */, handler);
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
targetManager.addEventListener("InspectedURLChanged" /* SDK.TargetManager.Events.INSPECTED_URL_CHANGED */, handler);
|
|
167
|
+
});
|
|
168
|
+
}
|
|
169
|
+
return inspectedURL;
|
|
170
|
+
}
|
|
171
|
+
async #onFrameNavigated(event) {
|
|
172
|
+
if (!event.data.isPrimaryFrame()) {
|
|
173
|
+
return;
|
|
174
|
+
}
|
|
175
|
+
this.#mainDocumentUrl = event.data.url;
|
|
176
|
+
await this.refresh();
|
|
177
|
+
}
|
|
178
|
+
async refresh() {
|
|
179
|
+
// This does 2 things:
|
|
180
|
+
// - Tells listeners to clear old data so it isn't shown during a URL transition
|
|
181
|
+
// - Tells listeners to clear old data when field data is disabled.
|
|
182
|
+
this.#pageResult = undefined;
|
|
183
|
+
this.dispatchEventToListeners("field-data-changed" /* Events.FIELD_DATA_CHANGED */, undefined);
|
|
184
|
+
if (!this.#configSetting.get().enabled) {
|
|
185
|
+
return;
|
|
186
|
+
}
|
|
187
|
+
this.#pageResult = await this.#getFieldDataForCurrentPage();
|
|
188
|
+
this.dispatchEventToListeners("field-data-changed" /* Events.FIELD_DATA_CHANGED */, this.#pageResult);
|
|
189
|
+
}
|
|
190
|
+
#normalizeUrl(inputUrl) {
|
|
191
|
+
const normalizedUrl = new URL(inputUrl);
|
|
192
|
+
normalizedUrl.hash = '';
|
|
193
|
+
normalizedUrl.search = '';
|
|
194
|
+
return normalizedUrl;
|
|
195
|
+
}
|
|
196
|
+
async #getScopedData(normalizedUrl, pageScope, deviceScope) {
|
|
197
|
+
const { origin, href: url, hostname } = normalizedUrl;
|
|
198
|
+
if (hostname === 'localhost' || hostname === '127.0.0.1' || !origin.startsWith('http')) {
|
|
199
|
+
return null;
|
|
200
|
+
}
|
|
201
|
+
const cache = pageScope === 'origin' ? this.#originCache : this.#urlCache;
|
|
202
|
+
const cacheKey = pageScope === 'origin' ? `${origin}-${deviceScope}` : `${url}-${deviceScope}`;
|
|
203
|
+
const cachedResponse = cache.get(cacheKey);
|
|
204
|
+
if (cachedResponse !== undefined) {
|
|
205
|
+
return cachedResponse;
|
|
206
|
+
}
|
|
207
|
+
// We shouldn't cache the result in the case of an error
|
|
208
|
+
// The error could be a transient issue with the network/CrUX server/etc.
|
|
209
|
+
try {
|
|
210
|
+
const formFactor = deviceScope === 'ALL' ? undefined : deviceScope;
|
|
211
|
+
const result = pageScope === 'origin' ? await this.#makeRequest({ origin, metrics, formFactor }) :
|
|
212
|
+
await this.#makeRequest({ url, metrics, formFactor });
|
|
213
|
+
cache.set(cacheKey, result);
|
|
214
|
+
return result;
|
|
215
|
+
}
|
|
216
|
+
catch (err) {
|
|
217
|
+
console.error(err);
|
|
218
|
+
return null;
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
async #makeRequest(request) {
|
|
222
|
+
const body = JSON.stringify(request);
|
|
223
|
+
const response = await fetch(this.#endpoint, {
|
|
224
|
+
method: 'POST',
|
|
225
|
+
body,
|
|
226
|
+
});
|
|
227
|
+
if (!response.ok && response.status !== 404) {
|
|
228
|
+
throw new Error(`Failed to fetch data from CrUX server (Status code: ${response.status})`);
|
|
229
|
+
}
|
|
230
|
+
const responseData = await response.json();
|
|
231
|
+
if (response.status === 404) {
|
|
232
|
+
// This is how CrUX tells us that there is not data available for the provided url/origin
|
|
233
|
+
// Since it's a valid response, just return null instead of throwing an error.
|
|
234
|
+
if (responseData?.error?.status === 'NOT_FOUND') {
|
|
235
|
+
return null;
|
|
236
|
+
}
|
|
237
|
+
throw new Error(`Failed to fetch data from CrUX server (Status code: ${response.status})`);
|
|
238
|
+
}
|
|
239
|
+
if (!('record' in responseData)) {
|
|
240
|
+
throw new Error(`Failed to find data in CrUX response: ${JSON.stringify(responseData)}`);
|
|
241
|
+
}
|
|
242
|
+
return responseData;
|
|
243
|
+
}
|
|
244
|
+
#getAutoDeviceScope() {
|
|
245
|
+
const emulationModel = EmulationModel.DeviceModeModel.DeviceModeModel.tryInstance();
|
|
246
|
+
if (emulationModel === null) {
|
|
247
|
+
return 'ALL';
|
|
248
|
+
}
|
|
249
|
+
if (emulationModel.isMobile()) {
|
|
250
|
+
if (this.#pageResult?.[`${this.fieldPageScope}-PHONE`]) {
|
|
251
|
+
return 'PHONE';
|
|
252
|
+
}
|
|
253
|
+
return 'ALL';
|
|
254
|
+
}
|
|
255
|
+
if (this.#pageResult?.[`${this.fieldPageScope}-DESKTOP`]) {
|
|
256
|
+
return 'DESKTOP';
|
|
257
|
+
}
|
|
258
|
+
return 'ALL';
|
|
259
|
+
}
|
|
260
|
+
resolveDeviceOptionToScope(option) {
|
|
261
|
+
return option === 'AUTO' ? this.#getAutoDeviceScope() : option;
|
|
262
|
+
}
|
|
263
|
+
getSelectedDeviceScope() {
|
|
264
|
+
return this.resolveDeviceOptionToScope(this.fieldDeviceOption);
|
|
265
|
+
}
|
|
266
|
+
getSelectedScope() {
|
|
267
|
+
return { pageScope: this.fieldPageScope, deviceScope: this.getSelectedDeviceScope() };
|
|
268
|
+
}
|
|
269
|
+
getSelectedFieldResponse() {
|
|
270
|
+
const pageScope = this.fieldPageScope;
|
|
271
|
+
const deviceScope = this.getSelectedDeviceScope();
|
|
272
|
+
return this.getFieldResponse(pageScope, deviceScope);
|
|
273
|
+
}
|
|
274
|
+
getSelectedFieldMetricData(fieldMetric) {
|
|
275
|
+
return this.getSelectedFieldResponse()?.record.metrics[fieldMetric];
|
|
276
|
+
}
|
|
277
|
+
getFieldResponse(pageScope, deviceScope) {
|
|
278
|
+
return this.#pageResult?.[`${pageScope}-${deviceScope}`];
|
|
279
|
+
}
|
|
280
|
+
setEndpointForTesting(endpoint) {
|
|
281
|
+
this.#endpoint = endpoint;
|
|
282
|
+
}
|
|
283
|
+
}
|