blob: 5e6e5f1a0eb2fdfdb613203392fa1c63c5890372 [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) {
312 return Runtime._queryParamsObject[name] || null;
313 }
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
567/**
568 * @type {!Object.<string, string>}
569 */
570Runtime._queryParamsObject = {
571 __proto__: null
572};
573
574Runtime._instanceSymbol = Symbol('instance');
575
576/**
577 * @type {!Object.<string, string>}
578 */
579Runtime.cachedResources = {
580 __proto__: null
581};
582
583
584Runtime._console = console;
585Runtime._originalAssert = console.assert;
586
587
588Runtime._platform = '';
589
590
591/**
592 * @unrestricted
593 */
594Runtime.ModuleDescriptor = class {
595 constructor() {
596 /**
597 * @type {string}
598 */
599 this.name;
600
601 /**
602 * @type {!Array.<!Runtime.ExtensionDescriptor>}
603 */
604 this.extensions;
605
606 /**
607 * @type {!Array.<string>|undefined}
608 */
609 this.dependencies;
610
611 /**
612 * @type {!Array.<string>}
613 */
614 this.scripts;
615
616 /**
617 * @type {string|undefined}
618 */
619 this.condition;
620
621 /**
622 * @type {boolean|undefined}
623 */
624 this.remote;
625 }
626};
627
628/**
629 * @unrestricted
630 */
631Runtime.ExtensionDescriptor = class {
632 constructor() {
633 /**
634 * @type {string}
635 */
636 this.type;
637
638 /**
639 * @type {string|undefined}
640 */
641 this.className;
642
643 /**
644 * @type {string|undefined}
645 */
646 this.factoryName;
647
648 /**
649 * @type {!Array.<string>|undefined}
650 */
651 this.contextTypes;
652 }
653};
654
655/**
656 * @unrestricted
657 */
658Runtime.Module = class {
659 /**
660 * @param {!Runtime} manager
661 * @param {!Runtime.ModuleDescriptor} descriptor
662 */
663 constructor(manager, descriptor) {
664 this._manager = manager;
665 this._descriptor = descriptor;
666 this._name = descriptor.name;
667 /** @type {!Array<!Runtime.Extension>} */
668 this._extensions = [];
669
670 /** @type {!Map<string, !Array<!Runtime.Extension>>} */
671 this._extensionsByClassName = new Map();
672 const extensions = /** @type {?Array.<!Runtime.ExtensionDescriptor>} */ (descriptor.extensions);
673 for (let i = 0; extensions && i < extensions.length; ++i) {
674 const extension = new Runtime.Extension(this, extensions[i]);
675 this._manager._extensions.push(extension);
676 this._extensions.push(extension);
677 }
678 this._loadedForTest = false;
679 }
680
681 /**
682 * @return {string}
683 */
684 name() {
685 return this._name;
686 }
687
688 /**
689 * @return {boolean}
690 */
691 enabled() {
692 return Runtime._isDescriptorEnabled(this._descriptor);
693 }
694
695 /**
696 * @param {string} name
697 * @return {string}
698 */
699 resource(name) {
700 const fullName = this._name + '/' + name;
701 const content = Runtime.cachedResources[fullName];
702 if (!content)
703 throw new Error(fullName + ' not preloaded. Check module.json');
704 return content;
705 }
706
707 /**
708 * @return {!Promise.<undefined>}
709 */
710 _loadPromise() {
711 if (!this.enabled())
712 return Promise.reject(new Error('Module ' + this._name + ' is not enabled'));
713
714 if (this._pendingLoadPromise)
715 return this._pendingLoadPromise;
716
717 const dependencies = this._descriptor.dependencies;
718 const dependencyPromises = [];
719 for (let i = 0; dependencies && i < dependencies.length; ++i)
720 dependencyPromises.push(this._manager._modulesMap[dependencies[i]]._loadPromise());
721
722 this._pendingLoadPromise = Promise.all(dependencyPromises)
723 .then(this._loadResources.bind(this))
724 .then(this._loadScripts.bind(this))
725 .then(() => this._loadedForTest = true);
726
727 return this._pendingLoadPromise;
728 }
729
730 /**
731 * @return {!Promise.<undefined>}
732 * @this {Runtime.Module}
733 */
734 _loadResources() {
735 const resources = this._descriptor['resources'];
736 if (!resources || !resources.length)
737 return Promise.resolve();
738 const promises = [];
739 for (let i = 0; i < resources.length; ++i) {
740 const url = this._modularizeURL(resources[i]);
741 promises.push(Runtime._loadResourceIntoCache(url, true));
742 }
743 return Promise.all(promises).then(undefined);
744 }
745
746 /**
747 * @return {!Promise.<undefined>}
748 */
749 _loadScripts() {
750 if (!this._descriptor.scripts || !this._descriptor.scripts.length)
751 return Promise.resolve();
752
753 // Module namespaces.
754 // NOTE: Update scripts/special_case_namespaces.json if you add a special cased namespace.
755 // The namespace keyword confuses clang-format.
756 // clang-format off
757 const specialCases = {
758 'sdk': 'SDK',
759 'js_sdk': 'JSSDK',
760 'browser_sdk': 'BrowserSDK',
761 'ui': 'UI',
762 'object_ui': 'ObjectUI',
Joel Einbinder3f23eb22018-05-14 23:27:51 +0000763 'javascript_metadata': 'JavaScriptMetadata',
Blink Reformat4c46d092018-04-07 15:32:37 +0000764 'perf_ui': 'PerfUI',
765 'har_importer': 'HARImporter',
766 'sdk_test_runner': 'SDKTestRunner',
767 'cpu_profiler_test_runner': 'CPUProfilerTestRunner'
768 };
769 const namespace = specialCases[this._name] || this._name.split('_').map(a => a.substring(0, 1).toUpperCase() + a.substring(1)).join('');
770 self[namespace] = self[namespace] || {};
771 // clang-format on
772 return Runtime._loadScriptsPromise(this._descriptor.scripts.map(this._modularizeURL, this), this._remoteBase());
773 }
774
775 /**
776 * @param {string} resourceName
777 */
778 _modularizeURL(resourceName) {
779 return Runtime.normalizePath(this._name + '/' + resourceName);
780 }
781
782 /**
783 * @return {string|undefined}
784 */
785 _remoteBase() {
786 return !Runtime.queryParam('debugFrontend') && this._descriptor.remote && Runtime._remoteBase || undefined;
787 }
788
789 /**
790 * @param {string} value
791 * @return {string}
792 */
793 substituteURL(value) {
794 const base = this._remoteBase() || '';
795 return value.replace(/@url\(([^\)]*?)\)/g, convertURL.bind(this));
796
797 function convertURL(match, url) {
798 return base + this._modularizeURL(url);
799 }
800 }
801};
802
803
804/**
805 * @unrestricted
806 */
807Runtime.Extension = class {
808 /**
809 * @param {!Runtime.Module} module
810 * @param {!Runtime.ExtensionDescriptor} descriptor
811 */
812 constructor(module, descriptor) {
813 this._module = module;
814 this._descriptor = descriptor;
815
816 this._type = descriptor.type;
817 this._hasTypeClass = this._type.charAt(0) === '@';
818
819 /**
820 * @type {?string}
821 */
822 this._className = descriptor.className || null;
823 this._factoryName = descriptor.factoryName || null;
824 }
825
826 /**
827 * @return {!Object}
828 */
829 descriptor() {
830 return this._descriptor;
831 }
832
833 /**
834 * @return {!Runtime.Module}
835 */
836 module() {
837 return this._module;
838 }
839
840 /**
841 * @return {boolean}
842 */
843 enabled() {
844 return this._module.enabled() && Runtime._isDescriptorEnabled(this.descriptor());
845 }
846
847 /**
848 * @return {?function(new:Object)}
849 */
850 _typeClass() {
851 if (!this._hasTypeClass)
852 return null;
853 return this._module._manager._resolve(this._type.substring(1));
854 }
855
856 /**
857 * @param {?Object} context
858 * @return {boolean}
859 */
860 isApplicable(context) {
861 return this._module._manager.isExtensionApplicableToContext(this, context);
862 }
863
864 /**
865 * @return {!Promise.<!Object>}
866 */
867 instance() {
868 return this._module._loadPromise().then(this._createInstance.bind(this));
869 }
870
871 /**
872 * @return {boolean}
873 */
874 canInstantiate() {
875 return !!(this._className || this._factoryName);
876 }
877
878 /**
879 * @return {!Object}
880 */
881 _createInstance() {
882 const className = this._className || this._factoryName;
883 if (!className)
884 throw new Error('Could not instantiate extension with no class');
885 const constructorFunction = self.eval(/** @type {string} */ (className));
886 if (!(constructorFunction instanceof Function))
887 throw new Error('Could not instantiate: ' + className);
888 if (this._className)
889 return this._module._manager.sharedInstance(constructorFunction);
890 return new constructorFunction(this);
891 }
892
893 /**
894 * @return {string}
895 */
896 title() {
897 // FIXME: should be Common.UIString() but runtime is not l10n aware yet.
898 return this._descriptor['title-' + Runtime._platform] || this._descriptor['title'];
899 }
900
901 /**
902 * @param {function(new:Object)} contextType
903 * @return {boolean}
904 */
905 hasContextType(contextType) {
906 const contextTypes = this.descriptor().contextTypes;
907 if (!contextTypes)
908 return false;
909 for (let i = 0; i < contextTypes.length; ++i) {
910 if (contextType === this._module._manager._resolve(contextTypes[i]))
911 return true;
912 }
913 return false;
914 }
915};
916
917/**
918 * @unrestricted
919 */
920Runtime.ExperimentsSupport = class {
921 constructor() {
922 this._supportEnabled = Runtime.queryParam('experiments') !== null;
923 this._experiments = [];
924 this._experimentNames = {};
925 this._enabledTransiently = {};
926 }
927
928 /**
929 * @return {!Array.<!Runtime.Experiment>}
930 */
931 allConfigurableExperiments() {
932 const result = [];
933 for (let i = 0; i < this._experiments.length; i++) {
934 const experiment = this._experiments[i];
935 if (!this._enabledTransiently[experiment.name])
936 result.push(experiment);
937 }
938 return result;
939 }
940
941 /**
942 * @return {boolean}
943 */
944 supportEnabled() {
945 return this._supportEnabled;
946 }
947
948 /**
949 * @param {!Object} value
950 */
951 _setExperimentsSetting(value) {
952 if (!self.localStorage)
953 return;
954 self.localStorage['experiments'] = JSON.stringify(value);
955 }
956
957 /**
958 * @param {string} experimentName
959 * @param {string} experimentTitle
960 * @param {boolean=} hidden
961 */
962 register(experimentName, experimentTitle, hidden) {
963 Runtime._assert(!this._experimentNames[experimentName], 'Duplicate registration of experiment ' + experimentName);
964 this._experimentNames[experimentName] = true;
965 this._experiments.push(new Runtime.Experiment(this, experimentName, experimentTitle, !!hidden));
966 }
967
968 /**
969 * @param {string} experimentName
970 * @return {boolean}
971 */
972 isEnabled(experimentName) {
973 this._checkExperiment(experimentName);
Pavel Feldmanc0395912018-08-03 02:05:00 +0000974 // Check for explicitly disabled experiments first - the code could call setEnable(false) on the experiment enabled
975 // by default and we should respect that.
976 if (Runtime._experimentsSetting()[experimentName] === false)
977 return false;
Blink Reformat4c46d092018-04-07 15:32:37 +0000978 if (this._enabledTransiently[experimentName])
979 return true;
980 if (!this.supportEnabled())
981 return false;
982
983 return !!Runtime._experimentsSetting()[experimentName];
984 }
985
986 /**
987 * @param {string} experimentName
988 * @param {boolean} enabled
989 */
990 setEnabled(experimentName, enabled) {
991 this._checkExperiment(experimentName);
992 const experimentsSetting = Runtime._experimentsSetting();
993 experimentsSetting[experimentName] = enabled;
994 this._setExperimentsSetting(experimentsSetting);
995 }
996
997 /**
998 * @param {!Array.<string>} experimentNames
999 */
1000 setDefaultExperiments(experimentNames) {
1001 for (let i = 0; i < experimentNames.length; ++i) {
1002 this._checkExperiment(experimentNames[i]);
1003 this._enabledTransiently[experimentNames[i]] = true;
1004 }
1005 }
1006
1007 /**
1008 * @param {string} experimentName
1009 */
1010 enableForTest(experimentName) {
1011 this._checkExperiment(experimentName);
1012 this._enabledTransiently[experimentName] = true;
1013 }
1014
1015 clearForTest() {
1016 this._experiments = [];
1017 this._experimentNames = {};
1018 this._enabledTransiently = {};
1019 }
1020
1021 cleanUpStaleExperiments() {
1022 const experimentsSetting = Runtime._experimentsSetting();
1023 const cleanedUpExperimentSetting = {};
1024 for (let i = 0; i < this._experiments.length; ++i) {
1025 const experimentName = this._experiments[i].name;
1026 if (experimentsSetting[experimentName])
1027 cleanedUpExperimentSetting[experimentName] = true;
1028 }
1029 this._setExperimentsSetting(cleanedUpExperimentSetting);
1030 }
1031
1032 /**
1033 * @param {string} experimentName
1034 */
1035 _checkExperiment(experimentName) {
1036 Runtime._assert(this._experimentNames[experimentName], 'Unknown experiment ' + experimentName);
1037 }
1038};
1039
1040/**
1041 * @unrestricted
1042 */
1043Runtime.Experiment = class {
1044 /**
1045 * @param {!Runtime.ExperimentsSupport} experiments
1046 * @param {string} name
1047 * @param {string} title
1048 * @param {boolean} hidden
1049 */
1050 constructor(experiments, name, title, hidden) {
1051 this.name = name;
1052 this.title = title;
1053 this.hidden = hidden;
1054 this._experiments = experiments;
1055 }
1056
1057 /**
1058 * @return {boolean}
1059 */
1060 isEnabled() {
1061 return this._experiments.isEnabled(this.name);
1062 }
1063
1064 /**
1065 * @param {boolean} enabled
1066 */
1067 setEnabled(enabled) {
1068 this._experiments.setEnabled(this.name, enabled);
1069 }
1070};
1071
1072{
1073 (function parseQueryParameters() {
1074 const queryParams = Runtime.queryParamsString();
1075 if (!queryParams)
1076 return;
1077 const params = queryParams.substring(1).split('&');
1078 for (let i = 0; i < params.length; ++i) {
1079 const pair = params[i].split('=');
1080 const name = pair.shift();
1081 Runtime._queryParamsObject[name] = pair.join('=');
1082 }
1083 })();
1084}
1085
1086// This must be constructed after the query parameters have been parsed.
1087Runtime.experiments = new Runtime.ExperimentsSupport();
1088
1089/** @type {Function} */
1090Runtime._runtimeReadyPromiseCallback;
1091Runtime._runtimeReadyPromise = new Promise(fulfil => Runtime._runtimeReadyPromiseCallback = fulfil);
1092/**
1093 * @type {?string}
1094 */
1095Runtime._remoteBase;
1096(function validateRemoteBase() {
1097 if (location.href.startsWith('chrome-devtools://devtools/bundled/') && Runtime.queryParam('remoteBase')) {
1098 const versionMatch = /\/serve_file\/(@[0-9a-zA-Z]+)\/?$/.exec(Runtime.queryParam('remoteBase'));
1099 if (versionMatch)
1100 Runtime._remoteBase = `${location.origin}/remote/serve_file/${versionMatch[1]}/`;
1101 }
1102})();
1103
1104
1105/**
1106 * @interface
1107 */
1108function ServicePort() {
1109}
1110
1111ServicePort.prototype = {
1112 /**
1113 * @param {function(string)} messageHandler
1114 * @param {function(string)} closeHandler
1115 */
1116 setHandlers(messageHandler, closeHandler) {},
1117
1118 /**
1119 * @param {string} message
1120 * @return {!Promise<boolean>}
1121 */
1122 send(message) {},
1123
1124 /**
1125 * @return {!Promise<boolean>}
1126 */
1127 close() {}
1128};
1129
1130/** @type {!Runtime} */
1131var runtime; // eslint-disable-line