@agametis/create-app-for-fm
Version:
Tool for selecting a starter project for FileMaker applications
297 lines (273 loc) • 9.13 kB
JavaScript
import fs from "fs-extra";
import path from "path";
import prompts from "prompts";
import {GITHUB_CONFIG} from "../config.js";
import {t} from "../i18n.js";
import {
cloneRepository,
initializePackageManager,
installDependencies,
cleanupAndExit,
cancelOnce,
} from "../utils.js";
export async function createProject() {
console.log(t("create.title"));
// Start interactive prompts (sequential to avoid rendering next prompt after cancel)
const responses = {};
try {
// 1) Where to create the project
const locAns = await prompts(
{
type: "select",
name: "location",
message: t("create.locationChoice"),
choices: [
{ title: t("create.newDirectory"), value: "new" },
{ title: t("create.currentDirectory"), value: "current" },
],
initial: 0,
},
{
onCancel: () => {
cancelOnce(1);
return true;
},
}
);
if (!locAns || !locAns.location) {
cancelOnce(1);
return;
}
responses.location = locAns.location;
// 2) Ask for project name only when creating a new directory
if (responses.location === "new") {
const nameAns = await prompts(
{
type: "text",
name: "projectName",
message: t("create.projectName"),
initial: t("create.initialProjectName"),
},
{
onCancel: () => {
cancelOnce(1);
return true;
},
}
);
if (!nameAns || !nameAns.projectName) {
cancelOnce(1);
return;
}
responses.projectName = nameAns.projectName;
}
// 3) Select template
const tmplAns = await prompts(
{
type: "select",
name: "template",
message: t("common.selectTemplate"),
choices: GITHUB_CONFIG.repositories.map((repo) => ({
title: repo.name,
value: `${GITHUB_CONFIG.baseUrl}${repo.path}`,
})),
initial: 0,
},
{
onCancel: () => {
cancelOnce(1);
return true;
},
}
);
if (!tmplAns || !tmplAns.template) {
cancelOnce(1);
return;
}
responses.template = tmplAns.template;
} catch (e) {
if (e instanceof Error && e.message === "canceled") {
cancelOnce(1);
return;
}
throw e;
}
// Determine project directory
let projectDir;
if (responses.location === "current") {
projectDir = process.cwd();
console.log(`${t("create.setupInCurrentDir")}${projectDir}`);
} else {
projectDir = path.join(process.cwd(), responses.projectName);
console.log(`${t("create.setupInNewDir")}${responses.projectName}`);
// Create project directory if it doesn't exist
if (!fs.existsSync(projectDir)) {
fs.mkdirSync(projectDir);
}
}
// Always use npm as package manager
const packageManager = "npm";
console.log(`${t("common.selectedPackageManager")}${packageManager}`);
console.log(`${t("create.selectedTemplate")}${responses.template}`);
// Skip initialization - we'll check if we need it after cloning the template
console.log(t("common.preparingTemplate"));
// Clone template from GitHub repo
try {
// Check if directory is not empty (for current directory option)
if (fs.existsSync(projectDir)) {
const files = fs.readdirSync(projectDir);
if (files.length > 0 && responses.location === "new") {
// Only package.json should be there from the init
if (files.length > 1 || !files.includes("package.json")) {
console.error(t("create.directoryExists"));
console.log(t("create.chooseAnotherName"));
cleanupAndExit(1);
return; // abort flow
}
} else if (files.length > 0 && responses.location === "current") {
// For current directory, ask for confirmation before proceeding
try {
const { confirmOverwrite } = await prompts({
type: "confirm",
name: "confirmOverwrite",
message: t("common.directoryNotEmpty"),
initial: false,
}, {
onCancel: () => {
cancelOnce(1);
return true;
},
});
if (!confirmOverwrite) {
console.log(t("common.operationCancelled"));
cleanupAndExit(1);
return; // user chose not to overwrite
}
} catch (e) {
if (e instanceof Error && e.message === "canceled") {
cancelOnce(1);
return; // cancelled during confirm
}
throw e;
}
}
}
try {
// Clone the template repository silently using helper function
const tempCloneDir = path.join(process.cwd(), "temp-clone");
const cloneSuccess = await cloneRepository(
responses.template,
tempCloneDir
);
if (!cloneSuccess) {
cleanupAndExit(1);
return; // stop if clone failed
}
// Copy all files from the cloned repo to the project directory (excluding .git)
fs.copySync(tempCloneDir, projectDir, {
filter: (src) => !src.includes(".git"),
overwrite: true,
});
// Check if the template has a package.json
const templatePackageJsonPath = path.join(projectDir, "package.json");
const hasPackageJson = fs.existsSync(templatePackageJsonPath);
// Initialize package manager if needed
if (!hasPackageJson) {
initializePackageManager(packageManager, projectDir);
} else {
console.log(t("create.packageJsonFound"));
// Install dependencies
installDependencies(packageManager, projectDir);
}
// Clean up the temporary clone silently
fs.removeSync(tempCloneDir);
console.log(t("common.templateCopied"));
} catch (cloneError) {
// If there's an error during cloning, show a more user-friendly message
console.error(
`${t("create.downloadError")}${cloneError.message.split("\n")[0]}`
);
cleanupAndExit(1);
return; // abort after clone error
}
} catch (error) {
console.error(`${t("common.error")}${error.message}`);
cleanupAndExit(1);
return; // ensure no further execution
}
console.log(t("create.setupComplete"));
// Display next steps information
console.log(t("common.nextSteps"));
// Different instructions based on whether it's a new directory or current directory
if (responses.location === "new") {
console.log(`${t("create.changeToDirectory")}${responses.projectName}`);
}
// Check if package.json exists in the project directory to provide appropriate instructions
const packageJsonPath = path.join(projectDir, "package.json");
const nodeModulesPath = path.join(projectDir, "node_modules");
const dependenciesInstalled = fs.existsSync(nodeModulesPath);
if (fs.existsSync(packageJsonPath)) {
try {
const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, "utf8"));
const stepNumber = responses.location === "new" ? 2 : 1;
// Check if there are scripts in package.json
if (packageJson.scripts && Object.keys(packageJson.scripts).length > 0) {
// Check for common scripts and suggest them
if (
packageJson.scripts.dev ||
packageJson.scripts.start ||
packageJson.scripts.serve
) {
const devCommand = packageJson.scripts.dev
? "dev"
: packageJson.scripts.start
? "start"
: "serve";
console.log(
` ${stepNumber}. ${t(
"common.startDevServer"
)}: ${packageManager} ${
packageManager === "npm" ? "run " : ""
}${devCommand}`
);
} else {
// If no dev/start script, show available scripts
const scriptKeys = Object.keys(packageJson.scripts);
console.log(` ${stepNumber}. ${t("common.availableScripts")}`);
scriptKeys.forEach((script) => {
console.log(
` - ${packageManager} ${
packageManager === "npm" ? "run " : ""
}${script}`
);
});
}
} else if (!dependenciesInstalled && packageJson) {
// No scripts found and dependencies not installed
console.log(
` ${stepNumber}${t(
"common.installDependencies"
)}${packageManager} install`
);
}
} catch (error) {
// Fallback if package.json can't be parsed
if (packageJsonPath) {
console.log(
` ${responses.location === "new" ? "2" : "1"}${t(
"common.installDependencies"
)}${packageManager} install`
);
}
}
} else {
// No package.json found
console.log(
` ${responses.location === "new" ? "2" : "1"}${t("common.checkReadme")}`
);
}
console.log(t("create.checkReadmeInDirectory"));
console.log(t("create.successMessage"));
cleanupAndExit(0);
return; // explicit end of function
}