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 +67 -0
- package/bin/index.js +8 -0
- package/package.json +43 -0
- package/src/cli.js +46 -0
- package/src/generators/createProject.js +55 -0
- package/src/generators/templateEngine.js +118 -0
- package/src/generators/templates/addons.js +119 -0
- package/src/generators/templates/backend.js +265 -0
- package/src/generators/templates/database.js +45 -0
- package/src/generators/templates/frontend.js +271 -0
- package/src/generators/templates/root.js +170 -0
- package/src/install/installDependencies.js +30 -0
- package/src/install/packageMap.js +3 -0
- package/src/prompts/index.js +106 -0
- package/src/stacks.js +94 -0
- package/src/utils/names.js +15 -0
- package/templates/README.md +10 -0
- package/templates/addons/auth/.gitkeep +1 -0
- package/templates/addons/docker/.gitkeep +1 -0
- package/templates/addons/eslint/.gitkeep +1 -0
- package/templates/addons/testing/.gitkeep +1 -0
- package/templates/backend/django/.gitkeep +1 -0
- package/templates/backend/express/.gitkeep +1 -0
- package/templates/backend/laravel/.gitkeep +1 -0
- package/templates/backend/nest/.gitkeep +1 -0
- package/templates/databases/mongodb/.gitkeep +1 -0
- package/templates/databases/mysql/.gitkeep +1 -0
- package/templates/databases/postgres/.gitkeep +1 -0
- package/templates/frontend/angular/.gitkeep +1 -0
- package/templates/frontend/next/.gitkeep +1 -0
- package/templates/frontend/react/.gitkeep +1 -0
- package/templates/frontend/vue/.gitkeep +1 -0
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
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
|
+
}
|