blob: 11093b84ce99d24163ddd5530639cebdd22c16d4 [file] [log] [blame]
Blink Reformat4c46d092018-04-07 15:32:37 +00001/*
2 * Copyright (C) 2014 Google Inc. All rights reserved.
3 *
4 * Redistribution and use in source and binary forms, with or without
5 * modification, are permitted provided that the following conditions are
6 * met:
7 *
8 * * Redistributions of source code must retain the above copyright
9 * notice, this list of conditions and the following disclaimer.
10 * * Redistributions in binary form must reproduce the above
11 * copyright notice, this list of conditions and the following disclaimer
12 * in the documentation and/or other materials provided with the
13 * distribution.
14 * * Neither the name of Google Inc. nor the names of its
15 * contributors may be used to endorse or promote products derived from
16 * this software without specific prior written permission.
17 *
18 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
19 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
20 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
21 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
22 * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
23 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
24 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
25 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
26 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
27 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
28 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
29 */
30// This gets all concatenated module descriptors in the release mode.
31const allDescriptors = [];
32let applicationDescriptor;
33const _loadedScripts = {};
34
35// FIXME: This is a workaround to force Closure compiler provide
36// the standard ES6 runtime for all modules. This should be removed
37// once Closure provides standard externs for Map et al.
38for (const k of []) { // eslint-disable-line
39}
40
41(function() {
42const baseUrl = self.location ? self.location.origin + self.location.pathname : '';
43self._importScriptPathPrefix = baseUrl.substring(0, baseUrl.lastIndexOf('/') + 1);
44})();
45
Patrick Hulceb8c09f32018-06-11 21:28:22 +000046const REMOTE_MODULE_FALLBACK_REVISION = '@010ddcfda246975d194964ccf20038ebbdec6084';
47
Blink Reformat4c46d092018-04-07 15:32:37 +000048/**
49 * @unrestricted
50 */
51var Runtime = class { // eslint-disable-line
52 /**
53 * @param {!Array.<!Runtime.ModuleDescriptor>} descriptors
54 */
55 constructor(descriptors) {
56 /** @type {!Array<!Runtime.Module>} */
57 this._modules = [];
58 /** @type {!Object<string, !Runtime.Module>} */
59 this._modulesMap = {};
60 /** @type {!Array<!Runtime.Extension>} */
61 this._extensions = [];
62 /** @type {!Object<string, !function(new:Object)>} */
63 this._cachedTypeClasses = {};
64 /** @type {!Object<string, !Runtime.ModuleDescriptor>} */
65 this._descriptorsMap = {};
66
67 for (let i = 0; i < descriptors.length; ++i)
68 this._registerModule(descriptors[i]);
69
70 Runtime._runtimeReadyPromiseCallback();
71 }
72
73 /**
74 * @param {string} url
75 * @return {!Promise.<string>}
76 */
77 static loadResourcePromise(url) {
78 return new Promise(load);
79
80 /**
81 * @param {function(?)} fulfill
82 * @param {function(*)} reject
83 */
84 function load(fulfill, reject) {
85 const xhr = new XMLHttpRequest();
86 xhr.open('GET', url, true);
87 xhr.onreadystatechange = onreadystatechange;
88
89 /**
90 * @param {Event} e
91 */
92 function onreadystatechange(e) {
93 if (xhr.readyState !== XMLHttpRequest.DONE)
94 return;
95
Patrick Hulceb8c09f32018-06-11 21:28:22 +000096 // DevTools Proxy server can mask 404s as 200s, check the body to be sure
97 const status = /^HTTP\/1.1 404/.test(e.target.response) ? 404 : xhr.status;
98
99 if ([0, 200, 304].indexOf(status) === -1) // Testing harness file:/// results in 0.
100 reject(new Error('While loading from url ' + url + ' server responded with a status of ' + status));
Blink Reformat4c46d092018-04-07 15:32:37 +0000101 else
102 fulfill(e.target.response);
103 }
104 xhr.send(null);
105 }
106 }
107
108 /**
Patrick Hulceb8c09f32018-06-11 21:28:22 +0000109 * @param {string} url
110 * @return {!Promise.<string>}
111 */
112 static loadResourcePromiseWithFallback(url) {
113 return Runtime.loadResourcePromise(url).catch(err => {
114 const urlWithFallbackVersion = url.replace(/@[0-9a-f]{40}/, REMOTE_MODULE_FALLBACK_REVISION);
115 // TODO(phulce): mark fallbacks in module.json and modify build script instead
116 if (urlWithFallbackVersion === url || !url.includes('audits2_worker_module'))
117 throw err;
118 return Runtime.loadResourcePromise(urlWithFallbackVersion);
119 });
120 }
121
122 /**
Blink Reformat4c46d092018-04-07 15:32:37 +0000123 * http://tools.ietf.org/html/rfc3986#section-5.2.4
124 * @param {string} path
125 * @return {string}
126 */
127 static normalizePath(path) {
128 if (path.indexOf('..') === -1 && path.indexOf('.') === -1)
129 return path;
130
131 const normalizedSegments = [];
132 const segments = path.split('/');
133 for (let i = 0; i < segments.length; i++) {
134 const segment = segments[i];
135 if (segment === '.')
136 continue;
137 else if (segment === '..')
138 normalizedSegments.pop();
139 else if (segment)
140 normalizedSegments.push(segment);
141 }
142 let normalizedPath = normalizedSegments.join('/');
143 if (normalizedPath[normalizedPath.length - 1] === '/')
144 return normalizedPath;
145 if (path[0] === '/' && normalizedPath)
146 normalizedPath = '/' + normalizedPath;
147 if ((path[path.length - 1] === '/') || (segments[segments.length - 1] === '.') ||
148 (segments[segments.length - 1] === '..'))
149 normalizedPath = normalizedPath + '/';
150
151 return normalizedPath;
152 }
153
154 /**
155 * @param {!Array.<string>} scriptNames
156 * @param {string=} base
157 * @return {!Promise.<undefined>}
158 */
159 static _loadScriptsPromise(scriptNames, base) {
160 /** @type {!Array<!Promise<undefined>>} */
161 const promises = [];
162 /** @type {!Array<string>} */
163 const urls = [];
164 const sources = new Array(scriptNames.length);
165 let scriptToEval = 0;
166 for (let i = 0; i < scriptNames.length; ++i) {
167 const scriptName = scriptNames[i];
168 let sourceURL = (base || self._importScriptPathPrefix) + scriptName;
169
170 const schemaIndex = sourceURL.indexOf('://') + 3;
171 let pathIndex = sourceURL.indexOf('/', schemaIndex);
172 if (pathIndex === -1)
173 pathIndex = sourceURL.length;
174 sourceURL = sourceURL.substring(0, pathIndex) + Runtime.normalizePath(sourceURL.substring(pathIndex));
175
176 if (_loadedScripts[sourceURL])
177 continue;
178 urls.push(sourceURL);
Patrick Hulceb8c09f32018-06-11 21:28:22 +0000179 const loadResourcePromise =
180 base ? Runtime.loadResourcePromiseWithFallback(sourceURL) : Runtime.loadResourcePromise(sourceURL);
181 promises.push(
182 loadResourcePromise.then(scriptSourceLoaded.bind(null, i), scriptSourceLoaded.bind(null, i, undefined)));
Blink Reformat4c46d092018-04-07 15:32:37 +0000183 }
184 return Promise.all(promises).then(undefined);
185
186 /**
187 * @param {number} scriptNumber
188 * @param {string=} scriptSource
189 */
190 function scriptSourceLoaded(scriptNumber, scriptSource) {
191 sources[scriptNumber] = scriptSource || '';
192 // Eval scripts as fast as possible.
193 while (typeof sources[scriptToEval] !== 'undefined') {
194 evaluateScript(urls[scriptToEval], sources[scriptToEval]);
195 ++scriptToEval;
196 }
197 }
198
199 /**
200 * @param {string} sourceURL
201 * @param {string=} scriptSource
202 */
203 function evaluateScript(sourceURL, scriptSource) {
204 _loadedScripts[sourceURL] = true;
205 if (!scriptSource) {
206 // Do not reject, as this is normal in the hosted mode.
207 console.error('Empty response arrived for script \'' + sourceURL + '\'');
208 return;
209 }
210 self.eval(scriptSource + '\n//# sourceURL=' + sourceURL);
211 }
212 }
213
214 /**
215 * @param {string} url
216 * @param {boolean} appendSourceURL
217 * @return {!Promise<undefined>}
218 */
219 static _loadResourceIntoCache(url, appendSourceURL) {
220 return Runtime.loadResourcePromise(url).then(
221 cacheResource.bind(this, url), cacheResource.bind(this, url, undefined));
222
223 /**
224 * @param {string} path
225 * @param {string=} content
226 */
227 function cacheResource(path, content) {
228 if (!content) {
229 console.error('Failed to load resource: ' + path);
230 return;
231 }
232 const sourceURL = appendSourceURL ? Runtime.resolveSourceURL(path) : '';
233 Runtime.cachedResources[path] = content + sourceURL;
234 }
235 }
236
237 /**
238 * @return {!Promise}
239 */
240 static async runtimeReady() {
241 return Runtime._runtimeReadyPromise;
242 }
243
244 /**
245 * @param {string} appName
246 * @return {!Promise.<undefined>}
247 */
248 static async startApplication(appName) {
249 console.timeStamp('Runtime.startApplication');
250
251 const allDescriptorsByName = {};
252 for (let i = 0; i < allDescriptors.length; ++i) {
253 const d = allDescriptors[i];
254 allDescriptorsByName[d['name']] = d;
255 }
256
257 if (!applicationDescriptor) {
258 let data = await Runtime.loadResourcePromise(appName + '.json');
259 applicationDescriptor = JSON.parse(data);
260 let descriptor = applicationDescriptor;
261 while (descriptor.extends) {
262 data = await Runtime.loadResourcePromise(descriptor.extends + '.json');
263 descriptor = JSON.parse(data);
264 applicationDescriptor.modules = descriptor.modules.concat(applicationDescriptor.modules);
265 }
266 }
267
268 const configuration = applicationDescriptor.modules;
269 const moduleJSONPromises = [];
270 const coreModuleNames = [];
271 for (let i = 0; i < configuration.length; ++i) {
272 const descriptor = configuration[i];
273 const name = descriptor['name'];
274 const moduleJSON = allDescriptorsByName[name];
275 if (moduleJSON)
276 moduleJSONPromises.push(Promise.resolve(moduleJSON));
277 else
278 moduleJSONPromises.push(Runtime.loadResourcePromise(name + '/module.json').then(JSON.parse.bind(JSON)));
279 if (descriptor['type'] === 'autostart')
280 coreModuleNames.push(name);
281 }
282
283 const moduleDescriptors = await Promise.all(moduleJSONPromises);
284
285 for (let i = 0; i < moduleDescriptors.length; ++i) {
286 moduleDescriptors[i].name = configuration[i]['name'];
287 moduleDescriptors[i].condition = configuration[i]['condition'];
288 moduleDescriptors[i].remote = configuration[i]['type'] === 'remote';
289 }
290 self.runtime = new Runtime(moduleDescriptors);
291 if (coreModuleNames)
292 return /** @type {!Promise<undefined>} */ (self.runtime._loadAutoStartModules(coreModuleNames));
293 }
294
295 /**
296 * @param {string} appName
297 * @return {!Promise.<undefined>}
298 */
299 static startWorker(appName) {
300 return Runtime.startApplication(appName).then(sendWorkerReady);
301
302 function sendWorkerReady() {
303 self.postMessage('workerReady');
304 }
305 }
306
307 /**
308 * @param {string} name
309 * @return {?string}
310 */
311 static queryParam(name) {
Ingvar Stepanyand48ae082018-11-07 04:38:32 +0000312 return Runtime._queryParamsObject.get(name);
Blink Reformat4c46d092018-04-07 15:32:37 +0000313 }
314
315 /**
316 * @return {string}
317 */
318 static queryParamsString() {
319 return location.search;
320 }
321
322 /**
323 * @return {!Object}
324 */
325 static _experimentsSetting() {
326 try {
327 return /** @type {!Object} */ (
328 JSON.parse(self.localStorage && self.localStorage['experiments'] ? self.localStorage['experiments'] : '{}'));
329 } catch (e) {
330 console.error('Failed to parse localStorage[\'experiments\']');
331 return {};
332 }
333 }
334
335 static _assert(value, message) {
336 if (value)
337 return;
338 Runtime._originalAssert.call(Runtime._console, value, message + ' ' + new Error().stack);
339 }
340
341 /**
342 * @param {string} platform
343 */
344 static setPlatform(platform) {
345 Runtime._platform = platform;
346 }
347
348 /**
349 * @param {!Object} descriptor
350 * @return {boolean}
351 */
352 static _isDescriptorEnabled(descriptor) {
353 const activatorExperiment = descriptor['experiment'];
354 if (activatorExperiment === '*')
355 return Runtime.experiments.supportEnabled();
356 if (activatorExperiment && activatorExperiment.startsWith('!') &&
357 Runtime.experiments.isEnabled(activatorExperiment.substring(1)))
358 return false;
359 if (activatorExperiment && !activatorExperiment.startsWith('!') &&
360 !Runtime.experiments.isEnabled(activatorExperiment))
361 return false;
362 const condition = descriptor['condition'];
363 if (condition && !condition.startsWith('!') && !Runtime.queryParam(condition))
364 return false;
365 if (condition && condition.startsWith('!') && Runtime.queryParam(condition.substring(1)))
366 return false;
367 return true;
368 }
369
370 /**
371 * @param {string} path
372 * @return {string}
373 */
374 static resolveSourceURL(path) {
375 let sourceURL = self.location.href;
376 if (self.location.search)
377 sourceURL = sourceURL.replace(self.location.search, '');
378 sourceURL = sourceURL.substring(0, sourceURL.lastIndexOf('/') + 1) + path;
379 return '\n/*# sourceURL=' + sourceURL + ' */';
380 }
381
382 useTestBase() {
383 Runtime._remoteBase = 'http://localhost:8000/inspector-sources/';
384 if (Runtime.queryParam('debugFrontend'))
385 Runtime._remoteBase += 'debug/';
386 }
387
388 /**
389 * @param {!Runtime.ModuleDescriptor} descriptor
390 */
391 _registerModule(descriptor) {
392 const module = new Runtime.Module(this, descriptor);
393 this._modules.push(module);
394 this._modulesMap[descriptor['name']] = module;
395 }
396
397 /**
398 * @param {string} moduleName
399 * @return {!Promise.<undefined>}
400 */
401 loadModulePromise(moduleName) {
402 return this._modulesMap[moduleName]._loadPromise();
403 }
404
405 /**
406 * @param {!Array.<string>} moduleNames
407 * @return {!Promise.<!Array.<*>>}
408 */
409 _loadAutoStartModules(moduleNames) {
410 const promises = [];
411 for (let i = 0; i < moduleNames.length; ++i)
412 promises.push(this.loadModulePromise(moduleNames[i]));
413 return Promise.all(promises);
414 }
415
416 /**
417 * @param {!Runtime.Extension} extension
418 * @param {?function(function(new:Object)):boolean} predicate
419 * @return {boolean}
420 */
421 _checkExtensionApplicability(extension, predicate) {
422 if (!predicate)
423 return false;
424 const contextTypes = extension.descriptor().contextTypes;
425 if (!contextTypes)
426 return true;
427 for (let i = 0; i < contextTypes.length; ++i) {
428 const contextType = this._resolve(contextTypes[i]);
429 const isMatching = !!contextType && predicate(contextType);
430 if (isMatching)
431 return true;
432 }
433 return false;
434 }
435
436 /**
437 * @param {!Runtime.Extension} extension
438 * @param {?Object} context
439 * @return {boolean}
440 */
441 isExtensionApplicableToContext(extension, context) {
442 if (!context)
443 return true;
444 return this._checkExtensionApplicability(extension, isInstanceOf);
445
446 /**
447 * @param {!Function} targetType
448 * @return {boolean}
449 */
450 function isInstanceOf(targetType) {
451 return context instanceof targetType;
452 }
453 }
454
455 /**
456 * @param {!Runtime.Extension} extension
457 * @param {!Set.<!Function>=} currentContextTypes
458 * @return {boolean}
459 */
460 isExtensionApplicableToContextTypes(extension, currentContextTypes) {
461 if (!extension.descriptor().contextTypes)
462 return true;
463
464 return this._checkExtensionApplicability(extension, currentContextTypes ? isContextTypeKnown : null);
465
466 /**
467 * @param {!Function} targetType
468 * @return {boolean}
469 */
470 function isContextTypeKnown(targetType) {
471 return currentContextTypes.has(targetType);
472 }
473 }
474
475 /**
476 * @param {*} type
477 * @param {?Object=} context
478 * @param {boolean=} sortByTitle
479 * @return {!Array.<!Runtime.Extension>}
480 */
481 extensions(type, context, sortByTitle) {
482 return this._extensions.filter(filter).sort(sortByTitle ? titleComparator : orderComparator);
483
484 /**
485 * @param {!Runtime.Extension} extension
486 * @return {boolean}
487 */
488 function filter(extension) {
489 if (extension._type !== type && extension._typeClass() !== type)
490 return false;
491 if (!extension.enabled())
492 return false;
493 return !context || extension.isApplicable(context);
494 }
495
496 /**
497 * @param {!Runtime.Extension} extension1
498 * @param {!Runtime.Extension} extension2
499 * @return {number}
500 */
501 function orderComparator(extension1, extension2) {
502 const order1 = extension1.descriptor()['order'] || 0;
503 const order2 = extension2.descriptor()['order'] || 0;
504 return order1 - order2;
505 }
506
507 /**
508 * @param {!Runtime.Extension} extension1
509 * @param {!Runtime.Extension} extension2
510 * @return {number}
511 */
512 function titleComparator(extension1, extension2) {
513 const title1 = extension1.title() || '';
514 const title2 = extension2.title() || '';
515 return title1.localeCompare(title2);
516 }
517 }
518
519 /**
520 * @param {*} type
521 * @param {?Object=} context
522 * @return {?Runtime.Extension}
523 */
524 extension(type, context) {
525 return this.extensions(type, context)[0] || null;
526 }
527
528 /**
529 * @param {*} type
530 * @param {?Object=} context
531 * @return {!Promise.<!Array.<!Object>>}
532 */
533 allInstances(type, context) {
534 return Promise.all(this.extensions(type, context).map(extension => extension.instance()));
535 }
536
537 /**
538 * @return {?function(new:Object)}
539 */
540 _resolve(typeName) {
541 if (!this._cachedTypeClasses[typeName]) {
542 const path = typeName.split('.');
543 let object = self;
544 for (let i = 0; object && (i < path.length); ++i)
545 object = object[path[i]];
546 if (object)
547 this._cachedTypeClasses[typeName] = /** @type function(new:Object) */ (object);
548 }
549 return this._cachedTypeClasses[typeName] || null;
550 }
551
552 /**
553 * @param {!Function} constructorFunction
554 * @return {!Object}
555 */
556 sharedInstance(constructorFunction) {
557 if (Runtime._instanceSymbol in constructorFunction &&
558 Object.getOwnPropertySymbols(constructorFunction).includes(Runtime._instanceSymbol))
559 return constructorFunction[Runtime._instanceSymbol];
560
561 const instance = new constructorFunction();
562 constructorFunction[Runtime._instanceSymbol] = instance;
563 return instance;
564 }
565};
566
Ingvar Stepanyand48ae082018-11-07 04:38:32 +0000567/** @type {!URLSearchParams} */
568Runtime._queryParamsObject = new URLSearchParams(Runtime.queryParamsString());
Blink Reformat4c46d092018-04-07 15:32:37 +0000569
570Runtime._instanceSymbol = Symbol('instance');
571
572/**
573 * @type {!Object.<string, string>}
574 */
575Runtime.cachedResources = {
576 __proto__: null
577};
578
579
580Runtime._console = console;
581Runtime._originalAssert = console.assert;
582
583
584Runtime._platform = '';
585
586
587/**
588 * @unrestricted
589 */
590Runtime.ModuleDescriptor = class {
591 constructor() {
592 /**
593 * @type {string}
594 */
595 this.name;
596
597 /**
598 * @type {!Array.<!Runtime.ExtensionDescriptor>}
599 */
600 this.extensions;
601
602 /**
603 * @type {!Array.<string>|undefined}
604 */
605 this.dependencies;
606
607 /**
608 * @type {!Array.<string>}
609 */
610 this.scripts;
611
612 /**
613 * @type {string|undefined}
614 */
615 this.condition;
616
617 /**
618 * @type {boolean|undefined}
619 */
620 this.remote;
621 }
622};
623
624/**
625 * @unrestricted
626 */
627Runtime.ExtensionDescriptor = class {
628 constructor() {
629 /**
630 * @type {string}
631 */
632 this.type;
633
634 /**
635 * @type {string|undefined}
636 */
637 this.className;
638
639 /**
640 * @type {string|undefined}
641 */
642 this.factoryName;
643
644 /**
645 * @type {!Array.<string>|undefined}
646 */
647 this.contextTypes;
648 }
649};
650
651/**
652 * @unrestricted
653 */
654Runtime.Module = class {
655 /**
656 * @param {!Runtime} manager
657 * @param {!Runtime.ModuleDescriptor} descriptor
658 */
659 constructor(manager, descriptor) {
660 this._manager = manager;
661 this._descriptor = descriptor;
662 this._name = descriptor.name;
663 /** @type {!Array<!Runtime.Extension>} */
664 this._extensions = [];
665
666 /** @type {!Map<string, !Array<!Runtime.Extension>>} */
667 this._extensionsByClassName = new Map();
668 const extensions = /** @type {?Array.<!Runtime.ExtensionDescriptor>} */ (descriptor.extensions);
669 for (let i = 0; extensions && i < extensions.length; ++i) {
670 const extension = new Runtime.Extension(this, extensions[i]);
671 this._manager._extensions.push(extension);
672 this._extensions.push(extension);
673 }
674 this._loadedForTest = false;
675 }
676
677 /**
678 * @return {string}
679 */
680 name() {
681 return this._name;
682 }
683
684 /**
685 * @return {boolean}
686 */
687 enabled() {
688 return Runtime._isDescriptorEnabled(this._descriptor);
689 }
690
691 /**
692 * @param {string} name
693 * @return {string}
694 */
695 resource(name) {
696 const fullName = this._name + '/' + name;
697 const content = Runtime.cachedResources[fullName];
698 if (!content)
699 throw new Error(fullName + ' not preloaded. Check module.json');
700 return content;
701 }
702
703 /**
704 * @return {!Promise.<undefined>}
705 */
706 _loadPromise() {
707 if (!this.enabled())
708 return Promise.reject(new Error('Module ' + this._name + ' is not enabled'));
709
710 if (this._pendingLoadPromise)
711 return this._pendingLoadPromise;
712
713 const dependencies = this._descriptor.dependencies;
714 const dependencyPromises = [];
715 for (let i = 0; dependencies && i < dependencies.length; ++i)
716 dependencyPromises.push(this._manager._modulesMap[dependencies[i]]._loadPromise());
717
718 this._pendingLoadPromise = Promise.all(dependencyPromises)
719 .then(this._loadResources.bind(this))
720 .then(this._loadScripts.bind(this))
721 .then(() => this._loadedForTest = true);
722
723 return this._pendingLoadPromise;
724 }
725
726 /**
727 * @return {!Promise.<undefined>}
728 * @this {Runtime.Module}
729 */
730 _loadResources() {
731 const resources = this._descriptor['resources'];
732 if (!resources || !resources.length)
733 return Promise.resolve();
734 const promises = [];
735 for (let i = 0; i < resources.length; ++i) {
736 const url = this._modularizeURL(resources[i]);
737 promises.push(Runtime._loadResourceIntoCache(url, true));
738 }
739 return Promise.all(promises).then(undefined);
740 }
741
742 /**
743 * @return {!Promise.<undefined>}
744 */
745 _loadScripts() {
746 if (!this._descriptor.scripts || !this._descriptor.scripts.length)
747 return Promise.resolve();
748
749 // Module namespaces.
750 // NOTE: Update scripts/special_case_namespaces.json if you add a special cased namespace.
751 // The namespace keyword confuses clang-format.
752 // clang-format off
753 const specialCases = {
754 'sdk': 'SDK',
755 'js_sdk': 'JSSDK',
756 'browser_sdk': 'BrowserSDK',
757 'ui': 'UI',
758 'object_ui': 'ObjectUI',
Joel Einbinder3f23eb22018-05-14 23:27:51 +0000759 'javascript_metadata': 'JavaScriptMetadata',
Blink Reformat4c46d092018-04-07 15:32:37 +0000760 'perf_ui': 'PerfUI',
761 'har_importer': 'HARImporter',
762 'sdk_test_runner': 'SDKTestRunner',
763 'cpu_profiler_test_runner': 'CPUProfilerTestRunner'
764 };
765 const namespace = specialCases[this._name] || this._name.split('_').map(a => a.substring(0, 1).toUpperCase() + a.substring(1)).join('');
766 self[namespace] = self[namespace] || {};
767 // clang-format on
768 return Runtime._loadScriptsPromise(this._descriptor.scripts.map(this._modularizeURL, this), this._remoteBase());
769 }
770
771 /**
772 * @param {string} resourceName
773 */
774 _modularizeURL(resourceName) {
775 return Runtime.normalizePath(this._name + '/' + resourceName);
776 }
777
778 /**
779 * @return {string|undefined}
780 */
781 _remoteBase() {
782 return !Runtime.queryParam('debugFrontend') && this._descriptor.remote && Runtime._remoteBase || undefined;
783 }
784
785 /**
786 * @param {string} value
787 * @return {string}
788 */
789 substituteURL(value) {
790 const base = this._remoteBase() || '';
791 return value.replace(/@url\(([^\)]*?)\)/g, convertURL.bind(this));
792
793 function convertURL(match, url) {
794 return base + this._modularizeURL(url);
795 }
796 }
797};
798
799
800/**
801 * @unrestricted
802 */
803Runtime.Extension = class {
804 /**
805 * @param {!Runtime.Module} module
806 * @param {!Runtime.ExtensionDescriptor} descriptor
807 */
808 constructor(module, descriptor) {
809 this._module = module;
810 this._descriptor = descriptor;
811
812 this._type = descriptor.type;
813 this._hasTypeClass = this._type.charAt(0) === '@';
814
815 /**
816 * @type {?string}
817 */
818 this._className = descriptor.className || null;
819 this._factoryName = descriptor.factoryName || null;
820 }
821
822 /**
823 * @return {!Object}
824 */
825 descriptor() {
826 return this._descriptor;
827 }
828
829 /**
830 * @return {!Runtime.Module}
831 */
832 module() {
833 return this._module;
834 }
835
836 /**
837 * @return {boolean}
838 */
839 enabled() {
840 return this._module.enabled() && Runtime._isDescriptorEnabled(this.descriptor());
841 }
842
843 /**
844 * @return {?function(new:Object)}
845 */
846 _typeClass() {
847 if (!this._hasTypeClass)
848 return null;
849 return this._module._manager._resolve(this._type.substring(1));
850 }
851
852 /**
853 * @param {?Object} context
854 * @return {boolean}
855 */
856 isApplicable(context) {
857 return this._module._manager.isExtensionApplicableToContext(this, context);
858 }
859
860 /**
861 * @return {!Promise.<!Object>}
862 */
863 instance() {
864 return this._module._loadPromise().then(this._createInstance.bind(this));
865 }
866
867 /**
868 * @return {boolean}
869 */
870 canInstantiate() {
871 return !!(this._className || this._factoryName);
872 }
873
874 /**
875 * @return {!Object}
876 */
877 _createInstance() {
878 const className = this._className || this._factoryName;
879 if (!className)
880 throw new Error('Could not instantiate extension with no class');
881 const constructorFunction = self.eval(/** @type {string} */ (className));
882 if (!(constructorFunction instanceof Function))
883 throw new Error('Could not instantiate: ' + className);
884 if (this._className)
885 return this._module._manager.sharedInstance(constructorFunction);
886 return new constructorFunction(this);
887 }
888
889 /**
890 * @return {string}
891 */
892 title() {
893 // FIXME: should be Common.UIString() but runtime is not l10n aware yet.
894 return this._descriptor['title-' + Runtime._platform] || this._descriptor['title'];
895 }
896
897 /**
898 * @param {function(new:Object)} contextType
899 * @return {boolean}
900 */
901 hasContextType(contextType) {
902 const contextTypes = this.descriptor().contextTypes;
903 if (!contextTypes)
904 return false;
905 for (let i = 0; i < contextTypes.length; ++i) {
906 if (contextType === this._module._manager._resolve(contextTypes[i]))
907 return true;
908 }
909 return false;
910 }
911};
912
913/**
914 * @unrestricted
915 */
916Runtime.ExperimentsSupport = class {
917 constructor() {
918 this._supportEnabled = Runtime.queryParam('experiments') !== null;
919 this._experiments = [];
920 this._experimentNames = {};
921 this._enabledTransiently = {};
922 }
923
924 /**
925 * @return {!Array.<!Runtime.Experiment>}
926 */
927 allConfigurableExperiments() {
928 const result = [];
929 for (let i = 0; i < this._experiments.length; i++) {
930 const experiment = this._experiments[i];
931 if (!this._enabledTransiently[experiment.name])
932 result.push(experiment);
933 }
934 return result;
935 }
936
937 /**
938 * @return {boolean}
939 */
940 supportEnabled() {
941 return this._supportEnabled;
942 }
943
944 /**
945 * @param {!Object} value
946 */
947 _setExperimentsSetting(value) {
948 if (!self.localStorage)
949 return;
950 self.localStorage['experiments'] = JSON.stringify(value);
951 }
952
953 /**
954 * @param {string} experimentName
955 * @param {string} experimentTitle
956 * @param {boolean=} hidden
957 */
958 register(experimentName, experimentTitle, hidden) {
959 Runtime._assert(!this._experimentNames[experimentName], 'Duplicate registration of experiment ' + experimentName);
960 this._experimentNames[experimentName] = true;
961 this._experiments.push(new Runtime.Experiment(this, experimentName, experimentTitle, !!hidden));
962 }
963
964 /**
965 * @param {string} experimentName
966 * @return {boolean}
967 */
968 isEnabled(experimentName) {
969 this._checkExperiment(experimentName);
Pavel Feldmanc0395912018-08-03 02:05:00 +0000970 // Check for explicitly disabled experiments first - the code could call setEnable(false) on the experiment enabled
971 // by default and we should respect that.
972 if (Runtime._experimentsSetting()[experimentName] === false)
973 return false;
Blink Reformat4c46d092018-04-07 15:32:37 +0000974 if (this._enabledTransiently[experimentName])
975 return true;
976 if (!this.supportEnabled())
977 return false;
978
979 return !!Runtime._experimentsSetting()[experimentName];
980 }
981
982 /**
983 * @param {string} experimentName
984 * @param {boolean} enabled
985 */
986 setEnabled(experimentName, enabled) {
987 this._checkExperiment(experimentName);
988 const experimentsSetting = Runtime._experimentsSetting();
989 experimentsSetting[experimentName] = enabled;
990 this._setExperimentsSetting(experimentsSetting);
991 }
992
993 /**
994 * @param {!Array.<string>} experimentNames
995 */
996 setDefaultExperiments(experimentNames) {
997 for (let i = 0; i < experimentNames.length; ++i) {
998 this._checkExperiment(experimentNames[i]);
999 this._enabledTransiently[experimentNames[i]] = true;
1000 }
1001 }
1002
1003 /**
1004 * @param {string} experimentName
1005 */
1006 enableForTest(experimentName) {
1007 this._checkExperiment(experimentName);
1008 this._enabledTransiently[experimentName] = true;
1009 }
1010
1011 clearForTest() {
1012 this._experiments = [];
1013 this._experimentNames = {};
1014 this._enabledTransiently = {};
1015 }
1016
1017 cleanUpStaleExperiments() {
1018 const experimentsSetting = Runtime._experimentsSetting();
1019 const cleanedUpExperimentSetting = {};
1020 for (let i = 0; i < this._experiments.length; ++i) {
1021 const experimentName = this._experiments[i].name;
1022 if (experimentsSetting[experimentName])
1023 cleanedUpExperimentSetting[experimentName] = true;
1024 }
1025 this._setExperimentsSetting(cleanedUpExperimentSetting);
1026 }
1027
1028 /**
1029 * @param {string} experimentName
1030 */
1031 _checkExperiment(experimentName) {
1032 Runtime._assert(this._experimentNames[experimentName], 'Unknown experiment ' + experimentName);
1033 }
1034};
1035
1036/**
1037 * @unrestricted
1038 */
1039Runtime.Experiment = class {
1040 /**
1041 * @param {!Runtime.ExperimentsSupport} experiments
1042 * @param {string} name
1043 * @param {string} title
1044 * @param {boolean} hidden
1045 */
1046 constructor(experiments, name, title, hidden) {
1047 this.name = name;
1048 this.title = title;
1049 this.hidden = hidden;
1050 this._experiments = experiments;
1051 }
1052
1053 /**
1054 * @return {boolean}
1055 */
1056 isEnabled() {
1057 return this._experiments.isEnabled(this.name);
1058 }
1059
1060 /**
1061 * @param {boolean} enabled
1062 */
1063 setEnabled(enabled) {
1064 this._experiments.setEnabled(this.name, enabled);
1065 }
1066};
1067
Blink Reformat4c46d092018-04-07 15:32:37 +00001068// This must be constructed after the query parameters have been parsed.
1069Runtime.experiments = new Runtime.ExperimentsSupport();
1070
1071/** @type {Function} */
1072Runtime._runtimeReadyPromiseCallback;
1073Runtime._runtimeReadyPromise = new Promise(fulfil => Runtime._runtimeReadyPromiseCallback = fulfil);
1074/**
1075 * @type {?string}
1076 */
1077Runtime._remoteBase;
1078(function validateRemoteBase() {
1079 if (location.href.startsWith('chrome-devtools://devtools/bundled/') && Runtime.queryParam('remoteBase')) {
1080 const versionMatch = /\/serve_file\/(@[0-9a-zA-Z]+)\/?$/.exec(Runtime.queryParam('remoteBase'));
1081 if (versionMatch)
1082 Runtime._remoteBase = `${location.origin}/remote/serve_file/${versionMatch[1]}/`;
1083 }
1084})();
1085
1086
1087/**
1088 * @interface
1089 */
1090function ServicePort() {
1091}
1092
1093ServicePort.prototype = {
1094 /**
1095 * @param {function(string)} messageHandler
1096 * @param {function(string)} closeHandler
1097 */
1098 setHandlers(messageHandler, closeHandler) {},
1099
1100 /**
1101 * @param {string} message
1102 * @return {!Promise<boolean>}
1103 */
1104 send(message) {},
1105
1106 /**
1107 * @return {!Promise<boolean>}
1108 */
1109 close() {}
1110};
1111
1112/** @type {!Runtime} */
1113var runtime; // eslint-disable-line