UNPKG

@quick-game/cli

Version:

Command line interface for rapid qg development

352 lines 15.9 kB
// Copyright 2021 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. import * as TextUtils from '../../models/text_utils/text_utils.js'; import * as Common from '../common/common.js'; import * as i18n from '../i18n/i18n.js'; import { Location, COND_BREAKPOINT_SOURCE_URL, LOGPOINT_SOURCE_URL, Events, } from './DebuggerModel.js'; import { ResourceTreeModel } from './ResourceTreeModel.js'; const UIStrings = { /** *@description Error message for when a script can't be loaded which had been previously */ scriptRemovedOrDeleted: 'Script removed or deleted.', /** *@description Error message when failing to load a script source text */ unableToFetchScriptSource: 'Unable to fetch script source.', }; const str_ = i18n.i18n.registerUIStrings('core/sdk/Script.ts', UIStrings); const i18nString = i18n.i18n.getLocalizedString.bind(undefined, str_); let scriptCacheInstance = null; export class Script { debuggerModel; scriptId; sourceURL; lineOffset; columnOffset; endLine; endColumn; executionContextId; hash; #isContentScriptInternal; #isLiveEditInternal; sourceMapURL; debugSymbols; hasSourceURL; contentLength; originStackTrace; #codeOffsetInternal; #language; #contentPromise; #embedderNameInternal; isModule; constructor(debuggerModel, scriptId, sourceURL, startLine, startColumn, endLine, endColumn, executionContextId, hash, isContentScript, isLiveEdit, sourceMapURL, hasSourceURL, length, isModule, originStackTrace, codeOffset, scriptLanguage, debugSymbols, embedderName) { this.debuggerModel = debuggerModel; this.scriptId = scriptId; this.sourceURL = sourceURL; this.lineOffset = startLine; this.columnOffset = startColumn; this.endLine = endLine; this.endColumn = endColumn; this.isModule = isModule; this.executionContextId = executionContextId; this.hash = hash; this.#isContentScriptInternal = isContentScript; this.#isLiveEditInternal = isLiveEdit; this.sourceMapURL = sourceMapURL; this.debugSymbols = debugSymbols; this.hasSourceURL = hasSourceURL; this.contentLength = length; this.originStackTrace = originStackTrace; this.#codeOffsetInternal = codeOffset; this.#language = scriptLanguage; this.#contentPromise = null; this.#embedderNameInternal = embedderName; } embedderName() { return this.#embedderNameInternal; } target() { return this.debuggerModel.target(); } static trimSourceURLComment(source) { let sourceURLIndex = source.lastIndexOf('//# sourceURL='); if (sourceURLIndex === -1) { sourceURLIndex = source.lastIndexOf('//@ sourceURL='); if (sourceURLIndex === -1) { return source; } } const sourceURLLineIndex = source.lastIndexOf('\n', sourceURLIndex); if (sourceURLLineIndex === -1) { return source; } const sourceURLLine = source.substr(sourceURLLineIndex + 1); if (!sourceURLLine.match(sourceURLRegex)) { return source; } return source.substr(0, sourceURLLineIndex); } isContentScript() { return this.#isContentScriptInternal; } codeOffset() { return this.#codeOffsetInternal; } isJavaScript() { return this.#language === "JavaScript" /* Protocol.Debugger.ScriptLanguage.JavaScript */; } isWasm() { return this.#language === "WebAssembly" /* Protocol.Debugger.ScriptLanguage.WebAssembly */; } scriptLanguage() { return this.#language; } executionContext() { return this.debuggerModel.runtimeModel().executionContext(this.executionContextId); } isLiveEdit() { return this.#isLiveEditInternal; } contentURL() { return this.sourceURL; } contentType() { return Common.ResourceType.resourceTypes.Script; } async loadTextContent() { const result = await this.debuggerModel.target().debuggerAgent().invoke_getScriptSource({ scriptId: this.scriptId }); if (result.getError()) { throw new Error(result.getError()); } const { scriptSource, bytecode } = result; if (bytecode) { return { content: bytecode, isEncoded: true }; } let content = scriptSource || ''; if (this.hasSourceURL && this.sourceURL.startsWith('snippet://')) { // TODO(crbug.com/1330846): Find a better way to establish the snippet automapping binding then adding // a sourceURL comment before evaluation and removing it here. content = Script.trimSourceURLComment(content); } return { content, isEncoded: false }; } async loadWasmContent() { if (!this.isWasm()) { throw new Error('Not a wasm script'); } const result = await this.debuggerModel.target().debuggerAgent().invoke_disassembleWasmModule({ scriptId: this.scriptId }); if (result.getError()) { // Fall through to text content loading if v8-based disassembly fails. This is to ensure backwards compatibility with // older v8 versions; return this.loadTextContent(); } const { streamId, functionBodyOffsets, chunk: { lines, bytecodeOffsets } } = result; const lineChunks = []; const bytecodeOffsetChunks = []; let totalLength = lines.reduce((sum, line) => sum + line.length + 1, 0); const truncationMessage = '<truncated>'; // This is a magic number used in code mirror which, when exceeded, sends it into an infinite loop. const cmSizeLimit = 1000000000 - truncationMessage.length; if (streamId) { while (true) { const result = await this.debuggerModel.target().debuggerAgent().invoke_nextWasmDisassemblyChunk({ streamId }); if (result.getError()) { throw new Error(result.getError()); } const { chunk: { lines: linesChunk, bytecodeOffsets: bytecodeOffsetsChunk } } = result; totalLength += linesChunk.reduce((sum, line) => sum + line.length + 1, 0); if (linesChunk.length === 0) { break; } if (totalLength >= cmSizeLimit) { lineChunks.push([truncationMessage]); bytecodeOffsetChunks.push([0]); break; } lineChunks.push(linesChunk); bytecodeOffsetChunks.push(bytecodeOffsetsChunk); } } const functionBodyRanges = []; // functionBodyOffsets contains a sequence of pairs of start and end offsets for (let i = 0; i < functionBodyOffsets.length; i += 2) { functionBodyRanges.push({ start: functionBodyOffsets[i], end: functionBodyOffsets[i + 1] }); } const wasmDisassemblyInfo = new Common.WasmDisassembly.WasmDisassembly(lines.concat(...lineChunks), bytecodeOffsets.concat(...bytecodeOffsetChunks), functionBodyRanges); return { content: '', isEncoded: false, wasmDisassemblyInfo }; } requestContent() { if (!this.#contentPromise) { const fileSizeToCache = 65535; // We won't bother cacheing files under 64K if (this.hash && !this.#isLiveEditInternal && this.contentLength > fileSizeToCache) { // For large files that aren't live edits and have a hash, we keep a content-addressed cache // so we don't need to load multiple copies or disassemble wasm modules multiple times. if (!scriptCacheInstance) { // Initialize script cache singleton. Add a finalizer for removing keys from the map. scriptCacheInstance = { cache: new Map(), registry: new FinalizationRegistry(hashCode => scriptCacheInstance?.cache.delete(hashCode)), }; } // This key should be sufficient to identify scripts that are known to have the same content. const fullHash = [ this.#language, this.contentLength, this.lineOffset, this.columnOffset, this.endLine, this.endColumn, this.#codeOffsetInternal, this.hash, ].join(':'); const cachedContentPromise = scriptCacheInstance.cache.get(fullHash)?.deref(); if (cachedContentPromise) { this.#contentPromise = cachedContentPromise; } else { this.#contentPromise = this.requestContentInternal(); scriptCacheInstance.cache.set(fullHash, new WeakRef(this.#contentPromise)); scriptCacheInstance.registry.register(this.#contentPromise, fullHash); } } else { this.#contentPromise = this.requestContentInternal(); } } return this.#contentPromise; } async requestContentInternal() { if (!this.scriptId) { return { content: null, error: i18nString(UIStrings.scriptRemovedOrDeleted), isEncoded: false }; } try { return this.isWasm() ? await this.loadWasmContent() : await this.loadTextContent(); } catch (err) { // TODO(bmeurer): Propagate errors as exceptions / rejections. return { content: null, error: i18nString(UIStrings.unableToFetchScriptSource), isEncoded: false }; } } async getWasmBytecode() { const base64 = await this.debuggerModel.target().debuggerAgent().invoke_getWasmBytecode({ scriptId: this.scriptId }); const response = await fetch(`data:application/wasm;base64,${base64.bytecode}`); return response.arrayBuffer(); } originalContentProvider() { return new TextUtils.StaticContentProvider.StaticContentProvider(this.contentURL(), this.contentType(), () => this.requestContent()); } async searchInContent(query, caseSensitive, isRegex) { if (!this.scriptId) { return []; } const matches = await this.debuggerModel.target().debuggerAgent().invoke_searchInContent({ scriptId: this.scriptId, query, caseSensitive, isRegex }); return (matches.result || []) .map(match => new TextUtils.ContentProvider.SearchMatch(match.lineNumber, match.lineContent)); } appendSourceURLCommentIfNeeded(source) { if (!this.hasSourceURL) { return source; } return source + '\n //# sourceURL=' + this.sourceURL; } async editSource(newSource) { newSource = Script.trimSourceURLComment(newSource); // We append correct #sourceURL to script for consistency only. It's not actually needed for things to work correctly. newSource = this.appendSourceURLCommentIfNeeded(newSource); const { content: oldSource } = await this.requestContent(); if (oldSource === newSource) { return { changed: false, status: "Ok" /* Protocol.Debugger.SetScriptSourceResponseStatus.Ok */ }; } const response = await this.debuggerModel.target().debuggerAgent().invoke_setScriptSource({ scriptId: this.scriptId, scriptSource: newSource, allowTopFrameEditing: true }); if (response.getError()) { // Something went seriously wrong, like the V8 inspector no longer knowing about this script without // shutting down the Debugger agent etc. throw new Error(`Script#editSource failed for script with id ${this.scriptId}: ${response.getError()}`); } if (!response.getError() && response.status === "Ok" /* Protocol.Debugger.SetScriptSourceResponseStatus.Ok */) { this.#contentPromise = Promise.resolve({ content: newSource, isEncoded: false }); } this.debuggerModel.dispatchEventToListeners(Events.ScriptSourceWasEdited, { script: this, status: response.status }); return { changed: true, status: response.status, exceptionDetails: response.exceptionDetails }; } rawLocation(lineNumber, columnNumber) { if (this.containsLocation(lineNumber, columnNumber)) { return new Location(this.debuggerModel, this.scriptId, lineNumber, columnNumber); } return null; } isInlineScript() { const startsAtZero = !this.lineOffset && !this.columnOffset; return !this.isWasm() && Boolean(this.sourceURL) && !startsAtZero; } isAnonymousScript() { return !this.sourceURL; } async setBlackboxedRanges(positions) { const response = await this.debuggerModel.target().debuggerAgent().invoke_setBlackboxedRanges({ scriptId: this.scriptId, positions }); return !response.getError(); } containsLocation(lineNumber, columnNumber) { const afterStart = (lineNumber === this.lineOffset && columnNumber >= this.columnOffset) || lineNumber > this.lineOffset; const beforeEnd = lineNumber < this.endLine || (lineNumber === this.endLine && columnNumber <= this.endColumn); return afterStart && beforeEnd; } get frameId() { // TODO(crbug.com/1172300) Ignored during the jsdoc to ts migration // @ts-expect-error if (typeof this[frameIdSymbol] !== 'string') { // TODO(crbug.com/1172300) Ignored during the jsdoc to ts migration // @ts-expect-error this[frameIdSymbol] = frameIdForScript(this); } // TODO(crbug.com/1172300) Ignored during the jsdoc to ts migration // @ts-expect-error return this[frameIdSymbol]; } /** * @returns true, iff this script originates from a breakpoint/logpoint condition */ get isBreakpointCondition() { return [COND_BREAKPOINT_SOURCE_URL, LOGPOINT_SOURCE_URL].includes(this.sourceURL); } createPageResourceLoadInitiator() { return { target: this.target(), frameId: this.frameId, initiatorUrl: this.embedderName() }; } rawLocationToRelativeLocation(rawLocation) { let { lineNumber, columnNumber } = rawLocation; if (!this.hasSourceURL && this.isInlineScript()) { lineNumber -= this.lineOffset; if (lineNumber === 0 && columnNumber !== undefined) { columnNumber -= this.columnOffset; } } return { lineNumber, columnNumber }; } relativeLocationToRawLocation(relativeLocation) { let { lineNumber, columnNumber } = relativeLocation; if (!this.hasSourceURL && this.isInlineScript()) { if (lineNumber === 0 && columnNumber !== undefined) { columnNumber += this.columnOffset; } lineNumber += this.lineOffset; } return { lineNumber, columnNumber }; } } const frameIdSymbol = Symbol('frameid'); function frameIdForScript(script) { const executionContext = script.executionContext(); if (executionContext) { return executionContext.frameId || null; } // This is to overcome compilation cache which doesn't get reset. const resourceTreeModel = script.debuggerModel.target().model(ResourceTreeModel); if (!resourceTreeModel || !resourceTreeModel.mainFrame) { return null; } return resourceTreeModel.mainFrame.id; } export const sourceURLRegex = /^[\040\t]*\/\/[@#] sourceURL=\s*(\S*?)\s*$/; //# sourceMappingURL=Script.js.map