create-fstack-app 1.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,67 @@
1
+ # create-my-fullstack-app
2
+
3
+ Interactive npm scaffolding for modern full-stack apps.
4
+
5
+ ```bash
6
+ npm create my-fullstack-app my-app
7
+ ```
8
+
9
+ or:
10
+
11
+ ```bash
12
+ npx create-my-fullstack-app my-app
13
+ ```
14
+
15
+ ## Features
16
+
17
+ - Multi-stack setup: MERN, PERN, MEAN, T3 Stack, Firebase Stack, Django + React, Laravel + React
18
+ - Custom setup mode for frontend, language, styling, backend, database, auth, and add-ons
19
+ - Smart project naming from an argument or prompt
20
+ - Dynamic folder and file generation
21
+ - Optional dependency installation
22
+ - Ready-to-run root `npm run dev` script for Node-based split stacks
23
+
24
+ ## Local Development
25
+
26
+ ```bash
27
+ npm install
28
+ npm run dev
29
+ ```
30
+
31
+ Run a non-interactive smoke scaffold:
32
+
33
+ ```bash
34
+ npm run smoke
35
+ ```
36
+
37
+ ## CLI
38
+
39
+ ```bash
40
+ create-my-fullstack-app [project-name] [options]
41
+ ```
42
+
43
+ Options:
44
+
45
+ - `--stack <stack>`: `MERN`, `PERN`, `MEAN`, `T3`, `Firebase`, `Django React`, `Laravel React`, `Custom`
46
+ - `--yes`: use sensible defaults
47
+ - `--no-install`: generate files without running `npm install`
48
+ - `--force`: empty an existing target folder before generating
49
+
50
+ ## Generated MERN Example
51
+
52
+ ```txt
53
+ my-app/
54
+ ├── frontend/
55
+ │ ├── src/
56
+ │ ├── public/
57
+ │ ├── package.json
58
+ │ └── vite.config.js
59
+ ├── backend/
60
+ │ ├── src/
61
+ │ ├── package.json
62
+ │ └── .env
63
+ ├── database/
64
+ ├── README.md
65
+ ├── .gitignore
66
+ └── package.json
67
+ ```
package/bin/index.js ADDED
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { runCli } from "../src/cli.js";
4
+
5
+ runCli().catch((error) => {
6
+ console.error(error?.message || error);
7
+ process.exit(1);
8
+ });
package/package.json ADDED
@@ -0,0 +1,43 @@
1
+ {
2
+ "name": "create-fstack-app",
3
+ "version": "1.0.1",
4
+ "description": "A modern interactive CLI for scaffolding customizable full-stack applications.",
5
+ "type": "module",
6
+ "bin": {
7
+ "create-fstack-app": "bin/index.js"
8
+ },
9
+ "files": [
10
+ "bin",
11
+ "src",
12
+ "templates",
13
+ "README.md"
14
+ ],
15
+ "scripts": {
16
+ "start": "node ./bin/index.js",
17
+ "dev": "node ./bin/index.js --no-install",
18
+ "smoke": "node ./bin/index.js demo-app --stack MERN --yes --no-install --force"
19
+ },
20
+ "keywords": [
21
+ "create",
22
+ "scaffold",
23
+ "fullstack",
24
+ "mern",
25
+ "pern",
26
+ "react",
27
+ "express",
28
+ "vite",
29
+ "cli"
30
+ ],
31
+ "author": "",
32
+ "license": "MIT",
33
+ "engines": {
34
+ "node": ">=18.17"
35
+ },
36
+ "dependencies": {
37
+ "@inquirer/prompts": "^5.5.0",
38
+ "chalk": "^5.3.0",
39
+ "commander": "^12.1.0",
40
+ "fs-extra": "^11.2.0",
41
+ "ora": "^8.1.0"
42
+ }
43
+ }
package/src/cli.js ADDED
@@ -0,0 +1,46 @@
1
+ import process from "node:process";
2
+ import { Command } from "commander";
3
+ import chalk from "chalk";
4
+ import { collectAnswers } from "./prompts/index.js";
5
+ import { createProject } from "./generators/createProject.js";
6
+
7
+ export async function runCli(argv = process.argv) {
8
+ const program = new Command()
9
+ .name("create-my-fullstack-app")
10
+ .description("Scaffold a modern full-stack application.")
11
+ .argument("[project-name]", "project directory name")
12
+ .option("-s, --stack <stack>", "stack preset: MERN, PERN, MEAN, T3, Firebase, Django React, Laravel React, Custom")
13
+ .option("-y, --yes", "accept sensible defaults and skip prompts where possible")
14
+ .option("--no-install", "skip dependency installation")
15
+ .option("-f, --force", "overwrite an existing target directory")
16
+ .version("0.1.0")
17
+ .parse(argv);
18
+
19
+ const options = program.opts();
20
+ const [projectName] = program.args;
21
+
22
+ const answers = await collectAnswers({
23
+ projectName,
24
+ stack: options.stack,
25
+ yes: options.yes,
26
+ install: options.install
27
+ });
28
+
29
+ const result = await createProject({
30
+ ...answers,
31
+ install: options.install,
32
+ force: options.force
33
+ });
34
+
35
+ console.log();
36
+ console.log(chalk.green.bold("Project created successfully!"));
37
+ console.log();
38
+ console.log(chalk.bold("Next steps:"));
39
+ console.log(chalk.cyan(` cd ${result.projectName}`));
40
+ if (!options.install) {
41
+ console.log(chalk.cyan(" npm install"));
42
+ }
43
+ console.log(chalk.cyan(" npm run dev"));
44
+ console.log();
45
+ console.log(chalk.dim(`Generated ${result.stackLabel} at ${result.targetDir}`));
46
+ }
@@ -0,0 +1,55 @@
1
+ import path from "node:path";
2
+ import process from "node:process";
3
+ import fs from "fs-extra";
4
+ import ora from "ora";
5
+ import chalk from "chalk";
6
+ import { composeProject } from "./templateEngine.js";
7
+ import { installDependencies } from "../install/installDependencies.js";
8
+ import { normalizeProjectName } from "../utils/names.js";
9
+
10
+ export async function createProject({ projectName, stack, custom, install, force }) {
11
+ const targetDir = path.resolve(process.cwd(), projectName);
12
+ const packageName = normalizeProjectName(path.basename(targetDir));
13
+ const relativeTargetDir = path.relative(process.cwd(), targetDir) || ".";
14
+
15
+ if (!packageName) {
16
+ throw new Error("Project name must include at least one letter or number.");
17
+ }
18
+
19
+ if (await fs.pathExists(targetDir)) {
20
+ const entries = await fs.readdir(targetDir);
21
+ if (entries.length > 0 && !force) {
22
+ throw new Error(`Directory "${projectName}" already exists and is not empty. Use --force to overwrite.`);
23
+ }
24
+ if (force) {
25
+ await fs.emptyDir(targetDir);
26
+ }
27
+ }
28
+
29
+ const spinner = ora(`Generating ${stack.label} project...`).start();
30
+ const project = composeProject({ projectName: packageName, stack, custom });
31
+
32
+ await fs.ensureDir(targetDir);
33
+ await writeFiles(targetDir, project.files);
34
+ spinner.succeed("Project files generated");
35
+
36
+ if (install) {
37
+ await installDependencies(targetDir, project.installTargets);
38
+ } else {
39
+ console.log(chalk.yellow("Skipped dependency installation."));
40
+ }
41
+
42
+ return {
43
+ projectName: relativeTargetDir,
44
+ stackLabel: stack.label,
45
+ targetDir
46
+ };
47
+ }
48
+
49
+ async function writeFiles(targetDir, files) {
50
+ await Promise.all(Object.entries(files).map(async ([relativePath, content]) => {
51
+ const filePath = path.join(targetDir, relativePath);
52
+ await fs.ensureDir(path.dirname(filePath));
53
+ await fs.writeFile(filePath, content);
54
+ }));
55
+ }
@@ -0,0 +1,118 @@
1
+ import { createRootFiles } from "./templates/root.js";
2
+ import { createFrontendFiles } from "./templates/frontend.js";
3
+ import { createBackendFiles } from "./templates/backend.js";
4
+ import { createDatabaseFiles } from "./templates/database.js";
5
+ import { createAddonFiles } from "./templates/addons.js";
6
+ import { packageManagerInstallTargets } from "../install/packageMap.js";
7
+
8
+ export function composeProject({ projectName, stack, custom }) {
9
+ const config = toProjectConfig({ stack, custom });
10
+ const files = {
11
+ ...createRootFiles({ projectName, stack, config }),
12
+ ...createFrontendFiles(config),
13
+ ...createBackendFiles(config),
14
+ ...createDatabaseFiles(config),
15
+ ...createAddonFiles(config)
16
+ };
17
+
18
+ return {
19
+ files,
20
+ installTargets: packageManagerInstallTargets(config)
21
+ };
22
+ }
23
+
24
+ function toProjectConfig({ stack, custom }) {
25
+ if (stack.id === "custom") {
26
+ return {
27
+ stackId: stack.id,
28
+ stackLabel: stack.label,
29
+ frontend: custom.frontend,
30
+ language: custom.language,
31
+ styling: custom.styling,
32
+ backend: custom.backend,
33
+ database: custom.database,
34
+ auth: custom.auth,
35
+ features: custom.features,
36
+ structure: "custom"
37
+ };
38
+ }
39
+
40
+ const configByStack = {
41
+ mern: {
42
+ frontend: "React",
43
+ language: "JavaScript",
44
+ styling: "Tailwind CSS",
45
+ backend: "Express.js",
46
+ database: "MongoDB",
47
+ auth: "None",
48
+ features: ["ESLint", "Prettier"],
49
+ structure: "split"
50
+ },
51
+ pern: {
52
+ frontend: "React",
53
+ language: "JavaScript",
54
+ styling: "Tailwind CSS",
55
+ backend: "Express.js",
56
+ database: "PostgreSQL",
57
+ auth: "None",
58
+ features: ["ESLint", "Prettier"],
59
+ structure: "split"
60
+ },
61
+ mean: {
62
+ frontend: "Angular",
63
+ language: "TypeScript",
64
+ styling: "SCSS",
65
+ backend: "Express.js",
66
+ database: "MongoDB",
67
+ auth: "None",
68
+ features: ["ESLint", "Prettier"],
69
+ structure: "split"
70
+ },
71
+ t3: {
72
+ frontend: "Next.js",
73
+ language: "TypeScript",
74
+ styling: "Tailwind CSS",
75
+ backend: "tRPC",
76
+ database: "PostgreSQL",
77
+ auth: "Auth.js",
78
+ features: ["ESLint", "Prettier", "Prisma"],
79
+ structure: "t3"
80
+ },
81
+ firebase: {
82
+ frontend: "React",
83
+ language: "JavaScript",
84
+ styling: "Tailwind CSS",
85
+ backend: "Firebase Functions",
86
+ database: "Firebase",
87
+ auth: "Firebase Auth",
88
+ features: ["ESLint", "Prettier"],
89
+ structure: "firebase"
90
+ },
91
+ "django-react": {
92
+ frontend: "React",
93
+ language: "JavaScript",
94
+ styling: "Tailwind CSS",
95
+ backend: "Django",
96
+ database: "PostgreSQL",
97
+ auth: "JWT",
98
+ features: ["ESLint", "Prettier"],
99
+ structure: "split"
100
+ },
101
+ "laravel-react": {
102
+ frontend: "React",
103
+ language: "JavaScript",
104
+ styling: "Tailwind CSS",
105
+ backend: "Laravel",
106
+ database: "MySQL",
107
+ auth: "JWT",
108
+ features: ["ESLint", "Prettier"],
109
+ structure: "split"
110
+ }
111
+ };
112
+
113
+ return {
114
+ stackId: stack.id,
115
+ stackLabel: stack.label,
116
+ ...configByStack[stack.id]
117
+ };
118
+ }
@@ -0,0 +1,119 @@
1
+ export function createAddonFiles(config) {
2
+ const files = {};
3
+
4
+ if (config.features.includes("Docker")) {
5
+ files["Dockerfile"] = `FROM node:20-alpine
6
+ WORKDIR /app
7
+ COPY package*.json ./
8
+ RUN npm install
9
+ COPY . .
10
+ CMD ["npm", "run", "dev"]
11
+ `;
12
+ files["docker-compose.yml"] = dockerCompose(config);
13
+ }
14
+
15
+ if (config.features.includes("CI/CD")) {
16
+ files[".github/workflows/ci.yml"] = `name: CI
17
+
18
+ on: [push, pull_request]
19
+
20
+ jobs:
21
+ build:
22
+ runs-on: ubuntu-latest
23
+ steps:
24
+ - uses: actions/checkout@v4
25
+ - uses: actions/setup-node@v4
26
+ with:
27
+ node-version: 20
28
+ - run: npm install
29
+ - run: npm run dev -- --help || true
30
+ `;
31
+ }
32
+
33
+ if (config.features.includes("ESLint")) {
34
+ files["eslint.config.js"] = `export default [
35
+ {
36
+ ignores: ["node_modules", "dist", "build", ".next"]
37
+ }
38
+ ];
39
+ `;
40
+ }
41
+
42
+ if (config.features.includes("Prettier")) {
43
+ files[".prettierrc"] = `${JSON.stringify({ semi: true, singleQuote: false, printWidth: 100 }, null, 2)}\n`;
44
+ }
45
+
46
+ if (config.features.includes("Testing Setup")) {
47
+ files["tests/smoke.test.js"] = `import { describe, expect, it } from "vitest";
48
+
49
+ describe("generated project", () => {
50
+ it("has a selected stack", () => {
51
+ expect("${config.stackLabel}").toBeTruthy();
52
+ });
53
+ });
54
+ `;
55
+ }
56
+
57
+ if (config.features.includes("Husky")) {
58
+ files[".husky/pre-commit"] = `npm run lint
59
+ `;
60
+ }
61
+
62
+ if (config.auth !== "None") {
63
+ files["docs/auth.md"] = `# Authentication
64
+
65
+ Selected auth starter: ${config.auth}
66
+
67
+ Wire provider secrets in \`.env\` before building protected routes.
68
+ `;
69
+ }
70
+
71
+ return files;
72
+ }
73
+
74
+ function dockerCompose(config) {
75
+ const services = {
76
+ app: {
77
+ build: ".",
78
+ ports: ["5173:5173", "5000:5000"],
79
+ volumes: [".:/app"]
80
+ }
81
+ };
82
+
83
+ if (config.database === "MongoDB") {
84
+ services.mongo = { image: "mongo:7", ports: ["27017:27017"] };
85
+ }
86
+ if (config.database === "PostgreSQL") {
87
+ services.postgres = {
88
+ image: "postgres:16",
89
+ environment: { POSTGRES_PASSWORD: "postgres", POSTGRES_DB: "my_fullstack_app" },
90
+ ports: ["5432:5432"]
91
+ };
92
+ }
93
+ if (config.database === "MySQL") {
94
+ services.mysql = {
95
+ image: "mysql:8",
96
+ environment: { MYSQL_ROOT_PASSWORD: "password", MYSQL_DATABASE: "my_fullstack_app" },
97
+ ports: ["3306:3306"]
98
+ };
99
+ }
100
+
101
+ return `services:
102
+ ${Object.entries(services).map(([name, service]) => yamlService(name, service)).join("")}`;
103
+ }
104
+
105
+ function yamlService(name, service) {
106
+ const lines = [` ${name}:`];
107
+ for (const [key, value] of Object.entries(service)) {
108
+ if (Array.isArray(value)) {
109
+ lines.push(` ${key}:`);
110
+ value.forEach((item) => lines.push(` - ${JSON.stringify(item)}`));
111
+ } else if (typeof value === "object") {
112
+ lines.push(` ${key}:`);
113
+ Object.entries(value).forEach(([nestedKey, nestedValue]) => lines.push(` ${nestedKey}: ${JSON.stringify(nestedValue)}`));
114
+ } else {
115
+ lines.push(` ${key}: ${JSON.stringify(value)}`);
116
+ }
117
+ }
118
+ return `${lines.join("\n")}\n`;
119
+ }