UNPKG

studiocms

Version:

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

234 lines (231 loc) 8.82 kB
import { existsSync, promises as fs } from "node:fs"; import { fileURLToPath } from "node:url"; import { cancelled, success } from "@withstudiocms/cli-kit/messages"; import { exists, pathToFileURL, resolveRoot } from "@withstudiocms/cli-kit/utils"; import { intro, note, outro } from "@withstudiocms/effect/clack"; import { builders, loadFile } from "magicast"; import { getDefaultExportOptions } from "magicast/helpers"; import { Cli, Console, Effect, genLogger, Layer } from "../../effect.js"; import { CliContext, genContext } from "../utils/context.js"; import { logger } from "../utils/logger.js"; import { TryToInstallPlugins } from "./tryToInstallPlugins.js"; import { UpdateStudioCMSConfig } from "./updateStudioCMSConfig.js"; import { ValidatePlugins } from "./validatePlugins.js"; const ALIASES = /* @__PURE__ */ new Map([ ["auth0", "@studiocms/auth0"], ["blog", "@studiocms/blog"], ["cloudinary", "@studiocms/cloudinary-image-service"], ["discord", "@studiocms/discord"], ["github", "@studiocms/github"], ["google", "@studiocms/google"], ["html", "@studiocms/html"], ["markdoc", "@studiocms/markdoc"], ["md", "@studiocms/md"], ["mdx", "@studiocms/mdx"], ["wysiwyg", "@studiocms/wysiwyg"] ]); const StudioCMSScopes = ["@studiocms", "@withstudiocms"]; var UpdateResult = /* @__PURE__ */ ((UpdateResult2) => { UpdateResult2[UpdateResult2["none"] = 0] = "none"; UpdateResult2[UpdateResult2["updated"] = 1] = "updated"; UpdateResult2[UpdateResult2["cancelled"] = 2] = "cancelled"; UpdateResult2[UpdateResult2["failure"] = 3] = "failure"; return UpdateResult2; })(UpdateResult || {}); const STUBS = { STUDIOCMS_CONFIG: `import { defineStudioCMSConfig } from 'studiocms/config'; export default defineStudioCMSConfig({ dbStartPage: false, });` }; const toIdent = (name) => { const ident = name.trim().replace(/[-_./]?studiocms?[-_.]?/g, "").replace(/\.js/, "").replace(/[.\-_/]+([a-zA-Z])/g, (_, w) => w.toUpperCase()).replace(/^[^a-zA-Z$_]+/, "").replace(/@.*$/, ""); return `${ident[0].toLowerCase()}${ident.slice(1)}`; }; const createPrettyError = (err) => Effect.sync(() => { err.message = `StudioCMS could not update your studiocms.config.mjs file safely. Reason: ${err.message} You will need to add these plugins(s) manually. Documentation: https://docs.studiocms.dev`; return err; }); const configPaths = Object.freeze([ "./studiocms.config.js", "./studiocms.config.mjs", "./studiocms.config.cjs", "./studiocms.config.ts", "./studiocms.config.mts", "./studiocms.config.cts" ]); const resolveConfigPath = (root) => genLogger("studiocms/cli/add.resolveConfigPath")(function* () { for (const path of configPaths) { const url = yield* Effect.try(() => new URL(path, root)); if (exists(url)) return url; } yield* Console.error( `No StudioCMS config file found in ${root.toString()}. Searched for: ${configPaths.join(", ")}` ); return void 0; }); function appendForwardSlash(path) { return path.endsWith("/") ? path : `${path}/`; } const plugin = Cli.Args.text({ name: "plugin" }).pipe( Cli.Args.withDescription(" name of the plugin to add"), Cli.Args.repeated ); const resolveOrCreateConfig = (root) => genLogger("studiocms/cli/add.resolveOrCreateConfig")(function* () { const existingConfig = yield* resolveConfigPath(root); if (existingConfig) return existingConfig; yield* Console.debug("Unable to locate a config file, generating one for you."); const newConfigURL = new URL("./studiocms.config.mjs", root); yield* Effect.tryPromise( () => fs.writeFile(fileURLToPath(newConfigURL), STUBS.STUDIOCMS_CONFIG, { encoding: "utf-8" }) ); return newConfigURL; }); const loadConfigModule = (configURL, validatedPlugins) => genLogger("studiocms/cli/add.loadConfigModule")(function* () { let mod; mod = yield* Effect.tryPromise(() => loadFile(fileURLToPath(configURL))); yield* Console.debug("Parsed StudioCMS Config"); if (mod.exports.default.$type !== "function-call") { mod.imports.$prepend({ imported: "defineStudioCMSConfig", from: "studiocms/config" }); mod.exports.default = builders.functionCall("defineStudioCMSConfig", mod.exports.default); } else if (mod.exports.default.$args[0] == null) { mod.exports.default.$args[0] = { dbStartPage: false }; } yield* Console.debug("StudioCMS config ensured `defineStudioCMSConfig`"); for (const plugin2 of validatedPlugins) { const config = getDefaultExportOptions(mod); const pluginId = toIdent(plugin2.id); if (!mod.imports.$items.some((imp) => imp.local === pluginId)) { mod.imports.$append({ imported: "default", local: pluginId, from: plugin2.packageName }); } config.plugins ??= []; if (!config.plugins.$ast.elements.some( (el) => el.type === "CallExpression" && el.callee.type === "Identifier" && el.callee.name === pluginId )) { config.plugins.push(builders.functionCall(pluginId)); } yield* Console.debug(`StudioCMS config added plugin ${plugin2.id}`); } return mod; }); const addPluginDeps = Layer.mergeAll( ValidatePlugins.Default, TryToInstallPlugins.Default, UpdateStudioCMSConfig.Default ); const addPlugin = Cli.Command.make( "add", { plugin }, ({ plugin: plugin2 }) => Effect.gen(function* () { const [validator, installer, updater, context] = yield* Effect.all([ ValidatePlugins, TryToInstallPlugins, UpdateStudioCMSConfig, genContext ]); const { cwd, chalk } = context; yield* intro("StudioCMS CLI Utilities (add)"); const pluginNames = plugin2.map((name) => { const aliasName = ALIASES.get(name); if (!aliasName) return name; return aliasName; }); const validatedPlugins = yield* validator.run(pluginNames).pipe(CliContext.makeProvide(context)); const installResult = yield* installer.run(validatedPlugins).pipe(CliContext.makeProvide(context)); const rootPath = resolveRoot(cwd); const root = pathToFileURL(rootPath); root.href = appendForwardSlash(root.href); switch (installResult) { case 1 /* updated */: { break; } case 2 /* cancelled */: { yield* note( cancelled( `Dependencies ${chalk.bold("NOT")} installed.`, "Be sure to install them manually before continuing!" ) ); break; } case 3 /* failure */: { throw createPrettyError(new Error("Unable to install dependencies")); } case 0 /* none */: break; } const configURL = yield* resolveOrCreateConfig(root); yield* Console.debug(`found config at ${configURL}`); const mod = yield* loadConfigModule(configURL, validatedPlugins); let configResult; if (mod) { configResult = yield* updater.run(configURL, mod).pipe( CliContext.makeProvide(context), Effect.catchAll((err) => { logger.debug(`Error updating studiocms config ${err}`); return Effect.fail(createPrettyError(err)); }) ); } switch (configResult) { case 2 /* cancelled */: { yield* outro(cancelled(`Your configuration has ${chalk.bold("NOT")} been updated.`)); break; } case 0 /* none */: { const pkgURL = new URL("./package.json", configURL); if (existsSync(fileURLToPath(pkgURL))) { const { dependencies = {}, devDependencies = {} } = yield* Effect.tryPromise( () => fs.readFile(fileURLToPath(pkgURL)).then((res) => JSON.parse(res.toString())) ); const deps = Object.keys(Object.assign(dependencies, devDependencies)); const missingDeps = validatedPlugins.filter( (plugin3) => !deps.includes(plugin3.packageName) ); if (missingDeps.length === 0) { yield* outro("Configuration up-to-date."); break; } } yield* outro("Configuration up-to-date."); break; } // NOTE: failure shouldn't happen in practice because `updateAstroConfig` doesn't return that. // Pipe this to the same handling as `UpdateResult.updated` for now. case 3 /* failure */: case 1 /* updated */: case void 0: { const list = validatedPlugins.map((plugin3) => ` - ${plugin3.pluginName}`).join("\n"); yield* outro( success( `Added the following plugin${validatedPlugins.length === 1 ? "" : "s"} to your project: ${list}` ) ); } } }).pipe(Effect.provide(addPluginDeps)) ).pipe(Cli.Command.withDescription("Add StudioCMS plugin(s) to your project")); export { ALIASES, STUBS, StudioCMSScopes, UpdateResult, addPlugin, appendForwardSlash, createPrettyError, plugin, resolveConfigPath, toIdent };