studiocms
Version:
Astro Native CMS for AstroDB. Built from the ground up by the Astro community.
736 lines (708 loc) • 26.9 kB
JavaScript
import {
integrationLogger,
pluginLogger
} from "@withstudiocms/internal_helpers/astro-integration";
import {
convertToSafeString,
pageContentComponentFilter,
readJson,
rendererComponentFilter
} from "@withstudiocms/internal_helpers/utils";
import { AstroError } from "astro/errors";
import { addVirtualImports, createResolver, defineUtility } from "astro-integration-kit";
import boxen from "boxen";
import { compare as semCompare } from "semver";
import { loadEnv } from "vite";
import { routesDir, StudioCMSDefaultRobotsConfig } from "../consts.js";
import { StudioCMSError } from "../errors.js";
import {
checkForWebVitals,
dynamicSitemap,
robotsTXT
} from "../integrations/plugins.js";
const { resolve } = createResolver(import.meta.url);
const { version: pkgVersion } = readJson(
resolve("../../package.json")
);
const defaultPlugin = {
name: "StudioCMS (Built-in)",
identifier: "studiocms",
studiocmsMinimumVersion: pkgVersion,
hooks: {
"studiocms:config:setup": ({ setDashboard }) => {
setDashboard({
dashboardGridItems: [
{
name: "overview",
span: 1,
variant: "default",
requiresPermission: "editor",
header: { title: "Overview", icon: "heroicons:bolt" },
body: {
html: "<totals></totals>",
components: {
totals: resolve("../components/default-grid-items/Totals.astro")
}
}
},
{
name: "recently-updated-pages",
span: 2,
variant: "default",
requiresPermission: "editor",
header: { title: "Recently Updated Pages", icon: "heroicons:document-arrow-up" },
body: {
html: "<recentlyupdatedpages></recentlyupdatedpages>",
components: {
recentlyupdatedpages: resolve(
"../components/default-grid-items/Recently-updated-pages.astro"
)
}
}
},
{
name: "recently-signed-up-users",
span: 1,
variant: "default",
requiresPermission: "admin",
header: { title: "Recently Signed Up Users", icon: "heroicons:user-group" },
body: {
html: "<recentlysignedupusers></recentlysignedupusers>",
components: {
recentlysignedupusers: resolve(
"../components/default-grid-items/Recently-signed-up.astro"
)
}
}
},
{
name: "recently-created-pages",
span: 2,
variant: "default",
requiresPermission: "editor",
header: { title: "Recently Created Pages", icon: "heroicons:document-plus" },
body: {
html: "<recentlycreatedpages></recentlycreatedpages>",
components: {
recentlycreatedpages: resolve(
"../components/default-grid-items/Recently-created-pages.astro"
)
}
}
}
]
});
}
}
};
function verifyPluginRequires(sourceList, requires) {
const missingRequirements = [];
for (const req of requires) {
const { source, requires: requiredDeps } = req;
const missing = requiredDeps.filter((r) => !sourceList.includes(r));
if (missing.length > 0) {
missingRequirements.push({ source, missing });
}
}
if (missingRequirements.length > 0) {
const errorMessage = missingRequirements.map(
({ source, missing }) => `Plugin ${source} requires the following plugins that are not installed: ${missing.join(
", "
)}`
).join("\n");
throw new StudioCMSError(
`Plugins missing requirements: ${errorMessage}`,
"Some plugins require other plugins to be installed. Please install the required plugins."
);
}
}
const pluginHandler = defineUtility("astro:config:setup")(
async (params, options) => {
const { logger, config } = params;
const { dbStartPage, verbose, name, pkgVersion: pkgVersion2, plugins, robotsTXTConfig, dashboardRoute } = options;
const logInfo = { logger, logLevel: "info", verbose };
const integrations = [];
const availableDashboardGridItems = [];
const availableDashboardPages = {
user: [],
admin: []
};
const pluginSettingsEndpoints = [];
let sitemapEnabled = false;
const sitemaps = [];
const pluginEndpoints = [];
const pluginRenderers = [];
const safePluginList = [];
const extraRoutes = [];
const messages = [];
let renderingPluginCount = 0;
const sourcePluginsList = [];
const pluginRequires = [];
const imageServiceKeys = [];
const imageServiceEndpoints = [];
const unInjectedAuthProviders = [];
let oAuthProvidersConfigured = false;
const VirtualImports = [];
function getPlugins() {
const wvPlugin = checkForWebVitals(params, { name, verbose, version: pkgVersion2 });
const pluginsToProcess = [defaultPlugin];
if (wvPlugin) pluginsToProcess.push(wvPlugin);
if (plugins) pluginsToProcess.push(...plugins);
return pluginsToProcess;
}
function getPluginData(plugin) {
const { studiocmsMinimumVersion = "0.0.0", hooks = {}, requires, ...safeData } = plugin;
let comparison;
try {
comparison = semCompare(studiocmsMinimumVersion, pkgVersion2);
} catch (_error) {
throw new StudioCMSError(
`Plugin ${safeData.name} has invalid version requirement: ${studiocmsMinimumVersion}`,
"The minimum version requirement must be a valid semver string."
);
}
if (comparison === 1) {
throw new StudioCMSError(
`Plugin ${safeData.name} requires StudioCMS version ${studiocmsMinimumVersion} or higher.`,
`Plugin ${safeData.name} requires StudioCMS version ${studiocmsMinimumVersion} or higher. Please update StudioCMS to the required version, contact the plugin author to update the minimum version requirement or remove the plugin from the StudioCMS config.`
);
}
return {
hooks,
requires,
...safeData
};
}
function registerOAuthProvider(oAuthProvider, messages2, unInjectedAuthProviders2) {
const { endpointPath, formattedName, name: name2, svg, requiredEnvVariables } = oAuthProvider;
const safeName = convertToSafeString(name2);
let enabled = true;
const endpoints = `export { initSession as ${safeName}_initSession, initCallback as ${safeName}_initCallback } from '${endpointPath}';`;
const env = loadEnv("", process.cwd(), "");
if (requiredEnvVariables) {
const missingKeys = requiredEnvVariables.filter((key) => !env[key] || env[key] === "");
if (missingKeys.length > 0) {
messages2.push({
label: `studiocms:plugins:${safeName}:missing-env-keys`,
logLevel: "error",
message: boxen(
`The following environment variables are required for ${name2} to work: ${missingKeys.join(", ")}. Please set them in your environment.`,
{ title: `Missing ${name2} Environment Variables`, borderColor: "red" }
)
});
enabled = false;
}
}
unInjectedAuthProviders2.push({
name: name2,
safeName,
formattedName,
svg,
endpoints,
enabled
});
}
function buildOAuthArtifacts(entries) {
return entries.map(({ enabled, endpoints, formattedName, safeName, svg }) => ({
endpoints: {
content: endpoints,
enabled,
safeName
},
button: {
label: formattedName,
image: svg,
enabled,
safeName
}
})).reduce(
(acc, { endpoints, button }) => {
acc.oAuthEndpoints.push(endpoints);
acc.oAuthButtons.push(button);
return acc;
},
{ oAuthEndpoints: [], oAuthButtons: [] }
);
}
integrationLogger(logInfo, "Setting up StudioCMS plugins...");
if (dbStartPage) {
const pluginsToProcess = getPlugins();
for (const plugin of pluginsToProcess) {
const { hooks, requires, ...safeData } = getPluginData(plugin);
if (typeof hooks["studiocms:astro:config"] === "function") {
await hooks["studiocms:astro:config"]({
logger: pluginLogger(safeData.identifier, logger),
addIntegrations() {
return void 0;
}
});
}
if (typeof hooks["studiocms:config:setup"] === "function") {
await hooks["studiocms:config:setup"]({
logger: pluginLogger(safeData.identifier, logger),
setDashboard() {
return void 0;
},
setSitemap() {
return void 0;
},
setFrontend() {
return void 0;
},
setRendering() {
return void 0;
},
setImageService() {
return void 0;
},
setAuthService({ oAuthProvider }) {
if (oAuthProvider)
registerOAuthProvider(oAuthProvider, messages, unInjectedAuthProviders);
}
});
}
if (requires) {
pluginRequires.push({
source: safeData.identifier,
requires
});
}
sourcePluginsList.push(safeData.identifier);
}
verifyPluginRequires(sourcePluginsList, pluginRequires);
const { oAuthButtons, oAuthEndpoints } = buildOAuthArtifacts(unInjectedAuthProviders);
if (oAuthEndpoints.length > 0) {
oAuthProvidersConfigured = true;
}
VirtualImports.push(
{
id: "virtual:studiocms:plugins/auth/providers",
content: `
${oAuthEndpoints.map(({ content }) => content).join("\n")}
`
},
{
id: "studiocms:plugins/auth/providers",
content: `
import * as providers from 'virtual:studiocms:plugins/auth/providers';
const oAuthEndpoints = ${JSON.stringify(oAuthEndpoints.map(({ safeName, enabled }) => ({ safeName, enabled })))};
export const oAuthButtons = ${JSON.stringify(oAuthButtons)};
export const oAuthProviders = oAuthEndpoints.map(({ safeName, enabled }) => ({
safeName,
enabled,
initSession: providers[safeName + '_initSession'] || null,
initCallback: providers[safeName + '_initCallback'] || null,
}));
`
}
);
}
if (!dbStartPage) {
const pluginsToProcess = getPlugins();
for (const plugin of pluginsToProcess) {
const { hooks, requires, ...safeData } = getPluginData(plugin);
let foundSettingsPage;
let foundFrontendNavigationLinks;
let foundPageTypes;
if (typeof hooks["studiocms:astro:config"] === "function") {
await hooks["studiocms:astro:config"]({
logger: pluginLogger(safeData.identifier, logger),
// Add the plugin Integration to the Astro config
addIntegrations(integration) {
if (integration) {
if (Array.isArray(integration)) {
integrations.push(...integration.map((integration2) => ({ integration: integration2 })));
return;
}
integrations.push({ integration });
}
}
});
}
if (typeof hooks["studiocms:config:setup"] === "function") {
await hooks["studiocms:config:setup"]({
logger: pluginLogger(safeData.identifier, logger),
setDashboard({ dashboardGridItems, dashboardPages, settingsPage }) {
if (dashboardGridItems) {
availableDashboardGridItems.push(
...dashboardGridItems.map((item) => ({
...item,
name: `${convertToSafeString(safeData.identifier)}/${convertToSafeString(item.name)}`
}))
);
}
if (dashboardPages) {
if (dashboardPages.user) {
availableDashboardPages.user?.push(
...dashboardPages.user.map((page) => ({
...page,
slug: `${convertToSafeString(safeData.identifier)}/${convertToSafeString(page.route)}`
}))
);
}
if (dashboardPages.admin) {
availableDashboardPages.admin?.push(
...dashboardPages.admin.map((page) => ({
...page,
slug: `${convertToSafeString(safeData.identifier)}/${convertToSafeString(page.route)}`
}))
);
}
}
if (settingsPage) {
const { endpoint } = settingsPage;
if (endpoint) {
pluginSettingsEndpoints.push({
identifier: safeData.identifier,
safeIdentifier: convertToSafeString(safeData.identifier),
apiEndpoint: `
export { onSave as ${convertToSafeString(safeData.identifier)}_onSave } from '${endpoint}';
`
});
}
foundSettingsPage = settingsPage;
}
},
setSitemap({ sitemaps: pluginSitemaps, triggerSitemap }) {
if (triggerSitemap) sitemapEnabled = triggerSitemap;
if (pluginSitemaps) {
sitemaps.push(...pluginSitemaps);
}
},
setFrontend({ frontendNavigationLinks }) {
if (frontendNavigationLinks) {
foundFrontendNavigationLinks = frontendNavigationLinks;
}
},
setRendering({ pageTypes }) {
for (const { apiEndpoint, identifier, rendererComponent } of pageTypes || []) {
if (apiEndpoint) {
pluginEndpoints.push({
identifier,
safeIdentifier: convertToSafeString(identifier),
apiEndpoint: `
export { onCreate as ${convertToSafeString(identifier)}_onCreate } from '${apiEndpoint}';
export { onEdit as ${convertToSafeString(identifier)}_onEdit } from '${apiEndpoint}';
export { onDelete as ${convertToSafeString(identifier)}_onDelete } from '${apiEndpoint}';
`
});
}
if (rendererComponent) {
const builtIns = rendererComponentFilter(
rendererComponent,
convertToSafeString(identifier)
);
pluginRenderers.push({
pageType: identifier,
safePageType: convertToSafeString(identifier),
content: builtIns
});
renderingPluginCount++;
}
}
foundPageTypes = pageTypes;
},
setImageService({ imageService }) {
if (imageService) {
imageServiceKeys.push({
identifier: imageService.identifier,
safe: convertToSafeString(imageService.identifier)
});
imageServiceEndpoints.push(
`export { default as ${convertToSafeString(imageService.identifier)} } from '${imageService.servicePath}';`
);
}
},
setAuthService({ oAuthProvider }) {
if (oAuthProvider)
registerOAuthProvider(oAuthProvider, messages, unInjectedAuthProviders);
}
});
}
if (requires) {
pluginRequires.push({
source: safeData.identifier,
requires
});
}
sourcePluginsList.push(safeData.identifier);
const safePlugin = {
...safeData,
settingsPage: foundSettingsPage,
frontendNavigationLinks: foundFrontendNavigationLinks,
pageTypes: foundPageTypes
};
safePluginList.push(safePlugin);
}
if (renderingPluginCount === 0) {
throw new AstroError(
"No rendering plugins found, StudioCMS requires at least one rendering plugin. Please install one, such as '@studiocms/md' or '@studiocms/html'."
);
}
verifyPluginRequires(sourcePluginsList, pluginRequires);
const robotsDefaultConfig = StudioCMSDefaultRobotsConfig({
config,
sitemapEnabled,
dashboardRoute
});
if (robotsTXTConfig === true) {
integrations.push({
integration: robotsTXT(robotsDefaultConfig)
});
} else if (typeof robotsTXTConfig === "object") {
integrations.push({
integration: robotsTXT({
...robotsDefaultConfig,
...robotsTXTConfig
})
});
}
if (sitemapEnabled) {
integrations.push({
integration: dynamicSitemap({ sitemaps })
});
}
if (availableDashboardPages.user && availableDashboardPages.user.length > 0 || availableDashboardPages.admin && availableDashboardPages.admin.length > 0) {
extraRoutes.push({
enabled: true,
pattern: dashboardRoute("[...pluginPage]"),
entrypoint: routesDir.dashRoute("[...pluginPage].astro")
});
}
const allPageTypes = safePluginList.flatMap(({ pageTypes }) => pageTypes || []);
const { oAuthButtons, oAuthEndpoints } = buildOAuthArtifacts(unInjectedAuthProviders);
if (oAuthEndpoints.length > 0) {
oAuthProvidersConfigured = true;
}
VirtualImports.push(
{
id: "virtual:studiocms/components/Editors",
content: `
import { convertToSafeString } from '${resolve("../utils/safeString.js")}';
export const editorKeys = ${JSON.stringify([
...allPageTypes.map(({ identifier }) => convertToSafeString(identifier))
])};
${allPageTypes.map(({ identifier, pageContentComponent }) => {
return pageContentComponentFilter(
pageContentComponent,
convertToSafeString(identifier)
);
}).join("\n")}
`
},
{
id: "studiocms:components/dashboard-grid-components",
content: `
${availableDashboardGridItems.map((item) => {
const components = item.body?.components || {};
const remappedComps = Object.entries(components).map(
([key, value]) => `export { default as ${key} } from '${value}';`
);
return remappedComps.join("\n");
}).join("\n")}
`
},
{
id: "studiocms:components/dashboard-grid-items",
content: `
import * as components from 'studiocms:components/dashboard-grid-components';
const currentComponents = ${JSON.stringify(availableDashboardGridItems)};
const dashboardGridItems = currentComponents.map((item) => {
const gridItem = { ...item };
if (gridItem.body?.components) {
gridItem.body.components = Object.entries(gridItem.body.components).reduce(
(acc, [key, value]) => ({
...acc,
[key]: components[key],
}),
{}
);
}
return gridItem;
});
export default dashboardGridItems;
`
},
{
id: "studiocms:plugins/dashboard-pages/components/user",
content: `
${availableDashboardPages.user?.map(({ pageBodyComponent, pageActionsComponent, ...item }) => {
const components = {
pageBodyComponent
};
if (item.sidebar === "double") {
components.innerSidebarComponent = item.innerSidebarComponent;
}
if (pageActionsComponent) {
components.pageActionsComponent = pageActionsComponent;
}
const remappedComps = Object.entries(components).map(
([key, value]) => `export { default as ${convertToSafeString(item.title + key)} } from '${value}';`
);
return remappedComps.join("\n");
}).join("\n") || ""}
`
},
{
id: "studiocms:plugins/dashboard-pages/components/admin",
content: `
${availableDashboardPages.admin?.map(({ pageBodyComponent, pageActionsComponent, ...item }) => {
const components = {
pageBodyComponent
};
if (item.sidebar === "double") {
components.innerSidebarComponent = item.innerSidebarComponent;
}
if (pageActionsComponent) {
components.pageActionsComponent = pageActionsComponent;
}
const remappedComps = Object.entries(components).map(
([key, value]) => `export { default as ${convertToSafeString(item.title + key)} } from '${value}';`
);
return remappedComps.join("\n");
}).join("\n") || ""}
`
},
{
id: "studiocms:plugins/dashboard-pages/user",
content: `
import { convertToSafeString } from '${resolve("../utils/safeString.js")}';
import * as components from 'studiocms:plugins/dashboard-pages/components/user';
const currentComponents = ${JSON.stringify(availableDashboardPages.user || [])};
const dashboardPages = currentComponents.map((item) => {
const page = {
...item,
components: {
PageBodyComponent: components[convertToSafeString(item.title + 'pageBodyComponent')],
PageActionsComponent: components[convertToSafeString(item.title + 'pageActionsComponent')] || null,
InnerSidebarComponent: item.sidebar === 'double' ? components[convertToSafeString(item.title + 'innerSidebarComponent')] || null : null,
},
};
return page;
});
export default dashboardPages;
`
},
{
id: "studiocms:plugins/dashboard-pages/admin",
content: `
import { convertToSafeString } from '${resolve("../utils/safeString.js")}';
import * as components from 'studiocms:plugins/dashboard-pages/components/admin';
const currentComponents = ${JSON.stringify(availableDashboardPages.admin || [])};
const dashboardPages = currentComponents.map((item) => {
const page = {
...item,
components: {
PageBodyComponent: components[convertToSafeString(item.title + 'pageBodyComponent')],
PageActionsComponent: components[convertToSafeString(item.title + 'pageActionsComponent')] || null,
InnerSidebarComponent: item.sidebar === 'double' ? components[convertToSafeString(item.title + 'innerSidebarComponent')] || null : null,
},
};
return page;
});
export default dashboardPages;
`
},
{
id: "virtual:studiocms/plugins/endpoints",
content: `
${pluginEndpoints.map(({ apiEndpoint }) => apiEndpoint).join("\n")}
${pluginSettingsEndpoints.map(({ apiEndpoint }) => apiEndpoint).join("\n")}
`
},
{
id: "studiocms:plugins/endpoints",
content: `
import * as endpoints from 'virtual:studiocms/plugins/endpoints';
const pluginEndpoints = ${JSON.stringify(
pluginEndpoints.map(({ identifier, safeIdentifier }) => ({
identifier,
safeIdentifier
})) || []
)};
const pluginSettingsEndpoints = ${JSON.stringify(pluginSettingsEndpoints.map(({ identifier, safeIdentifier }) => ({ identifier, safeIdentifier })) || [])};
export const apiEndpoints = pluginEndpoints.map(({ identifier, safeIdentifier }) => ({
identifier,
onCreate: endpoints[safeIdentifier + '_onCreate'] || null,
onEdit: endpoints[safeIdentifier + '_onEdit'] || null,
onDelete: endpoints[safeIdentifier + '_onDelete'] || null,
}));
export const settingsEndpoints = pluginSettingsEndpoints.map(({ identifier, safeIdentifier }) => ({
identifier,
onSave: endpoints[safeIdentifier + '_onSave'] || null,
}));
`
},
{
id: "virtual:studiocms/plugins/renderers",
content: `
${pluginRenderers ? pluginRenderers.map(({ content }) => content).join("\n") : ""}
`
},
{
id: "studiocms:plugins/renderers",
content: `
export const pluginRenderers = ${JSON.stringify(pluginRenderers.map(({ pageType, safePageType }) => ({ pageType, safePageType })) || [])};
`
},
{
id: "studiocms:plugins/imageService",
content: `
export const imageServiceKeys = ${JSON.stringify(imageServiceKeys)};
${imageServiceEndpoints.length > 0 ? imageServiceEndpoints.join("\n") : ""}
`
},
{
id: "virtual:studiocms:plugins/auth/providers",
content: `
${oAuthEndpoints.map(({ content }) => content).join("\n")}
`
},
{
id: "studiocms:plugins/auth/providers",
content: `
import * as providers from 'virtual:studiocms:plugins/auth/providers';
const oAuthEndpoints = ${JSON.stringify(oAuthEndpoints.map(({ safeName, enabled }) => ({ safeName, enabled })))};
export const oAuthButtons = ${JSON.stringify(oAuthButtons)};
export const oAuthProviders = oAuthEndpoints.map(({ safeName, enabled }) => ({
safeName,
enabled,
initSession: providers[safeName + '_initSession'] || null,
initCallback: providers[safeName + '_initCallback'] || null,
}));
`
}
);
}
addVirtualImports(params, {
name,
imports: VirtualImports
});
let pluginListLength = 0;
let pluginListMessage = "";
pluginListLength = safePluginList.length;
pluginListMessage = safePluginList.map((p, i) => `${i + 1}. ${p.name}`).join("\n");
const messageBox = boxen(pluginListMessage, {
padding: 1,
title: `Currently Installed StudioCMS Modules (${pluginListLength})`
});
messages.push({
label: "studiocms:plugins",
logLevel: "info",
message: `
${messageBox}
`
});
return {
integrations,
extraRoutes,
safePluginList,
messages,
oAuthProvidersConfigured
};
}
);
export {
defaultPlugin,
pluginHandler
};