UNPKG

studiocms

Version:

Astro Native CMS for AstroDB. Built from the ground up by the Astro community.

736 lines (708 loc) 26.9 kB
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 };