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.
@@ -0,0 +1,170 @@
1
+ import { slugToTitle } from "../../utils/names.js";
2
+
3
+ export function createRootFiles({ projectName, stack, config }) {
4
+ const scripts = rootScripts(config);
5
+
6
+ return {
7
+ "package.json": `${JSON.stringify({
8
+ name: projectName,
9
+ version: "0.1.0",
10
+ private: true,
11
+ type: "module",
12
+ workspaces: rootWorkspaces(config),
13
+ scripts,
14
+ dependencies: rootDependencies(config),
15
+ devDependencies: rootDevDependencies(config)
16
+ }, null, 2)}\n`,
17
+ "README.md": readme({ projectName, stack, config }),
18
+ ".gitignore": gitignore(),
19
+ ".env.example": envExample(config)
20
+ };
21
+ }
22
+
23
+ function rootScripts(config) {
24
+ if (config.structure === "t3") {
25
+ const scripts = {
26
+ dev: "next dev",
27
+ build: "next build",
28
+ start: "next start",
29
+ lint: "next lint"
30
+ };
31
+ return withAddonScripts(scripts, config);
32
+ }
33
+
34
+ const scripts = {};
35
+ if (hasFrontend(config) && hasNodeBackend(config)) {
36
+ scripts.dev = "concurrently \"npm run dev --workspace frontend\" \"npm run dev --workspace backend\"";
37
+ } else if (hasFrontend(config)) {
38
+ scripts.dev = "npm run dev --workspace frontend";
39
+ } else if (hasNodeBackend(config)) {
40
+ scripts.dev = "npm run dev --workspace backend";
41
+ } else {
42
+ scripts.dev = "echo \"Start your selected framework from its generated folder.\"";
43
+ }
44
+ return withAddonScripts(scripts, config);
45
+ }
46
+
47
+ function rootDevDependencies(config) {
48
+ if (config.structure === "t3") {
49
+ return withAddonDevDependencies({
50
+ "@types/node": "^20.14.10",
51
+ "@types/react": "^18.3.3",
52
+ "@types/react-dom": "^18.3.0",
53
+ eslint: "^8.57.0",
54
+ "eslint-config-next": "^14.2.5",
55
+ prisma: "^5.16.1",
56
+ typescript: "^5.5.3"
57
+ }, config);
58
+ }
59
+ const deps = hasFrontend(config) && hasNodeBackend(config) ? { concurrently: "^8.2.2" } : {};
60
+ return withAddonDevDependencies(deps, config);
61
+ }
62
+
63
+ function withAddonDevDependencies(deps, config) {
64
+ if (config.features.includes("ESLint")) deps.eslint = "^8.57.0";
65
+ if (config.features.includes("Prettier")) deps.prettier = "^3.3.2";
66
+ if (config.features.includes("Husky")) deps.husky = "^9.1.1";
67
+ if (config.features.includes("Testing Setup")) deps.vitest = "^2.0.3";
68
+ return deps;
69
+ }
70
+
71
+ function rootDependencies(config) {
72
+ if (config.structure !== "t3") return {};
73
+ return {
74
+ "@prisma/client": "^5.16.1",
75
+ "@trpc/client": "^10.45.2",
76
+ "@trpc/next": "^10.45.2",
77
+ "@trpc/react-query": "^10.45.2",
78
+ "@trpc/server": "^10.45.2",
79
+ next: "^14.2.5",
80
+ react: "^18.3.1",
81
+ "react-dom": "^18.3.1",
82
+ superjson: "^2.2.1",
83
+ zod: "^3.23.8"
84
+ };
85
+ }
86
+
87
+ function rootWorkspaces(config) {
88
+ if (config.structure === "t3") return [];
89
+ const workspaces = [];
90
+ if (hasFrontend(config)) workspaces.push("frontend");
91
+ if (hasNodeBackend(config)) workspaces.push("backend");
92
+ return workspaces;
93
+ }
94
+
95
+ function readme({ projectName, stack, config }) {
96
+ return `# ${slugToTitle(projectName)}
97
+
98
+ Generated with \`create-my-fullstack-app\`.
99
+
100
+ ## Stack
101
+
102
+ - Preset: ${stack.label}
103
+ - Frontend: ${config.frontend}
104
+ - Backend: ${config.backend}
105
+ - Database: ${config.database}
106
+ - Styling: ${config.styling}
107
+ - Auth: ${config.auth}
108
+
109
+ ## Getting Started
110
+
111
+ \`\`\`bash
112
+ npm install
113
+ npm run dev
114
+ \`\`\`
115
+
116
+ ## Folders
117
+
118
+ - \`frontend/\`: client application when your stack uses a separate frontend
119
+ - \`backend/\`: server application when your stack uses a separate backend
120
+ - \`database/\`: schema, seed, or connection notes when relevant
121
+ - \`src/\`, \`server/\`, \`prisma/\`, \`app/\`: T3-style structure when selected
122
+ `;
123
+ }
124
+
125
+ function gitignore() {
126
+ return `node_modules
127
+ dist
128
+ build
129
+ .next
130
+ .env
131
+ .DS_Store
132
+ coverage
133
+ *.log
134
+ `;
135
+ }
136
+
137
+ function envExample(config) {
138
+ const lines = ["NODE_ENV=development"];
139
+ if (config.database === "MongoDB") lines.push("MONGODB_URI=mongodb://localhost:27017/my_fullstack_app");
140
+ if (config.database === "PostgreSQL") lines.push("DATABASE_URL=postgresql://postgres:postgres@localhost:5432/my_fullstack_app");
141
+ if (config.database === "MySQL") lines.push("DATABASE_URL=mysql://root:password@localhost:3306/my_fullstack_app");
142
+ if (config.database === "Firebase") lines.push("VITE_FIREBASE_API_KEY=", "VITE_FIREBASE_PROJECT_ID=");
143
+ if (config.auth !== "None") lines.push("AUTH_SECRET=change-me");
144
+ return `${lines.join("\n")}\n`;
145
+ }
146
+
147
+ function hasFrontend(config) {
148
+ return config.frontend !== "None";
149
+ }
150
+
151
+ function hasNodeBackend(config) {
152
+ return ["Express.js", "Fastify", "NestJS", "Firebase Functions"].includes(config.backend);
153
+ }
154
+
155
+ function withAddonScripts(scripts, config) {
156
+ const nextScripts = { ...scripts };
157
+ if (config.features.includes("ESLint") && !nextScripts.lint) {
158
+ nextScripts.lint = "eslint .";
159
+ }
160
+ if (config.features.includes("Prettier")) {
161
+ nextScripts.format = "prettier --write .";
162
+ }
163
+ if (config.features.includes("Testing Setup")) {
164
+ nextScripts.test = "vitest";
165
+ }
166
+ if (config.features.includes("Husky")) {
167
+ nextScripts.prepare = "husky";
168
+ }
169
+ return nextScripts;
170
+ }
@@ -0,0 +1,30 @@
1
+ import { spawn } from "node:child_process";
2
+ import path from "node:path";
3
+ import ora from "ora";
4
+
5
+ export async function installDependencies(targetDir, installTargets) {
6
+ for (const relativeTarget of installTargets) {
7
+ const cwd = path.join(targetDir, relativeTarget);
8
+ const label = relativeTarget === "." ? "root" : relativeTarget;
9
+ const spinner = ora(`Installing ${label} dependencies...`).start();
10
+
11
+ await runNpmInstall(cwd);
12
+ spinner.succeed(`Installed ${label} dependencies`);
13
+ }
14
+ }
15
+
16
+ function runNpmInstall(cwd) {
17
+ return new Promise((resolve, reject) => {
18
+ const child = spawn("npm", ["install"], {
19
+ cwd,
20
+ shell: true,
21
+ stdio: "ignore"
22
+ });
23
+
24
+ child.on("error", reject);
25
+ child.on("exit", (code) => {
26
+ if (code === 0) resolve();
27
+ else reject(new Error(`npm install failed in ${cwd}`));
28
+ });
29
+ });
30
+ }
@@ -0,0 +1,3 @@
1
+ export function packageManagerInstallTargets(config) {
2
+ return ["."];
3
+ }
@@ -0,0 +1,106 @@
1
+ import { input, select, confirm, checkbox } from "@inquirer/prompts";
2
+ import { normalizeProjectName } from "../utils/names.js";
3
+ import { STACKS, stackChoices, resolveStack } from "../stacks.js";
4
+
5
+ const defaultCustom = {
6
+ frontend: "React",
7
+ language: "JavaScript",
8
+ styling: "Tailwind CSS",
9
+ backend: "Express.js",
10
+ database: "MongoDB",
11
+ auth: "None",
12
+ features: ["ESLint", "Prettier"]
13
+ };
14
+
15
+ export async function collectAnswers({ projectName, stack, yes = false, install = true }) {
16
+ const resolvedProjectName = projectName
17
+ ? projectName.trim()
18
+ : yes
19
+ ? "my-app"
20
+ : normalizeProjectName(await input({
21
+ message: "Enter project name:",
22
+ default: "my-app",
23
+ validate: validateProjectName
24
+ }));
25
+
26
+ const resolvedStack = stack
27
+ ? resolveStack(stack)
28
+ : yes
29
+ ? STACKS.MERN
30
+ : await select({
31
+ message: "Choose your stack:",
32
+ choices: stackChoices
33
+ });
34
+
35
+ if (resolvedStack.id === "custom") {
36
+ return {
37
+ projectName: resolvedProjectName,
38
+ stack: resolvedStack,
39
+ custom: yes ? defaultCustom : await collectCustomAnswers(),
40
+ install: install && (yes || await confirm({ message: "Install dependencies?", default: true }))
41
+ };
42
+ }
43
+
44
+ return {
45
+ projectName: resolvedProjectName,
46
+ stack: resolvedStack,
47
+ custom: null,
48
+ install: install && (yes || await confirm({ message: "Install dependencies?", default: true }))
49
+ };
50
+ }
51
+
52
+ async function collectCustomAnswers() {
53
+ const frontend = await select({
54
+ message: "Choose frontend framework:",
55
+ choices: ["React", "Next.js", "Vue", "Nuxt", "Angular", "Svelte", "SolidJS"].map(toChoice)
56
+ });
57
+
58
+ const language = await select({
59
+ message: "Choose language:",
60
+ choices: ["JavaScript", "TypeScript"].map(toChoice)
61
+ });
62
+
63
+ const styling = await select({
64
+ message: "Choose styling solution:",
65
+ choices: ["Tailwind CSS", "CSS Modules", "Styled Components", "SCSS", "Chakra UI", "Material UI", "None"].map(toChoice)
66
+ });
67
+
68
+ const backend = await select({
69
+ message: "Choose backend framework:",
70
+ choices: ["Express.js", "Fastify", "NestJS", "Django", "Laravel", "Spring Boot", "ASP.NET", "None"].map(toChoice)
71
+ });
72
+
73
+ const database = await select({
74
+ message: "Choose database:",
75
+ choices: ["MongoDB", "PostgreSQL", "MySQL", "SQLite", "Firebase", "Supabase", "None"].map(toChoice)
76
+ });
77
+
78
+ const auth = await select({
79
+ message: "Add authentication starter?",
80
+ choices: ["JWT", "Clerk", "Auth.js", "Firebase Auth", "Supabase Auth", "None"].map(toChoice)
81
+ });
82
+
83
+ const features = await checkbox({
84
+ message: "Select optional features:",
85
+ choices: ["ESLint", "Prettier", "Husky", "Docker", "Testing Setup", "CI/CD", "Zustand", "Redux Toolkit", "Prisma", "Swagger Docs"].map((name) => ({
86
+ name,
87
+ value: name,
88
+ checked: ["ESLint", "Prettier"].includes(name)
89
+ }))
90
+ });
91
+
92
+ return { frontend, language, styling, backend, database, auth, features };
93
+ }
94
+
95
+ function toChoice(value) {
96
+ return { name: value, value };
97
+ }
98
+
99
+ function validateProjectName(value) {
100
+ const normalized = normalizeProjectName(value);
101
+ if (!normalized) return "Project name is required.";
102
+ if (!/^[a-z0-9._-]+$/.test(normalized)) {
103
+ return "Use letters, numbers, dots, dashes, or underscores.";
104
+ }
105
+ return true;
106
+ }
package/src/stacks.js ADDED
@@ -0,0 +1,94 @@
1
+ export const STACKS = {
2
+ MERN: {
3
+ id: "mern",
4
+ label: "MERN",
5
+ frontend: "React",
6
+ backend: "Node + Express",
7
+ database: "MongoDB",
8
+ structure: "split"
9
+ },
10
+ PERN: {
11
+ id: "pern",
12
+ label: "PERN",
13
+ frontend: "React",
14
+ backend: "Node + Express",
15
+ database: "PostgreSQL",
16
+ structure: "split"
17
+ },
18
+ MEAN: {
19
+ id: "mean",
20
+ label: "MEAN",
21
+ frontend: "Angular",
22
+ backend: "Node + Express",
23
+ database: "MongoDB",
24
+ structure: "split"
25
+ },
26
+ T3: {
27
+ id: "t3",
28
+ label: "T3 Stack",
29
+ frontend: "Next.js + TypeScript",
30
+ backend: "tRPC",
31
+ database: "PostgreSQL",
32
+ structure: "t3"
33
+ },
34
+ FIREBASE: {
35
+ id: "firebase",
36
+ label: "Firebase Stack",
37
+ frontend: "React",
38
+ backend: "Firebase Functions",
39
+ database: "Firestore",
40
+ structure: "firebase"
41
+ },
42
+ DJANGO_REACT: {
43
+ id: "django-react",
44
+ label: "Django + React",
45
+ frontend: "React",
46
+ backend: "Django",
47
+ database: "PostgreSQL",
48
+ structure: "split"
49
+ },
50
+ LARAVEL_REACT: {
51
+ id: "laravel-react",
52
+ label: "Laravel + React",
53
+ frontend: "React",
54
+ backend: "Laravel",
55
+ database: "MySQL",
56
+ structure: "split"
57
+ },
58
+ CUSTOM: {
59
+ id: "custom",
60
+ label: "Custom Setup",
61
+ frontend: "User-selected",
62
+ backend: "User-selected",
63
+ database: "User-selected",
64
+ structure: "custom"
65
+ }
66
+ };
67
+
68
+ export const stackChoices = [
69
+ STACKS.MERN,
70
+ STACKS.PERN,
71
+ STACKS.MEAN,
72
+ STACKS.T3,
73
+ STACKS.FIREBASE,
74
+ STACKS.DJANGO_REACT,
75
+ STACKS.LARAVEL_REACT,
76
+ STACKS.CUSTOM
77
+ ].map((stack) => ({
78
+ name: stack.label,
79
+ value: stack
80
+ }));
81
+
82
+ export function resolveStack(value) {
83
+ const normalized = String(value).toLowerCase().replace(/[\s_+]+/g, "-");
84
+ const stack = Object.values(STACKS).find((candidate) => {
85
+ return candidate.id === normalized || candidate.label.toLowerCase().replace(/[\s_+]+/g, "-") === normalized;
86
+ });
87
+
88
+ if (!stack) {
89
+ const valid = Object.values(STACKS).map((candidate) => candidate.label).join(", ");
90
+ throw new Error(`Unknown stack "${value}". Choose one of: ${valid}`);
91
+ }
92
+
93
+ return stack;
94
+ }
@@ -0,0 +1,15 @@
1
+ export function normalizeProjectName(value) {
2
+ return String(value || "")
3
+ .trim()
4
+ .toLowerCase()
5
+ .replace(/\s+/g, "-")
6
+ .replace(/[^a-z0-9._-]/g, "");
7
+ }
8
+
9
+ export function slugToTitle(value) {
10
+ return normalizeProjectName(value)
11
+ .split(/[-_]/g)
12
+ .filter(Boolean)
13
+ .map((part) => part.charAt(0).toUpperCase() + part.slice(1))
14
+ .join(" ");
15
+ }
@@ -0,0 +1,10 @@
1
+ # Templates
2
+
3
+ This CLI uses a modular JavaScript template engine in `src/generators/templates`.
4
+
5
+ The folders below document the intended public template architecture for future static or community templates:
6
+
7
+ - `frontend/react`, `frontend/next`, `frontend/vue`, `frontend/angular`
8
+ - `backend/express`, `backend/django`, `backend/laravel`, `backend/nest`
9
+ - `databases/mongodb`, `databases/postgres`, `databases/mysql`
10
+ - `addons/auth`, `addons/docker`, `addons/testing`, `addons/eslint`
@@ -0,0 +1 @@
1
+
@@ -0,0 +1 @@
1
+
@@ -0,0 +1 @@
1
+
@@ -0,0 +1 @@
1
+
@@ -0,0 +1 @@
1
+
@@ -0,0 +1 @@
1
+
@@ -0,0 +1 @@
1
+
@@ -0,0 +1 @@
1
+
@@ -0,0 +1 @@
1
+
@@ -0,0 +1 @@
1
+
@@ -0,0 +1 @@
1
+
@@ -0,0 +1 @@
1
+