create-init-mtv-app 1.0.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.
Files changed (2) hide show
  1. package/dist/index.mjs +492 -0
  2. package/package.json +46 -0
package/dist/index.mjs ADDED
@@ -0,0 +1,492 @@
1
+ #!/usr/bin/env node
2
+ import { execa } from "execa";
3
+ import fs from "fs-extra";
4
+ import path from "path";
5
+ import inquirer from "inquirer";
6
+
7
+ //#region src/enum/db_provider.ts
8
+ let DB_PROVIDERS = /* @__PURE__ */ function(DB_PROVIDERS$1) {
9
+ DB_PROVIDERS$1["SUPABASE"] = "Supabase";
10
+ DB_PROVIDERS$1["DOCKER"] = "Docker";
11
+ return DB_PROVIDERS$1;
12
+ }({});
13
+
14
+ //#endregion
15
+ //#region src/enum/db_tools.ts
16
+ let DB_TOOLS = /* @__PURE__ */ function(DB_TOOLS$1) {
17
+ DB_TOOLS$1["DRIZZLE_ORM"] = "Drizzle ORM";
18
+ DB_TOOLS$1["SUPABASE_JS_SDK"] = "Supabase JS SDK";
19
+ return DB_TOOLS$1;
20
+ }({});
21
+
22
+ //#endregion
23
+ //#region src/enum/framework.ts
24
+ let FRAMEWORKS = /* @__PURE__ */ function(FRAMEWORKS$1) {
25
+ FRAMEWORKS$1["NEXT"] = "next";
26
+ FRAMEWORKS$1["REACT"] = "react";
27
+ FRAMEWORKS$1["NEXT_EXPRESS"] = "next-express";
28
+ FRAMEWORKS$1["REACT_EXPRESS"] = "react-express";
29
+ FRAMEWORKS$1["NEXT_NEST"] = "next-nest";
30
+ FRAMEWORKS$1["REACT_NEST"] = "react-nest";
31
+ return FRAMEWORKS$1;
32
+ }({});
33
+
34
+ //#endregion
35
+ //#region src/enum/ui_lib.ts
36
+ let UI_LIB = /* @__PURE__ */ function(UI_LIB$1) {
37
+ UI_LIB$1["SHADCN"] = "shadcn";
38
+ UI_LIB$1["HERO_UI_V3"] = "hero-ui-v3";
39
+ return UI_LIB$1;
40
+ }({});
41
+
42
+ //#endregion
43
+ //#region src/installers/dependencies.ts
44
+ async function installDependencies(projectName, dependencies) {
45
+ await execa("npm", ["install", ...dependencies], {
46
+ cwd: projectName,
47
+ stdio: "inherit"
48
+ });
49
+ }
50
+
51
+ //#endregion
52
+ //#region src/installers/devDependencies.ts
53
+ async function installDevDependencies(projectName, devDependencies) {
54
+ await execa("npm", [
55
+ "install",
56
+ "-D",
57
+ ...devDependencies
58
+ ], {
59
+ cwd: projectName,
60
+ stdio: "inherit"
61
+ });
62
+ }
63
+
64
+ //#endregion
65
+ //#region src/installers/nexjs.ts
66
+ async function createNextApp(projectName) {
67
+ await execa("npx", [
68
+ "create-next-app@latest",
69
+ projectName,
70
+ "--ts",
71
+ "--tailwind",
72
+ "--react-compiler",
73
+ "--biome",
74
+ "--app",
75
+ "--src-dir",
76
+ "--import-alias",
77
+ "@/*"
78
+ ], { stdio: "inherit" });
79
+ }
80
+
81
+ //#endregion
82
+ //#region src/installers/react.ts
83
+ async function createReactApp(projectName) {
84
+ await execa("npm", [
85
+ "create",
86
+ "vite@latest",
87
+ projectName,
88
+ "--",
89
+ "--template",
90
+ "react"
91
+ ], { stdio: "inherit" });
92
+ }
93
+
94
+ //#endregion
95
+ //#region src/installers/expressjs.ts
96
+ async function createNextExpressApp(projectName) {
97
+ await execa("npx", [
98
+ "create-next-app@latest",
99
+ projectName,
100
+ "--use-npm"
101
+ ], { stdio: "inherit" });
102
+ }
103
+
104
+ //#endregion
105
+ //#region src/installers/nestjs.ts
106
+ async function createNestApp(projectName) {
107
+ await execa("npx", [
108
+ "@nestjs/cli",
109
+ "new",
110
+ projectName
111
+ ], { stdio: "inherit" });
112
+ }
113
+
114
+ //#endregion
115
+ //#region src/setups/husky_lint-staged.ts
116
+ async function setupHuskyLintStaged(projectName) {
117
+ const preCommitPath = path.join(projectName, ".husky", "pre-commit");
118
+ const prePushPath = path.join(projectName, ".husky", "pre-push");
119
+ await Promise.all([fs.ensureFile(preCommitPath), fs.ensureFile(prePushPath)]);
120
+ await Promise.all([fs.writeFile(preCommitPath, `npx lint-staged`), fs.writeFile(prePushPath, `npm run build`)]);
121
+ const packageJsonPath = path.join(projectName, "package.json");
122
+ const packageJson = await fs.readJson(packageJsonPath);
123
+ packageJson["lint-staged"] = { "*.{ts,tsx,js,jsx,json,css}": ["npm run format", "npm run lint"] };
124
+ await fs.writeJson(packageJsonPath, packageJson, { spaces: 2 });
125
+ }
126
+
127
+ //#endregion
128
+ //#region src/setups/next_supabase_client.ts
129
+ async function setupNextSupabaseClient(projectDir) {
130
+ const supabaseDir = path.join(projectDir, "src", "lib", "supabase");
131
+ await fs.ensureDir(supabaseDir);
132
+ const supabaseClientPath = path.join(supabaseDir, "client.ts");
133
+ const supabaseServerPath = path.join(supabaseDir, "server.ts");
134
+ const supabaseAdminPath = path.join(supabaseDir, "admin.ts");
135
+ await Promise.all([
136
+ fs.writeFile(supabaseClientPath, supabaseClientTemplate),
137
+ fs.writeFile(supabaseServerPath, supabaseServerTemplate),
138
+ fs.writeFile(supabaseAdminPath, supabaseAdminTemplate)
139
+ ]);
140
+ }
141
+ const supabaseClientTemplate = `import { createBrowserClient } from "@supabase/ssr";
142
+ import { sharedEnv } from "../../../env.shared";
143
+
144
+ export function createClient() {
145
+ return createBrowserClient(sharedEnv.NEXT_PUBLIC_APP_URL, sharedEnv.NEXT_PUBLIC_SUPABASE_ANON_KEY);
146
+ }
147
+ `.trimStart();
148
+ const supabaseServerTemplate = `import "server-only";
149
+ import { createServerClient } from "@supabase/ssr";
150
+ import { cookies } from "next/headers";
151
+ import { sharedEnv } from "../../../env.shared";
152
+
153
+ export async function createClient() {
154
+ const cookieStore = await cookies();
155
+
156
+ return createServerClient(
157
+ sharedEnv.NEXT_PUBLIC_SUPABASE_URL,
158
+ sharedEnv.NEXT_PUBLIC_SUPABASE_ANON_KEY,
159
+ {
160
+ cookies: {
161
+ getAll() {
162
+ return cookieStore.getAll();
163
+ },
164
+ setAll(cookiesToSet) {
165
+ try {
166
+ cookiesToSet.forEach(({ name, value, options }) => {
167
+ cookieStore.set(name, value, options);
168
+ });
169
+ } catch {
170
+ // The 'setAll' method was called from a Server Component.
171
+ // This can be ignored if you have middleware refreshing
172
+ // user sessions.
173
+ }
174
+ },
175
+ },
176
+ },
177
+ );
178
+ }
179
+ `.trimStart();
180
+ const supabaseAdminTemplate = `import "server-only";
181
+ import { createClient } from "@supabase/supabase-js";
182
+ import { serverEnv } from "@/backend/env.server";
183
+ import { sharedEnv } from "../../../env.shared";
184
+ export function createAdminClient() {
185
+ return createClient(
186
+ sharedEnv.NEXT_PUBLIC_SUPABASE_URL,
187
+ serverEnv.SUPABASE_SERVICE_ROLE_KEY,
188
+ {
189
+ auth: {
190
+ autoRefreshToken: false,
191
+ persistSession: false,
192
+ },
193
+ },
194
+ );
195
+ }
196
+ `.trimStart();
197
+
198
+ //#endregion
199
+ //#region src/setups/next_env.ts
200
+ async function setupNextEnv(projectName, isSupabase = false) {
201
+ const sharedEnvPath = path.join(projectName, "env.shared.ts");
202
+ const backendDir = path.join(projectName, "src", "backend");
203
+ const serverEnvPath = path.join(backendDir, "env.server.ts");
204
+ await fs.ensureDir(backendDir);
205
+ await Promise.all([fs.ensureFile(sharedEnvPath), fs.ensureFile(serverEnvPath)]);
206
+ await Promise.all([fs.writeFile(sharedEnvPath, sharedEnvTemplate(isSupabase)), fs.writeFile(serverEnvPath, serverEnvTemplate(isSupabase))]);
207
+ }
208
+ const serverEnvTemplate = (isSupabase) => `import "server-only";
209
+ import { z } from "zod";
210
+
211
+ const envSchema = z.object({
212
+ NODE_ENV: z
213
+ .enum(["development", "production", "test"])
214
+ .default("development"),
215
+ DATABASE_URL: z.string().min(1, "Database URL is required"),
216
+ ${isSupabase ? "SUPABASE_SERVICE_ROLE_KEY: z.string().min(1, \"Supabase service role key is required\")," : ""}
217
+ });
218
+
219
+ const parsed = envSchema.safeParse(process.env);
220
+
221
+ if (!parsed.success) {
222
+ console.error("❌ Invalid environment variables:");
223
+ console.error(JSON.stringify(parsed.error.format(), null, 2));
224
+ throw new Error("Invalid environment variables");
225
+ }
226
+
227
+ export const serverEnv = parsed.data;
228
+ `.trimStart();
229
+ const sharedEnvTemplate = (isSupabase) => `import { z } from "zod";
230
+
231
+ const envSchema = z.object({
232
+ NEXT_PUBLIC_APP_URL: z
233
+ .url("Invalid app URL")
234
+ .default("http://localhost:3000"),
235
+ ${isSupabase ? "NEXT_PUBLIC_SUPABASE_URL: z.url(\"Invalid Supabase URL\")," : "".trim()}
236
+ ${isSupabase ? "NEXT_PUBLIC_SUPABASE_ANON_KEY: z.string().min(1, \"Supabase anon key is required\")," : ""}
237
+ });
238
+
239
+ const parsed = envSchema.safeParse({
240
+ NEXT_PUBLIC_APP_URL: process.env.NEXT_PUBLIC_APP_URL,
241
+ ${isSupabase ? "NEXT_PUBLIC_SUPABASE_URL: process.env.NEXT_PUBLIC_SUPABASE_URL," : ""}
242
+ ${isSupabase ? "NEXT_PUBLIC_SUPABASE_ANON_KEY: process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY," : ""}
243
+ });
244
+
245
+ if (!parsed.success) {
246
+ console.error("❌ Invalid environment variables:");
247
+ console.error(JSON.stringify(parsed.error.format(), null, 2));
248
+ throw new Error("Invalid environment variables");
249
+ }
250
+
251
+ export const sharedEnv = parsed.data;
252
+ `.trimStart();
253
+
254
+ //#endregion
255
+ //#region src/setups/shadcn.ts
256
+ async function setupShadcn(projectName) {
257
+ await execa("npx", [
258
+ "shadcn@latest",
259
+ "init",
260
+ "dialog",
261
+ "avatar",
262
+ "button-group",
263
+ "calendar",
264
+ "card",
265
+ "carousel",
266
+ "chart",
267
+ "checkbox",
268
+ "drawer",
269
+ "dropdown-menu",
270
+ "input-group",
271
+ "label",
272
+ "pagination",
273
+ "select",
274
+ "sheet",
275
+ "skeleton",
276
+ "sonner",
277
+ "spinner",
278
+ "table",
279
+ "textarea"
280
+ ], {
281
+ cwd: projectName,
282
+ stdio: "inherit"
283
+ });
284
+ }
285
+
286
+ //#endregion
287
+ //#region src/setups/biome.ts
288
+ async function setupBiome(projectName, isShadcn) {
289
+ const biomePath = path.join(projectName, "biome.json");
290
+ const biomeConfig = await fs.readJson(biomePath);
291
+ const ignoreFiles = [...biomeConfig.files.includes, "!**/app/globals.css"];
292
+ if (isShadcn) ignoreFiles.push("!**/components/ui");
293
+ biomeConfig.files.includes = ignoreFiles;
294
+ await fs.writeJson(biomePath, biomeConfig, { spaces: 2 });
295
+ }
296
+
297
+ //#endregion
298
+ //#region src/setups/next_supabase_local.ts
299
+ async function setupNextSupabaseLocal(projectDir) {
300
+ execa("npx", ["supabase", "init"], {
301
+ cwd: projectDir,
302
+ stdio: "inherit"
303
+ });
304
+ const migrationsDir = path.join(projectDir, "supabase", "migrations");
305
+ const schemaDir = path.join(projectDir, "supabase", "schemas");
306
+ const dbDir = path.join(schemaDir, "db");
307
+ const storageDir = path.join(schemaDir, "storage");
308
+ await Promise.all([fs.ensureDir(migrationsDir), fs.ensureDir(schemaDir)]);
309
+ await Promise.all([fs.ensureDir(dbDir), fs.ensureDir(storageDir)]);
310
+ }
311
+
312
+ //#endregion
313
+ //#region src/setups/jest.ts
314
+ async function setupJest(projectName) {
315
+ const jestConfigPath = path.join(projectName, "jest.config.ts");
316
+ await fs.ensureFile(jestConfigPath);
317
+ await fs.writeFile(jestConfigPath, jestConfigTemplate);
318
+ const packageJsonPath = path.join(projectName, "package.json");
319
+ const packageJson = await fs.readJson(packageJsonPath);
320
+ packageJson.scripts["test:unit"] = "jest --testPathPatterns=unit";
321
+ packageJson.scripts["test:integration"] = "jest --testPathPatterns=integration";
322
+ await fs.writeJson(packageJsonPath, packageJson, { spaces: 2 });
323
+ const testUnitDir = path.join(projectName, "src", "backend", "test", "unit");
324
+ const testIntegrationDir = path.join(projectName, "src", "backend", "test", "integration");
325
+ await Promise.all([fs.ensureDir(testUnitDir), fs.ensureDir(testIntegrationDir)]);
326
+ }
327
+ const jestConfigTemplate = `import type { Config } from "jest";
328
+
329
+ const config: Config = {
330
+ clearMocks: true,
331
+ coverageProvider: "v8",
332
+ moduleNameMapper: {
333
+ "^@/(.*)$": "<rootDir>/src/$1",
334
+ },
335
+ preset: "ts-jest",
336
+ testEnvironment: "node",
337
+ transform: {
338
+ "^.+\\.tsx?$": [
339
+ "ts-jest",
340
+ {
341
+ tsconfig: "tsconfig.json",
342
+ },
343
+ ],
344
+ },
345
+ };
346
+
347
+ export default config;
348
+ `.trimStart();
349
+
350
+ //#endregion
351
+ //#region src/create.ts
352
+ async function createProject(answers) {
353
+ const { projectName, framework, dbProvider, dbTool, uiLibrary, useMonorepo } = answers;
354
+ switch (framework) {
355
+ case FRAMEWORKS.NEXT: {
356
+ await createNextApp(projectName);
357
+ const isSupabase = dbProvider === DB_PROVIDERS.SUPABASE;
358
+ const dependencies = [
359
+ "react-hook-form",
360
+ "@hookform/resolvers",
361
+ "zod",
362
+ "server-only"
363
+ ];
364
+ const devDependencies = [
365
+ "husky",
366
+ "lint-staged",
367
+ "jest",
368
+ "@types/jest",
369
+ "ts-jest"
370
+ ];
371
+ if (isSupabase) dependencies.push("@supabase/ssr", "@supabase/supabase-js");
372
+ if (dbTool === DB_TOOLS.DRIZZLE_ORM) {
373
+ dependencies.push("drizzle-orm", "postgres");
374
+ devDependencies.push("drizzle-kit");
375
+ }
376
+ await installDependencies(projectName, dependencies);
377
+ await installDevDependencies(projectName, devDependencies);
378
+ await setupHuskyLintStaged(projectName);
379
+ await setupNextEnv(projectName, isSupabase);
380
+ if (isSupabase) await setupNextSupabaseClient(projectName);
381
+ if (dbTool === DB_TOOLS.SUPABASE_JS_SDK) await setupNextSupabaseLocal(projectName);
382
+ if (uiLibrary === UI_LIB.SHADCN) await setupShadcn(projectName);
383
+ await setupBiome(projectName, uiLibrary === UI_LIB.SHADCN);
384
+ await setupJest(projectName);
385
+ break;
386
+ }
387
+ case FRAMEWORKS.NEXT_EXPRESS:
388
+ createNextApp(projectName + "-fe");
389
+ createNextExpressApp(projectName + "-be");
390
+ break;
391
+ case FRAMEWORKS.NEXT_NEST:
392
+ createNextApp(projectName + "-fe");
393
+ createNestApp(projectName + "-be");
394
+ break;
395
+ case FRAMEWORKS.REACT_EXPRESS:
396
+ createReactApp(projectName + "-fe");
397
+ createNextExpressApp(projectName + "-be");
398
+ break;
399
+ case FRAMEWORKS.REACT_NEST:
400
+ createReactApp(projectName + "-fe");
401
+ createNestApp(projectName + "-be");
402
+ break;
403
+ }
404
+ }
405
+
406
+ //#endregion
407
+ //#region src/ask.ts
408
+ const DB_TOOLS_MAP = {
409
+ [DB_PROVIDERS.SUPABASE]: [DB_TOOLS.DRIZZLE_ORM, DB_TOOLS.SUPABASE_JS_SDK],
410
+ [DB_PROVIDERS.DOCKER]: [DB_TOOLS.DRIZZLE_ORM]
411
+ };
412
+ async function ask() {
413
+ return await inquirer.prompt([
414
+ {
415
+ type: "input",
416
+ name: "projectName",
417
+ message: "What is your project name?",
418
+ default: "my-app",
419
+ validate(input) {
420
+ if (!input.trim()) return "Project name cannot be empty";
421
+ if (input.includes(" ")) return "Project name cannot contain spaces";
422
+ if (!/^[a-zA-Z0-9-_]+$/.test(input)) return "Project name can only contain letters, numbers, - and _";
423
+ return true;
424
+ }
425
+ },
426
+ {
427
+ type: "select",
428
+ name: "framework",
429
+ message: "What framework do you want to use?",
430
+ choices: [
431
+ {
432
+ name: "Next.js",
433
+ value: FRAMEWORKS.NEXT
434
+ },
435
+ {
436
+ name: "Next.js + Express",
437
+ value: FRAMEWORKS.NEXT_EXPRESS
438
+ },
439
+ {
440
+ name: "React.js + Express",
441
+ value: FRAMEWORKS.REACT_EXPRESS
442
+ },
443
+ {
444
+ name: "Next.js + NestJS",
445
+ value: FRAMEWORKS.NEXT_NEST
446
+ },
447
+ {
448
+ name: "React.js + NestJS",
449
+ value: FRAMEWORKS.REACT_NEST
450
+ }
451
+ ]
452
+ },
453
+ {
454
+ type: "select",
455
+ name: "dbProvider",
456
+ message: "Which database provider do you want to use?",
457
+ choices: [DB_PROVIDERS.SUPABASE, DB_PROVIDERS.DOCKER]
458
+ },
459
+ {
460
+ type: "select",
461
+ name: "dbTool",
462
+ message: "Which tool do you want to use for database?",
463
+ choices: (answers) => DB_TOOLS_MAP[answers.dbProvider]
464
+ },
465
+ {
466
+ type: "select",
467
+ name: "uiLibrary",
468
+ message: "Which UI library do you want to use?",
469
+ choices: [UI_LIB.SHADCN, UI_LIB.HERO_UI_V3]
470
+ },
471
+ {
472
+ type: "confirm",
473
+ name: "useMonorepo",
474
+ message: "Do you want to use monorepo?",
475
+ default: true,
476
+ when: (answers) => ![FRAMEWORKS.NEXT, FRAMEWORKS.REACT].includes(answers.framework)
477
+ }
478
+ ]);
479
+ }
480
+
481
+ //#endregion
482
+ //#region src/index.ts
483
+ async function main() {
484
+ await createProject(await ask());
485
+ }
486
+ main().catch((error) => {
487
+ console.error("error:", error);
488
+ process.exit(1);
489
+ });
490
+
491
+ //#endregion
492
+ export { };
package/package.json ADDED
@@ -0,0 +1,46 @@
1
+ {
2
+ "name": "create-init-mtv-app",
3
+ "version": "1.0.0",
4
+ "description": "CLI for create init app",
5
+ "keywords": [
6
+ "create-init-app"
7
+ ],
8
+ "homepage": "https://github.com/ThangBui2703/create-init-app#readme",
9
+ "bugs": {
10
+ "url": "https://github.com/ThangBui2703/create-init-app/issues"
11
+ },
12
+ "repository": {
13
+ "type": "git",
14
+ "url": "git+https://github.com/ThangBui2703/create-init-app.git"
15
+ },
16
+ "license": "ISC",
17
+ "author": "init",
18
+ "type": "module",
19
+ "main": "dist/index.mjs",
20
+ "bin": {
21
+ "create-init-app": "dist/index.mjs"
22
+ },
23
+ "files": [
24
+ "dist"
25
+ ],
26
+ "scripts": {
27
+ "test": "echo \"Error: no test specified\" && exit 1",
28
+ "build": "npx tsdown",
29
+ "start": "node dist/index.mjs",
30
+ "prepublishOnly": "npm run build"
31
+ },
32
+ "dependencies": {
33
+ "@types/fs-extra": "^11.0.4",
34
+ "chalk": "^5.6.2",
35
+ "commander": "^14.0.2",
36
+ "execa": "^9.6.1",
37
+ "fs-extra": "^11.3.3",
38
+ "inquirer": "^13.1.0",
39
+ "ora": "^9.0.0"
40
+ },
41
+ "devDependencies": {
42
+ "@types/node": "^25.0.3",
43
+ "tsdown": "^0.18.2",
44
+ "typescript": "^5.9.3"
45
+ }
46
+ }