@jbrowse/core
Version:
JBrowse 2 core libraries used by plugins
175 lines (174 loc) • 5.99 kB
JavaScript
import Plugin from "./Plugin.js";
import ReExports from "./ReExports/index.js";
import { isElectron } from "./util/index.js";
export function isUMDPluginDefinition(def) {
return ((def.umdUrl !== undefined ||
def.url !== undefined ||
def.umdLoc !== undefined) &&
def.name !== undefined);
}
export function isESMPluginDefinition(def) {
return (def.esmUrl !== undefined ||
def.esmLoc !== undefined);
}
function promisifiedLoadScript(src) {
return new Promise((resolve, reject) => {
const script = document.createElement('script');
script.type = 'text/javascript';
script.async = true;
script.src = src;
script.onload = () => {
resolve(script.src);
};
script.onerror = () => {
reject(new Error(`Failed to load script: ${src}`));
};
document.head.append(script);
});
}
async function loadScript(scriptUrl) {
if (!isInWebWorker()) {
return promisifiedLoadScript(scriptUrl);
}
if (globalThis.importScripts) {
await globalThis.importScripts(scriptUrl);
return;
}
throw new Error('cannot figure out how to load external JS scripts in this environment');
}
export function isCJSPluginDefinition(def) {
return def.cjsUrl !== undefined;
}
export function pluginDescriptionString(d) {
if (isUMDPluginDefinition(d)) {
return `UMD plugin ${d.name}`;
}
else if (isESMPluginDefinition(d)) {
return `ESM plugin ${d.esmUrl ||
d.esmLoc.uri}`;
}
else if (isCJSPluginDefinition(d)) {
return `CJS plugin ${d.cjsUrl}`;
}
else {
return 'unknown plugin';
}
}
export function pluginUrl(d) {
if (isUMDPluginDefinition(d)) {
return d.url ?? d.umdLoc?.uri ?? d.umdUrl;
}
else if (isESMPluginDefinition(d)) {
return d.esmUrl ?? d.esmUri ?? d.esmLoc?.uri;
}
else if (isCJSPluginDefinition(d)) {
return d.cjsUrl || d.cjsLoc.uri;
}
else {
return 'unknown url';
}
}
function isInWebWorker() {
return 'WorkerGlobalScope' in globalThis;
}
function addCacheBuster(url) {
if (!globalThis.__jbrowseCacheBuster) {
return url;
}
const u = new URL(url);
u.searchParams.set('_cb', Date.now().toString());
return u.href;
}
export default class PluginLoader {
definitions = [];
fetchESM;
fetchCJS;
constructor(defs = [], args) {
this.fetchESM = args?.fetchESM;
this.fetchCJS = args?.fetchCJS;
this.definitions = JSON.parse(JSON.stringify(defs));
}
async loadCJSPlugin(def, baseUri) {
const parsedUrl = new URL(def.cjsUrl, baseUri);
if (parsedUrl.protocol !== 'http:' && parsedUrl.protocol !== 'https:') {
throw new Error(`Cannot load plugins using protocol "${parsedUrl.protocol}"`);
}
if (!this.fetchCJS) {
throw new Error('No fetchCJS callback provided');
}
return this.fetchCJS(parsedUrl.href);
}
async loadESMPlugin(def, baseUri) {
const parsedUrl = 'esmUrl' in def
? new URL(def.esmUrl, baseUri)
: new URL(def.esmLoc.uri, def.esmLoc.baseUri);
if (parsedUrl.protocol !== 'http:' && parsedUrl.protocol !== 'https:') {
throw new Error(`cannot load plugins using protocol "${parsedUrl.protocol}"`);
}
if (!this.fetchESM) {
throw new Error('No ESM fetcher installed');
}
const plugin = await this.fetchESM(addCacheBuster(parsedUrl.href));
if (!plugin) {
throw new Error(`Could not load ESM plugin: ${parsedUrl}`);
}
return plugin;
}
async loadUMDPlugin(def, baseUri) {
const parsedUrl = 'url' in def
? new URL(def.url, baseUri)
: 'umdUrl' in def
? new URL(def.umdUrl, baseUri)
: new URL(def.umdLoc.uri, def.umdLoc.baseUri);
if (parsedUrl.protocol !== 'http:' && parsedUrl.protocol !== 'https:') {
throw new Error(`cannot load plugins using protocol "${parsedUrl.protocol}"`);
}
const moduleName = def.name;
const umdName = `JBrowsePlugin${moduleName}`;
if (typeof jest === 'undefined') {
await loadScript(addCacheBuster(parsedUrl.href));
}
else {
globalThis[umdName] = { default: Plugin };
}
const plugin = globalThis[umdName];
if (!plugin) {
throw new Error(`Failed to load UMD bundle for ${moduleName}, ${umdName} is undefined`);
}
return plugin;
}
async loadPlugin(def, baseUri) {
let plugin;
if (isElectron && isCJSPluginDefinition(def)) {
plugin = await this.loadCJSPlugin(def, baseUri);
}
else if (isESMPluginDefinition(def)) {
plugin = await this.loadESMPlugin(def, baseUri);
}
else if (isUMDPluginDefinition(def)) {
plugin = await this.loadUMDPlugin(def, baseUri);
}
else if (!isElectron && isCJSPluginDefinition(def)) {
throw new Error(`CommonJS plugin found, but not in a NodeJS environment: ${JSON.stringify(def)}`);
}
else {
throw new Error(`Could not determine plugin type: ${JSON.stringify(def)}`);
}
if (!plugin.default) {
throw new Error(`${pluginDescriptionString(def)} does not have a default export, cannot load`);
}
return plugin.default;
}
installGlobalReExports(target) {
target.JBrowseExports = Object.fromEntries(Object.entries(ReExports).map(([moduleName, module]) => {
return [moduleName, module];
}));
return this;
}
async load(baseUri) {
return Promise.all(this.definitions.map(async (definition) => ({
plugin: await this.loadPlugin(definition, baseUri),
definition,
})));
}
}