create-ncf 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.
Files changed (44) hide show
  1. package/README.md +145 -0
  2. package/dist/index.js +813 -0
  3. package/dist/index.js.map +1 -0
  4. package/package.json +28 -0
  5. package/templates/auth/src/app/(auth)/sign-in/page.tsx +99 -0
  6. package/templates/auth/src/app/(auth)/sign-up/page.tsx +118 -0
  7. package/templates/auth/src/app/api/auth/[...all]/route.ts +11 -0
  8. package/templates/auth/src/middleware.ts +74 -0
  9. package/templates/auth/src/server/auth/auth-client.ts +8 -0
  10. package/templates/auth/src/server/auth/auth.ts +77 -0
  11. package/templates/auth/src/server/db/schema/auth.ts +115 -0
  12. package/templates/base/biome.jsonc +68 -0
  13. package/templates/base/open-next.config.ts +3 -0
  14. package/templates/base/postcss.config.mjs +5 -0
  15. package/templates/base/src/app/layout.tsx +30 -0
  16. package/templates/base/src/app/page.tsx +24 -0
  17. package/templates/base/src/app/robots.ts +13 -0
  18. package/templates/base/src/app/sitemap.ts +12 -0
  19. package/templates/base/src/lib/utils.ts +6 -0
  20. package/templates/base/src/middleware.ts +9 -0
  21. package/templates/base/src/styles/globals.css +121 -0
  22. package/templates/base/tsconfig.json +37 -0
  23. package/templates/drizzle/drizzle.config.ts +7 -0
  24. package/templates/drizzle/migrations/.gitkeep +0 -0
  25. package/templates/drizzle/src/server/db/index.ts +9 -0
  26. package/templates/drizzle/src/server/db/schema/example.ts +15 -0
  27. package/templates/drizzle/src/server/db/schema/index.ts +1 -0
  28. package/templates/image-loader/src/lib/image-loader.ts +34 -0
  29. package/templates/posthog/instrumentation-client.ts +15 -0
  30. package/templates/queues/src/server/queues/handler.ts +43 -0
  31. package/templates/r2/src/server/services/storage.ts +53 -0
  32. package/templates/shadcn/components.json +22 -0
  33. package/templates/shadcn/src/components/ui/button.tsx +59 -0
  34. package/templates/shadcn/src/components/ui/card.tsx +92 -0
  35. package/templates/shadcn/src/components/ui/input.tsx +21 -0
  36. package/templates/shadcn/src/components/ui/skeleton.tsx +13 -0
  37. package/templates/shadcn/src/components/ui/sonner.tsx +28 -0
  38. package/templates/trpc/src/app/api/trpc/[trpc]/route.ts +29 -0
  39. package/templates/trpc/src/server/api/root.ts +10 -0
  40. package/templates/trpc/src/server/api/routes/example.ts +12 -0
  41. package/templates/trpc/src/server/api/trpc.ts +45 -0
  42. package/templates/trpc/src/server/api/trpc.with-auth.ts +67 -0
  43. package/templates/trpc/src/trpc/query-client.ts +23 -0
  44. package/templates/trpc/src/trpc/react.tsx +65 -0
