UNPKG

chrome-devtools-frontend

Version:
324 lines (281 loc) • 10.3 kB
// Copyright 2020 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 Platform from '../platform/platform.js'; const queryParamsObject = new URLSearchParams(location.search); let runtimePlatform = ''; let runtimeInstance: Runtime|undefined; export function getRemoteBase(location: string = self.location.toString()): { base: string, version: string, }|null { const url = new URL(location); const remoteBase = url.searchParams.get('remoteBase'); if (!remoteBase) { return null; } const version = /\/serve_file\/(@[0-9a-zA-Z]+)\/?$/.exec(remoteBase); if (!version) { return null; } return {base: `devtools://devtools/remote/serve_file/${version[1]}/`, version: version[1]}; } export class Runtime { private constructor() { } static instance(opts: { forceNew: boolean|null, }|undefined = {forceNew: null}): Runtime { const {forceNew} = opts; if (!runtimeInstance || forceNew) { runtimeInstance = new Runtime(); } return runtimeInstance; } static removeInstance(): void { runtimeInstance = undefined; } static queryParam(name: string): string|null { return queryParamsObject.get(name); } static setQueryParamForTesting(name: string, value: string): void { queryParamsObject.set(name, value); } static experimentsSetting(): { [x: string]: boolean, } { try { return JSON.parse( self.localStorage && self.localStorage['experiments'] ? self.localStorage['experiments'] : '{}') as { [x: string]: boolean, }; } catch (e) { console.error('Failed to parse localStorage[\'experiments\']'); return {}; } } static setPlatform(platform: string): void { runtimePlatform = platform; } static platform(): string { return runtimePlatform; } static isDescriptorEnabled(descriptor: { experiment: ((string | undefined)|null), condition: ((string | undefined)|null), }): boolean { const activatorExperiment = descriptor['experiment']; if (activatorExperiment === '*') { return true; } if (activatorExperiment && activatorExperiment.startsWith('!') && experiments.isEnabled(activatorExperiment.substring(1))) { return false; } if (activatorExperiment && !activatorExperiment.startsWith('!') && !experiments.isEnabled(activatorExperiment)) { return false; } const condition = descriptor['condition']; if (condition && !condition.startsWith('!') && !Runtime.queryParam(condition)) { return false; } if (condition && condition.startsWith('!') && Runtime.queryParam(condition.substring(1))) { return false; } return true; } loadLegacyModule(modulePath: string): Promise<void> { return import(`../../${modulePath}`); } } export interface Option { title: string; value: string|boolean; raw?: boolean; text?: string; } export class ExperimentsSupport { #experiments: Experiment[]; #experimentNames: Set<string>; #enabledTransiently: Set<string>; readonly #enabledByDefault: Set<string>; readonly #serverEnabled: Set<string>; // Experiments in this set won't be shown to the user readonly #nonConfigurable: Set<string>; constructor() { this.#experiments = []; this.#experimentNames = new Set(); this.#enabledTransiently = new Set(); this.#enabledByDefault = new Set(); this.#serverEnabled = new Set(); this.#nonConfigurable = new Set(); } allConfigurableExperiments(): Experiment[] { const result = []; for (const experiment of this.#experiments) { if (!this.#enabledTransiently.has(experiment.name) && !this.#nonConfigurable.has(experiment.name)) { result.push(experiment); } } return result; } enabledExperiments(): Experiment[] { return this.#experiments.filter(experiment => experiment.isEnabled()); } private setExperimentsSetting(value: Object): void { if (!self.localStorage) { return; } self.localStorage['experiments'] = JSON.stringify(value); } register( experimentName: string, experimentTitle: string, unstable?: boolean, docLink?: string, feedbackLink?: string): void { Platform.DCHECK( () => !this.#experimentNames.has(experimentName), 'Duplicate registration of experiment ' + experimentName); this.#experimentNames.add(experimentName); this.#experiments.push(new Experiment( this, experimentName, experimentTitle, Boolean(unstable), docLink as Platform.DevToolsPath.UrlString ?? Platform.DevToolsPath.EmptyUrlString, feedbackLink as Platform.DevToolsPath.UrlString ?? Platform.DevToolsPath.EmptyUrlString)); } isEnabled(experimentName: string): boolean { this.checkExperiment(experimentName); // Check for explicitly disabled #experiments first - the code could call setEnable(false) on the experiment enabled // by default and we should respect that. if (Runtime.experimentsSetting()[experimentName] === false) { return false; } if (this.#enabledTransiently.has(experimentName) || this.#enabledByDefault.has(experimentName)) { return true; } if (this.#serverEnabled.has(experimentName)) { return true; } return Boolean(Runtime.experimentsSetting()[experimentName]); } setEnabled(experimentName: string, enabled: boolean): void { this.checkExperiment(experimentName); const experimentsSetting = Runtime.experimentsSetting(); experimentsSetting[experimentName] = enabled; this.setExperimentsSetting(experimentsSetting); } enableExperimentsTransiently(experimentNames: string[]): void { for (const experimentName of experimentNames) { this.checkExperiment(experimentName); this.#enabledTransiently.add(experimentName); } } enableExperimentsByDefault(experimentNames: string[]): void { for (const experimentName of experimentNames) { this.checkExperiment(experimentName); this.#enabledByDefault.add(experimentName); } } setServerEnabledExperiments(experimentNames: string[]): void { for (const experiment of experimentNames) { this.checkExperiment(experiment); this.#serverEnabled.add(experiment); } } setNonConfigurableExperiments(experimentNames: string[]): void { for (const experiment of experimentNames) { this.checkExperiment(experiment); this.#nonConfigurable.add(experiment); } } enableForTest(experimentName: string): void { this.checkExperiment(experimentName); this.#enabledTransiently.add(experimentName); } disableForTest(experimentName: string): void { this.checkExperiment(experimentName); this.#enabledTransiently.delete(experimentName); } clearForTest(): void { this.#experiments = []; this.#experimentNames.clear(); this.#enabledTransiently.clear(); this.#enabledByDefault.clear(); this.#serverEnabled.clear(); } cleanUpStaleExperiments(): void { const experimentsSetting = Runtime.experimentsSetting(); const cleanedUpExperimentSetting: { [x: string]: boolean, } = {}; for (const {name: experimentName} of this.#experiments) { if (experimentsSetting.hasOwnProperty(experimentName)) { const isEnabled = experimentsSetting[experimentName]; if (isEnabled || this.#enabledByDefault.has(experimentName)) { cleanedUpExperimentSetting[experimentName] = isEnabled; } } } this.setExperimentsSetting(cleanedUpExperimentSetting); } private checkExperiment(experimentName: string): void { Platform.DCHECK(() => this.#experimentNames.has(experimentName), 'Unknown experiment ' + experimentName); } } export class Experiment { name: string; title: string; unstable: boolean; docLink?: Platform.DevToolsPath.UrlString; readonly feedbackLink?: Platform.DevToolsPath.UrlString; readonly #experiments: ExperimentsSupport; constructor( experiments: ExperimentsSupport, name: string, title: string, unstable: boolean, docLink: Platform.DevToolsPath.UrlString, feedbackLink: Platform.DevToolsPath.UrlString) { this.name = name; this.title = title; this.unstable = unstable; this.docLink = docLink; this.feedbackLink = feedbackLink; this.#experiments = experiments; } isEnabled(): boolean { return this.#experiments.isEnabled(this.name); } setEnabled(enabled: boolean): void { this.#experiments.setEnabled(this.name, enabled); } } // This must be constructed after the query parameters have been parsed. export const experiments = new ExperimentsSupport(); // TODO(crbug.com/1167717): Make this a const enum again // eslint-disable-next-line rulesdir/const_enum export enum ExperimentName { CAPTURE_NODE_CREATION_STACKS = 'captureNodeCreationStacks', CSS_OVERVIEW = 'cssOverview', LIVE_HEAP_PROFILE = 'liveHeapProfile', DEVELOPER_RESOURCES_VIEW = 'developerResourcesView', CSP_VIOLATIONS_VIEW = 'cspViolationsView', WASM_DWARF_DEBUGGING = 'wasmDWARFDebugging', ALL = '*', PROTOCOL_MONITOR = 'protocolMonitor', WEBAUTHN_PANE = 'webauthnPane', FULL_ACCESSIBILITY_TREE = 'fullAccessibilityTree', PRECISE_CHANGES = 'preciseChanges', STYLES_PANE_CSS_CHANGES = 'stylesPaneCSSChanges', HEADER_OVERRIDES = 'headerOverrides', EYEDROPPER_COLOR_PICKER = 'eyedropperColorPicker', INSTRUMENTATION_BREAKPOINTS = 'instrumentationBreakpoints', AUTHORED_DEPLOYED_GROUPING = 'authoredDeployedGrouping', IMPORTANT_DOM_PROPERTIES = 'importantDOMProperties', JUST_MY_CODE = 'justMyCode', PRELOADING_STATUS_PANEL = 'preloadingStatusPanel', DISABLE_COLOR_FORMAT_SETTING = 'disableColorFormatSetting', TIMELINE_AS_CONSOLE_PROFILE_RESULT_PANEL = 'timelineAsConsoleProfileResultPanel', OUTERMOST_TARGET_SELECTOR = 'outermostTargetSelector', JS_PROFILER_TEMP_ENABLE = 'jsProfilerTemporarilyEnable', HIGHLIGHT_ERRORS_ELEMENTS_PANEL = 'highlightErrorsElementsPanel', SET_ALL_BREAKPOINTS_EAGERLY = 'setAllBreakpointsEagerly', } // TODO(crbug.com/1167717): Make this a const enum again // eslint-disable-next-line rulesdir/const_enum export enum ConditionName { CAN_DOCK = 'can_dock', NOT_SOURCES_HIDE_ADD_FOLDER = '!sources.hide_add_folder', }