create-next-plus
Version:

546 lines (523 loc) • 15.7 kB
JavaScript
#! /usr/bin/env node
// src/cli.ts
import { Command } from "commander";
import path10 from "node:path";
// src/actions/intro.ts
import figlet from "figlet";
// src/utils/log.ts
import chalk from "chalk";
import "chalk-animation";
import gradient from "gradient-string";
var log = (value, opts) => {
if (!!opts) {
if (gradient)
console.log(gradient.morning(value));
} else {
console.log(chalk.green(value));
}
};
// src/actions/intro.ts
var intro = async () => {
await figlet(
"Next+",
{
font: "Ogre"
},
(err, data) => {
if (err) {
log("Something went wrong...");
return;
}
log(data, { gradient: true });
}
);
log("Welcome to the Next+ CLI tool, a supercharged create-next-app", {
gradient: true
});
};
// src/actions/init.ts
import prompts from "prompts";
var init = async (defaultFlags) => {
const res = await prompts([
{
type: "text",
name: "name",
message: "What should we call your project?"
},
{
type: defaultFlags.noGit ? null : "confirm",
name: "noGit",
message: "Do you want to skip git init?"
},
{
type: defaultFlags.noInstall ? null : "confirm",
name: "noInstall",
message: "Do you want to skip dependency installation?"
},
{
type: "confirm",
name: "db-confirm",
message: "Do you want to start with a database?"
},
{
type: (prev) => prev === true ? "select" : null,
name: "db",
message: "Please select a dabatase provider: ",
choices: [
{
title: "supabase",
description: "Supabase (Postgres)",
value: "sb"
},
{
title: "firestore",
description: "Firestore (NoSQL)",
value: "fs"
},
{
title: "planetscale",
description: "PlanetScale (MySQL)",
value: "ps"
}
]
},
{
type: "multiselect",
name: "extras",
message: "Do you want to install any extras?",
choices: [
{
title: "langchain",
value: "langchain"
},
{
title: "million.js",
value: "millionjs"
},
{
title: "zustand",
value: "zustand"
}
]
}
]);
return res;
};
// src/actions/create.ts
import chalk2 from "chalk";
import fs from "fs-extra";
import path2 from "node:path";
// src/utils/path.ts
import path from "node:path";
import { fileURLToPath } from "node:url";
var __filename = fileURLToPath(import.meta.url);
var distPath = path.dirname(__filename);
var PKG_ROOT = path.join(distPath, "../");
// src/actions/create.ts
var create = async ({ name }) => {
if (name === "" || name === void 0) {
console.error(chalk2.red("Project name cannot be empty!"));
process.exit(1);
}
const projectPath = path2.join(process.cwd(), name);
if (fs.existsSync(projectPath)) {
console.error(chalk2.red(`Error: Directory "${name}" already exists.`));
process.exit(1);
}
fs.mkdirSync(projectPath);
const templatePath = path2.join(`${PKG_ROOT}`, "template/base");
try {
await fs.copy(templatePath, projectPath);
} catch (err) {
console.error(chalk2.red(`Error copying template files: ${err}`));
process.exit(1);
}
};
// src/actions/deps.ts
import chalk3 from "chalk";
import { execa } from "execa";
var exec = async (projectDir, options) => {
const { args = ["install"], stdout = "pipe" } = options;
execa("bun", args, { cwd: projectDir, stdout });
};
var runInstallCommand = async (projectDir) => {
exec(projectDir, { stdout: "ignore" });
};
var installDependencies = async (projectDir) => {
console.log(chalk3.yellow("Installing dependencies..."));
await runInstallCommand(projectDir);
log("Successfully installed dependencies!\n", { gradient: true });
};
// src/actions/git.ts
import { execSync } from "node:child_process";
import path3 from "node:path";
import fs2 from "fs-extra";
import { execa as execa2 } from "execa";
import chalk4 from "chalk";
import prompts2 from "prompts";
var isGitInstalled = (dir) => {
try {
execSync("git --version", { cwd: dir });
return true;
} catch (_e) {
return false;
}
};
var isRootGitRepo = (dir) => {
return fs2.existsSync(path3.join(dir, ".git"));
};
var isInsideGitRepo = async (dir) => {
try {
await execa2("git", ["rev-parse", "--is-inside-work-tree"], {
cwd: dir,
stdout: "ignore"
});
return true;
} catch (_e) {
return false;
}
};
var getGitVersion = () => {
const stdout = execSync("git --version").toString().trim();
const gitVersionTag = stdout.split(" ")[2];
const major = gitVersionTag?.split(".")[0];
const minor = gitVersionTag?.split(".")[1];
return { major: Number(major), minor: Number(minor) };
};
var getDefaultBranch = () => {
const stdout = execSync("git config --global init.defaultBranch || echo main").toString().trim();
return stdout;
};
var initGit = async (projectPath) => {
if (!isGitInstalled(projectPath)) {
console.log(
chalk4.red("Git already installed. Skipping git initialization.")
);
return;
}
log("Initializing git repository...", { gradient: true });
const isRoot = isRootGitRepo(projectPath);
const isInside = await isInsideGitRepo(projectPath);
const dirName = path3.parse(projectPath).name;
if (isInside && isRoot) {
const overwriteGit = await prompts2({
type: "confirm",
initial: false,
name: "overwriteGit",
message: chalk4.redBright.bold(
`Directory ${chalk4.cyan(
dirName
)} is already a git repository. Do you want to overwrite it?`
)
});
if (!overwriteGit) {
console.log(chalk4.blue("Skipping Git initialization."));
return;
}
fs2.removeSync(path3.join(projectPath, ".git"));
} else if (isInside && !isRoot) {
const initializeChildGitRepo = await prompts2({
type: "confirm",
initial: false,
name: "initializeChildGitRepo",
message: chalk4.redBright.bold(
`Directory ${chalk4.cyan(
dirName
)} is inside a git worktree. Do you want to initialize it as a git repository?
`
)
});
if (!initializeChildGitRepo) {
console.log(chalk4.blue("Skipping Git initialization."));
return;
}
}
try {
const branchName = getDefaultBranch();
const { major, minor } = getGitVersion();
if (major < 2 || major == 2 && minor < 28) {
await execa2("git", ["init"], { cwd: projectPath });
await execa2("git", ["symbolic-ref", "HEAD", `refs/heads/${branchName}`], {
cwd: projectPath
});
} else {
await execa2("git", ["init", `--initial-branch=${branchName}`], {
cwd: projectPath
});
}
await execa2("git", ["add", "."], { cwd: projectPath });
log("Successfully initialized and staged git", { gradient: true });
} catch (error) {
console.log(
chalk4.red(
"Failed to initialize git repository. Please report this as a bug on GitHub!"
)
);
}
};
// src/installers/drizzle.ts
import path5 from "node:path";
import fs4 from "fs-extra";
// src/utils/deps.ts
import path4 from "node:path";
import fs3 from "fs-extra";
import sortPackageJson from "sort-package-json";
// src/installers/dependencyVersionMap.ts
var dependencyVersionMap = {
// Drizzle
"@planetscale/database": "^1.11.0",
"drizzle-orm": "^0.29.3",
"drizzle-kit": "^0.20.9",
firebase: "^10.7.1",
mysql2: "^3.6.1",
pg: "^8.11.3",
postgres: "^3.4.3",
langchain: "^0.1.2",
"@langchain/openai": "^0.0.11",
"@redux-devtools/extension": "^3.3.0",
zustand: "^4.4.7",
million: "^2.6.4"
};
// src/utils/deps.ts
var addPackageDependency = (opts) => {
const { dependencies, devMode, projectDir } = opts;
const pkgJson = fs3.readJSONSync(path4.join(projectDir, "package.json"));
dependencies.forEach((pkgName) => {
const version = dependencyVersionMap[pkgName];
if (devMode && pkgJson.devDependencies) {
pkgJson.devDependencies[pkgName] = version;
} else if (pkgJson.dependencies) {
pkgJson.dependencies[pkgName] = version;
}
});
const sortedPkgJson = sortPackageJson(pkgJson);
fs3.writeJSONSync(path4.join(projectDir, "package.json"), sortedPkgJson, {
spaces: 2
});
};
// src/installers/drizzle.ts
var drizzleInstaller = ({ projectDir, scopedAppName }, type) => {
const extrasDir = path5.join(PKG_ROOT, "template/extra");
if (type === "ps") {
addPackageDependency({
projectDir,
dependencies: ["drizzle-kit", "mysql2"],
devMode: true
});
addPackageDependency({
projectDir,
dependencies: ["drizzle-orm", "@planetscale/database"],
devMode: false
});
}
if (type === "sb") {
addPackageDependency({
projectDir,
dependencies: ["drizzle-kit", "pg"],
devMode: true
});
addPackageDependency({
projectDir,
dependencies: ["drizzle-orm", "postgres"],
devMode: false
});
}
const configFile = path5.join(extrasDir, `config/drizzle-${type}.config.ts`);
const configDest = path5.join(projectDir, "drizzle.config.ts");
const schemaScr = path5.join(
extrasDir,
"src/server/db",
`drizzle-${type}-schema.ts`
);
const schemaDest = path5.join(projectDir, "src/server/db/schema.ts");
let schemaContent = fs4.readFileSync(schemaScr, "utf-8");
schemaContent = schemaContent.replace(
"project1_${name}",
`${scopedAppName}_\${name}`
);
let configContent = fs4.readFileSync(configFile, "utf-8");
configContent = configContent.replace("project1_*", `${scopedAppName}_*`);
const clientSrc = path5.join(
extrasDir,
`src/server/db/index-drizzle-${type}.ts`
);
const clientDest = path5.join(projectDir, "src/server/db/index.ts");
const trpcSrc = path5.join(extrasDir, "src/server/trpc/with-db.ts");
const trpcDest = path5.join(projectDir, "src/server/trpc.ts");
const pkgJsonPath = path5.join(projectDir, "package.json");
const pkgJsonContent = fs4.readJSONSync(pkgJsonPath);
pkgJsonContent.scripts = {
...pkgJsonContent.scripts,
"db:push": type === "sb" ? "drizzle-kit push:pg" : "drizzle-kit push:mysql",
"db:studio": "drizzle-kit studio"
};
fs4.copySync(configFile, configDest);
fs4.mkdirSync(path5.dirname(schemaDest), { recursive: true });
fs4.writeFileSync(schemaDest, schemaContent);
fs4.writeFileSync(configDest, configContent);
fs4.copySync(clientSrc, clientDest);
fs4.copySync(trpcSrc, trpcDest);
fs4.writeJSONSync(pkgJsonPath, pkgJsonContent, {
spaces: 2
});
log("Drizzle has been installed and setup.", { gradient: true });
};
// src/installers/firestore.ts
import path6 from "node:path";
import fs5 from "fs-extra";
var firestoreInstaller = ({ projectDir }) => {
const extrasDir = path6.join(PKG_ROOT, "template/extra");
addPackageDependency({
projectDir,
dependencies: ["firebase"],
devMode: false
});
const libSrc = path6.join(extrasDir, "src/lib/firebase.ts");
const libDest = path6.join(projectDir, "src/lib/firebase.ts");
const clientSrc = path6.join(extrasDir, "src/server/db/index-firestore.ts");
const clientDest = path6.join(projectDir, "src/server/db.ts");
const trpcSrc = path6.join(extrasDir, "src/server/trpc/with-db.ts");
const trpcDest = path6.join(projectDir, "src/server/trpc.ts");
fs5.copySync(libSrc, libDest);
fs5.copySync(clientSrc, clientDest);
fs5.copySync(trpcSrc, trpcDest);
log("Firebase has been installed and Firestore has been setup.", {
gradient: true
});
};
// src/installers/langchain.ts
import path7 from "node:path";
import fs6 from "fs-extra";
var langchainInstaller = ({ projectDir }) => {
const extrasDir = path7.join(PKG_ROOT, "template/extra");
addPackageDependency({
projectDir,
dependencies: ["langchain", "@langchain/openai"],
devMode: false
});
const clientSrc = path7.join(extrasDir, "src/langchain");
const clientDest = path7.join(projectDir, "src/langchain");
fs6.copySync(clientSrc, clientDest);
log("Langchain has been installed and setup", { gradient: true });
};
// src/installers/million.ts
import path8 from "node:path";
import fs7 from "fs-extra";
var millionInstaller = ({ projectDir }) => {
const extrasDir = path8.join(PKG_ROOT, "template/extra");
addPackageDependency({
projectDir,
dependencies: ["million"],
devMode: false
});
const configFile = path8.join(extrasDir, `config/next-million.config.mjs`);
const configDest = path8.join(projectDir, "next.config.mjs");
fs7.copySync(configFile, configDest);
log("Million.js has been installed and setup.", { gradient: true });
};
// src/installers/zustand.ts
import path9 from "node:path";
import fs8 from "fs-extra";
var zustandInstaller = ({ projectDir }) => {
const extrasDir = path9.join(PKG_ROOT, "template/extra");
addPackageDependency({
projectDir,
dependencies: ["zustand"],
devMode: false
});
addPackageDependency({
projectDir,
dependencies: ["@redux-devtools/extension"],
devMode: true
});
const clientSrc = path9.join(extrasDir, "src/store");
const clientDest = path9.join(projectDir, "src/store");
fs8.copySync(clientSrc, clientDest);
log("Zustand has been installed and setup", { gradient: true });
};
// src/utils/version.ts
import fs9 from "fs-extra";
var getVersion = async () => {
try {
const pkgJson = await fs9.readJson(`${PKG_ROOT}/package.json`);
return pkgJson.version;
} catch (err) {
console.error(`Error reading package.json: ${err}`);
throw err;
}
};
// src/cli.ts
var program = new Command();
var runCli = async () => {
program.name("create-next-plus").description("Supercharged create-next-app!").version(await getVersion());
program.option("--no-git", "Skip git initialization").option("--no-install", "Skip dependency installation").action(async (opts) => {
await intro();
const { name, db, extras } = await init(opts);
await create({ name });
const projectPath = path10.join(process.cwd(), name);
if (!opts.noGit) {
await initGit(projectPath);
}
if (!opts.noInstall) {
await installDependencies(projectPath);
}
if (db && db !== "fs") {
drizzleInstaller(
{
noInstall: opts.noInstall,
pkgManager: "bun",
projectDir: projectPath,
projectName: name,
scopedAppName: name
},
db
);
}
if (db === "fs") {
firestoreInstaller({
noInstall: opts.noInstall,
pkgManager: "bun",
projectDir: projectPath,
projectName: name,
scopedAppName: name
});
}
if (extras.includes("langchain")) {
langchainInstaller({
noInstall: opts.noInstall,
pkgManager: "bun",
projectDir: projectPath,
projectName: name,
scopedAppName: name
});
}
if (extras.includes("zustand")) {
zustandInstaller({
noInstall: opts.noInstall,
pkgManager: "bun",
projectDir: projectPath,
projectName: name,
scopedAppName: name
});
}
if (extras.includes("millionjs")) {
millionInstaller({
noInstall: opts.noInstall,
pkgManager: "bun",
projectDir: projectPath,
projectName: name,
scopedAppName: name
});
}
log("Congratulations! Everything is setup.", { gradient: true });
});
program.parse(process.argv);
};
// src/index.ts
var main = async () => {
await runCli();
};
main().catch((err) => {
console.error("Aborting installation, due to error: ", err);
process.exit(1);
});