package/dist/index.js ADDED
@@ -0,0 +1,813 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/cli.ts
4
+ import * as p2 from "@clack/prompts";
5
+ import { execSync } from "child_process";
6
+ import fs3 from "fs-extra";
7
+ import path3 from "path";
8
+ import pc2 from "picocolors";
9
+
10
+ // src/prompts.ts
11
+ import * as p from "@clack/prompts";
12
+ import pc from "picocolors";
13
+
14
+ // src/types.ts
15
+ var FEATURE_LABELS = {
16
+ trpc: {
17
+ label: "tRPC",
18
+ hint: "Type-safe API with React Query"
19
+ },
20
+ drizzle: {
21
+ label: "Drizzle ORM + D1",
22
+ hint: "Database with SQLite on Cloudflare"
23
+ },
24
+ auth: {
25
+ label: "better-auth",
26
+ hint: "Authentication with sign-in/sign-up pages"
27
+ },
28
+ r2: {
29
+ label: "Cloudflare R2",
30
+ hint: "Object storage helper"
31
+ },
32
+ queues: {
33
+ label: "Cloudflare Queues",
34
+ hint: "Background job processing with Worker consumer"
35
+ },
36
+ imageLoader: {
37
+ label: "Cloudflare Image Loader",
38
+ hint: "Custom Next.js image loader for CF Image Transformations"
39
+ },
40
+ posthog: {
41
+ label: "PostHog Analytics",
42
+ hint: "Product analytics"
43
+ },
44
+ shadcn: {
45
+ label: "shadcn/ui",
46
+ hint: "UI component library"
47
+ }
48
+ };
49
+
50
+ // src/prompts.ts
51
+ async function runPrompts(cliProjectName) {
52
+ p.intro(pc.bgCyan(pc.black(" create-ncf ")));
53
+ const projectName = cliProjectName ?? await p.text({
54
+ message: "Where should we create your project?",
55
+ placeholder: "./my-app",
56
+ defaultValue: "./my-app",
57
+ validate(value) {
58
+ if (!value.trim()) return "Please enter a project name.";
59
+ }
60
+ });
61
+ if (p.isCancel(projectName)) {
62
+ p.cancel("Operation cancelled.");
63
+ process.exit(0);
64
+ }
65
+ const selectedFeatures = await p.multiselect({
66
+ message: "Which features would you like to include?",
67
+ options: [
68
+ {
69
+ value: "trpc",
70
+ label: FEATURE_LABELS.trpc.label,
71
+ hint: FEATURE_LABELS.trpc.hint
72
+ },
73
+ {
74
+ value: "drizzle",
75
+ label: FEATURE_LABELS.drizzle.label,
76
+ hint: FEATURE_LABELS.drizzle.hint
77
+ },
78
+ {
79
+ value: "auth",
80
+ label: FEATURE_LABELS.auth.label,
81
+ hint: FEATURE_LABELS.auth.hint
82
+ },
83
+ {
84
+ value: "r2",
85
+ label: FEATURE_LABELS.r2.label,
86
+ hint: FEATURE_LABELS.r2.hint
87
+ },
88
+ {
89
+ value: "queues",
90
+ label: FEATURE_LABELS.queues.label,
91
+ hint: FEATURE_LABELS.queues.hint
92
+ },
93
+ {
94
+ value: "imageLoader",
95
+ label: FEATURE_LABELS.imageLoader.label,
96
+ hint: FEATURE_LABELS.imageLoader.hint
97
+ },
98
+ {
99
+ value: "posthog",
100
+ label: FEATURE_LABELS.posthog.label,
101
+ hint: FEATURE_LABELS.posthog.hint
102
+ },
103
+ {
104
+ value: "shadcn",
105
+ label: FEATURE_LABELS.shadcn.label,
106
+ hint: FEATURE_LABELS.shadcn.hint
107
+ }
108
+ ],
109
+ initialValues: [
110
+ "trpc",
111
+ "drizzle",
112
+ "auth",
113
+ "shadcn"
114
+ ],
115
+ required: false
116
+ });
117
+ if (p.isCancel(selectedFeatures)) {
118
+ p.cancel("Operation cancelled.");
119
+ process.exit(0);
120
+ }
121
+ const features = [...selectedFeatures];
122
+ if (features.includes("auth") && !features.includes("drizzle")) {
123
+ features.push("drizzle");
124
+ p.log.info("better-auth requires Drizzle + D1. It has been automatically included.");
125
+ }
126
+ const featureList = features.length > 0 ? features.map((f) => FEATURE_LABELS[f].label).join(", ") : "None (base only)";
127
+ p.log.info(`Project: ${pc.cyan(projectName)}`);
128
+ p.log.info(`Features: ${pc.cyan(featureList)}`);
129
+ const confirmed = await p.confirm({
130
+ message: "Continue with these settings?"
131
+ });
132
+ if (p.isCancel(confirmed) || !confirmed) {
133
+ p.cancel("Operation cancelled.");
134
+ process.exit(0);
135
+ }
136
+ return {
137
+ projectName: projectName.replace(/^\.\//, ""),
138
+ features
139
+ };
140
+ }
141
+
142
+ // src/scaffolder.ts
143
+ import fs2 from "fs-extra";
144
+ import path2 from "path";
145
+
146
+ // src/utils.ts
147
+ import fs from "fs-extra";
148
+ import path from "path";
149
+ function detectPackageManager() {
150
+ const userAgent = process.env.npm_config_user_agent ?? "";
151
+ if (userAgent.startsWith("bun")) return "bun";
152
+ if (userAgent.startsWith("pnpm")) return "pnpm";
153
+ if (userAgent.startsWith("yarn")) return "yarn";
154
+ return "npm";
155
+ }
156
+ function getRunCommand(pm) {
157
+ return pm === "npm" ? "npm run" : `${pm} run`;
158
+ }
159
+ function getInstallCommand(pm) {
160
+ if (pm === "yarn") return "yarn";
161
+ return `${pm} install`;
162
+ }
163
+ async function copyDirectory(src, dest, filter) {
164
+ await fs.copy(src, dest, {
165
+ overwrite: true,
166
+ filter: (srcPath) => {
167
+ if (path.basename(srcPath) === "_dependencies.json") return false;
168
+ if (filter) return filter(srcPath);
169
+ return true;
170
+ }
171
+ });
172
+ }
173
+ function hasFeature(features, feature) {
174
+ return features.includes(feature);
175
+ }
176
+ function getTemplatesDir() {
177
+ return path.resolve(new URL(".", import.meta.url).pathname, "..", "templates");
178
+ }
179
+
180
+ // src/builders/cloudflare-env.ts
181
+ function buildCloudflareEnvDts(features) {
182
+ const bindings = [" ASSETS: Fetcher;"];
183
+ if (hasFeature(features, "drizzle")) {
184
+ bindings.push(" DB: D1Database;");
185
+ }
186
+ if (hasFeature(features, "auth")) {
187
+ bindings.push(" KV: KVNamespace;");
188
+ }
189
+ if (hasFeature(features, "r2")) {
190
+ bindings.push(" STORAGE: R2Bucket;");
191
+ }
192
+ if (hasFeature(features, "queues")) {
193
+ bindings.push(" QUEUE: Queue;");
194
+ }
195
+ return `// Generated by create-ncf. Run \`wrangler types\` to regenerate with full Cloudflare types.
196
+ /// <reference types="@cloudflare/workers-types" />
197
+
198
+ interface CloudflareEnv {
199
+ ${bindings.join("\n")}
200
+ }
201
+ `;
202
+ }
203
+ function buildLibEnvDts(features) {
204
+ const entries = [];
205
+ if (hasFeature(features, "drizzle")) {
206
+ entries.push(" DB: D1Database;");
207
+ }
208
+ if (hasFeature(features, "auth")) {
209
+ entries.push(" KV: KVNamespace;");
210
+ }
211
+ if (hasFeature(features, "r2")) {
212
+ entries.push(" STORAGE: R2Bucket;");
213
+ }
214
+ if (hasFeature(features, "queues")) {
215
+ entries.push(" QUEUE: Queue;");
216
+ }
217
+ if (entries.length === 0) {
218
+ return `declare namespace NodeJS {
219
+ interface ProcessEnv {
220
+ [key: string]: string | undefined;
221
+ }
222
+ }
223
+ `;
224
+ }
225
+ return `declare namespace NodeJS {
226
+ interface ProcessEnv {
227
+ ${entries.join("\n")}
228
+ [key: string]: string | undefined;
229
+ }
230
+ }
231
+ `;
232
+ }
233
+
234
+ // src/builders/env.ts
235
+ function buildEnvJs(features) {
236
+ const serverVars = [
237
+ " NODE_ENV: z",
238
+ ' .enum(["development", "test", "production"])',
239
+ ' .default("development"),',
240
+ " SITE_URL: z.string().url(),"
241
+ ];
242
+ const clientVars = [];
243
+ const runtimeEnvServer = [
244
+ " NODE_ENV: process.env.NODE_ENV,",
245
+ " SITE_URL: process.env.SITE_URL,"
246
+ ];
247
+ const runtimeEnvClient = [];
248
+ if (hasFeature(features, "auth")) {
249
+ serverVars.push(
250
+ " BETTER_AUTH_SECRET: z.string().min(1),",
251
+ " BETTER_AUTH_URL: z.string().url(),"
252
+ );
253
+ runtimeEnvServer.push(
254
+ " BETTER_AUTH_SECRET: process.env.BETTER_AUTH_SECRET,",
255
+ " BETTER_AUTH_URL: process.env.BETTER_AUTH_URL,"
256
+ );
257
+ }
258
+ if (hasFeature(features, "r2")) {
259
+ clientVars.push(" NEXT_PUBLIC_R2_DOMAIN: z.string().url().optional(),");
260
+ runtimeEnvClient.push(
261
+ " NEXT_PUBLIC_R2_DOMAIN: process.env.NEXT_PUBLIC_R2_DOMAIN,"
262
+ );
263
+ }
264
+ if (hasFeature(features, "imageLoader")) {
265
+ clientVars.push(
266
+ " NEXT_PUBLIC_CDN_DOMAIN: z.string().url().optional(),"
267
+ );
268
+ runtimeEnvClient.push(
269
+ " NEXT_PUBLIC_CDN_DOMAIN: process.env.NEXT_PUBLIC_CDN_DOMAIN,"
270
+ );
271
+ }
272
+ if (hasFeature(features, "posthog")) {
273
+ clientVars.push(
274
+ " NEXT_PUBLIC_POSTHOG_KEY: z.string().min(1).optional(),",
275
+ " NEXT_PUBLIC_POSTHOG_HOST: z.string().url().optional(),"
276
+ );
277
+ runtimeEnvClient.push(
278
+ " NEXT_PUBLIC_POSTHOG_KEY: process.env.NEXT_PUBLIC_POSTHOG_KEY,",
279
+ " NEXT_PUBLIC_POSTHOG_HOST: process.env.NEXT_PUBLIC_POSTHOG_HOST,"
280
+ );
281
+ }
282
+ const serverSection = serverVars.join("\n");
283
+ const clientSection = clientVars.length > 0 ? clientVars.join("\n") : " // Add NEXT_PUBLIC_ client vars here";
284
+ const runtimeSection = [
285
+ ...runtimeEnvServer,
286
+ ...runtimeEnvClient
287
+ ].join("\n");
288
+ return `import { createEnv } from "@t3-oss/env-nextjs";
289
+ import { z } from "zod";
290
+
291
+ export const env = createEnv({
292
+ server: {
293
+ ${serverSection}
294
+ },
295
+ client: {
296
+ ${clientSection}
297
+ },
298
+ runtimeEnv: {
299
+ ${runtimeSection}
300
+ },
301
+ skipValidation: !!process.env.SKIP_ENV_VALIDATION,
302
+ emptyStringAsUndefined: true,
303
+ });
304
+ `;
305
+ }
306
+ function buildEnvExample(features) {
307
+ const lines = [
308
+ "NODE_ENV=development",
309
+ "SITE_URL=http://localhost:3000",
310
+ ""
311
+ ];
312
+ if (hasFeature(features, "auth")) {
313
+ lines.push(
314
+ "# Authentication (better-auth)",
315
+ "BETTER_AUTH_SECRET=replace-with-a-secure-random-string",
316
+ "BETTER_AUTH_URL=http://localhost:3000",
317
+ ""
318
+ );
319
+ }
320
+ if (hasFeature(features, "r2")) {
321
+ lines.push(
322
+ "# Cloudflare R2 Storage",
323
+ "NEXT_PUBLIC_R2_DOMAIN=https://your-r2-domain.com",
324
+ ""
325
+ );
326
+ }
327
+ if (hasFeature(features, "imageLoader")) {
328
+ lines.push(
329
+ "# Cloudflare Image Loader",
330
+ "NEXT_PUBLIC_CDN_DOMAIN=https://your-cdn-domain.com",
331
+ ""
332
+ );
333
+ }
334
+ if (hasFeature(features, "posthog")) {
335
+ lines.push(
336
+ "# PostHog Analytics",
337
+ "NEXT_PUBLIC_POSTHOG_KEY=your-posthog-project-api-key",
338
+ "NEXT_PUBLIC_POSTHOG_HOST=https://us.i.posthog.com",
339
+ ""
340
+ );
341
+ }
342
+ return lines.join("\n");
343
+ }
344
+
345
+ // src/builders/next-config.ts
346
+ function buildNextConfig(features) {
347
+ const imports = [
348
+ 'import { initOpenNextCloudflareForDev } from "@opennextjs/cloudflare";',
349
+ 'import type { NextConfig } from "next";'
350
+ ];
351
+ const configParts = [];
352
+ if (hasFeature(features, "imageLoader")) {
353
+ configParts.push(` images: {
354
+ loader: "custom",
355
+ loaderFile: "./src/lib/image-loader.ts",
356
+ },`);
357
+ }
358
+ if (hasFeature(features, "posthog")) {
359
+ configParts.push(` async rewrites() {
360
+ return [
361
+ {
362
+ source: "/ingest/static/:path*",
363
+ destination: "https://us-assets.i.posthog.com/static/:path*",
364
+ },
365
+ {
366
+ source: "/ingest/:path*",
367
+ destination: "https://us.i.posthog.com/:path*",
368
+ },
369
+ ];
370
+ },
371
+ skipTrailingSlashRedirect: true,`);
372
+ }
373
+ const configBody = configParts.length > 0 ? `
374
+ ${configParts.join("\n")}
375
+ ` : "";
376
+ return `${imports.join("\n")}
377
+
378
+ initOpenNextCloudflareForDev();
379
+
380
+ const nextConfig: NextConfig = {${configBody}};
381
+
382
+ export default nextConfig;
383
+ `;
384
+ }
385
+
386
+ // src/builders/package-json.ts
387
+ function buildPackageJson(projectName, features) {
388
+ const pkg = {
389
+ name: projectName,
390
+ version: "0.1.0",
391
+ private: true,
392
+ type: "module",
393
+ scripts: {
394
+ check: "biome check .",
395
+ "check:write": "biome check --write .",
396
+ dev: "next dev --turbopack",
397
+ build: "next build",
398
+ start: "next start",
399
+ deploy: "opennextjs-cloudflare build && opennextjs-cloudflare deploy",
400
+ preview: "opennextjs-cloudflare build && opennextjs-cloudflare preview",
401
+ "cf-typegen": "wrangler types --env-interface CloudflareEnv ./cloudflare-env.d.ts"
402
+ },
403
+ dependencies: {
404
+ "@opennextjs/cloudflare": "^1.3.0",
405
+ "@t3-oss/env-nextjs": "^0.13.8",
406
+ next: "15.4.6",
407
+ react: "19.1.0",
408
+ "react-dom": "19.1.0",
409
+ zod: "^4.1.9",
410
+ clsx: "^2.1.1",
411
+ "tailwind-merge": "^3.5.0"
412
+ },
413
+ devDependencies: {
414
+ "@biomejs/biome": "^2.3.11",
415
+ "@cloudflare/workers-types": "^4.20260116.0",
416
+ "@tailwindcss/postcss": "^4",
417
+ "@types/node": "^24.5.2",
418
+ "@types/react": "^19",
419
+ "@types/react-dom": "^19",
420
+ tailwindcss: "^4",
421
+ typescript: "^5",
422
+ wrangler: "^4.47.0"
423
+ }
424
+ };
425
+ if (hasFeature(features, "trpc")) {
426
+ Object.assign(pkg.dependencies, {
427
+ "@trpc/client": "^11.1.2",
428
+ "@trpc/react-query": "^11.1.2",
429
+ "@trpc/server": "^11.1.2",
430
+ "@tanstack/react-query": "^5.89.0",
431
+ superjson: "^2.2.2"
432
+ });
433
+ }
434
+ if (hasFeature(features, "drizzle")) {
435
+ Object.assign(pkg.dependencies, {
436
+ "drizzle-orm": "^0.44.5"
437
+ });
438
+ Object.assign(pkg.devDependencies, {
439
+ "drizzle-kit": "^0.31.4"
440
+ });
441
+ Object.assign(pkg.scripts, {
442
+ "db:generate": "drizzle-kit generate",
443
+ "db:migrate": `wrangler d1 migrations apply ${projectName}`,
444
+ "db:migrate-remote": `wrangler d1 migrations apply ${projectName} --remote`
445
+ });
446
+ }
447
+ if (hasFeature(features, "auth")) {
448
+ Object.assign(pkg.dependencies, {
449
+ "better-auth": "^1.4.13",
450
+ "better-auth-cloudflare": "^0.2.9"
451
+ });
452
+ }
453
+ if (hasFeature(features, "posthog")) {
454
+ Object.assign(pkg.dependencies, {
455
+ "posthog-js": "^1.352.0",
456
+ "posthog-node": "^5.24.17"
457
+ });
458
+ }
459
+ if (hasFeature(features, "shadcn")) {
460
+ Object.assign(pkg.dependencies, {
461
+ "class-variance-authority": "^0.7.1",
462
+ "lucide-react": "^0.575.0",
463
+ "radix-ui": "^1.4.3",
464
+ sonner: "^2.0.7"
465
+ });
466
+ Object.assign(pkg.devDependencies, {
467
+ "tw-animate-css": "^1.3.8"
468
+ });
469
+ }
470
+ return JSON.stringify(pkg, null, " ");
471
+ }
472
+
473
+ // src/builders/wrangler.ts
474
+ function buildWrangler(projectName, features) {
475
+ const lines = [
476
+ "{",
477
+ ' "$schema": "node_modules/wrangler/config-schema.json",',
478
+ ` "name": "${projectName}",`,
479
+ ' "main": "worker.ts",',
480
+ ' "compatibility_date": "2025-03-01",',
481
+ ' "compatibility_flags": ["nodejs_compat"],',
482
+ ' "assets": {',
483
+ ' "binding": "ASSETS",',
484
+ ' "directory": ".open-next/assets"',
485
+ " },",
486
+ ' "observability": {',
487
+ ' "enabled": true',
488
+ " }"
489
+ ];
490
+ if (hasFeature(features, "drizzle")) {
491
+ lines.push(
492
+ ",",
493
+ ' "d1_databases": [',
494
+ " {",
495
+ ' "binding": "DB",',
496
+ ` "database_name": "${projectName}",`,
497
+ ' "database_id": "YOUR_DATABASE_ID",',
498
+ ' "migrations_dir": "migrations"',
499
+ " }",
500
+ " ]"
501
+ );
502
+ }
503
+ if (hasFeature(features, "auth")) {
504
+ lines.push(
505
+ ",",
506
+ ' "kv_namespaces": [',
507
+ " {",
508
+ ' "binding": "KV",',
509
+ ' "id": "YOUR_KV_NAMESPACE_ID"',
510
+ " }",
511
+ " ]"
512
+ );
513
+ }
514
+ if (hasFeature(features, "r2")) {
515
+ lines.push(
516
+ ",",
517
+ ' "r2_buckets": [',
518
+ " {",
519
+ ' "binding": "STORAGE",',
520
+ ` "bucket_name": "${projectName}"`,
521
+ " }",
522
+ " ]"
523
+ );
524
+ }
525
+ if (hasFeature(features, "queues")) {
526
+ lines.push(
527
+ ",",
528
+ ' "queues": {',
529
+ ' "consumers": [',
530
+ " {",
531
+ ` "queue": "${projectName}",`,
532
+ ' "max_retries": 3,',
533
+ ` "dead_letter_queue": "${projectName}-dlq"`,
534
+ " },",
535
+ " {",
536
+ ` "queue": "${projectName}-dlq"`,
537
+ " }",
538
+ " ],",
539
+ ' "producers": [',
540
+ " {",
541
+ ' "binding": "QUEUE",',
542
+ ` "queue": "${projectName}"`,
543
+ " }",
544
+ " ]",
545
+ " }"
546
+ );
547
+ }
548
+ const vars = {
549
+ SITE_URL: "http://localhost:3000"
550
+ };
551
+ if (hasFeature(features, "auth")) {
552
+ vars.BETTER_AUTH_URL = "http://localhost:3000";
553
+ }
554
+ if (hasFeature(features, "r2")) {
555
+ vars.NEXT_PUBLIC_R2_DOMAIN = "https://your-r2-domain.com";
556
+ }
557
+ if (hasFeature(features, "imageLoader")) {
558
+ vars.NEXT_PUBLIC_CDN_DOMAIN = "https://your-cdn-domain.com";
559
+ }
560
+ if (hasFeature(features, "posthog")) {
561
+ vars.NEXT_PUBLIC_POSTHOG_KEY = "your-posthog-key";
562
+ vars.NEXT_PUBLIC_POSTHOG_HOST = "https://us.i.posthog.com";
563
+ }
564
+ if (Object.keys(vars).length > 0) {
565
+ const varEntries = Object.entries(vars).map(([k, v]) => ` "${k}": "${v}"`).join(",\n");
566
+ lines.push(",", ' "vars": {', varEntries, " }");
567
+ }
568
+ lines.push("}");
569
+ return lines.join("\n");
570
+ }
571
+
572
+ // src/builders/worker.ts
573
+ function buildWorkerTs(features) {
574
+ const lines = [
575
+ "// @ts-ignore \u2014 .open-next is generated by opennextjs-cloudflare build",
576
+ 'import openNextWorker from "./.open-next/worker";',
577
+ "",
578
+ "// Durable Objects exported from the package directly",
579
+ 'export { BucketCachePurge } from "@opennextjs/cloudflare/durable-objects/bucket-cache-purge";',
580
+ 'export { DOQueueHandler } from "@opennextjs/cloudflare/durable-objects/queue";',
581
+ 'export { DOShardedTagCache } from "@opennextjs/cloudflare/durable-objects/sharded-tag-cache";'
582
+ ];
583
+ if (hasFeature(features, "queues")) {
584
+ lines.push(
585
+ "",
586
+ "async function handleQueue(",
587
+ " batch: MessageBatch<unknown>,",
588
+ " env: CloudflareEnv,",
589
+ ") {",
590
+ " // Bridge Cloudflare env vars to process.env so modules importing ~/env work.",
591
+ " for (const [key, value] of Object.entries(env)) {",
592
+ ' if (typeof value === "string") {',
593
+ " process.env[key] = value;",
594
+ " }",
595
+ " }",
596
+ "",
597
+ ' const { handleBatch, handleDlqBatch } = await import("./src/server/queues/handler");',
598
+ "",
599
+ ` if (batch.queue.endsWith("-dlq")) {`,
600
+ " await handleDlqBatch(batch, env);",
601
+ " return;",
602
+ " }",
603
+ "",
604
+ " await handleBatch(batch, env);",
605
+ "}"
606
+ );
607
+ }
608
+ lines.push("", "export default {");
609
+ lines.push(
610
+ " fetch(request: Request, env: CloudflareEnv, ctx: ExecutionContext) {",
611
+ " return openNextWorker.fetch(request, env, ctx);",
612
+ " },"
613
+ );
614
+ if (hasFeature(features, "queues")) {
615
+ lines.push(
616
+ " async queue(batch: MessageBatch<unknown>, env: CloudflareEnv) {",
617
+ " await handleQueue(batch, env);",
618
+ " },"
619
+ );
620
+ }
621
+ lines.push("};");
622
+ return lines.join("\n");
623
+ }
624
+
625
+ // src/scaffolder.ts
626
+ async function scaffold(targetDir, selections) {
627
+ const { projectName, features } = selections;
628
+ const templatesDir = getTemplatesDir();
629
+ await fs2.ensureDir(targetDir);
630
+ await copyDirectory(path2.join(templatesDir, "base"), targetDir);
631
+ const featureDirs = [];
632
+ if (hasFeature(features, "trpc")) {
633
+ featureDirs.push("trpc");
634
+ }
635
+ if (hasFeature(features, "drizzle")) {
636
+ featureDirs.push("drizzle");
637
+ }
638
+ if (hasFeature(features, "auth")) {
639
+ featureDirs.push("auth");
640
+ }
641
+ if (hasFeature(features, "r2")) {
642
+ featureDirs.push("r2");
643
+ }
644
+ if (hasFeature(features, "queues")) {
645
+ featureDirs.push("queues");
646
+ }
647
+ if (hasFeature(features, "imageLoader")) {
648
+ featureDirs.push("image-loader");
649
+ }
650
+ if (hasFeature(features, "posthog")) {
651
+ featureDirs.push("posthog");
652
+ }
653
+ if (hasFeature(features, "shadcn")) {
654
+ featureDirs.push("shadcn");
655
+ }
656
+ for (const dir of featureDirs) {
657
+ const featurePath = path2.join(templatesDir, dir);
658
+ if (await fs2.pathExists(featurePath)) {
659
+ await copyDirectory(featurePath, targetDir);
660
+ }
661
+ }
662
+ if (hasFeature(features, "trpc")) {
663
+ const trpcDir = path2.join(targetDir, "src", "server", "api");
664
+ if (hasFeature(features, "auth") && hasFeature(features, "drizzle")) {
665
+ const withAuthPath = path2.join(trpcDir, "trpc.with-auth.ts");
666
+ const trpcPath = path2.join(trpcDir, "trpc.ts");
667
+ if (await fs2.pathExists(withAuthPath)) {
668
+ await fs2.remove(trpcPath);
669
+ await fs2.rename(withAuthPath, trpcPath);
670
+ }
671
+ } else {
672
+ const withAuthPath = path2.join(trpcDir, "trpc.with-auth.ts");
673
+ if (await fs2.pathExists(withAuthPath)) {
674
+ await fs2.remove(withAuthPath);
675
+ }
676
+ }
677
+ }
678
+ if (hasFeature(features, "drizzle")) {
679
+ const schemaIndexPath = path2.join(
680
+ targetDir,
681
+ "src",
682
+ "server",
683
+ "db",
684
+ "schema",
685
+ "index.ts"
686
+ );
687
+ if (hasFeature(features, "auth")) {
688
+ await fs2.writeFile(
689
+ schemaIndexPath,
690
+ 'export * from "./example";\nexport * from "./auth";\n'
691
+ );
692
+ }
693
+ }
694
+ await fs2.writeFile(
695
+ path2.join(targetDir, "package.json"),
696
+ buildPackageJson(projectName, features)
697
+ );
698
+ await fs2.writeFile(
699
+ path2.join(targetDir, "wrangler.jsonc"),
700
+ buildWrangler(projectName, features)
701
+ );
702
+ await fs2.writeFile(
703
+ path2.join(targetDir, "next.config.ts"),
704
+ buildNextConfig(features)
705
+ );
706
+ await fs2.writeFile(
707
+ path2.join(targetDir, "worker.ts"),
708
+ buildWorkerTs(features)
709
+ );
710
+ await fs2.writeFile(
711
+ path2.join(targetDir, "cloudflare-env.d.ts"),
712
+ buildCloudflareEnvDts(features)
713
+ );
714
+ await fs2.writeFile(
715
+ path2.join(targetDir, "lib.env.d.ts"),
716
+ buildLibEnvDts(features)
717
+ );
718
+ await fs2.writeFile(
719
+ path2.join(targetDir, "src", "env.js"),
720
+ buildEnvJs(features)
721
+ );
722
+ await fs2.writeFile(
723
+ path2.join(targetDir, ".env.example"),
724
+ buildEnvExample(features)
725
+ );
726
+ }
727
+
728
+ // src/cli.ts
729
+ async function cli() {
730
+ const args = process.argv.slice(2);
731
+ const cliProjectName = args[0] && !args[0].startsWith("-") ? args[0] : void 0;
732
+ const selections = await runPrompts(cliProjectName);
733
+ const targetDir = path3.resolve(process.cwd(), selections.projectName);
734
+ if (await fs3.pathExists(targetDir)) {
735
+ const files = await fs3.readdir(targetDir);
736
+ if (files.length > 0) {
737
+ p2.log.error(`Directory ${pc2.cyan(selections.projectName)} is not empty.`);
738
+ process.exit(1);
739
+ }
740
+ }
741
+ const spinner2 = p2.spinner();
742
+ spinner2.start("Creating project structure...");
743
+ await scaffold(targetDir, selections);
744
+ spinner2.stop("Project structure created.");
745
+ const pm = detectPackageManager();
746
+ spinner2.start("Installing dependencies...");
747
+ try {
748
+ execSync(getInstallCommand(pm), {
749
+ cwd: targetDir,
750
+ stdio: "ignore"
751
+ });
752
+ spinner2.stop("Dependencies installed.");
753
+ } catch {
754
+ spinner2.stop("Failed to install dependencies. You can install them manually.");
755
+ }
756
+ spinner2.start("Initializing git repository...");
757
+ try {
758
+ execSync("git init && git add -A && git commit -m 'Initial commit from create-ncf'", {
759
+ cwd: targetDir,
760
+ stdio: "ignore"
761
+ });
762
+ spinner2.stop("Git repository initialized.");
763
+ } catch {
764
+ spinner2.stop("Failed to initialize git. You can do this manually.");
765
+ }
766
+ const runCmd = getRunCommand(pm);
767
+ p2.note(
768
+ [
769
+ `cd ${selections.projectName}`,
770
+ "cp .env.example .env.local",
771
+ "# Edit .env.local with your values",
772
+ `${runCmd} dev`
773
+ ].join("\n"),
774
+ "Next steps"
775
+ );
776
+ const cfSteps = [];
777
+ if (selections.features.includes("drizzle")) {
778
+ cfSteps.push(
779
+ `wrangler d1 create ${selections.projectName}`,
780
+ "# Update wrangler.jsonc with your database_id",
781
+ `${runCmd} db:generate`,
782
+ `${runCmd} db:migrate`
783
+ );
784
+ }
785
+ if (selections.features.includes("r2")) {
786
+ cfSteps.push(
787
+ `wrangler r2 bucket create ${selections.projectName}`,
788
+ "# Update wrangler.jsonc with your bucket name"
789
+ );
790
+ }
791
+ if (selections.features.includes("queues")) {
792
+ cfSteps.push(
793
+ `wrangler queues create ${selections.projectName}`,
794
+ `wrangler queues create ${selections.projectName}-dlq`
795
+ );
796
+ }
797
+ if (cfSteps.length > 0) {
798
+ p2.note(cfSteps.join("\n"), "Cloudflare setup");
799
+ }
800
+ if (selections.features.includes("imageLoader")) {
801
+ p2.log.warn(
802
+ pc2.yellow(
803
+ "Remember to enable Cloudflare Image Transformations on your zone:\nCloudflare Dashboard \u2192 Speed \u2192 Image Transformations \u2192 Enable"
804
+ )
805
+ );
806
+ }
807
+ p2.note(`${runCmd} deploy`, "Deploy");
808
+ p2.outro(pc2.green("Happy building!"));
809
+ }
810
+
811
+ // src/index.ts
812
+ cli().catch(console.error);
813
+ //# sourceMappingURL=index.js.map