@craftercms/studio-ui
Version:
Services, components, models & utils to build CrafterCMS authoring extensions.
145 lines (143 loc) • 5.36 kB
JavaScript
/*
* 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);
}