studiocms
Version:
Astro Native CMS for AstroDB. Built from the ground up by the Astro community.
234 lines (231 loc) • 8.82 kB
JavaScript
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
};