Files
gerrit/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-plugin-loader.js
Dmitrii Filippov b82003c49a Update eslint version and eslint rules
Legacy indent rules doesn't handle all cases. As a result there are
different indents in .js files. This commit update eslint rules and add
autofix for incorrect indents. It is expected that fix should be run
after converting to class-based elements.

Change-Id: I9d37a3d4319e2af71ddb93100a6791b8ddb7de79
2019-11-06 13:00:04 +01:00

393 lines
11 KiB
JavaScript

/**
* @license
* Copyright (C) 2019 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
(function(window) {
'use strict';
// Import utils methods
const {
PLUGIN_LOADING_TIMEOUT_MS,
PRELOADED_PROTOCOL,
getPluginNameFromUrl,
getBaseUrl,
} = window._apiUtils;
/**
* @enum {string}
*/
const PluginState = {
/**
* State that indicates the plugin is pending to be loaded.
*/
PENDING: 'PENDING',
/**
* State that indicates the plugin is already loaded.
*/
LOADED: 'LOADED',
/**
* State that indicates the plugin is already loaded.
*/
PRE_LOADED: 'PRE_LOADED',
/**
* State that indicates the plugin failed to load.
*/
LOAD_FAILED: 'LOAD_FAILED',
};
// Prefix for any unrecognized plugin urls.
// Url should match following patterns:
// /plugins/PLUGINNAME/static/SCRIPTNAME.(html|js)
// /plugins/PLUGINNAME.(js|html)
const UNKNOWN_PLUGIN_PREFIX = '__$$__';
// Current API version for Plugin,
// plugins with incompatible version will not be laoded.
const API_VERSION = '0.1';
/**
* PluginLoader, responsible for:
*
* Loading all plugins and handling errors etc.
* Recording plugin state.
* Reporting on plugin loading status.
* Retrieve plugin.
* Check plugin status and if all plugins loaded.
*/
class PluginLoader {
constructor() {
this._pluginListLoaded = false;
/** @type {Map<string,PluginLoader.PluginObject>} */
this._plugins = new Map();
this._reporting = null;
// Promise that resolves when all plugins loaded
this._loadingPromise = null;
// Resolver to resolve _loadingPromise once all plugins loaded
this._loadingResolver = null;
}
_getReporting() {
if (!this._reporting) {
this._reporting = document.createElement('gr-reporting');
}
return this._reporting;
}
/**
* Use the plugin name or use the full url if not recognized.
* @see gr-api-utils#getPluginNameFromUrl
* @param {string|URL} url
*/
_getPluginKeyFromUrl(url) {
return getPluginNameFromUrl(url) ||
`${UNKNOWN_PLUGIN_PREFIX}${url}`;
}
/**
* Load multiple plugins with certain options.
*
* @param {Array<string>} plugins
* @param {Object<string, PluginLoader.PluginOption>} opts
*/
loadPlugins(plugins = [], opts = {}) {
this._pluginListLoaded = true;
plugins.forEach(path => {
const url = this._urlFor(path);
// Skip if preloaded, for bundling.
if (this.isPluginPreloaded(url)) return;
const pluginKey = this._getPluginKeyFromUrl(url);
// Skip if already installed.
if (this._plugins.has(pluginKey)) return;
this._plugins.set(pluginKey, {
name: pluginKey,
url,
state: PluginState.PENDING,
plugin: null,
});
if (this._isPathEndsWith(url, '.html')) {
this._importHtmlPlugin(url, opts && opts[path]);
} else if (this._isPathEndsWith(url, '.js')) {
this._loadJsPlugin(url);
} else {
this._failToLoad(`Unrecognized plugin url ${url}`, url);
}
});
this.awaitPluginsLoaded().then(() => {
console.info('Plugins loaded');
this._getReporting().pluginsLoaded(this._getAllInstalledPluginNames());
});
}
_isPathEndsWith(url, suffix) {
if (!(url instanceof URL)) {
try {
url = new URL(url);
} catch (e) {
console.warn(e);
return false;
}
}
return url.pathname && url.pathname.endsWith(suffix);
}
_getAllInstalledPluginNames() {
const installedPlugins = [];
for (const plugin of this._plugins.values()) {
if (plugin.state === PluginState.LOADED) {
installedPlugins.push(plugin.name);
}
}
return installedPlugins;
}
install(callback, opt_version, opt_src) {
// HTML import polyfill adds __importElement pointing to the import tag.
const script = document.currentScript &&
(document.currentScript.__importElement || document.currentScript);
let src = opt_src || (script && script.src);
if (!src || src.startsWith('data:')) {
src = script && script.baseURI;
}
if (opt_version && opt_version !== API_VERSION) {
this._failToLoad(`Plugin ${src} install error: only version ` +
API_VERSION + ' is supported in PolyGerrit. ' + opt_version +
' was given.', src);
return;
}
const pluginObject = this.getPlugin(src);
let plugin = pluginObject && pluginObject.plugin;
if (!plugin) {
plugin = new Plugin(src);
}
try {
callback(plugin);
this._pluginInstalled(src, plugin);
} catch (e) {
this._failToLoad(`${e.name}: ${e.message}`, src);
}
}
get arePluginsLoaded() {
// As the size of plugins is relatively small,
// so the performance of this check should be reasonable
if (!this._pluginListLoaded) return false;
for (const plugin of this._plugins.values()) {
if (plugin.state === PluginState.PENDING) return false;
}
return true;
}
_checkIfCompleted() {
if (this.arePluginsLoaded && this._loadingResolver) {
this._loadingResolver();
this._loadingResolver = null;
this._loadingPromise = null;
}
}
_timeout() {
const pendingPlugins = [];
for (const plugin of this._plugins.values()) {
if (plugin.state === PluginState.PENDING) {
this._updatePluginState(plugin.url, PluginState.LOAD_FAILED);
this._checkIfCompleted();
pendingPlugins.push(plugin.url);
}
}
return `Timeout when loading plugins: ${pendingPlugins.join(',')}`;
}
_failToLoad(message, pluginUrl) {
// Show an alert with the error
document.dispatchEvent(new CustomEvent('show-alert', {
detail: {
message: `Plugin install error: ${message} from ${pluginUrl}`,
},
}));
this._updatePluginState(pluginUrl, PluginState.LOAD_FAILED);
this._checkIfCompleted();
}
_updatePluginState(pluginUrl, state) {
const key = this._getPluginKeyFromUrl(pluginUrl);
if (this._plugins.has(key)) {
this._plugins.get(key).state = state;
} else {
// Plugin is not recorded for some reason.
console.warn(`Plugin loaded separately: ${pluginUrl}`);
this._plugins.set(key, {
name: key,
url: pluginUrl,
state,
plugin: null,
});
}
return this._plugins.get(key);
}
_pluginInstalled(url, plugin) {
const pluginObj = this._updatePluginState(url, PluginState.LOADED);
pluginObj.plugin = plugin;
this._getReporting().pluginLoaded(plugin.getPluginName() || url);
console.log(`Plugin ${plugin.getPluginName() || url} installed.`);
this._checkIfCompleted();
}
installPreloadedPlugins() {
if (!window.Gerrit || !window.Gerrit._preloadedPlugins) { return; }
const Gerrit = window.Gerrit;
for (const name in Gerrit._preloadedPlugins) {
if (!Gerrit._preloadedPlugins.hasOwnProperty(name)) { continue; }
const callback = Gerrit._preloadedPlugins[name];
this.install(callback, API_VERSION, PRELOADED_PROTOCOL + name);
}
}
isPluginPreloaded(pathOrUrl) {
const url = this._urlFor(pathOrUrl);
const name = getPluginNameFromUrl(url);
if (name && window.Gerrit._preloadedPlugins) {
return window.Gerrit._preloadedPlugins.hasOwnProperty(name);
} else {
return false;
}
}
/**
* Checks if given plugin path/url is enabled or not.
* @param {string} pathOrUrl
*/
isPluginEnabled(pathOrUrl) {
const url = this._urlFor(pathOrUrl);
if (this.isPluginPreloaded(url)) return true;
const key = this._getPluginKeyFromUrl(url);
return this._plugins.has(key);
}
/**
* Returns the plugin object with a given url.
* @param {string} pathOrUrl
*/
getPlugin(pathOrUrl) {
const key = this._getPluginKeyFromUrl(this._urlFor(pathOrUrl));
return this._plugins.get(key);
}
/**
* Checks if given plugin path/url is loaded or not.
* @param {string} pathOrUrl
*/
isPluginLoaded(pathOrUrl) {
const url = this._urlFor(pathOrUrl);
const key = this._getPluginKeyFromUrl(url);
return this._plugins.has(key) ?
this._plugins.get(key).state === PluginState.LOADED :
false;
}
_importHtmlPlugin(pluginUrl, opts = {}) {
// onload (second param) needs to be a function. When null or undefined
// were passed, plugins were not loaded correctly.
(Polymer.importHref || Polymer.Base.importHref)(
this._urlFor(pluginUrl), () => {},
() => this._failToLoad(`${pluginUrl} import error`, pluginUrl),
!opts.sync);
}
_loadJsPlugin(pluginUrl) {
this._createScriptTag(this._urlFor(pluginUrl));
}
_createScriptTag(url) {
const el = document.createElement('script');
el.defer = true;
el.setAttribute('src', url);
el.onerror = () => this._failToLoad(`${url} load error`, url);
return document.body.appendChild(el);
}
_urlFor(pathOrUrl) {
if (!pathOrUrl) {
return pathOrUrl;
}
if (pathOrUrl.startsWith(PRELOADED_PROTOCOL) ||
pathOrUrl.startsWith('http')) {
// Plugins are loaded from another domain or preloaded.
return pathOrUrl;
}
if (!pathOrUrl.startsWith('/')) {
pathOrUrl = '/' + pathOrUrl;
}
return window.location.origin + getBaseUrl() + pathOrUrl;
}
awaitPluginsLoaded() {
// Resolve if completed.
this._checkIfCompleted();
if (this.arePluginsLoaded) {
return Promise.resolve();
}
if (!this._loadingPromise) {
let timerId;
this._loadingPromise =
Promise.race([
new Promise(resolve => this._loadingResolver = resolve),
new Promise((_, reject) => timerId = setTimeout(
() => {
reject(this._timeout());
}, PLUGIN_LOADING_TIMEOUT_MS)),
]).then(() => {
if (timerId) clearTimeout(timerId);
});
}
return this._loadingPromise;
}
}
/**
* @typedef {{
* name:string,
* url:string,
* state:PluginState,
* plugin:Object
* }}
*/
PluginLoader.PluginObject;
/**
* @typedef {{
* sync:boolean,
* }}
*/
PluginLoader.PluginOption;
window.PluginLoader = PluginLoader;
})(window);