UNPKG

debug-server-next

Version:

Dev server for hippy-core.

474 lines (473 loc) 18.1 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. /* eslint-disable rulesdir/no_underscored_properties */ const originalConsole = console; const originalAssert = console.assert; const queryParamsObject = new URLSearchParams(location.search); // The following variable are initialized all the way at the bottom of this file let importScriptPathPrefix; let runtimePlatform = ''; let runtimeInstance; export function getRemoteBase(location = self.location.toString()) { 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: `${url.origin}/remote/serve_file/${version[1]}/`, version: version[1] }; } export const mappingForLayoutTests = new Map([ ['panels/animation', 'animation'], ['panels/browser_debugger', 'browser_debugger'], ['panels/changes', 'changes'], ['panels/console', 'console'], ['panels/core_memory', 'core_memory'], ['panels/ui_inspector', 'ui_inspector'], ['panels/cdp_debug', 'cdp_debug'], ['panels/core_performance', 'core_performance'], ['panels/elements', 'elements'], ['panels/emulation', 'emulation'], ['panels/mobile_throttling', 'mobile_throttling'], ['panels/network', 'network'], ['panels/profiler', 'profiler'], ['panels/application', 'resources'], ['panels/search', 'search'], ['panels/sources', 'sources'], ['panels/snippets', 'snippets'], ['panels/settings', 'settings'], ['panels/timeline', 'timeline'], ['panels/web_audio', 'web_audio'], ['models/persistence', 'persistence'], ['models/workspace_diff', 'workspace_diff'], ['entrypoints/main', 'main'], ['third_party/diff', 'diff'], ['ui/legacy/components/inline_editor', 'inline_editor'], ['ui/legacy/components/data_grid', 'data_grid'], ['ui/legacy/components/perf_ui', 'perf_ui'], ['ui/legacy/components/source_frame', 'source_frame'], ['ui/legacy/components/color_picker', 'color_picker'], ['ui/legacy/components/cookie_table', 'cookie_table'], ['ui/legacy/components/text_editor', 'text_editor'], ['ui/legacy/components/quick_open', 'quick_open'], ['ui/legacy/components/utils', 'components'], ]); export class Runtime { _modules; _modulesMap; _descriptorsMap; constructor(descriptors) { this._modules = []; this._modulesMap = {}; this._descriptorsMap = {}; for (const descriptor of descriptors) { this._registerModule(descriptor); } } static instance(opts = { forceNew: null, moduleDescriptors: null }) { const { forceNew, moduleDescriptors } = opts; if (!runtimeInstance || forceNew) { if (!moduleDescriptors) { throw new Error(`Unable to create runtime: moduleDescriptors must be provided: ${new Error().stack}`); } runtimeInstance = new Runtime(moduleDescriptors); } return runtimeInstance; } static removeInstance() { runtimeInstance = undefined; } /** * http://tools.ietf.org/html/rfc3986#section-5.2.4 */ static normalizePath(path) { if (path.indexOf('..') === -1 && path.indexOf('.') === -1) { return path; } const normalizedSegments = []; const segments = path.split('/'); for (const segment of segments) { if (segment === '.') { continue; } else if (segment === '..') { normalizedSegments.pop(); } else if (segment) { normalizedSegments.push(segment); } } let normalizedPath = normalizedSegments.join('/'); if (normalizedPath[normalizedPath.length - 1] === '/') { return normalizedPath; } if (path[0] === '/' && normalizedPath) { normalizedPath = '/' + normalizedPath; } if ((path[path.length - 1] === '/') || (segments[segments.length - 1] === '.') || (segments[segments.length - 1] === '..')) { normalizedPath = normalizedPath + '/'; } return normalizedPath; } static queryParam(name) { return queryParamsObject.get(name); } static _experimentsSetting() { try { return JSON.parse(self.localStorage && self.localStorage['experiments'] ? self.localStorage['experiments'] : '{}'); } catch (e) { console.error('Failed to parse localStorage[\'experiments\']'); return {}; } } static _assert(value, message) { if (value) { return; } originalAssert.call(originalConsole, value, message + ' ' + new Error().stack); } static setPlatform(platform) { runtimePlatform = platform; } static platform() { return runtimePlatform; } static isDescriptorEnabled(descriptor) { 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; } static resolveSourceURL(path) { let sourceURL = self.location.href; if (self.location.search) { sourceURL = sourceURL.replace(self.location.search, ''); } sourceURL = sourceURL.substring(0, sourceURL.lastIndexOf('/') + 1) + path; return '\n/*# sourceURL=' + sourceURL + ' */'; } module(moduleName) { return this._modulesMap[moduleName]; } _registerModule(descriptor) { const module = new Module(this, descriptor); this._modules.push(module); this._modulesMap[descriptor['name']] = module; const mappedName = mappingForLayoutTests.get(descriptor['name']); if (mappedName !== undefined) { this._modulesMap[mappedName] = module; } } loadModulePromise(moduleName) { return this._modulesMap[moduleName]._loadPromise(); } loadAutoStartModules(moduleNames) { const promises = []; for (const moduleName of moduleNames) { promises.push(this.loadModulePromise(moduleName)); } return Promise.all(promises); } } export class ModuleDescriptor { name; dependencies; modules; resources; condition; experiment; constructor() { } } function computeContainingFolderName(name) { if (name.includes('/')) { return name.substring(name.lastIndexOf('/') + 1, name.length); } return name; } export class Module { _manager; _descriptor; _name; _loadedForTest; _pendingLoadPromise; constructor(manager, descriptor) { this._manager = manager; this._descriptor = descriptor; this._name = descriptor.name; this._loadedForTest = false; } name() { return this._name; } enabled() { return Runtime.isDescriptorEnabled(this._descriptor); } resource(name) { const fullName = this._name + '/' + name; const content = cachedResources.get(fullName); if (!content) { throw new Error(fullName + ' not preloaded. Check module.json'); } return content; } _loadPromise() { if (!this.enabled()) { return Promise.reject(new Error('Module ' + this._name + ' is not enabled')); } if (this._pendingLoadPromise) { return this._pendingLoadPromise; } const dependencies = this._descriptor.dependencies; const dependencyPromises = []; for (let i = 0; dependencies && i < dependencies.length; ++i) { dependencyPromises.push(this._manager._modulesMap[dependencies[i]]._loadPromise()); } this._pendingLoadPromise = Promise.all(dependencyPromises).then(this._loadModules.bind(this)).then(() => { this._loadedForTest = true; return this._loadedForTest; }); return this._pendingLoadPromise; } async _loadModules() { const containingFolderName = computeContainingFolderName(this._name); const moduleFileName = `${containingFolderName}_module.js`; const entrypointFileName = `${containingFolderName}.js`; // If a module has resources, they are part of the `_module.js` files that are generated // by `build_release_applications`. These need to be loaded before any other code is // loaded, to make sure that the resource content is properly cached in `cachedResources`. if (this._descriptor.modules && this._descriptor.modules.includes(moduleFileName)) { await import(`../../${this._name}/${moduleFileName}`); } await import(`../../${this._name}/${entrypointFileName}`); } _modularizeURL(resourceName) { return Runtime.normalizePath(this._name + '/' + resourceName); } fetchResource(resourceName) { const sourceURL = getResourceURL(this._modularizeURL(resourceName)); return loadResourcePromise(sourceURL); } substituteURL(value) { return value.replace(/@url\(([^\)]*?)\)/g, convertURL.bind(this)); function convertURL(match, url) { return importScriptPathPrefix + this._modularizeURL(url); } } } export class ExperimentsSupport { _experiments; _experimentNames; _enabledTransiently; _enabledByDefault; _serverEnabled; constructor() { this._experiments = []; this._experimentNames = new Set(); this._enabledTransiently = new Set(); this._enabledByDefault = new Set(); this._serverEnabled = new Set(); } allConfigurableExperiments() { const result = []; for (const experiment of this._experiments) { if (!this._enabledTransiently.has(experiment.name)) { result.push(experiment); } } return result; } enabledExperiments() { return this._experiments.filter(experiment => experiment.isEnabled()); } _setExperimentsSetting(value) { if (!self.localStorage) { return; } self.localStorage['experiments'] = JSON.stringify(value); } register(experimentName, experimentTitle, unstable, docLink) { Runtime._assert(!this._experimentNames.has(experimentName), 'Duplicate registration of experiment ' + experimentName); this._experimentNames.add(experimentName); this._experiments.push(new Experiment(this, experimentName, experimentTitle, Boolean(unstable), docLink ?? '')); } isEnabled(experimentName) { 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, enabled) { this._checkExperiment(experimentName); const experimentsSetting = Runtime._experimentsSetting(); experimentsSetting[experimentName] = enabled; this._setExperimentsSetting(experimentsSetting); } enableExperimentsTransiently(experimentNames) { for (const experimentName of experimentNames) { this._checkExperiment(experimentName); this._enabledTransiently.add(experimentName); } } enableExperimentsByDefault(experimentNames) { for (const experimentName of experimentNames) { this._checkExperiment(experimentName); this._enabledByDefault.add(experimentName); } } setServerEnabledExperiments(experimentNames) { for (const experiment of experimentNames) { this._checkExperiment(experiment); this._serverEnabled.add(experiment); } } enableForTest(experimentName) { this._checkExperiment(experimentName); this._enabledTransiently.add(experimentName); } clearForTest() { this._experiments = []; this._experimentNames.clear(); this._enabledTransiently.clear(); this._enabledByDefault.clear(); this._serverEnabled.clear(); } cleanUpStaleExperiments() { const experimentsSetting = Runtime._experimentsSetting(); const cleanedUpExperimentSetting = {}; 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); } _checkExperiment(experimentName) { Runtime._assert(this._experimentNames.has(experimentName), 'Unknown experiment ' + experimentName); } } export class Experiment { name; title; unstable; docLink; _experiments; constructor(experiments, name, title, unstable, docLink) { this.name = name; this.title = title; this.unstable = unstable; this.docLink = docLink; this._experiments = experiments; } isEnabled() { return this._experiments.isEnabled(this.name); } setEnabled(enabled) { this._experiments.setEnabled(this.name, enabled); } } export function loadResourcePromise(url) { return new Promise(load); function load(fulfill, reject) { const xhr = new XMLHttpRequest(); xhr.open('GET', url, true); xhr.onreadystatechange = onreadystatechange; function onreadystatechange(_e) { if (xhr.readyState !== XMLHttpRequest.DONE) { return; } const response = this.response; // DevTools Proxy server can mask 404s as 200s, check the body to be sure const status = /^HTTP\/1.1 404/.test(response) ? 404 : xhr.status; if ([0, 200, 304].indexOf(status) === -1) // Testing harness file:/// results in 0. { reject(new Error('While loading from url ' + url + ' server responded with a status of ' + status)); } else { fulfill(response); } } xhr.send(null); } } function getResourceURL(scriptName, base) { const sourceURL = (base || importScriptPathPrefix) + scriptName; const schemaIndex = sourceURL.indexOf('://') + 3; let pathIndex = sourceURL.indexOf('/', schemaIndex); if (pathIndex === -1) { pathIndex = sourceURL.length; } return sourceURL.substring(0, pathIndex) + Runtime.normalizePath(sourceURL.substring(pathIndex)); } (function () { const baseUrl = self.location ? self.location.origin + self.location.pathname : ''; importScriptPathPrefix = baseUrl.substring(0, baseUrl.lastIndexOf('/') + 1); })(); // This must be constructed after the query parameters have been parsed. export const experiments = new ExperimentsSupport(); export const cachedResources = new Map(); // Only exported for LightHouse, which uses it in `report-generator.js`. // Do not use this global in DevTools' implementation. // TODO(crbug.com/1127292): remove this global // @ts-ignore globalThis.EXPORTED_CACHED_RESOURCES_ONLY_FOR_LIGHTHOUSE = cachedResources; export let appStartedPromiseCallback; export const appStarted = new Promise(fulfill => { appStartedPromiseCallback = fulfill; }); // TODO(crbug.com/1167717): Make this a const enum again // eslint-disable-next-line rulesdir/const_enum export var ExperimentName; (function (ExperimentName) { ExperimentName["CAPTURE_NODE_CREATION_STACKS"] = "captureNodeCreationStacks"; ExperimentName["CSS_OVERVIEW"] = "cssOverview"; ExperimentName["LIVE_HEAP_PROFILE"] = "liveHeapProfile"; ExperimentName["DEVELOPER_RESOURCES_VIEW"] = "developerResourcesView"; ExperimentName["TIMELINE_REPLAY_EVENT"] = "timelineReplayEvent"; ExperimentName["CSP_VIOLATIONS_VIEW"] = "cspViolationsView"; ExperimentName["WASM_DWARF_DEBUGGING"] = "wasmDWARFDebugging"; ExperimentName["ALL"] = "*"; ExperimentName["PROTOCOL_MONITOR"] = "protocolMonitor"; ExperimentName["WEBAUTHN_PANE"] = "webauthnPane"; ExperimentName["LOCALIZED_DEVTOOLS"] = "localizedDevTools"; })(ExperimentName || (ExperimentName = {})); // TODO(crbug.com/1167717): Make this a const enum again // eslint-disable-next-line rulesdir/const_enum export var ConditionName; (function (ConditionName) { ConditionName["CAN_DOCK"] = "can_dock"; ConditionName["NOT_SOURCES_HIDE_ADD_FOLDER"] = "!sources.hide_add_folder"; })(ConditionName || (ConditionName = {}));