diff --git a/public/app/features/plugins/systemjsPlugins/pluginCDN.test.ts b/public/app/features/plugins/cdn/utils.test.ts similarity index 77% rename from public/app/features/plugins/systemjsPlugins/pluginCDN.test.ts rename to public/app/features/plugins/cdn/utils.test.ts index d2c2bcfa8d3..b2fda523950 100644 --- a/public/app/features/plugins/systemjsPlugins/pluginCDN.test.ts +++ b/public/app/features/plugins/cdn/utils.test.ts @@ -1,29 +1,19 @@ import { config } from '@grafana/runtime'; -import { translateForCDN, extractPluginIdVersionFromUrl } from './pluginCDN'; -describe('Plugin CDN', () => { - describe('translateForCDN', () => { - const load = { - name: 'http://localhost:3000/public/plugin-cdn/grafana-worldmap-panel/0.3.3/grafana-worldmap-panel/module.js', - address: 'http://my-host.com/grafana-worldmap-panel/0.3.3/grafana-worldmap-panel/module.js', - source: 'public/plugins/grafana-worldmap-panel/template.html', - metadata: { - extension: '', - deps: [], - format: 'amd', - loader: 'cdn-loader', - encapsulateGlobal: false, - cjsRequireDetection: true, - cjsDeferDepsExecute: false, - esModule: true, - authorization: false, - }, - }; +import { extractPluginIdVersionFromUrl, transformPluginSourceForCDN } from './utils'; + +describe('Plugin CDN Utils', () => { + describe('transformPluginSourceForCdn', () => { + // const localUrl = + // 'http://localhost:3000/public/plugin-cdn/grafana-worldmap-panel/0.3.3/grafana-worldmap-panel/module.js'; + const pluginId = 'grafana-worldmap-panel'; + const version = '0.3.3'; config.pluginsCDNBaseURL = 'http://my-host.com'; it('should update the default local path to use the CDN path', () => { - const translatedLoad = translateForCDN({ - ...load, + const translatedLoad = transformPluginSourceForCDN({ + pluginId, + version, source: 'public/plugins/grafana-worldmap-panel/template.html', }); expect(translatedLoad).toBe( @@ -40,7 +30,7 @@ describe('Plugin CDN', () => { const a = "http://my-host.com/grafana-worldmap-panel/0.3.3/public/plugins/grafana-worldmap-panel/template.html"; const img = ""; `; - const translatedLoad = translateForCDN({ ...load, source }); + const translatedLoad = transformPluginSourceForCDN({ pluginId, version, source }); expect(translatedLoad).toBe(expectedSource); }); @@ -53,7 +43,7 @@ describe('Plugin CDN', () => { const a = "http://my-host.com/grafana-worldmap-panel/0.3.3/public/plugins/grafana-worldmap-panel/template.html"; const img = ""; `; - const translatedLoad = translateForCDN({ ...load, source }); + const translatedLoad = transformPluginSourceForCDN({ pluginId, version, source }); expect(translatedLoad).toBe(expectedSource); }); @@ -72,7 +62,8 @@ describe('Plugin CDN', () => { ".json" ) `; - const translatedLoad = translateForCDN({ ...load, source }); + const translatedLoad = transformPluginSourceForCDN({ pluginId, version, source }); + expect(translatedLoad).toBe(expectedSource); }); @@ -85,18 +76,19 @@ describe('Plugin CDN', () => { Zn(t,e)},t.Rectangle=ui,t.rectangle=function(t,e){return new ui(t,e)},t.Map=He,t.map=function(t,e){return new He(t,e)}}(e)}])}); //# sourceMappingURL=http://my-host.com/grafana-worldmap-panel/0.3.3/public/plugins/grafana-worldmap-panel/module.js.map `; - const translatedLoad = translateForCDN({ ...load, source }); + const translatedLoad = transformPluginSourceForCDN({ pluginId, version, source }); + expect(translatedLoad).toBe(expectedSource); }); it('should replace css paths', () => { const source = `(0,o.loadPluginCss)({dark:"plugins/grafana-worldmap-panel/css/worldmap.dark.css",light:"plugins/grafana-worldmap-panel/css/worldmap.light.css"}),`; const expectedSource = `(0,o.loadPluginCss)({dark:"http://my-host.com/grafana-worldmap-panel/0.3.3/public/plugins/grafana-worldmap-panel/css/worldmap.dark.css",light:"http://my-host.com/grafana-worldmap-panel/0.3.3/public/plugins/grafana-worldmap-panel/css/worldmap.light.css"}),`; - const translatedLoad = translateForCDN({ ...load, source }); + const translatedLoad = transformPluginSourceForCDN({ pluginId, version, source }); + expect(translatedLoad).toBe(expectedSource); }); }); - describe('extractPluginIdVersionFromUrl', () => { it('should extract the plugin id and version from a path', () => { const source = diff --git a/public/app/features/plugins/cdn/utils.ts b/public/app/features/plugins/cdn/utils.ts new file mode 100644 index 00000000000..04e8b7d4b6d --- /dev/null +++ b/public/app/features/plugins/cdn/utils.ts @@ -0,0 +1,45 @@ +import { config } from '@grafana/runtime'; + +import { PLUGIN_CDN_URL_KEY } from '../constants'; +/* + Given an "expected" address of `http://localhost/public/plugin-cdn/{pluginId}/{version}/public/plugins/{pluginId}` + this function will return the plugin id and version. + */ +export function extractPluginIdVersionFromUrl(address: string) { + const path = new URL(address).pathname; + const match = path.split('/'); + return { id: match[3], version: match[4] }; +} + +/* + Transforms plugin's source for CDNs loa. + Plugins that require loading via a CDN need to have their asset paths translated to point to the configured CDN. + e.g. public/plugins/my-plugin/data/ -> http://my-host.com/my-plugin/0.3.3/public/plugins/my-plugin/data/ + */ +export function transformPluginSourceForCDN({ + pluginId, + version, + source, +}: { + pluginId: string; + version: string; + source: string; +}): string { + const baseAddress = `${config.pluginsCDNBaseURL}/${pluginId}/${version}`; + // handle basic asset paths that include public/plugins + let newSource = source; + newSource = source.replace(/(\/?)(public\/plugins)/g, `${baseAddress}/$2`); + // handle custom plugin css (light and dark themes) + newSource = newSource.replace(/(["|'])(plugins\/.+?.css)(["|'])/g, `$1${baseAddress}/public/$2$3`); + // handle external sourcemap links + newSource = newSource.replace( + /(\/\/#\ssourceMappingURL=)(.+)\.map/g, + `$1${baseAddress}/public/plugins/${pluginId}/$2.map` + ); + return newSource; +} + +export function getPluginCdnResourceUrl(localPath: string): string { + const pluginPath = localPath.split(`/public/${PLUGIN_CDN_URL_KEY}/`); + return `${config.pluginsCDNBaseURL}/${pluginPath[1]}`; +} diff --git a/public/app/features/plugins/constants.ts b/public/app/features/plugins/constants.ts new file mode 100644 index 00000000000..5a49d1e3845 --- /dev/null +++ b/public/app/features/plugins/constants.ts @@ -0,0 +1 @@ +export const PLUGIN_CDN_URL_KEY = 'plugin-cdn'; diff --git a/public/app/features/plugins/plugin_loader.ts b/public/app/features/plugins/plugin_loader.ts index 1857c4dd6ea..471263a85cd 100644 --- a/public/app/features/plugins/plugin_loader.ts +++ b/public/app/features/plugins/plugin_loader.ts @@ -33,6 +33,7 @@ import * as ticks from 'app/core/utils/ticks'; import { GenericDataSourcePlugin } from '../datasources/types'; import builtInPlugins from './built_in_plugins'; +import { PLUGIN_CDN_URL_KEY } from './constants'; import { sandboxPluginDependencies } from './sandbox/plugin_dependencies'; import { importPluginModuleInSandbox } from './sandbox/sandbox_plugin_loader'; import { locateFromCDN, translateForCDN } from './systemjsPlugins/pluginCDN'; @@ -78,7 +79,7 @@ grafanaRuntime.SystemJS.config({ '*.css': { loader: 'css', }, - 'plugin-cdn/*': { + [`${PLUGIN_CDN_URL_KEY}/*`]: { esModule: true, authorization: false, loader: 'cdn-loader', diff --git a/public/app/features/plugins/sandbox/sandbox_plugin_loader.ts b/public/app/features/plugins/sandbox/sandbox_plugin_loader.ts index 89c3746307a..9405ccf2d81 100644 --- a/public/app/features/plugins/sandbox/sandbox_plugin_loader.ts +++ b/public/app/features/plugins/sandbox/sandbox_plugin_loader.ts @@ -3,6 +3,8 @@ import { ProxyTarget } from '@locker/near-membrane-shared'; import { PluginMeta } from '@grafana/data'; +import { extractPluginIdVersionFromUrl, getPluginCdnResourceUrl, transformPluginSourceForCDN } from '../cdn/utils'; +import { PLUGIN_CDN_URL_KEY } from '../constants'; import { getPluginSettings } from '../pluginSettings'; import { getGeneralSandboxDistortionMap } from './distortion_map'; @@ -120,11 +122,16 @@ async function doImportPluginModuleInSandbox(meta: PluginMeta): Promise }, }); - // fetch and evalute the plugin code inside the sandbox + // fetch plugin's code + let pluginCode = ''; try { - let pluginCode = await getPluginCode(meta.module); - pluginCode = patchPluginSourceMap(meta, pluginCode); + pluginCode = await getPluginCode(meta); + } catch (e) { + throw new Error(`Could not load plugin ${meta.id}: ` + e); + reject(new Error(`Could not load plugin ${meta.id}: ` + e)); + } + try { // runs the code inside the sandbox environment // this evaluate will eventually run the `define` function inside // of endowments. @@ -135,9 +142,26 @@ async function doImportPluginModuleInSandbox(meta: PluginMeta): Promise }); } -async function getPluginCode(modulePath: string) { - const response = await fetch('public/' + modulePath + '.js'); - return await response.text(); +async function getPluginCode(meta: PluginMeta): Promise { + if (meta.module.includes(`${PLUGIN_CDN_URL_KEY}/`)) { + // should load plugin from a CDN + const pluginUrl = getPluginCdnResourceUrl(`/public/${meta.module}`) + '.js'; + const response = await fetch(pluginUrl); + let pluginCode = await response.text(); + const { version } = extractPluginIdVersionFromUrl(pluginUrl); + pluginCode = transformPluginSourceForCDN({ + pluginId: meta.id, + version, + source: pluginCode, + }); + return pluginCode; + } else { + //local plugin loading + const response = await fetch('public/' + meta.module + '.js'); + let pluginCode = await response.text(); + pluginCode = patchPluginSourceMap(meta, pluginCode); + return pluginCode; + } } function getActivityErrorHandler(pluginId: string) { diff --git a/public/app/features/plugins/systemjsPlugins/pluginCDN.ts b/public/app/features/plugins/systemjsPlugins/pluginCDN.ts index 27d2b5382f4..8e3784e8d0d 100644 --- a/public/app/features/plugins/systemjsPlugins/pluginCDN.ts +++ b/public/app/features/plugins/systemjsPlugins/pluginCDN.ts @@ -1,17 +1,7 @@ -import { config } from '@grafana/runtime'; +import { extractPluginIdVersionFromUrl, getPluginCdnResourceUrl, transformPluginSourceForCDN } from '../cdn/utils'; import type { SystemJSLoad } from './types'; -/* - Given an "expected" address of `http://localhost/public/plugin-cdn/{pluginId}/{version}/public/plugins/{pluginId}` - this function will return the plugin id and version. - */ -export function extractPluginIdVersionFromUrl(address: string) { - const path = new URL(address).pathname; - const match = path.split('/'); - return { id: match[3], version: match[4] }; -} - /* Locate: Overrides the location of the plugin resource Plugins loaded via CDN fall into this plugin via the `plugin-cdn` keyword. @@ -21,27 +11,13 @@ export function extractPluginIdVersionFromUrl(address: string) { */ export function locateFromCDN(load: SystemJSLoad) { const { address } = load; - const pluginPath = address.split('/public/plugin-cdn/'); - return `${config.pluginsCDNBaseURL}/${pluginPath[1]}`; + return getPluginCdnResourceUrl(address); } /* - Translate: Returns the translated source from load.source, can also set load.metadata.sourceMap for full source maps support. - Plugins that require loading via a CDN need to have their asset paths translated to point to the configured CDN. - e.g. public/plugins/my-plugin/data/ -> http://my-host.com/my-plugin/0.3.3/public/plugins/my-plugin/data/ + Translate: Returns the translated source from load.source; */ export function translateForCDN(load: SystemJSLoad) { const { id, version } = extractPluginIdVersionFromUrl(load.name); - const baseAddress = `${config.pluginsCDNBaseURL}/${id}/${version}`; - // handle basic asset paths that include public/plugins - load.source = load.source.replace(/(\/?)(public\/plugins)/g, `${baseAddress}/$2`); - // handle custom plugin css (light and dark themes) - load.source = load.source.replace(/(["|'])(plugins\/.+?.css)(["|'])/g, `$1${baseAddress}/public/$2$3`); - // handle external sourcemap links - load.source = load.source.replace( - /(\/\/#\ssourceMappingURL=)(.+)\.map/g, - `$1${baseAddress}/public/plugins/${id}/$2.map` - ); - - return load.source; + return transformPluginSourceForCDN({ pluginId: id, version, source: load.source }); }