astro-ghostcms/packages/create-astro-ghostcms/src/scripts/createProject.js

284 lines
7.2 KiB
JavaScript

import path from "node:path";
import * as p from "@clack/prompts";
import { execa } from "execa";
import fse from "fs-extra";
import c from "picocolors";
import {
exitPrompt,
getModulePaths,
isPathname,
normalizePath,
wait,
} from "../utils/index.js";
//const runnerName = "basic";
/** @param {Context} ctx */
export async function createProject(ctx) {
let { args, dryRun, initGitRepo, installDeps, template } = ctx;
const s = p.spinner();
let cwd = process.cwd();
// 1. Set up project directory
const project = await getProjectDetails(args[0], { cwd });
if (dryRun) {
await wait(1);
} else {
await fse.ensureDir(project.pathname);
}
// 2. Create the damned thing
cwd = project.pathname;
const relativePath = path.relative(process.cwd(), project.pathname);
s.start(
`${c.yellow(`Creating a new Astro-GhostCMS project in ${relativePath}`)}`,
);
if (dryRun) {
await wait(2000);
} else {
await createApp(project.name, project.pathname, template, {
onError(error) {
s.stop(`${c.red("Failed to create new project")}`);
p.cancel();
console.error(error);
process.exit(1);
},
});
}
s.stop(
`${c.green("New Astro-GhostCMS project")} '${project.name}' ${c.green(
"created",
)} 🚀`,
);
const fCheck = await p.group(
{
installDeps: () =>
p.confirm({
message: `${c.cyan("Install dependencies? (Recommended)")}`,
initialValue: true,
}),
//GitRepo: () => p.confirm({
// message: `${c.cyan('Initialize a Git repository?')} ${c.italic(c.gray("( Tip: This Option gets Stuck Press Enter Twice if you get no reponse! )"))}`,
// initialValue: false,
//}),
readyCheck: () =>
p.confirm({
message: `${c.bgYellow(
c.black(
c.bold(
" CONFIRM: Press Enter Twice to continue or `Ctrl+C` to Cancel. ",
),
),
)}`,
initialValue: true,
}),
},
{
onCancel: () => {
exitPrompt();
},
},
);
if (fCheck.readyCheck) {
// 3. Initialize git repo
if (initGitRepo) {
if (dryRun) {
await wait(1);
} else {
await exec("git", ["init"], { cwd });
}
p.log.success(c.green("Initialized Git repository"));
} else {
p.log.info(`${c.gray("Skipped Git initialization")}`);
}
const nextSteps = `If you didnt opt to install Dependencies dont forget to run: \n ${c.yellow(
"npm install",
)} / ${c.yellow("pnpm install")} / ${c.yellow(
"yarn install",
)} inside your project directory! \n \n ${c.bgYellow(
c.black(
c.bold(
" Dont forget to modify your .env file for YOUR ghost install! ",
),
),
)} `;
// 4. Install dependencies
installDeps = installDeps ?? fCheck.installDeps;
const pm = ctx.pkgManager ?? "pnpm";
if (installDeps) {
s.start(`${c.cyan(`Installing dependencies with ${pm}`)} `);
if (dryRun) {
await wait(1);
} else {
await installDependencies(pm, { cwd });
}
s.stop(`${c.green(`Dependencies installed with ${pm}`)}`);
success();
} else {
p.log.info(`${c.gray("Skipped dependency installation")}`);
success();
}
async function success() {
p.note(nextSteps);
p.outro(c.green("Deployment Complete!"));
}
} else {
exitPrompt();
}
}
/**
*
* @param {string} projectName
* @param {string} projectPathname
* @param {string} template
* @param {{ onError: (err: unknown) => any }} opts
*/
async function createApp(projectName, projectPathname, template, { onError }) {
const { pathname } = getModulePaths(import.meta.url);
const templatesDir = path.resolve(pathname, "..", "..", "templates");
const sharedTemplateDir = path.join(templatesDir, "_shared");
const runnerTemplateDir = path.join(templatesDir, template);
await fse.ensureDir(projectPathname);
// TODO: Detect if project directory is empty, otherwise we
// can't create a new project here.
await fse.copy(runnerTemplateDir, projectPathname);
// Copy misc files from shared
const filesToCopy = [
{
src: path.join(sharedTemplateDir, ".env"),
dest: path.join(projectPathname, ".env"),
},
];
await Promise.all(
filesToCopy.map(async ({ src, dest }) => await fse.copy(src, dest)),
);
/** @type {Array<{ pathname: string; getUpdates: (contents: string) => string }>} */
const filesToUpdate = [
{
pathname: path.join(projectPathname, "package.json"),
getUpdates: updateProjectName,
},
];
await Promise.all(
filesToUpdate.map(async ({ pathname, getUpdates }) => {
const contents = await fse.readFile(pathname, "utf-8");
const updatedContents = getUpdates(contents);
await fse.writeFile(pathname, updatedContents, "utf-8");
}),
);
/** @param {string} contents */
function updateProjectName(contents) {
return contents.replace(/{{PROJECT_NAME}}/g, projectName);
}
}
/**
* @param {string|undefined} projectNameInput
* @param {{ cwd: string }} opts
*/
async function getProjectDetails(projectNameInput, opts) {
let projectName = projectNameInput;
if (!projectName) {
const defaultProjectName = "my-astro-ghost";
let answer = await p.text({
message: `${c.cyan("Where would you like to create your project?")}`,
placeholder: `.${path.sep}${defaultProjectName}`,
});
if (p.isCancel(answer)) exitPrompt();
answer = answer?.trim();
projectName = answer || defaultProjectName;
}
/** @type {string} */
let pathname;
if (isPathname(projectName)) {
const dir = path.resolve(
opts.cwd,
path.dirname(normalizePath(projectName)),
);
projectName = toValidProjectName(path.basename(projectName));
pathname = path.join(dir, projectName);
} else {
projectName = toValidProjectName(projectName);
pathname = path.resolve(opts.cwd, projectName);
}
return {
name: projectName,
pathname,
};
}
/**
* @param {string} command
* @param {readonly string[]} args
* @param {{ cwd: string }} options
* @returns {Promise<{ code: number | null; signal: NodeJS.Signals | null }>}
*/
async function exec(command, args, options) {
const installExec = execa(command, ["init"], { ...options, stdio: "ignore" });
return new Promise((resolve, reject) => {
installExec.on("error", (error) => reject(error));
installExec.on("close", (code, signal) => resolve({ code, signal }));
});
}
/**
* @param {"npm" | "yarn" | "pnpm"} packageManager
* @param {{ cwd: string }} opts
* @returns
*/
async function installDependencies(packageManager, { cwd }) {
const installExec = execa(packageManager, ["install"], { cwd });
return new Promise((resolve, reject) => {
installExec.on("error", (error) => reject(error));
installExec.on("close", () => resolve());
});
}
/**
* @param {string} projectName
*/
function toValidProjectName(projectName) {
if (isValidProjectName(projectName)) {
return projectName;
}
return projectName
.trim()
.toLowerCase()
.replace(/\s+/g, "-")
.replace(/^[._]/, "")
.replace(/[^a-z\d\-~]+/g, "-")
.replace(/^-+/, "")
.replace(/-+$/, "");
}
/**
* @param {string} projectName
*/
function isValidProjectName(projectName) {
return /^(?:@[a-z\d\-*~][a-z\d\-*._~]*\/)?[a-z\d\-~][a-z\d\-._~]*$/.test(
projectName,
);
}
/**
* @typedef {import("../../types.js").Template} Template
* @typedef {import("../../types.js").PackageManager} PackageManager
* @typedef {import("../../types.js").Context} Context
* @typedef {import("../../types.js").Serializable} Serializable
*/