UNPKG

@dcloudio/uni-debugger

Version:

uni-app debugger

511 lines (461 loc) 18.4 kB
// Copyright 2016 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. /** * @implements {Persistence.MappingSystem} * @unrestricted */ Persistence.Automapping = class { /** * @param {!Workspace.Workspace} workspace * @param {function(!Persistence.AutomappingStatus)} onStatusAdded * @param {function(!Persistence.AutomappingStatus)} onStatusRemoved */ constructor(workspace, onStatusAdded, onStatusRemoved) { this._workspace = workspace; this._onStatusAdded = onStatusAdded; this._onStatusRemoved = onStatusRemoved; /** @type {!Set<!Persistence.AutomappingStatus>} */ this._statuses = new Set(); /** @type {!Map<string, !Workspace.UISourceCode>} */ this._fileSystemUISourceCodes = new Map(); this._sweepThrottler = new Common.Throttler(100); const pathEncoder = new Persistence.PathEncoder(); this._filesIndex = new Persistence.Automapping.FilePathIndex(pathEncoder); this._projectFoldersIndex = new Persistence.Automapping.FolderIndex(pathEncoder); this._activeFoldersIndex = new Persistence.Automapping.FolderIndex(pathEncoder); this._eventListeners = [ this._workspace.addEventListener( Workspace.Workspace.Events.UISourceCodeAdded, event => this._onUISourceCodeAdded(/** @type {!Workspace.UISourceCode} */ (event.data))), this._workspace.addEventListener( Workspace.Workspace.Events.UISourceCodeRemoved, event => this._onUISourceCodeRemoved(/** @type {!Workspace.UISourceCode} */ (event.data))), this._workspace.addEventListener( Workspace.Workspace.Events.UISourceCodeRenamed, this._onUISourceCodeRenamed, this), this._workspace.addEventListener( Workspace.Workspace.Events.ProjectAdded, event => this._onProjectAdded(/** @type {!Workspace.Project} */ (event.data)), this), this._workspace.addEventListener( Workspace.Workspace.Events.ProjectRemoved, event => this._onProjectRemoved(/** @type {!Workspace.Project} */ (event.data)), this), ]; for (const fileSystem of workspace.projects()) this._onProjectAdded(fileSystem); for (const uiSourceCode of workspace.uiSourceCodes()) this._onUISourceCodeAdded(uiSourceCode); } _scheduleRemap() { for (const status of this._statuses.valuesArray()) this._clearNetworkStatus(status.network); this._scheduleSweep(); } _scheduleSweep() { this._sweepThrottler.schedule(sweepUnmapped.bind(this)); /** * @this {Persistence.Automapping} * @return {!Promise} */ function sweepUnmapped() { const networkProjects = this._workspace.projectsForType(Workspace.projectTypes.Network); for (const networkProject of networkProjects) { for (const uiSourceCode of networkProject.uiSourceCodes()) this._computeNetworkStatus(uiSourceCode); } this._onSweepHappenedForTest(); return Promise.resolve(); } } _onSweepHappenedForTest() { } /** * @param {!Workspace.Project} project */ _onProjectRemoved(project) { for (const uiSourceCode of project.uiSourceCodes()) this._onUISourceCodeRemoved(uiSourceCode); if (project.type() !== Workspace.projectTypes.FileSystem) return; const fileSystem = /** @type {!Persistence.FileSystemWorkspaceBinding.FileSystem} */ (project); for (const gitFolder of fileSystem.initialGitFolders()) this._projectFoldersIndex.removeFolder(gitFolder); this._projectFoldersIndex.removeFolder(fileSystem.fileSystemPath()); this._scheduleRemap(); } /** * @param {!Workspace.Project} project */ _onProjectAdded(project) { if (project.type() !== Workspace.projectTypes.FileSystem) return; const fileSystem = /** @type {!Persistence.FileSystemWorkspaceBinding.FileSystem} */ (project); for (const gitFolder of fileSystem.initialGitFolders()) this._projectFoldersIndex.addFolder(gitFolder); this._projectFoldersIndex.addFolder(fileSystem.fileSystemPath()); project.uiSourceCodes().forEach(this._onUISourceCodeAdded.bind(this)); this._scheduleRemap(); } /** * @param {!Workspace.UISourceCode} uiSourceCode */ _onUISourceCodeAdded(uiSourceCode) { const project = uiSourceCode.project(); if (project.type() === Workspace.projectTypes.FileSystem) { // Never do bindings to filesystems that are typed to another client. if (Persistence.FileSystemWorkspaceBinding.fileSystemType(project)) return; this._filesIndex.addPath(uiSourceCode.url()); this._fileSystemUISourceCodes.set(uiSourceCode.url(), uiSourceCode); this._scheduleSweep(); } else if (project.type() === Workspace.projectTypes.Network) { this._computeNetworkStatus(uiSourceCode); } } /** * @param {!Workspace.UISourceCode} uiSourceCode */ _onUISourceCodeRemoved(uiSourceCode) { if (uiSourceCode.project().type() === Workspace.projectTypes.FileSystem) { this._filesIndex.removePath(uiSourceCode.url()); this._fileSystemUISourceCodes.delete(uiSourceCode.url()); const status = uiSourceCode[Persistence.Automapping._status]; if (status) this._clearNetworkStatus(status.network); } else if (uiSourceCode.project().type() === Workspace.projectTypes.Network) { this._clearNetworkStatus(uiSourceCode); } } /** * @param {!Common.Event} event */ _onUISourceCodeRenamed(event) { const uiSourceCode = /** @type {!Workspace.UISourceCode} */ (event.data.uiSourceCode); const oldURL = /** @type {string} */ (event.data.oldURL); if (uiSourceCode.project().type() !== Workspace.projectTypes.FileSystem) return; this._filesIndex.removePath(oldURL); this._fileSystemUISourceCodes.delete(oldURL); const status = uiSourceCode[Persistence.Automapping._status]; if (status) this._clearNetworkStatus(status.network); this._filesIndex.addPath(uiSourceCode.url()); this._fileSystemUISourceCodes.set(uiSourceCode.url(), uiSourceCode); this._scheduleSweep(); } /** * @param {!Workspace.UISourceCode} networkSourceCode */ _computeNetworkStatus(networkSourceCode) { if (networkSourceCode[Persistence.Automapping._processingPromise] || networkSourceCode[Persistence.Automapping._status]) return; const createBindingPromise = this._createBinding(networkSourceCode).then(validateStatus.bind(this)).then(onStatus.bind(this)); networkSourceCode[Persistence.Automapping._processingPromise] = createBindingPromise; /** * @param {?Persistence.AutomappingStatus} status * @return {!Promise<?Persistence.AutomappingStatus>} * @this {Persistence.Automapping} */ async function validateStatus(status) { if (!status) return null; if (status.network.contentType().isFromSourceMap() || !status.fileSystem.contentType().isTextType()) return status; // At the time binding comes, there are multiple user scenarios: // 1. Both network and fileSystem files are **not** dirty. // This is a typical scenario when user hasn't done any edits yet to the // files in question. // 2. FileSystem file has unsaved changes, network is clear. // This typically happens with CSS files editing. Consider the following // scenario: // - user edits file that has been successfully mapped before // - user doesn't save the file // - user hits reload // 3. Network file has either unsaved changes or commits, but fileSystem file is clear. // This typically happens when we've been editing file and then realized we'd like to drop // a folder and persist all the changes. // 4. Network file has either unsaved changes or commits, and fileSystem file has unsaved changes. // We consider this to be un-realistic scenario and in this case just fail gracefully. // // To support usecase (3), we need to validate against original network content. if (status.fileSystem.isDirty() && (status.network.isDirty() || status.network.hasCommits())) return null; const contents = await Promise.all([ status.fileSystem.requestContent(), new Promise(x => status.network.project().requestFileContent(status.network, x)) ]); const fileSystemContent = contents[0]; const networkContent = contents[1]; if (fileSystemContent === null || networkContent === null) return null; if (networkSourceCode[Persistence.Automapping._processingPromise] !== createBindingPromise) return null; const target = Bindings.NetworkProject.targetForUISourceCode(status.network); let isValid = false; if (target && target.isNodeJS()) { const rewrappedNetworkContent = Persistence.Persistence.rewrapNodeJSContent(status.fileSystem, fileSystemContent, networkContent); isValid = fileSystemContent === rewrappedNetworkContent; } else { // Trim trailing whitespaces because V8 adds trailing newline. isValid = fileSystemContent.trimRight() === networkContent.trimRight(); } if (!isValid) { this._prevalidationFailedForTest(status); return null; } return status; } /** * @param {?Persistence.AutomappingStatus} status * @this {Persistence.Automapping} */ function onStatus(status) { if (networkSourceCode[Persistence.Automapping._processingPromise] !== createBindingPromise) return; networkSourceCode[Persistence.Automapping._processingPromise] = null; if (!status || this._disposed) { this._onBindingFailedForTest(); return; } // TODO(lushnikov): remove this check once there's a single uiSourceCode per url. @see crbug.com/670180 if (status.network[Persistence.Automapping._status] || status.fileSystem[Persistence.Automapping._status]) return; this._statuses.add(status); status.network[Persistence.Automapping._status] = status; status.fileSystem[Persistence.Automapping._status] = status; if (status.exactMatch) { const projectFolder = this._projectFoldersIndex.closestParentFolder(status.fileSystem.url()); const newFolderAdded = projectFolder ? this._activeFoldersIndex.addFolder(projectFolder) : false; if (newFolderAdded) this._scheduleSweep(); } this._onStatusAdded.call(null, status); } } /** * @param {!Persistence.AutomappingStatus} binding */ _prevalidationFailedForTest(binding) { } _onBindingFailedForTest() { } /** * @param {!Workspace.UISourceCode} networkSourceCode */ _clearNetworkStatus(networkSourceCode) { if (networkSourceCode[Persistence.Automapping._processingPromise]) { networkSourceCode[Persistence.Automapping._processingPromise] = null; return; } const status = networkSourceCode[Persistence.Automapping._status]; if (!status) return; this._statuses.delete(status); status.network[Persistence.Automapping._status] = null; status.fileSystem[Persistence.Automapping._status] = null; if (status.exactMatch) { const projectFolder = this._projectFoldersIndex.closestParentFolder(status.fileSystem.url()); if (projectFolder) this._activeFoldersIndex.removeFolder(projectFolder); } this._onStatusRemoved.call(null, status); } /** * @param {!Workspace.UISourceCode} networkSourceCode * @return {!Promise<?Persistence.AutomappingStatus>} */ _createBinding(networkSourceCode) { if (networkSourceCode.url().startsWith('file://')) { const fileSourceCode = this._fileSystemUISourceCodes.get(networkSourceCode.url()); const status = fileSourceCode ? new Persistence.AutomappingStatus(networkSourceCode, fileSourceCode, false) : null; return Promise.resolve(status); } let networkPath = Common.ParsedURL.extractPath(networkSourceCode.url()); if (networkPath === null) return Promise.resolve(/** @type {?Persistence.AutomappingStatus} */ (null)); if (networkPath.endsWith('/')) networkPath += 'index.html'; const similarFiles = this._filesIndex.similarFiles(networkPath).map(path => this._fileSystemUISourceCodes.get(path)); if (!similarFiles.length) return Promise.resolve(/** @type {?Persistence.AutomappingStatus} */ (null)); return this._pullMetadatas(similarFiles.concat(networkSourceCode)).then(onMetadatas.bind(this)); /** * @this {Persistence.Automapping} */ function onMetadatas() { const activeFiles = similarFiles.filter(file => !!this._activeFoldersIndex.closestParentFolder(file.url())); const networkMetadata = networkSourceCode[Persistence.Automapping._metadata]; if (!networkMetadata || (!networkMetadata.modificationTime && typeof networkMetadata.contentSize !== 'number')) { // If networkSourceCode does not have metadata, try to match against active folders. if (activeFiles.length !== 1) return null; return new Persistence.AutomappingStatus(networkSourceCode, activeFiles[0], false); } // Try to find exact matches, prioritizing active folders. let exactMatches = this._filterWithMetadata(activeFiles, networkMetadata); if (!exactMatches.length) exactMatches = this._filterWithMetadata(similarFiles, networkMetadata); if (exactMatches.length !== 1) return null; return new Persistence.AutomappingStatus(networkSourceCode, exactMatches[0], true); } } /** * @param {!Array<!Workspace.UISourceCode>} uiSourceCodes * @return {!Promise} */ _pullMetadatas(uiSourceCodes) { const promises = uiSourceCodes.map(file => fetchMetadata(file)); return Promise.all(promises); /** * @param {!Workspace.UISourceCode} file * @return {!Promise} */ function fetchMetadata(file) { return file.requestMetadata().then(metadata => file[Persistence.Automapping._metadata] = metadata); } } /** * @param {!Array<!Workspace.UISourceCode>} files * @param {!Workspace.UISourceCodeMetadata} networkMetadata * @return {!Array<!Workspace.UISourceCode>} */ _filterWithMetadata(files, networkMetadata) { return files.filter(file => { const fileMetadata = file[Persistence.Automapping._metadata]; if (!fileMetadata) return false; // Allow a second of difference due to network timestamps lack of precision. const timeMatches = !networkMetadata.modificationTime || Math.abs(networkMetadata.modificationTime - fileMetadata.modificationTime) < 1000; const contentMatches = !networkMetadata.contentSize || fileMetadata.contentSize === networkMetadata.contentSize; return timeMatches && contentMatches; }); } /** * @override */ dispose() { if (this._disposed) return; this._disposed = true; Common.EventTarget.removeEventListeners(this._eventListeners); for (const status of this._statuses.valuesArray()) this._clearNetworkStatus(status.network); } }; Persistence.Automapping._status = Symbol('Automapping.Status'); Persistence.Automapping._processingPromise = Symbol('Automapping.ProcessingPromise'); Persistence.Automapping._metadata = Symbol('Automapping.Metadata'); /** * @unrestricted */ Persistence.Automapping.FilePathIndex = class { /** * @param {!Persistence.PathEncoder} encoder */ constructor(encoder) { this._encoder = encoder; this._reversedIndex = new Common.Trie(); } /** * @param {string} path */ addPath(path) { const encodedPath = this._encoder.encode(path); this._reversedIndex.add(encodedPath.reverse()); } /** * @param {string} path */ removePath(path) { const encodedPath = this._encoder.encode(path); this._reversedIndex.remove(encodedPath.reverse()); } /** * @param {string} networkPath * @return {!Array<string>} */ similarFiles(networkPath) { const encodedPath = this._encoder.encode(networkPath); const longestCommonPrefix = this._reversedIndex.longestPrefix(encodedPath.reverse(), false); if (!longestCommonPrefix) return []; return this._reversedIndex.words(longestCommonPrefix) .map(encodedPath => this._encoder.decode(encodedPath.reverse())); } }; /** * @unrestricted */ Persistence.Automapping.FolderIndex = class { /** * @param {!Persistence.PathEncoder} encoder */ constructor(encoder) { this._encoder = encoder; this._index = new Common.Trie(); /** @type {!Map<string, number>} */ this._folderCount = new Map(); } /** * @param {string} path * @return {boolean} */ addFolder(path) { if (path.endsWith('/')) path = path.substring(0, path.length - 1); const encodedPath = this._encoder.encode(path); this._index.add(encodedPath); const count = this._folderCount.get(encodedPath) || 0; this._folderCount.set(encodedPath, count + 1); return count === 0; } /** * @param {string} path * @return {boolean} */ removeFolder(path) { if (path.endsWith('/')) path = path.substring(0, path.length - 1); const encodedPath = this._encoder.encode(path); const count = this._folderCount.get(encodedPath) || 0; if (!count) return false; if (count > 1) { this._folderCount.set(encodedPath, count - 1); return false; } this._index.remove(encodedPath); this._folderCount.delete(encodedPath); return true; } /** * @param {string} path * @return {string} */ closestParentFolder(path) { const encodedPath = this._encoder.encode(path); const commonPrefix = this._index.longestPrefix(encodedPath, true); return this._encoder.decode(commonPrefix); } }; /** * @unrestricted */ Persistence.AutomappingStatus = class { /** * @param {!Workspace.UISourceCode} network * @param {!Workspace.UISourceCode} fileSystem * @param {boolean} exactMatch */ constructor(network, fileSystem, exactMatch) { this.network = network; this.fileSystem = fileSystem; this.exactMatch = exactMatch; } };