UNPKG

@craftercms/studio-ui

Version:

Services, components, models & utils to build CrafterCMS authoring extensions.

145 lines (143 loc) 5.36 kB
/* * Copyright (C) 2007-2022 Crafter Software Corporation. All Rights Reserved. * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License version 3 as published by * the Free Software Foundation. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program. If not, see <http://www.gnu.org/licenses/>. */ /* * Copyright (C) 2007-2022 Crafter Software Corporation. All Rights Reserved. * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License version 3 as published by * the Free Software Foundation. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see <http://www.gnu.org/licenses/>. */ import { augmentTranslations } from '../utils/i18n'; import { components, plugins } from '../utils/constants'; const DEFAULT_FILE_NAME = 'index.js'; function isPluginFileBuilder(target) { return typeof target === 'object'; } export function buildFileUrl(siteOrBuilder, type, name, file, id) { let site = siteOrBuilder; if (isPluginFileBuilder(siteOrBuilder)) { const builder = siteOrBuilder; site = builder.site; type = builder.type; name = builder.name; file = builder.file; id = builder.id; } let url = `/studio/1/plugin/file?siteId=${site}&type=${type}&name=${name}&filename=${file ?? DEFAULT_FILE_NAME}`; if (id) { url += `&pluginId=${id}`; } return url; } export function createFileBuilder(site, type, name, file = DEFAULT_FILE_NAME, id) { return { site, type, name, file, ...(id ? { id } : {}) }; } export function importFile(siteOrBuilder, type, name, file, id) { // @ts-ignore — methods share same signature const url = buildFileUrl(...arguments); return import(/* @vite-ignore */ url); } export function importPlugin(siteOrBuilder, type, name, file, id) { // @ts-ignore — methods share the same signature(s) const args = arguments; return importFile(...args).then((module) => { const plugin = module.plugin ?? module.default; if (plugin) { // The file may have been previously loaded and hence the plugin registered previously. // This may however cause silent skips of legitimate duplicate plugin id registrations. // Perhaps we should consider keeping an internal registry of the plugin file URLs that // have been loaded if this is an issue. !isPluginRegistered(plugin) && registerPlugin(plugin, isPluginFileBuilder(siteOrBuilder) ? siteOrBuilder : createFileBuilder(...args)); } return plugin; }); } export function isPluginRegistered(plugin) { return plugins.has(plugin?.id); } export function registerPlugin(plugin, source) { // Skip registration if plugin with same id already exists if (!plugins.has(plugin.id)) { const extendedDescriptor = { ...plugin, source }; plugins.set(plugin.id, extendedDescriptor); registerComponents(plugin.widgets); augmentTranslations(plugin.locales); // TODO: Allow externals? if (source) { plugin.stylesheets?.forEach((href) => appendStylesheet( typeof href === 'string' ? (hasProtocol(href) ? href : buildFileUrl({ ...source, file: href })) : href ) ); plugin.scripts?.forEach((src) => appendScript(typeof src === 'string' ? (hasProtocol(src) ? src : buildFileUrl({ ...source, file: src })) : src) ); } else { console.error('Scripts & stylesheets not allowed for umd bundles'); } return true; } else { console.error(`Attempt to register a duplicate plugin "${plugin.id}" skipped.`); return false; } } export function registerComponents(widgets) { Object.entries(widgets).forEach(([id, widget]) => { // Skip registration if component with same id already exists if (!components.has(id)) { components.set(id, widget); } else { console.error(`Attempt to register a duplicate component id "${id}" skipped.`); } }); } export function appendStylesheet(href) { return appendLoadable('link', { rel: 'stylesheet', ...(typeof href === 'string' ? { href } : href) }); } export function appendScript(src) { return appendLoadable('script', typeof src === 'string' ? { src } : src); } function appendLoadable(type, attributes) { return new Promise((resolve, reject) => { const element = document.createElement(type); for (let attr in attributes) { if (Object.prototype.hasOwnProperty.call(attributes, attr)) { element.setAttribute(attr, attributes[attr]); } } element.onload = resolve; element.onerror = reject; document.head.appendChild(element); }); } function hasProtocol(url) { return /^(http)(s?):\/\//.test(url); }