UNPKG

chrome-devtools-frontend

Version:
248 lines (227 loc) • 10.4 kB
// Copyright 2025 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 Common from '../../core/common/common.js'; import * as Host from '../../core/host/host.js'; import type * as Platform from '../../core/platform/platform.js'; import type * as Root from '../../core/root/root.js'; import * as ProjectSettings from '../project_settings/project_settings.js'; /** * Description and state of the automatic file system. */ export interface AutomaticFileSystem { root: Platform.DevToolsPath.RawPathString; uuid: string; state: 'disconnected'|'connecting'|'connected'; } /** * Indicates the availability of the Automatic Workspace Folders feature. * * `'available'` means that the feature is enabled and the project settings * are also available. It doesn't indicate whether or not the page is actually * providing a `com.chrome.devtools.json` or not, and whether or not that file * (if it exists) provides workspace information. */ export type AutomaticFileSystemAvailability = 'available'|'unavailable'; let automaticFileSystemManagerInstance: AutomaticFileSystemManager|undefined; /** * Automatically connects and disconnects workspace folders. * * @see http://go/chrome-devtools:automatic-workspace-folders-design */ export class AutomaticFileSystemManager extends Common.ObjectWrapper.ObjectWrapper<EventTypes> { #automaticFileSystem: AutomaticFileSystem|null; #availability: AutomaticFileSystemAvailability = 'unavailable'; #inspectorFrontendHost: Host.InspectorFrontendHostAPI.InspectorFrontendHostAPI; #projectSettingsModel: ProjectSettings.ProjectSettingsModel.ProjectSettingsModel; /** * Yields the current `AutomaticFileSystem` (if any). * * @return the current automatic file system or `null`. */ get automaticFileSystem(): Readonly<AutomaticFileSystem>|null { return this.#automaticFileSystem; } /** * Yields the availability of the Automatic Workspace Folders feature. * * `'available'` means that the feature is enabled and the project settings * are also available. It doesn't indicate whether or not the page is actually * providing a `com.chrome.devtools.json` or not, and whether or not that file * (if it exists) provides workspace information. * * @return `'available'` if the feature is available and the project settings * feature is also available, otherwise `'unavailable'`. */ get availability(): AutomaticFileSystemAvailability { return this.#availability; } /** * @internal */ private constructor( hostConfig: Root.Runtime.HostConfig, inspectorFrontendHost: Host.InspectorFrontendHostAPI.InspectorFrontendHostAPI, projectSettingsModel: ProjectSettings.ProjectSettingsModel.ProjectSettingsModel) { super(); this.#automaticFileSystem = null; this.#inspectorFrontendHost = inspectorFrontendHost; this.#projectSettingsModel = projectSettingsModel; if (hostConfig.devToolsAutomaticFileSystems?.enabled) { this.#inspectorFrontendHost.events.addEventListener( Host.InspectorFrontendHostAPI.Events.FileSystemRemoved, this.#fileSystemRemoved, this); this.#projectSettingsModel.addEventListener( ProjectSettings.ProjectSettingsModel.Events.AVAILABILITY_CHANGED, this.#availabilityChanged, this); this.#availabilityChanged({data: this.#projectSettingsModel.availability}); this.#projectSettingsModel.addEventListener( ProjectSettings.ProjectSettingsModel.Events.PROJECT_SETTINGS_CHANGED, this.#projectSettingsChanged, this); this.#projectSettingsChanged({data: this.#projectSettingsModel.projectSettings}); } } /** * Yields the `AutomaticFileSystemManager` singleton. * * @returns the singleton. */ static instance({forceNew, hostConfig, inspectorFrontendHost, projectSettingsModel}: { forceNew: boolean|null, hostConfig: Root.Runtime.HostConfig|null, inspectorFrontendHost: Host.InspectorFrontendHostAPI.InspectorFrontendHostAPI|null, projectSettingsModel: ProjectSettings.ProjectSettingsModel.ProjectSettingsModel|null, } = {forceNew: false, hostConfig: null, inspectorFrontendHost: null, projectSettingsModel: null}): AutomaticFileSystemManager { if (!automaticFileSystemManagerInstance || forceNew) { if (!hostConfig || !inspectorFrontendHost || !projectSettingsModel) { throw new Error( 'Unable to create AutomaticFileSystemManager: ' + 'hostConfig, inspectorFrontendHost, and projectSettingsModel must be provided'); } automaticFileSystemManagerInstance = new AutomaticFileSystemManager( hostConfig, inspectorFrontendHost, projectSettingsModel, ); } return automaticFileSystemManagerInstance; } /** * Clears the `AutomaticFileSystemManager` singleton (if any); */ static removeInstance(): void { if (automaticFileSystemManagerInstance) { automaticFileSystemManagerInstance.#dispose(); automaticFileSystemManagerInstance = undefined; } } #dispose(): void { this.#inspectorFrontendHost.events.removeEventListener( Host.InspectorFrontendHostAPI.Events.FileSystemRemoved, this.#fileSystemRemoved, this); this.#projectSettingsModel.removeEventListener( ProjectSettings.ProjectSettingsModel.Events.AVAILABILITY_CHANGED, this.#availabilityChanged, this); this.#projectSettingsModel.removeEventListener( ProjectSettings.ProjectSettingsModel.Events.PROJECT_SETTINGS_CHANGED, this.#projectSettingsChanged, this); } #availabilityChanged( event: Common.EventTarget.EventTargetEvent<ProjectSettings.ProjectSettingsModel.ProjectSettingsAvailability>): void { const availability = event.data; if (this.#availability !== availability) { this.#availability = availability; this.dispatchEventToListeners(Events.AVAILABILITY_CHANGED, this.#availability); } } #fileSystemRemoved(event: Common.EventTarget.EventTargetEvent<Platform.DevToolsPath.RawPathString>): void { if (this.#automaticFileSystem === null) { return; } if (this.#automaticFileSystem.root === event.data) { this.#automaticFileSystem = Object.freeze({ ...this.#automaticFileSystem, state: 'disconnected', }); this.dispatchEventToListeners(Events.AUTOMATIC_FILE_SYSTEM_CHANGED, this.#automaticFileSystem); } } #projectSettingsChanged( event: Common.EventTarget.EventTargetEvent<ProjectSettings.ProjectSettingsModel.ProjectSettings>): void { const projectSettings = event.data; let automaticFileSystem = this.#automaticFileSystem; if (projectSettings.workspace) { const {root, uuid} = projectSettings.workspace; if (automaticFileSystem === null || automaticFileSystem.root !== root || automaticFileSystem.uuid !== uuid) { automaticFileSystem = Object.freeze({root, uuid, state: 'disconnected'}); } } else if (automaticFileSystem !== null) { automaticFileSystem = null; } if (this.#automaticFileSystem !== automaticFileSystem) { this.disconnectedAutomaticFileSystem(); this.#automaticFileSystem = automaticFileSystem; this.dispatchEventToListeners(Events.AUTOMATIC_FILE_SYSTEM_CHANGED, this.#automaticFileSystem); void this.connectAutomaticFileSystem(/* addIfMissing= */ false); } } /** * Attempt to connect the automatic workspace folder (if any). * * @param addIfMissing if `false` (the default), this will only try to connect * to a previously connected automatic workspace folder. * If the folder was never connected before and `true` is * specified, the user will be asked to grant permission * to allow Chrome DevTools to access the folder first. * @returns `true` if the automatic workspace folder was connected, `false` * if there wasn't any, or the connection attempt failed (e.g. the * user did not grant permission). */ async connectAutomaticFileSystem(addIfMissing = false): Promise<boolean> { if (!this.#automaticFileSystem) { return false; } const {root, uuid, state} = this.#automaticFileSystem; if (state === 'disconnected') { const automaticFileSystem = this.#automaticFileSystem = Object.freeze({...this.#automaticFileSystem, state: 'connecting'}); this.dispatchEventToListeners(Events.AUTOMATIC_FILE_SYSTEM_CHANGED, this.#automaticFileSystem); const {success} = await new Promise<{success: boolean}>( resolve => this.#inspectorFrontendHost.connectAutomaticFileSystem(root, uuid, addIfMissing, resolve)); if (this.#automaticFileSystem === automaticFileSystem) { const state = success ? 'connected' : 'disconnected'; this.#automaticFileSystem = Object.freeze({...automaticFileSystem, state}); this.dispatchEventToListeners(Events.AUTOMATIC_FILE_SYSTEM_CHANGED, this.#automaticFileSystem); } } return this.#automaticFileSystem?.state === 'connected'; } /** * Disconnects any automatic workspace folder. */ disconnectedAutomaticFileSystem(): void { if (this.#automaticFileSystem && this.#automaticFileSystem.state !== 'disconnected') { this.#inspectorFrontendHost.disconnectAutomaticFileSystem(this.#automaticFileSystem.root); this.#automaticFileSystem = Object.freeze({...this.#automaticFileSystem, state: 'disconnected'}); this.dispatchEventToListeners(Events.AUTOMATIC_FILE_SYSTEM_CHANGED, this.#automaticFileSystem); } } } /** * Events emitted by the `AutomaticFileSystemManager`. */ export const enum Events { /** * Emitted whenever the `automaticFileSystem` property of the * `AutomaticFileSystemManager` changes. */ AUTOMATIC_FILE_SYSTEM_CHANGED = 'AutomaticFileSystemChanged', /** * Emitted whenever the `availability` property of the * `AutomaticFileSystemManager` changes. */ AVAILABILITY_CHANGED = 'AvailabilityChanged', } /** * @internal */ export interface EventTypes { [Events.AUTOMATIC_FILE_SYSTEM_CHANGED]: Readonly<AutomaticFileSystem>|null; [Events.AVAILABILITY_CHANGED]: AutomaticFileSystemAvailability; }