UNPKG

16.9 kBPlain TextView Raw
1/*
2 * Copyright (C) 2012 Google Inc. All rights reserved.
3 *
4 * Redistribution and use in source and binary forms, with or without
5 * modification, are permitted provided that the following conditions are
6 * met:
7 *
8 * * Redistributions of source code must retain the above copyright
9 * notice, this list of conditions and the following disclaimer.
10 * * Redistributions in binary form must reproduce the above
11 * copyright notice, this list of conditions and the following disclaimer
12 * in the documentation and/or other materials provided with the
13 * distribution.
14 * * Neither the name of Google Inc. nor the names of its
15 * contributors may be used to endorse or promote products derived from
16 * this software without specific prior written permission.
17 *
18 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
19 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
20 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
21 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
22 * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
23 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
24 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
25 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
26 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
27 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
28 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
29 */
30
31import * as Common from '../../core/common/common.js';
32import * as i18n from '../../core/i18n/i18n.js';
33import * as SDK from '../../core/sdk/sdk.js';
34import * as Workspace from '../workspace/workspace.js';
35import type * as Protocol from '../../generated/protocol.js';
36
37import type {Breakpoint} from './BreakpointManager.js';
38import {BreakpointManager} from './BreakpointManager.js';
39import {ContentProviderBasedProject} from './ContentProviderBasedProject.js';
40import type {DebuggerSourceMapping, DebuggerWorkspaceBinding} from './DebuggerWorkspaceBinding.js';
41import {NetworkProject} from './NetworkProject.js';
42import {metadataForURL} from './ResourceUtils.js';
43
44const UIStrings = {
45 /**
46 *@description Error text displayed in the console when editing a live script fails. LiveEdit is
47 *the name of the feature for editing code that is already running.
48 *@example {warning} PH1
49 */
50 liveEditFailed: '`LiveEdit` failed: {PH1}',
51 /**
52 *@description Error text displayed in the console when compiling a live-edited script fails. LiveEdit is
53 *the name of the feature for editing code that is already running.
54 *@example {connection lost} PH1
55 */
56 liveEditCompileFailed: '`LiveEdit` compile failed: {PH1}',
57};
58const str_ = i18n.i18n.registerUIStrings('models/bindings/ResourceScriptMapping.ts', UIStrings);
59const i18nString = i18n.i18n.getLocalizedString.bind(undefined, str_);
60
61export class ResourceScriptMapping implements DebuggerSourceMapping {
62 readonly debuggerModel: SDK.DebuggerModel.DebuggerModel;
63 #workspace: Workspace.Workspace.WorkspaceImpl;
64 readonly debuggerWorkspaceBinding: DebuggerWorkspaceBinding;
65 readonly #uiSourceCodeToScriptFile: Map<Workspace.UISourceCode.UISourceCode, ResourceScriptFile>;
66 readonly #projects: Map<string, ContentProviderBasedProject>;
67 #acceptedScripts: Set<SDK.Script.Script>;
68 readonly #eventListeners: Common.EventTarget.EventDescriptor[];
69
70 constructor(
71 debuggerModel: SDK.DebuggerModel.DebuggerModel, workspace: Workspace.Workspace.WorkspaceImpl,
72 debuggerWorkspaceBinding: DebuggerWorkspaceBinding) {
73 this.debuggerModel = debuggerModel;
74 this.#workspace = workspace;
75 this.debuggerWorkspaceBinding = debuggerWorkspaceBinding;
76 this.#uiSourceCodeToScriptFile = new Map();
77
78 this.#projects = new Map();
79
80 this.#acceptedScripts = new Set();
81 const runtimeModel = debuggerModel.runtimeModel();
82 this.#eventListeners = [
83 this.debuggerModel.addEventListener(
84 SDK.DebuggerModel.Events.ParsedScriptSource,
85 event => {
86 this.parsedScriptSource(event);
87 },
88 this),
89 this.debuggerModel.addEventListener(SDK.DebuggerModel.Events.GlobalObjectCleared, this.globalObjectCleared, this),
90 runtimeModel.addEventListener(
91 SDK.RuntimeModel.Events.ExecutionContextDestroyed, this.executionContextDestroyed, this),
92 ];
93 }
94
95 private project(script: SDK.Script.Script): ContentProviderBasedProject {
96 const prefix = script.isContentScript() ? 'js:extensions:' : 'js::';
97 const projectId = prefix + this.debuggerModel.target().id() + ':' + script.frameId;
98 let project = this.#projects.get(projectId);
99 if (!project) {
100 const projectType = script.isContentScript() ? Workspace.Workspace.projectTypes.ContentScripts :
101 Workspace.Workspace.projectTypes.Network;
102 project = new ContentProviderBasedProject(
103 this.#workspace, projectId, projectType, '' /* displayName */, false /* isServiceProject */);
104 NetworkProject.setTargetForProject(project, this.debuggerModel.target());
105 this.#projects.set(projectId, project);
106 }
107 return project;
108 }
109
110 rawLocationToUILocation(rawLocation: SDK.DebuggerModel.Location): Workspace.UISourceCode.UILocation|null {
111 const script = rawLocation.script();
112 if (!script) {
113 return null;
114 }
115 const project = this.project(script);
116 const uiSourceCode = project.uiSourceCodeForURL(script.sourceURL);
117 if (!uiSourceCode) {
118 return null;
119 }
120 const scriptFile = this.#uiSourceCodeToScriptFile.get(uiSourceCode);
121 if (!scriptFile) {
122 return null;
123 }
124 if ((scriptFile.hasDivergedFromVM() && !scriptFile.isMergingToVM()) || scriptFile.isDivergingFromVM()) {
125 return null;
126 }
127 if (!scriptFile.hasScripts([script])) {
128 return null;
129 }
130 const {lineNumber, columnNumber = 0} = rawLocation;
131 return uiSourceCode.uiLocation(lineNumber, columnNumber);
132 }
133
134 uiLocationToRawLocations(uiSourceCode: Workspace.UISourceCode.UISourceCode, lineNumber: number, columnNumber: number):
135 SDK.DebuggerModel.Location[] {
136 const scriptFile = this.#uiSourceCodeToScriptFile.get(uiSourceCode);
137 if (!scriptFile) {
138 return [];
139 }
140
141 const {script} = scriptFile;
142 if (!script) {
143 return [];
144 }
145
146 return [this.debuggerModel.createRawLocation(script, lineNumber, columnNumber)];
147 }
148
149 private acceptsScript(script: SDK.Script.Script): boolean {
150 if (!script.sourceURL || script.isLiveEdit() || (script.isInlineScript() && !script.hasSourceURL)) {
151 return false;
152 }
153 // Filter out embedder injected content scripts.
154 if (script.isContentScript() && !script.hasSourceURL) {
155 const parsedURL = new Common.ParsedURL.ParsedURL(script.sourceURL);
156 if (!parsedURL.isValid) {
157 return false;
158 }
159 }
160 return true;
161 }
162
163 private async parsedScriptSource(event: Common.EventTarget.EventTargetEvent<SDK.Script.Script>): Promise<void> {
164 const script = event.data;
165 if (!this.acceptsScript(script)) {
166 return;
167 }
168 this.#acceptedScripts.add(script);
169 const originalContentProvider = script.originalContentProvider();
170
171 const url = script.sourceURL;
172 const project = this.project(script);
173
174 // Remove previous UISourceCode, if any
175 const oldUISourceCode = project.uiSourceCodeForURL(url);
176 if (oldUISourceCode) {
177 const scriptFile = this.#uiSourceCodeToScriptFile.get(oldUISourceCode);
178 if (scriptFile && scriptFile.script) {
179 await this.removeScript(scriptFile.script);
180 }
181 }
182
183 // Create UISourceCode.
184 const uiSourceCode = project.createUISourceCode(url, originalContentProvider.contentType());
185 NetworkProject.setInitialFrameAttribution(uiSourceCode, script.frameId);
186 const metadata = metadataForURL(this.debuggerModel.target(), script.frameId, url);
187
188 // Bind UISourceCode to scripts.
189 const scriptFile = new ResourceScriptFile(this, uiSourceCode, [script]);
190 this.#uiSourceCodeToScriptFile.set(uiSourceCode, scriptFile);
191
192 const mimeType = script.isWasm() ? 'application/wasm' : 'text/javascript';
193 project.addUISourceCodeWithProvider(uiSourceCode, originalContentProvider, metadata, mimeType);
194 await this.debuggerWorkspaceBinding.updateLocations(script);
195 }
196
197 scriptFile(uiSourceCode: Workspace.UISourceCode.UISourceCode): ResourceScriptFile|null {
198 return this.#uiSourceCodeToScriptFile.get(uiSourceCode) || null;
199 }
200
201 private async removeScript(script: SDK.Script.Script): Promise<void> {
202 if (!this.#acceptedScripts.has(script)) {
203 return;
204 }
205 this.#acceptedScripts.delete(script);
206 const project = this.project(script);
207 const uiSourceCode = (project.uiSourceCodeForURL(script.sourceURL) as Workspace.UISourceCode.UISourceCode);
208 const scriptFile = this.#uiSourceCodeToScriptFile.get(uiSourceCode);
209 if (scriptFile) {
210 scriptFile.dispose();
211 }
212 this.#uiSourceCodeToScriptFile.delete(uiSourceCode);
213 project.removeFile(script.sourceURL);
214 await this.debuggerWorkspaceBinding.updateLocations(script);
215 }
216
217 private executionContextDestroyed(event: Common.EventTarget.EventTargetEvent<SDK.RuntimeModel.ExecutionContext>):
218 void {
219 const executionContext = event.data;
220 const scripts = this.debuggerModel.scriptsForExecutionContext(executionContext);
221 for (const script of scripts) {
222 this.removeScript(script);
223 }
224 }
225
226 private globalObjectCleared(): void {
227 const scripts = Array.from(this.#acceptedScripts);
228 for (const script of scripts) {
229 this.removeScript(script);
230 }
231 }
232
233 resetForTest(): void {
234 const scripts = Array.from(this.#acceptedScripts);
235 for (const script of scripts) {
236 this.removeScript(script);
237 }
238 }
239
240 dispose(): void {
241 Common.EventTarget.removeEventListeners(this.#eventListeners);
242 const scripts = Array.from(this.#acceptedScripts);
243 for (const script of scripts) {
244 this.removeScript(script);
245 }
246 for (const project of this.#projects.values()) {
247 project.removeProject();
248 }
249 this.#projects.clear();
250 }
251}
252
253export class ResourceScriptFile extends Common.ObjectWrapper.ObjectWrapper<ResourceScriptFile.EventTypes> {
254 readonly #resourceScriptMapping: ResourceScriptMapping;
255 readonly #uiSourceCodeInternal: Workspace.UISourceCode.UISourceCode;
256 scriptInternal: SDK.Script.Script|undefined;
257 #scriptSource?: string|null;
258 #isDivergingFromVMInternal?: boolean;
259 #hasDivergedFromVMInternal?: boolean;
260 #isMergingToVMInternal?: boolean;
261 constructor(
262 resourceScriptMapping: ResourceScriptMapping, uiSourceCode: Workspace.UISourceCode.UISourceCode,
263 scripts: SDK.Script.Script[]) {
264 super();
265 console.assert(scripts.length > 0);
266
267 this.#resourceScriptMapping = resourceScriptMapping;
268 this.#uiSourceCodeInternal = uiSourceCode;
269
270 if (this.#uiSourceCodeInternal.contentType().isScript()) {
271 this.scriptInternal = scripts[scripts.length - 1];
272 }
273
274 this.#uiSourceCodeInternal.addEventListener(
275 Workspace.UISourceCode.Events.WorkingCopyChanged, this.workingCopyChanged, this);
276 this.#uiSourceCodeInternal.addEventListener(
277 Workspace.UISourceCode.Events.WorkingCopyCommitted, this.workingCopyCommitted, this);
278 }
279
280 hasScripts(scripts: SDK.Script.Script[]): boolean {
281 return Boolean(this.scriptInternal) && this.scriptInternal === scripts[0];
282 }
283
284 private isDiverged(): boolean {
285 if (this.#uiSourceCodeInternal.isDirty()) {
286 return true;
287 }
288 if (!this.scriptInternal) {
289 return false;
290 }
291 if (typeof this.#scriptSource === 'undefined' || this.#scriptSource === null) {
292 return false;
293 }
294 const workingCopy = this.#uiSourceCodeInternal.workingCopy();
295 if (!workingCopy) {
296 return false;
297 }
298
299 // Match ignoring sourceURL.
300 if (!workingCopy.startsWith(this.#scriptSource.trimRight())) {
301 return true;
302 }
303 const suffix = this.#uiSourceCodeInternal.workingCopy().substr(this.#scriptSource.length);
304 return Boolean(suffix.length) && !suffix.match(SDK.Script.sourceURLRegex);
305 }
306
307 private workingCopyChanged(): void {
308 this.update();
309 }
310
311 private workingCopyCommitted(): void {
312 if (this.#uiSourceCodeInternal.project().canSetFileContent()) {
313 return;
314 }
315 if (!this.scriptInternal) {
316 return;
317 }
318 const debuggerModel = this.#resourceScriptMapping.debuggerModel;
319 const breakpoints = BreakpointManager.instance()
320 .breakpointLocationsForUISourceCode(this.#uiSourceCodeInternal)
321 .map(breakpointLocation => breakpointLocation.breakpoint);
322 const source = this.#uiSourceCodeInternal.workingCopy();
323 debuggerModel.setScriptSource(this.scriptInternal.scriptId, source, (error, exceptionDetails) => {
324 this.scriptSourceWasSet(source, breakpoints, error, exceptionDetails);
325 });
326 }
327
328 async scriptSourceWasSet(
329 source: string, breakpoints: Breakpoint[], error: string|null,
330 exceptionDetails?: Protocol.Runtime.ExceptionDetails): Promise<void> {
331 if (!error && !exceptionDetails) {
332 this.#scriptSource = source;
333 }
334 await this.update();
335
336 if (!error && !exceptionDetails) {
337 // Live edit can cause #breakpoints to be in the wrong position, or to be lost altogether.
338 // If any #breakpoints were in the pre-live edit script, they need to be re-added.
339 await Promise.all(breakpoints.map(breakpoint => breakpoint.refreshInDebugger()));
340 return;
341 }
342 if (!exceptionDetails) {
343 Common.Console.Console.instance().addMessage(
344 i18nString(UIStrings.liveEditFailed, {PH1: String(error)}), Common.Console.MessageLevel.Warning);
345 return;
346 }
347 const messageText = i18nString(UIStrings.liveEditCompileFailed, {PH1: exceptionDetails.text});
348 this.#uiSourceCodeInternal.addLineMessage(
349 Workspace.UISourceCode.Message.Level.Error, messageText, exceptionDetails.lineNumber,
350 exceptionDetails.columnNumber);
351 }
352
353 private async update(): Promise<void> {
354 if (this.isDiverged() && !this.#hasDivergedFromVMInternal) {
355 await this.divergeFromVM();
356 } else if (!this.isDiverged() && this.#hasDivergedFromVMInternal) {
357 await this.mergeToVM();
358 }
359 }
360
361 private async divergeFromVM(): Promise<void> {
362 if (this.scriptInternal) {
363 this.#isDivergingFromVMInternal = true;
364 await this.#resourceScriptMapping.debuggerWorkspaceBinding.updateLocations(this.scriptInternal);
365 this.#isDivergingFromVMInternal = undefined;
366 this.#hasDivergedFromVMInternal = true;
367 this.dispatchEventToListeners(ResourceScriptFile.Events.DidDivergeFromVM);
368 }
369 }
370
371 private async mergeToVM(): Promise<void> {
372 if (this.scriptInternal) {
373 this.#hasDivergedFromVMInternal = undefined;
374 this.#isMergingToVMInternal = true;
375 await this.#resourceScriptMapping.debuggerWorkspaceBinding.updateLocations(this.scriptInternal);
376 this.#isMergingToVMInternal = undefined;
377 this.dispatchEventToListeners(ResourceScriptFile.Events.DidMergeToVM);
378 }
379 }
380
381 hasDivergedFromVM(): boolean {
382 return Boolean(this.#hasDivergedFromVMInternal);
383 }
384
385 isDivergingFromVM(): boolean {
386 return Boolean(this.#isDivergingFromVMInternal);
387 }
388
389 isMergingToVM(): boolean {
390 return Boolean(this.#isMergingToVMInternal);
391 }
392
393 checkMapping(): void {
394 if (!this.scriptInternal || typeof this.#scriptSource !== 'undefined') {
395 this.mappingCheckedForTest();
396 return;
397 }
398 this.scriptInternal.requestContent().then(deferredContent => {
399 this.#scriptSource = deferredContent.content;
400 this.update().then(() => this.mappingCheckedForTest());
401 });
402 }
403
404 private mappingCheckedForTest(): void {
405 }
406
407 dispose(): void {
408 this.#uiSourceCodeInternal.removeEventListener(
409 Workspace.UISourceCode.Events.WorkingCopyChanged, this.workingCopyChanged, this);
410 this.#uiSourceCodeInternal.removeEventListener(
411 Workspace.UISourceCode.Events.WorkingCopyCommitted, this.workingCopyCommitted, this);
412 }
413
414 addSourceMapURL(sourceMapURL: string): void {
415 if (!this.scriptInternal) {
416 return;
417 }
418 this.scriptInternal.debuggerModel.setSourceMapURL(this.scriptInternal, sourceMapURL);
419 }
420
421 hasSourceMapURL(): boolean {
422 return this.scriptInternal !== undefined && Boolean(this.scriptInternal.sourceMapURL);
423 }
424
425 get script(): SDK.Script.Script|null {
426 return this.scriptInternal || null;
427 }
428
429 get uiSourceCode(): Workspace.UISourceCode.UISourceCode {
430 return this.#uiSourceCodeInternal;
431 }
432}
433
434export namespace ResourceScriptFile {
435 export const enum Events {
436 DidMergeToVM = 'DidMergeToVM',
437 DidDivergeFromVM = 'DidDivergeFromVM',
438 }
439
440 export type EventTypes = {
441 [Events.DidMergeToVM]: void,
442 [Events.DidDivergeFromVM]: void,
443 };
444}