create-efc-app 0.1.0

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/dist/index.js ADDED
@@ -0,0 +1,306 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/index.ts
4
+ import * as p from "@clack/prompts";
5
+ import chalk from "chalk";
6
+ import { spawn } from "child_process";
7
+
8
+ // src/scaffold.ts
9
+ import fs from "fs-extra";
10
+ import path from "path";
11
+ import crypto from "crypto";
12
+ async function scaffold(opts) {
13
+ const dest = path.resolve(process.cwd(), opts.projectName);
14
+ await fs.ensureDir(dest);
15
+ await writePackageJson(dest, opts);
16
+ await writeTsConfig(dest, opts);
17
+ await writeEfcConfig(dest, opts);
18
+ await writeEntryPoint(dest, opts);
19
+ await writeGitignore(dest);
20
+ await writeEnvFiles(dest);
21
+ await writeExampleRoute(dest, opts);
22
+ if (opts.tasks) await writeExampleTask(dest, opts);
23
+ }
24
+ async function writePackageJson(dest, opts) {
25
+ const deps = {
26
+ "express-file-cluster": "^0.1.0"
27
+ };
28
+ if (opts.database === "mongodb") deps["mongoose"] = "^8.0.0";
29
+ if (opts.database === "postgresql") {
30
+ deps["pg"] = "^8.0.0";
31
+ deps["drizzle-orm"] = "^0.33.0";
32
+ }
33
+ if (opts.tasks && opts.taskBackend === "bullmq") deps["bullmq"] = "^5.0.0";
34
+ if (opts.tasks && opts.taskBackend === "pg-boss") deps["pg-boss"] = "^10.0.0";
35
+ const devDeps = {
36
+ vitest: "^2.0.0"
37
+ };
38
+ if (opts.language === "typescript") {
39
+ devDeps["typescript"] = "^5.5.0";
40
+ devDeps["@types/node"] = "^22.0.0";
41
+ devDeps["@types/express"] = "^4.17.21";
42
+ devDeps["tsup"] = "^8.2.0";
43
+ devDeps["tsx"] = "^4.0.0";
44
+ }
45
+ const pkg = {
46
+ name: opts.projectName,
47
+ version: "0.1.0",
48
+ type: "module",
49
+ scripts: {
50
+ dev: "efc start dev",
51
+ build: "efc build prod",
52
+ start: "efc start prod",
53
+ test: "efc run tests"
54
+ },
55
+ dependencies: deps,
56
+ devDependencies: devDeps
57
+ };
58
+ await fs.writeJson(path.join(dest, "package.json"), pkg, { spaces: 2 });
59
+ }
60
+ async function writeTsConfig(dest, opts) {
61
+ if (opts.language !== "typescript") return;
62
+ const config = {
63
+ compilerOptions: {
64
+ target: "ES2022",
65
+ module: "NodeNext",
66
+ moduleResolution: "NodeNext",
67
+ strict: true,
68
+ esModuleInterop: true,
69
+ skipLibCheck: true,
70
+ outDir: "./dist",
71
+ rootDir: "./src"
72
+ },
73
+ include: ["src/**/*"],
74
+ exclude: ["node_modules", "dist"]
75
+ };
76
+ await fs.writeJson(path.join(dest, "tsconfig.json"), config, { spaces: 2 });
77
+ }
78
+ async function writeEfcConfig(dest, opts) {
79
+ const ext = opts.language === "typescript" ? "ts" : "js";
80
+ const tasks = opts.tasks ? `{
81
+ backend: '${opts.taskBackend ?? "bullmq"}',
82
+ redisUrl: process.env.REDIS_URL,
83
+ concurrency: 5,
84
+ }` : "false";
85
+ const content = `import type { EFCConfig } from 'express-file-cluster';
86
+
87
+ const config: EFCConfig = {
88
+ port: Number(process.env.PORT) || 3000,
89
+ apiDir: './src/api',
90
+ tasksDir: './src/tasks',
91
+ database: '${opts.database}',
92
+ databaseUrl: process.env.DATABASE_URL!,
93
+ authStrategy: '${opts.authStrategy}',
94
+ jwtSecret: process.env.JWT_SECRET!,
95
+ cluster: process.env.NODE_ENV === 'production',
96
+ tasks: ${tasks},
97
+ globalMiddlewares: [],
98
+ };
99
+
100
+ export default config;
101
+ `;
102
+ await fs.outputFile(path.join(dest, `efc.config.${ext}`), content);
103
+ }
104
+ async function writeEntryPoint(dest, opts) {
105
+ const ext = opts.language === "typescript" ? "ts" : "js";
106
+ const content = `import { ignite } from 'express-file-cluster';
107
+ import { fileURLToPath } from 'url';
108
+ import path from 'path';
109
+
110
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
111
+
112
+ ignite({
113
+ port: Number(process.env.PORT) || 3000,
114
+ apiDir: path.join(__dirname, 'api'),
115
+ tasksDir: path.join(__dirname, 'tasks'),
116
+ database: '${opts.database}',
117
+ databaseUrl: process.env.DATABASE_URL,
118
+ authStrategy: '${opts.authStrategy}',
119
+ jwtSecret: process.env.JWT_SECRET,
120
+ cluster: process.env.NODE_ENV === 'production',
121
+ }).catch(console.error);
122
+ `;
123
+ await fs.outputFile(path.join(dest, "src", `index.${ext}`), content);
124
+ }
125
+ async function writeGitignore(dest) {
126
+ await fs.outputFile(
127
+ path.join(dest, ".gitignore"),
128
+ "node_modules/\ndist/\n.env\n.env.local\n*.log\n"
129
+ );
130
+ }
131
+ async function writeEnvFiles(dest) {
132
+ const secret = crypto.randomBytes(64).toString("hex");
133
+ const dotenv = `PORT=3000
134
+ NODE_ENV=development
135
+ DATABASE_URL=
136
+ JWT_SECRET=${secret}
137
+ REDIS_URL=redis://localhost:6379
138
+ `;
139
+ const example = `PORT=3000
140
+ NODE_ENV=development
141
+ DATABASE_URL=
142
+ JWT_SECRET=<generate with: openssl rand -hex 64>
143
+ REDIS_URL=redis://localhost:6379
144
+ `;
145
+ await fs.outputFile(path.join(dest, ".env"), dotenv);
146
+ await fs.outputFile(path.join(dest, ".env.example"), example);
147
+ }
148
+ async function writeExampleRoute(dest, opts) {
149
+ const ext = opts.language === "typescript" ? "ts" : "js";
150
+ const content = opts.language === "typescript" ? `import type { Request, Response } from 'express';
151
+
152
+ export const GET = async (_req: Request, res: Response) => {
153
+ res.json({ status: 'OK', timestamp: new Date().toISOString() });
154
+ };
155
+ ` : `export const GET = async (_req, res) => {
156
+ res.json({ status: 'OK', timestamp: new Date().toISOString() });
157
+ };
158
+ `;
159
+ await fs.outputFile(path.join(dest, "src", "api", `health.${ext}`), content);
160
+ }
161
+ async function writeExampleTask(dest, opts) {
162
+ const ext = opts.language === "typescript" ? "ts" : "js";
163
+ const content = opts.language === "typescript" ? `import { defineTask } from 'express-file-cluster/tasks';
164
+
165
+ interface SendEmailPayload {
166
+ to: string;
167
+ subject: string;
168
+ body: string;
169
+ }
170
+
171
+ export default defineTask<SendEmailPayload>(async (payload) => {
172
+ // TODO: wire up your mailer
173
+ console.log('[SendEmail] Sending to', payload.to);
174
+ });
175
+ ` : `import { defineTask } from 'express-file-cluster/tasks';
176
+
177
+ export default defineTask(async (payload) => {
178
+ console.log('[SendEmail] Sending to', payload.to);
179
+ });
180
+ `;
181
+ await fs.outputFile(path.join(dest, "src", "tasks", `SendEmail.${ext}`), content);
182
+ }
183
+
184
+ // src/index.ts
185
+ async function main() {
186
+ console.log();
187
+ p.intro(chalk.bgCyan(chalk.black(" create-efc-app ")));
188
+ const projectName = await p.text({
189
+ message: "Project name:",
190
+ placeholder: "my-api",
191
+ defaultValue: "my-api",
192
+ validate: (v) => !v.trim() ? "Project name is required" : void 0
193
+ });
194
+ if (p.isCancel(projectName)) {
195
+ p.cancel("Cancelled");
196
+ process.exit(0);
197
+ }
198
+ const language = await p.select({
199
+ message: "Language:",
200
+ options: [
201
+ { value: "typescript", label: "TypeScript", hint: "recommended" },
202
+ { value: "javascript", label: "JavaScript" }
203
+ ]
204
+ });
205
+ if (p.isCancel(language)) {
206
+ p.cancel("Cancelled");
207
+ process.exit(0);
208
+ }
209
+ const database = await p.select({
210
+ message: "Database:",
211
+ options: [
212
+ { value: "mongodb", label: "MongoDB", hint: "Mongoose" },
213
+ { value: "postgresql", label: "PostgreSQL", hint: "Drizzle ORM" }
214
+ ]
215
+ });
216
+ if (p.isCancel(database)) {
217
+ p.cancel("Cancelled");
218
+ process.exit(0);
219
+ }
220
+ const authStrategy = await p.select({
221
+ message: "Authentication strategy:",
222
+ options: [
223
+ { value: "http-only", label: "http-only", hint: "secure cookie \u2014 recommended for SSR" },
224
+ { value: "localStorage", label: "localStorage", hint: "bearer token \u2014 for SPAs" }
225
+ ]
226
+ });
227
+ if (p.isCancel(authStrategy)) {
228
+ p.cancel("Cancelled");
229
+ process.exit(0);
230
+ }
231
+ const cluster = await p.confirm({
232
+ message: "Enable multi-core clustering?",
233
+ initialValue: true
234
+ });
235
+ if (p.isCancel(cluster)) {
236
+ p.cancel("Cancelled");
237
+ process.exit(0);
238
+ }
239
+ const tasks = await p.confirm({
240
+ message: "Enable background tasks?",
241
+ initialValue: true
242
+ });
243
+ if (p.isCancel(tasks)) {
244
+ p.cancel("Cancelled");
245
+ process.exit(0);
246
+ }
247
+ let taskBackend;
248
+ if (tasks) {
249
+ const backend = await p.select({
250
+ message: "Task queue backend:",
251
+ options: [
252
+ { value: "bullmq", label: "BullMQ", hint: "Redis" },
253
+ { value: "pg-boss", label: "pg-boss", hint: "PostgreSQL" }
254
+ ]
255
+ });
256
+ if (p.isCancel(backend)) {
257
+ p.cancel("Cancelled");
258
+ process.exit(0);
259
+ }
260
+ taskBackend = backend;
261
+ }
262
+ const opts = {
263
+ projectName,
264
+ language,
265
+ database,
266
+ authStrategy,
267
+ cluster,
268
+ tasks,
269
+ ...taskBackend !== void 0 && { taskBackend }
270
+ };
271
+ const spinner2 = p.spinner();
272
+ spinner2.start("Scaffolding project\u2026");
273
+ try {
274
+ await scaffold(opts);
275
+ spinner2.stop("Project created");
276
+ } catch (err) {
277
+ spinner2.stop("Failed to scaffold project");
278
+ console.error(err);
279
+ process.exit(1);
280
+ }
281
+ spinner2.start("Installing dependencies\u2026");
282
+ await npmInstall(projectName);
283
+ spinner2.stop("Dependencies installed");
284
+ p.outro(
285
+ chalk.green(`
286
+ Your project is ready!
287
+
288
+ `) + chalk.dim(` cd ${projectName}
289
+ `) + chalk.dim(` npm run dev
290
+ `)
291
+ );
292
+ }
293
+ function npmInstall(projectDir) {
294
+ return new Promise((resolve, reject) => {
295
+ const child = spawn("npm", ["install"], {
296
+ cwd: projectDir,
297
+ stdio: "ignore"
298
+ });
299
+ child.on("exit", (code) => code === 0 ? resolve() : reject(new Error(`npm install failed`)));
300
+ });
301
+ }
302
+ main().catch((err) => {
303
+ console.error(err);
304
+ process.exit(1);
305
+ });
306
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/index.ts","../src/scaffold.ts"],"sourcesContent":["import * as p from '@clack/prompts';\nimport chalk from 'chalk';\nimport { spawn } from 'node:child_process';\nimport { scaffold, type ScaffoldOptions } from './scaffold.js';\n\nasync function main(): Promise<void> {\n console.log();\n p.intro(chalk.bgCyan(chalk.black(' create-efc-app ')));\n\n const projectName = await p.text({\n message: 'Project name:',\n placeholder: 'my-api',\n defaultValue: 'my-api',\n validate: (v) => (!v.trim() ? 'Project name is required' : undefined),\n });\n if (p.isCancel(projectName)) { p.cancel('Cancelled'); process.exit(0); }\n\n const language = await p.select({\n message: 'Language:',\n options: [\n { value: 'typescript', label: 'TypeScript', hint: 'recommended' },\n { value: 'javascript', label: 'JavaScript' },\n ],\n });\n if (p.isCancel(language)) { p.cancel('Cancelled'); process.exit(0); }\n\n const database = await p.select({\n message: 'Database:',\n options: [\n { value: 'mongodb', label: 'MongoDB', hint: 'Mongoose' },\n { value: 'postgresql', label: 'PostgreSQL', hint: 'Drizzle ORM' },\n ],\n });\n if (p.isCancel(database)) { p.cancel('Cancelled'); process.exit(0); }\n\n const authStrategy = await p.select({\n message: 'Authentication strategy:',\n options: [\n { value: 'http-only', label: 'http-only', hint: 'secure cookie — recommended for SSR' },\n { value: 'localStorage', label: 'localStorage', hint: 'bearer token — for SPAs' },\n ],\n });\n if (p.isCancel(authStrategy)) { p.cancel('Cancelled'); process.exit(0); }\n\n const cluster = await p.confirm({\n message: 'Enable multi-core clustering?',\n initialValue: true,\n });\n if (p.isCancel(cluster)) { p.cancel('Cancelled'); process.exit(0); }\n\n const tasks = await p.confirm({\n message: 'Enable background tasks?',\n initialValue: true,\n });\n if (p.isCancel(tasks)) { p.cancel('Cancelled'); process.exit(0); }\n\n let taskBackend: ScaffoldOptions['taskBackend'];\n if (tasks) {\n const backend = await p.select({\n message: 'Task queue backend:',\n options: [\n { value: 'bullmq', label: 'BullMQ', hint: 'Redis' },\n { value: 'pg-boss', label: 'pg-boss', hint: 'PostgreSQL' },\n ],\n });\n if (p.isCancel(backend)) { p.cancel('Cancelled'); process.exit(0); }\n taskBackend = backend as ScaffoldOptions['taskBackend'];\n }\n\n const opts: ScaffoldOptions = {\n projectName: projectName as string,\n language: language as ScaffoldOptions['language'],\n database: database as ScaffoldOptions['database'],\n authStrategy: authStrategy as ScaffoldOptions['authStrategy'],\n cluster: cluster as boolean,\n tasks: tasks as boolean,\n ...(taskBackend !== undefined && { taskBackend }),\n };\n\n const spinner = p.spinner();\n spinner.start('Scaffolding project…');\n\n try {\n await scaffold(opts);\n spinner.stop('Project created');\n } catch (err) {\n spinner.stop('Failed to scaffold project');\n console.error(err);\n process.exit(1);\n }\n\n spinner.start('Installing dependencies…');\n await npmInstall(projectName as string);\n spinner.stop('Dependencies installed');\n\n p.outro(\n chalk.green(`\\nYour project is ready!\\n\\n`) +\n chalk.dim(` cd ${projectName as string}\\n`) +\n chalk.dim(` npm run dev\\n`),\n );\n}\n\nfunction npmInstall(projectDir: string): Promise<void> {\n return new Promise((resolve, reject) => {\n const child = spawn('npm', ['install'], {\n cwd: projectDir,\n stdio: 'ignore',\n });\n child.on('exit', (code) => (code === 0 ? resolve() : reject(new Error(`npm install failed`))));\n });\n}\n\nmain().catch((err) => {\n console.error(err);\n process.exit(1);\n});\n","import fs from 'fs-extra';\nimport path from 'node:path';\nimport crypto from 'node:crypto';\n\nexport interface ScaffoldOptions {\n projectName: string;\n language: 'typescript' | 'javascript';\n database: 'mongodb' | 'postgresql';\n authStrategy: 'http-only' | 'localStorage';\n cluster: boolean;\n tasks: boolean;\n taskBackend?: 'bullmq' | 'pg-boss';\n}\n\nexport async function scaffold(opts: ScaffoldOptions): Promise<void> {\n const dest = path.resolve(process.cwd(), opts.projectName);\n await fs.ensureDir(dest);\n\n await writePackageJson(dest, opts);\n await writeTsConfig(dest, opts);\n await writeEfcConfig(dest, opts);\n await writeEntryPoint(dest, opts);\n await writeGitignore(dest);\n await writeEnvFiles(dest);\n await writeExampleRoute(dest, opts);\n if (opts.tasks) await writeExampleTask(dest, opts);\n}\n\nasync function writePackageJson(dest: string, opts: ScaffoldOptions): Promise<void> {\n const deps: Record<string, string> = {\n 'express-file-cluster': '^0.1.0',\n };\n if (opts.database === 'mongodb') deps['mongoose'] = '^8.0.0';\n if (opts.database === 'postgresql') {\n deps['pg'] = '^8.0.0';\n deps['drizzle-orm'] = '^0.33.0';\n }\n if (opts.tasks && opts.taskBackend === 'bullmq') deps['bullmq'] = '^5.0.0';\n if (opts.tasks && opts.taskBackend === 'pg-boss') deps['pg-boss'] = '^10.0.0';\n\n const devDeps: Record<string, string> = {\n vitest: '^2.0.0',\n };\n if (opts.language === 'typescript') {\n devDeps['typescript'] = '^5.5.0';\n devDeps['@types/node'] = '^22.0.0';\n devDeps['@types/express'] = '^4.17.21';\n devDeps['tsup'] = '^8.2.0';\n devDeps['tsx'] = '^4.0.0';\n }\n\n const pkg = {\n name: opts.projectName,\n version: '0.1.0',\n type: 'module',\n scripts: {\n dev: 'efc start dev',\n build: 'efc build prod',\n start: 'efc start prod',\n test: 'efc run tests',\n },\n dependencies: deps,\n devDependencies: devDeps,\n };\n\n await fs.writeJson(path.join(dest, 'package.json'), pkg, { spaces: 2 });\n}\n\nasync function writeTsConfig(dest: string, opts: ScaffoldOptions): Promise<void> {\n if (opts.language !== 'typescript') return;\n const config = {\n compilerOptions: {\n target: 'ES2022',\n module: 'NodeNext',\n moduleResolution: 'NodeNext',\n strict: true,\n esModuleInterop: true,\n skipLibCheck: true,\n outDir: './dist',\n rootDir: './src',\n },\n include: ['src/**/*'],\n exclude: ['node_modules', 'dist'],\n };\n await fs.writeJson(path.join(dest, 'tsconfig.json'), config, { spaces: 2 });\n}\n\nasync function writeEfcConfig(dest: string, opts: ScaffoldOptions): Promise<void> {\n const ext = opts.language === 'typescript' ? 'ts' : 'js';\n const tasks = opts.tasks\n ? `{\n backend: '${opts.taskBackend ?? 'bullmq'}',\n redisUrl: process.env.REDIS_URL,\n concurrency: 5,\n }`\n : 'false';\n\n const content = `import type { EFCConfig } from 'express-file-cluster';\n\nconst config: EFCConfig = {\n port: Number(process.env.PORT) || 3000,\n apiDir: './src/api',\n tasksDir: './src/tasks',\n database: '${opts.database}',\n databaseUrl: process.env.DATABASE_URL!,\n authStrategy: '${opts.authStrategy}',\n jwtSecret: process.env.JWT_SECRET!,\n cluster: process.env.NODE_ENV === 'production',\n tasks: ${tasks},\n globalMiddlewares: [],\n};\n\nexport default config;\n`;\n await fs.outputFile(path.join(dest, `efc.config.${ext}`), content);\n}\n\nasync function writeEntryPoint(dest: string, opts: ScaffoldOptions): Promise<void> {\n const ext = opts.language === 'typescript' ? 'ts' : 'js';\n const content = `import { ignite } from 'express-file-cluster';\nimport { fileURLToPath } from 'url';\nimport path from 'path';\n\nconst __dirname = path.dirname(fileURLToPath(import.meta.url));\n\nignite({\n port: Number(process.env.PORT) || 3000,\n apiDir: path.join(__dirname, 'api'),\n tasksDir: path.join(__dirname, 'tasks'),\n database: '${opts.database}',\n databaseUrl: process.env.DATABASE_URL,\n authStrategy: '${opts.authStrategy}',\n jwtSecret: process.env.JWT_SECRET,\n cluster: process.env.NODE_ENV === 'production',\n}).catch(console.error);\n`;\n await fs.outputFile(path.join(dest, 'src', `index.${ext}`), content);\n}\n\nasync function writeGitignore(dest: string): Promise<void> {\n await fs.outputFile(\n path.join(dest, '.gitignore'),\n 'node_modules/\\ndist/\\n.env\\n.env.local\\n*.log\\n',\n );\n}\n\nasync function writeEnvFiles(dest: string): Promise<void> {\n const secret = crypto.randomBytes(64).toString('hex');\n const dotenv = `PORT=3000\\nNODE_ENV=development\\nDATABASE_URL=\\nJWT_SECRET=${secret}\\nREDIS_URL=redis://localhost:6379\\n`;\n const example = `PORT=3000\\nNODE_ENV=development\\nDATABASE_URL=\\nJWT_SECRET=<generate with: openssl rand -hex 64>\\nREDIS_URL=redis://localhost:6379\\n`;\n await fs.outputFile(path.join(dest, '.env'), dotenv);\n await fs.outputFile(path.join(dest, '.env.example'), example);\n}\n\nasync function writeExampleRoute(dest: string, opts: ScaffoldOptions): Promise<void> {\n const ext = opts.language === 'typescript' ? 'ts' : 'js';\n const content =\n opts.language === 'typescript'\n ? `import type { Request, Response } from 'express';\n\nexport const GET = async (_req: Request, res: Response) => {\n res.json({ status: 'OK', timestamp: new Date().toISOString() });\n};\n`\n : `export const GET = async (_req, res) => {\n res.json({ status: 'OK', timestamp: new Date().toISOString() });\n};\n`;\n await fs.outputFile(path.join(dest, 'src', 'api', `health.${ext}`), content);\n}\n\nasync function writeExampleTask(dest: string, opts: ScaffoldOptions): Promise<void> {\n const ext = opts.language === 'typescript' ? 'ts' : 'js';\n const content =\n opts.language === 'typescript'\n ? `import { defineTask } from 'express-file-cluster/tasks';\n\ninterface SendEmailPayload {\n to: string;\n subject: string;\n body: string;\n}\n\nexport default defineTask<SendEmailPayload>(async (payload) => {\n // TODO: wire up your mailer\n console.log('[SendEmail] Sending to', payload.to);\n});\n`\n : `import { defineTask } from 'express-file-cluster/tasks';\n\nexport default defineTask(async (payload) => {\n console.log('[SendEmail] Sending to', payload.to);\n});\n`;\n await fs.outputFile(path.join(dest, 'src', 'tasks', `SendEmail.${ext}`), content);\n}\n"],"mappings":"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","names":["spinner"]}
package/package.json ADDED
@@ -0,0 +1,29 @@
1
+ {
2
+ "name": "create-efc-app",
3
+ "version": "0.1.0",
4
+ "description": "Interactive scaffolder for Express File Cluster projects",
5
+ "license": "MIT",
6
+ "type": "module",
7
+ "bin": {
8
+ "create-efc-app": "./dist/index.js"
9
+ },
10
+ "scripts": {
11
+ "build": "tsup",
12
+ "dev": "tsup --watch",
13
+ "typecheck": "tsc --noEmit"
14
+ },
15
+ "dependencies": {
16
+ "@clack/prompts": "^0.7.0",
17
+ "chalk": "^5.3.0",
18
+ "fs-extra": "^11.2.0"
19
+ },
20
+ "devDependencies": {
21
+ "@types/fs-extra": "^11.0.4",
22
+ "@types/node": "^22.0.0",
23
+ "tsup": "^8.2.0",
24
+ "typescript": "^5.5.0"
25
+ },
26
+ "engines": {
27
+ "node": ">=18.0.0"
28
+ }
29
+ }
package/src/index.ts ADDED
@@ -0,0 +1,116 @@
1
+ import * as p from '@clack/prompts';
2
+ import chalk from 'chalk';
3
+ import { spawn } from 'node:child_process';
4
+ import { scaffold, type ScaffoldOptions } from './scaffold.js';
5
+
6
+ async function main(): Promise<void> {
7
+ console.log();
8
+ p.intro(chalk.bgCyan(chalk.black(' create-efc-app ')));
9
+
10
+ const projectName = await p.text({
11
+ message: 'Project name:',
12
+ placeholder: 'my-api',
13
+ defaultValue: 'my-api',
14
+ validate: (v) => (!v.trim() ? 'Project name is required' : undefined),
15
+ });
16
+ if (p.isCancel(projectName)) { p.cancel('Cancelled'); process.exit(0); }
17
+
18
+ const language = await p.select({
19
+ message: 'Language:',
20
+ options: [
21
+ { value: 'typescript', label: 'TypeScript', hint: 'recommended' },
22
+ { value: 'javascript', label: 'JavaScript' },
23
+ ],
24
+ });
25
+ if (p.isCancel(language)) { p.cancel('Cancelled'); process.exit(0); }
26
+
27
+ const database = await p.select({
28
+ message: 'Database:',
29
+ options: [
30
+ { value: 'mongodb', label: 'MongoDB', hint: 'Mongoose' },
31
+ { value: 'postgresql', label: 'PostgreSQL', hint: 'Drizzle ORM' },
32
+ ],
33
+ });
34
+ if (p.isCancel(database)) { p.cancel('Cancelled'); process.exit(0); }
35
+
36
+ const authStrategy = await p.select({
37
+ message: 'Authentication strategy:',
38
+ options: [
39
+ { value: 'http-only', label: 'http-only', hint: 'secure cookie — recommended for SSR' },
40
+ { value: 'localStorage', label: 'localStorage', hint: 'bearer token — for SPAs' },
41
+ ],
42
+ });
43
+ if (p.isCancel(authStrategy)) { p.cancel('Cancelled'); process.exit(0); }
44
+
45
+ const cluster = await p.confirm({
46
+ message: 'Enable multi-core clustering?',
47
+ initialValue: true,
48
+ });
49
+ if (p.isCancel(cluster)) { p.cancel('Cancelled'); process.exit(0); }
50
+
51
+ const tasks = await p.confirm({
52
+ message: 'Enable background tasks?',
53
+ initialValue: true,
54
+ });
55
+ if (p.isCancel(tasks)) { p.cancel('Cancelled'); process.exit(0); }
56
+
57
+ let taskBackend: ScaffoldOptions['taskBackend'];
58
+ if (tasks) {
59
+ const backend = await p.select({
60
+ message: 'Task queue backend:',
61
+ options: [
62
+ { value: 'bullmq', label: 'BullMQ', hint: 'Redis' },
63
+ { value: 'pg-boss', label: 'pg-boss', hint: 'PostgreSQL' },
64
+ ],
65
+ });
66
+ if (p.isCancel(backend)) { p.cancel('Cancelled'); process.exit(0); }
67
+ taskBackend = backend as ScaffoldOptions['taskBackend'];
68
+ }
69
+
70
+ const opts: ScaffoldOptions = {
71
+ projectName: projectName as string,
72
+ language: language as ScaffoldOptions['language'],
73
+ database: database as ScaffoldOptions['database'],
74
+ authStrategy: authStrategy as ScaffoldOptions['authStrategy'],
75
+ cluster: cluster as boolean,
76
+ tasks: tasks as boolean,
77
+ ...(taskBackend !== undefined && { taskBackend }),
78
+ };
79
+
80
+ const spinner = p.spinner();
81
+ spinner.start('Scaffolding project…');
82
+
83
+ try {
84
+ await scaffold(opts);
85
+ spinner.stop('Project created');
86
+ } catch (err) {
87
+ spinner.stop('Failed to scaffold project');
88
+ console.error(err);
89
+ process.exit(1);
90
+ }
91
+
92
+ spinner.start('Installing dependencies…');
93
+ await npmInstall(projectName as string);
94
+ spinner.stop('Dependencies installed');
95
+
96
+ p.outro(
97
+ chalk.green(`\nYour project is ready!\n\n`) +
98
+ chalk.dim(` cd ${projectName as string}\n`) +
99
+ chalk.dim(` npm run dev\n`),
100
+ );
101
+ }
102
+
103
+ function npmInstall(projectDir: string): Promise<void> {
104
+ return new Promise((resolve, reject) => {
105
+ const child = spawn('npm', ['install'], {
106
+ cwd: projectDir,
107
+ stdio: 'ignore',
108
+ });
109
+ child.on('exit', (code) => (code === 0 ? resolve() : reject(new Error(`npm install failed`))));
110
+ });
111
+ }
112
+
113
+ main().catch((err) => {
114
+ console.error(err);
115
+ process.exit(1);
116
+ });
@@ -0,0 +1,196 @@
1
+ import fs from 'fs-extra';
2
+ import path from 'node:path';
3
+ import crypto from 'node:crypto';
4
+
5
+ export interface ScaffoldOptions {
6
+ projectName: string;
7
+ language: 'typescript' | 'javascript';
8
+ database: 'mongodb' | 'postgresql';
9
+ authStrategy: 'http-only' | 'localStorage';
10
+ cluster: boolean;
11
+ tasks: boolean;
12
+ taskBackend?: 'bullmq' | 'pg-boss';
13
+ }
14
+
15
+ export async function scaffold(opts: ScaffoldOptions): Promise<void> {
16
+ const dest = path.resolve(process.cwd(), opts.projectName);
17
+ await fs.ensureDir(dest);
18
+
19
+ await writePackageJson(dest, opts);
20
+ await writeTsConfig(dest, opts);
21
+ await writeEfcConfig(dest, opts);
22
+ await writeEntryPoint(dest, opts);
23
+ await writeGitignore(dest);
24
+ await writeEnvFiles(dest);
25
+ await writeExampleRoute(dest, opts);
26
+ if (opts.tasks) await writeExampleTask(dest, opts);
27
+ }
28
+
29
+ async function writePackageJson(dest: string, opts: ScaffoldOptions): Promise<void> {
30
+ const deps: Record<string, string> = {
31
+ 'express-file-cluster': '^0.1.0',
32
+ };
33
+ if (opts.database === 'mongodb') deps['mongoose'] = '^8.0.0';
34
+ if (opts.database === 'postgresql') {
35
+ deps['pg'] = '^8.0.0';
36
+ deps['drizzle-orm'] = '^0.33.0';
37
+ }
38
+ if (opts.tasks && opts.taskBackend === 'bullmq') deps['bullmq'] = '^5.0.0';
39
+ if (opts.tasks && opts.taskBackend === 'pg-boss') deps['pg-boss'] = '^10.0.0';
40
+
41
+ const devDeps: Record<string, string> = {
42
+ vitest: '^2.0.0',
43
+ };
44
+ if (opts.language === 'typescript') {
45
+ devDeps['typescript'] = '^5.5.0';
46
+ devDeps['@types/node'] = '^22.0.0';
47
+ devDeps['@types/express'] = '^4.17.21';
48
+ devDeps['tsup'] = '^8.2.0';
49
+ devDeps['tsx'] = '^4.0.0';
50
+ }
51
+
52
+ const pkg = {
53
+ name: opts.projectName,
54
+ version: '0.1.0',
55
+ type: 'module',
56
+ scripts: {
57
+ dev: 'efc start dev',
58
+ build: 'efc build prod',
59
+ start: 'efc start prod',
60
+ test: 'efc run tests',
61
+ },
62
+ dependencies: deps,
63
+ devDependencies: devDeps,
64
+ };
65
+
66
+ await fs.writeJson(path.join(dest, 'package.json'), pkg, { spaces: 2 });
67
+ }
68
+
69
+ async function writeTsConfig(dest: string, opts: ScaffoldOptions): Promise<void> {
70
+ if (opts.language !== 'typescript') return;
71
+ const config = {
72
+ compilerOptions: {
73
+ target: 'ES2022',
74
+ module: 'NodeNext',
75
+ moduleResolution: 'NodeNext',
76
+ strict: true,
77
+ esModuleInterop: true,
78
+ skipLibCheck: true,
79
+ outDir: './dist',
80
+ rootDir: './src',
81
+ },
82
+ include: ['src/**/*'],
83
+ exclude: ['node_modules', 'dist'],
84
+ };
85
+ await fs.writeJson(path.join(dest, 'tsconfig.json'), config, { spaces: 2 });
86
+ }
87
+
88
+ async function writeEfcConfig(dest: string, opts: ScaffoldOptions): Promise<void> {
89
+ const ext = opts.language === 'typescript' ? 'ts' : 'js';
90
+ const tasks = opts.tasks
91
+ ? `{
92
+ backend: '${opts.taskBackend ?? 'bullmq'}',
93
+ redisUrl: process.env.REDIS_URL,
94
+ concurrency: 5,
95
+ }`
96
+ : 'false';
97
+
98
+ const content = `import type { EFCConfig } from 'express-file-cluster';
99
+
100
+ const config: EFCConfig = {
101
+ port: Number(process.env.PORT) || 3000,
102
+ apiDir: './src/api',
103
+ tasksDir: './src/tasks',
104
+ database: '${opts.database}',
105
+ databaseUrl: process.env.DATABASE_URL!,
106
+ authStrategy: '${opts.authStrategy}',
107
+ jwtSecret: process.env.JWT_SECRET!,
108
+ cluster: process.env.NODE_ENV === 'production',
109
+ tasks: ${tasks},
110
+ globalMiddlewares: [],
111
+ };
112
+
113
+ export default config;
114
+ `;
115
+ await fs.outputFile(path.join(dest, `efc.config.${ext}`), content);
116
+ }
117
+
118
+ async function writeEntryPoint(dest: string, opts: ScaffoldOptions): Promise<void> {
119
+ const ext = opts.language === 'typescript' ? 'ts' : 'js';
120
+ const content = `import { ignite } from 'express-file-cluster';
121
+ import { fileURLToPath } from 'url';
122
+ import path from 'path';
123
+
124
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
125
+
126
+ ignite({
127
+ port: Number(process.env.PORT) || 3000,
128
+ apiDir: path.join(__dirname, 'api'),
129
+ tasksDir: path.join(__dirname, 'tasks'),
130
+ database: '${opts.database}',
131
+ databaseUrl: process.env.DATABASE_URL,
132
+ authStrategy: '${opts.authStrategy}',
133
+ jwtSecret: process.env.JWT_SECRET,
134
+ cluster: process.env.NODE_ENV === 'production',
135
+ }).catch(console.error);
136
+ `;
137
+ await fs.outputFile(path.join(dest, 'src', `index.${ext}`), content);
138
+ }
139
+
140
+ async function writeGitignore(dest: string): Promise<void> {
141
+ await fs.outputFile(
142
+ path.join(dest, '.gitignore'),
143
+ 'node_modules/\ndist/\n.env\n.env.local\n*.log\n',
144
+ );
145
+ }
146
+
147
+ async function writeEnvFiles(dest: string): Promise<void> {
148
+ const secret = crypto.randomBytes(64).toString('hex');
149
+ const dotenv = `PORT=3000\nNODE_ENV=development\nDATABASE_URL=\nJWT_SECRET=${secret}\nREDIS_URL=redis://localhost:6379\n`;
150
+ const example = `PORT=3000\nNODE_ENV=development\nDATABASE_URL=\nJWT_SECRET=<generate with: openssl rand -hex 64>\nREDIS_URL=redis://localhost:6379\n`;
151
+ await fs.outputFile(path.join(dest, '.env'), dotenv);
152
+ await fs.outputFile(path.join(dest, '.env.example'), example);
153
+ }
154
+
155
+ async function writeExampleRoute(dest: string, opts: ScaffoldOptions): Promise<void> {
156
+ const ext = opts.language === 'typescript' ? 'ts' : 'js';
157
+ const content =
158
+ opts.language === 'typescript'
159
+ ? `import type { Request, Response } from 'express';
160
+
161
+ export const GET = async (_req: Request, res: Response) => {
162
+ res.json({ status: 'OK', timestamp: new Date().toISOString() });
163
+ };
164
+ `
165
+ : `export const GET = async (_req, res) => {
166
+ res.json({ status: 'OK', timestamp: new Date().toISOString() });
167
+ };
168
+ `;
169
+ await fs.outputFile(path.join(dest, 'src', 'api', `health.${ext}`), content);
170
+ }
171
+
172
+ async function writeExampleTask(dest: string, opts: ScaffoldOptions): Promise<void> {
173
+ const ext = opts.language === 'typescript' ? 'ts' : 'js';
174
+ const content =
175
+ opts.language === 'typescript'
176
+ ? `import { defineTask } from 'express-file-cluster/tasks';
177
+
178
+ interface SendEmailPayload {
179
+ to: string;
180
+ subject: string;
181
+ body: string;
182
+ }
183
+
184
+ export default defineTask<SendEmailPayload>(async (payload) => {
185
+ // TODO: wire up your mailer
186
+ console.log('[SendEmail] Sending to', payload.to);
187
+ });
188
+ `
189
+ : `import { defineTask } from 'express-file-cluster/tasks';
190
+
191
+ export default defineTask(async (payload) => {
192
+ console.log('[SendEmail] Sending to', payload.to);
193
+ });
194
+ `;
195
+ await fs.outputFile(path.join(dest, 'src', 'tasks', `SendEmail.${ext}`), content);
196
+ }
package/tsconfig.json ADDED
@@ -0,0 +1,9 @@
1
+ {
2
+ "extends": "../../tsconfig.base.json",
3
+ "compilerOptions": {
4
+ "outDir": "./dist",
5
+ "rootDir": "./src"
6
+ },
7
+ "include": ["src/**/*"],
8
+ "exclude": ["node_modules", "dist"]
9
+ }
package/tsup.config.ts ADDED
@@ -0,0 +1,11 @@
1
+ import { defineConfig } from 'tsup';
2
+
3
+ export default defineConfig({
4
+ entry: { index: 'src/index.ts' },
5
+ format: ['esm'],
6
+ dts: false,
7
+ clean: true,
8
+ sourcemap: true,
9
+ target: 'node18',
10
+ banner: { js: '#!/usr/bin/env node' },
11
+ });