create-cfast 0.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.
Files changed (36) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +114 -0
  3. package/dist/index.js +681 -0
  4. package/package.json +37 -0
  5. package/templates/admin/app/admin.server.ts +87 -0
  6. package/templates/admin/app/routes/admin.tsx +18 -0
  7. package/templates/admin/package.json +6 -0
  8. package/templates/auth/app/auth.client.ts +6 -0
  9. package/templates/auth/app/auth.helpers.server.ts +35 -0
  10. package/templates/auth/app/auth.setup.server.ts +17 -0
  11. package/templates/auth/app/db/schema.ts +106 -0
  12. package/templates/auth/app/routes/auth.$.tsx +10 -0
  13. package/templates/auth/app/routes/login.tsx +25 -0
  14. package/templates/auth/package.json +8 -0
  15. package/templates/base/_gitignore +14 -0
  16. package/templates/base/app/entry.server.tsx +38 -0
  17. package/templates/base/app/permissions.ts +32 -0
  18. package/templates/base/app/routes/_index.tsx +8 -0
  19. package/templates/base/package.json +32 -0
  20. package/templates/base/react-router.config.ts +8 -0
  21. package/templates/base/tsconfig.cloudflare.json +26 -0
  22. package/templates/base/tsconfig.json +14 -0
  23. package/templates/base/tsconfig.node.json +13 -0
  24. package/templates/base/workers/app.ts +29 -0
  25. package/templates/db/app/db/client.ts +8 -0
  26. package/templates/db/app/db/schema.ts +9 -0
  27. package/templates/db/drizzle.config.ts +7 -0
  28. package/templates/db/package.json +14 -0
  29. package/templates/email/app/email/send.ts +10 -0
  30. package/templates/email/app/email/templates/welcome.tsx +19 -0
  31. package/templates/email/app/email.server.ts +33 -0
  32. package/templates/email/package.json +6 -0
  33. package/templates/storage/package.json +5 -0
  34. package/templates/ui/app/actions.server.ts +12 -0
  35. package/templates/ui/app/components/Header.tsx +30 -0
  36. package/templates/ui/package.json +10 -0
package/dist/index.js ADDED
@@ -0,0 +1,681 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/index.ts
4
+ import { green as green2, cyan, bold, yellow } from "kolorist";
5
+
6
+ // src/args.ts
7
+ var BOOLEAN_FLAGS = ["auth", "db", "storage", "email", "ui", "admin", "all", "help"];
8
+ function parseArgs(argv) {
9
+ const args = {
10
+ projectName: void 0,
11
+ auth: false,
12
+ db: false,
13
+ storage: false,
14
+ email: false,
15
+ ui: false,
16
+ admin: false,
17
+ all: false,
18
+ help: false
19
+ };
20
+ for (const arg of argv) {
21
+ if (arg.startsWith("--")) {
22
+ const flag = arg.slice(2);
23
+ if (BOOLEAN_FLAGS.includes(flag)) {
24
+ args[flag] = true;
25
+ }
26
+ } else if (!args.projectName) {
27
+ args.projectName = arg;
28
+ }
29
+ }
30
+ return args;
31
+ }
32
+ function printHelp() {
33
+ console.log(`
34
+ Usage: create-cfast [project-name] [options]
35
+
36
+ Options:
37
+ --auth Include @cfast/auth (magic email + passkeys)
38
+ --db Include @cfast/db (D1 + Drizzle ORM)
39
+ --storage Include @cfast/storage (R2 file uploads)
40
+ --email Include @cfast/email (email sending)
41
+ --ui Include @cfast/ui (components + actions)
42
+ --admin Include @cfast/admin (admin panel)
43
+ --all Include all packages
44
+ --help Show this help message
45
+ `);
46
+ }
47
+
48
+ // src/prompts.ts
49
+ import prompts from "prompts";
50
+ import { green } from "kolorist";
51
+
52
+ // src/types.ts
53
+ var FEATURE_NAMES = ["auth", "db", "storage", "email", "ui", "admin"];
54
+
55
+ // src/config.ts
56
+ function resolveFeatureDeps(features) {
57
+ const resolved = { ...features };
58
+ if (resolved.admin) {
59
+ resolved.db = true;
60
+ resolved.ui = true;
61
+ resolved.auth = true;
62
+ }
63
+ if (resolved.auth) {
64
+ resolved.db = true;
65
+ }
66
+ return resolved;
67
+ }
68
+ function getAutoAddedFeatures(original, resolved) {
69
+ const added = [];
70
+ for (const key of Object.keys(resolved)) {
71
+ if (resolved[key] && !original[key]) {
72
+ added.push(key);
73
+ }
74
+ }
75
+ return added;
76
+ }
77
+ function resolveConfig(raw) {
78
+ const features = resolveFeatureDeps(raw.features);
79
+ const uiLibrary = features.ui ? raw.uiLibrary ?? "joy" : null;
80
+ return { ...raw, features, uiLibrary };
81
+ }
82
+
83
+ // src/prompts.ts
84
+ var FEATURE_LABELS = {
85
+ auth: "@cfast/auth \u2014 Authentication (magic email + passkeys)",
86
+ db: "@cfast/db \u2014 D1 database with Drizzle ORM",
87
+ storage: "@cfast/storage \u2014 R2 file uploads",
88
+ email: "@cfast/email \u2014 Email via Mailgun",
89
+ ui: "@cfast/ui \u2014 Permission-aware components + actions",
90
+ admin: "@cfast/admin \u2014 Admin panel"
91
+ };
92
+ async function promptForConfig(args) {
93
+ let projectName = args.projectName;
94
+ if (!projectName) {
95
+ const result = await prompts({
96
+ type: "text",
97
+ name: "projectName",
98
+ message: "Project name:",
99
+ initial: "my-cfast-app"
100
+ });
101
+ if (!result.projectName) return null;
102
+ projectName = result.projectName;
103
+ }
104
+ const hasAnyFeatureFlag = FEATURE_NAMES.some((f) => args[f]) || args.all;
105
+ let selectedFeatures;
106
+ if (args.all) {
107
+ selectedFeatures = [...FEATURE_NAMES];
108
+ } else if (hasAnyFeatureFlag) {
109
+ selectedFeatures = FEATURE_NAMES.filter((f) => args[f]);
110
+ } else {
111
+ const result = await prompts({
112
+ type: "multiselect",
113
+ name: "features",
114
+ message: "Which packages do you need?",
115
+ choices: FEATURE_NAMES.map((name) => ({
116
+ title: FEATURE_LABELS[name],
117
+ value: name
118
+ }))
119
+ });
120
+ if (!result.features) return null;
121
+ selectedFeatures = result.features;
122
+ }
123
+ const features = {
124
+ auth: selectedFeatures.includes("auth"),
125
+ db: selectedFeatures.includes("db"),
126
+ storage: selectedFeatures.includes("storage"),
127
+ email: selectedFeatures.includes("email"),
128
+ ui: selectedFeatures.includes("ui"),
129
+ admin: selectedFeatures.includes("admin")
130
+ };
131
+ const resolved = resolveFeatureDeps(features);
132
+ const autoAdded = getAutoAddedFeatures(features, resolved);
133
+ if (autoAdded.length > 0) {
134
+ console.log(green(` Added automatically: ${autoAdded.join(", ")}`));
135
+ }
136
+ let uiLibrary = null;
137
+ if (resolved.ui) {
138
+ if (hasAnyFeatureFlag) {
139
+ uiLibrary = "joy";
140
+ } else {
141
+ const result = await prompts({
142
+ type: "select",
143
+ name: "uiLibrary",
144
+ message: "UI library:",
145
+ choices: [
146
+ { title: "MUI Joy UI", value: "joy" },
147
+ { title: "Headless (bring your own)", value: "headless" }
148
+ ]
149
+ });
150
+ if (result.uiLibrary === void 0) return null;
151
+ uiLibrary = result.uiLibrary;
152
+ }
153
+ }
154
+ const targetDir = projectName;
155
+ return resolveConfig({
156
+ projectName,
157
+ targetDir,
158
+ features: resolved,
159
+ uiLibrary
160
+ });
161
+ }
162
+
163
+ // src/scaffold.ts
164
+ import fs2 from "fs";
165
+ import path2 from "path";
166
+
167
+ // src/utils.ts
168
+ import fs from "fs";
169
+ import path from "path";
170
+ import { fileURLToPath } from "url";
171
+ var __dirname = path.dirname(fileURLToPath(import.meta.url));
172
+ function getTemplatesDir() {
173
+ return path.resolve(__dirname, "..", "templates");
174
+ }
175
+ function copyDir(src, dest) {
176
+ fs.mkdirSync(dest, { recursive: true });
177
+ for (const entry of fs.readdirSync(src, { withFileTypes: true })) {
178
+ const srcPath = path.join(src, entry.name);
179
+ const destName = entry.name === "_gitignore" ? ".gitignore" : entry.name;
180
+ const destPath = path.join(dest, destName);
181
+ if (entry.isDirectory()) {
182
+ copyDir(srcPath, destPath);
183
+ } else {
184
+ if (entry.name === "package.json" || entry.name === "wrangler.toml") {
185
+ continue;
186
+ }
187
+ fs.copyFileSync(srcPath, destPath);
188
+ }
189
+ }
190
+ }
191
+ function replaceInDir(dir, replacements) {
192
+ for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
193
+ const fullPath = path.join(dir, entry.name);
194
+ if (entry.isDirectory()) {
195
+ replaceInDir(fullPath, replacements);
196
+ } else {
197
+ replaceInFile(fullPath, replacements);
198
+ }
199
+ }
200
+ }
201
+ function replaceInFile(filePath, replacements) {
202
+ const ext = path.extname(filePath);
203
+ const textExts = [".ts", ".tsx", ".js", ".json", ".toml", ".md", ".html", ".css", ""];
204
+ if (!textExts.includes(ext) && !filePath.endsWith(".gitignore")) return;
205
+ let content = fs.readFileSync(filePath, "utf-8");
206
+ for (const [key, value] of Object.entries(replacements)) {
207
+ content = content.replaceAll(`{{${key}}}`, value);
208
+ }
209
+ fs.writeFileSync(filePath, content);
210
+ }
211
+ function readJsonFragment(filePath) {
212
+ if (!fs.existsSync(filePath)) return {};
213
+ return JSON.parse(fs.readFileSync(filePath, "utf-8"));
214
+ }
215
+ function writeFile(filePath, content) {
216
+ fs.mkdirSync(path.dirname(filePath), { recursive: true });
217
+ fs.writeFileSync(filePath, content);
218
+ }
219
+
220
+ // src/generators/package-json.ts
221
+ function mergePackageJsons(base, fragments) {
222
+ const merged = {
223
+ name: base.name,
224
+ private: base.private,
225
+ type: base.type,
226
+ scripts: { ...base.scripts },
227
+ dependencies: { ...base.dependencies },
228
+ devDependencies: { ...base.devDependencies }
229
+ };
230
+ for (const fragment of fragments) {
231
+ if (fragment.dependencies) {
232
+ merged.dependencies = { ...merged.dependencies, ...fragment.dependencies };
233
+ }
234
+ if (fragment.devDependencies) {
235
+ merged.devDependencies = {
236
+ ...merged.devDependencies,
237
+ ...fragment.devDependencies
238
+ };
239
+ }
240
+ if (fragment.scripts) {
241
+ merged.scripts = { ...merged.scripts, ...fragment.scripts };
242
+ }
243
+ }
244
+ if (merged.dependencies) {
245
+ merged.dependencies = sortKeys(merged.dependencies);
246
+ }
247
+ if (merged.devDependencies) {
248
+ merged.devDependencies = sortKeys(merged.devDependencies);
249
+ }
250
+ return merged;
251
+ }
252
+ function sortKeys(obj) {
253
+ return Object.fromEntries(
254
+ Object.entries(obj).sort(([a], [b]) => a.localeCompare(b))
255
+ );
256
+ }
257
+ function stringifyPackageJson(pkg) {
258
+ return JSON.stringify(pkg, null, 2) + "\n";
259
+ }
260
+
261
+ // src/generators/wrangler-toml.ts
262
+ function generateWranglerToml(config) {
263
+ const lines = [];
264
+ lines.push(`name = "${config.projectName}"`);
265
+ lines.push(`compatibility_date = "2025-12-01"`);
266
+ lines.push(`compatibility_flags = ["nodejs_compat"]`);
267
+ lines.push(`main = "./workers/app.ts"`);
268
+ const vars = [
269
+ ["APP_URL", "http://localhost:5173"]
270
+ ];
271
+ if (config.features.email) {
272
+ vars.push(["MAILGUN_DOMAIN", "sandbox.mailgun.org"]);
273
+ }
274
+ lines.push("");
275
+ lines.push("[vars]");
276
+ for (const [key, value] of vars) {
277
+ lines.push(`${key} = "${value}"`);
278
+ }
279
+ if (config.features.db) {
280
+ lines.push("");
281
+ lines.push("[[d1_databases]]");
282
+ lines.push(`binding = "DB"`);
283
+ lines.push(`database_name = "${config.projectName}"`);
284
+ lines.push(`database_id = "local"`);
285
+ lines.push(`migrations_dir = "drizzle"`);
286
+ }
287
+ if (config.features.storage) {
288
+ lines.push("");
289
+ lines.push("[[r2_buckets]]");
290
+ lines.push(`binding = "UPLOADS"`);
291
+ lines.push(`bucket_name = "${config.projectName}-uploads"`);
292
+ }
293
+ if (config.features.auth) {
294
+ lines.push("");
295
+ lines.push("[[kv_namespaces]]");
296
+ lines.push(`binding = "CACHE"`);
297
+ lines.push(`id = "local"`);
298
+ }
299
+ lines.push("");
300
+ return lines.join("\n");
301
+ }
302
+
303
+ // src/generators/env.ts
304
+ function generateEnv(config) {
305
+ const bindings = [];
306
+ bindings.push(
307
+ ` APP_URL: { type: "var" as const, default: "http://localhost:5173" },`
308
+ );
309
+ if (config.features.db) {
310
+ bindings.push(` DB: { type: "d1" as const },`);
311
+ }
312
+ if (config.features.storage) {
313
+ bindings.push(` UPLOADS: { type: "r2" as const },`);
314
+ }
315
+ if (config.features.auth) {
316
+ bindings.push(` CACHE: { type: "kv" as const },`);
317
+ }
318
+ if (config.features.email) {
319
+ bindings.push(` MAILGUN_API_KEY: { type: "secret" as const },`);
320
+ bindings.push(` MAILGUN_DOMAIN: { type: "var" as const },`);
321
+ }
322
+ return `import { defineEnv } from "@cfast/env";
323
+
324
+ export const envSchema = {
325
+ ${bindings.join("\n")}
326
+ };
327
+
328
+ export const env = defineEnv(envSchema);
329
+
330
+ export type Env = ReturnType<typeof env.get>;
331
+ `;
332
+ }
333
+
334
+ // src/generators/cfast-server.ts
335
+ function generateCfastServer(config) {
336
+ const imports = [
337
+ `import { createApp } from "@cfast/core";`,
338
+ `import { envSchema } from "./env";`,
339
+ `import { permissions } from "./permissions";`
340
+ ];
341
+ const pluginDefs = [];
342
+ const useChain = [];
343
+ if (config.features.auth) {
344
+ imports.push(`import { definePlugin } from "@cfast/core";`);
345
+ imports.push(`import { initAuth } from "./auth.setup.server";`);
346
+ imports.push(`import type { AuthUser } from "./permissions";`);
347
+ imports.push(`import type { Grant } from "@cfast/permissions";`);
348
+ pluginDefs.push(`
349
+ const authPlugin = definePlugin({
350
+ name: "auth",
351
+ async setup(ctx) {
352
+ const auth = initAuth({
353
+ d1: ctx.env.DB as D1Database,
354
+ appUrl: ctx.env.APP_URL as string,
355
+ });
356
+ const authCtx = await auth.createContext(ctx.request);
357
+ return {
358
+ user: authCtx.user as AuthUser | null,
359
+ grants: authCtx.grants as Grant[],
360
+ instance: auth,
361
+ };
362
+ },
363
+ });`);
364
+ useChain.push("authPlugin");
365
+ }
366
+ if (config.features.db) {
367
+ imports.push(`import { createDb } from "@cfast/db";`);
368
+ imports.push(`import * as schema from "./db/schema";`);
369
+ if (config.features.auth) {
370
+ pluginDefs.push(`
371
+ type AuthProvides = { auth: { user: AuthUser | null; grants: Grant[] } };
372
+ const dbPlugin = definePlugin<AuthProvides>()({
373
+ name: "db",
374
+ setup(ctx) {
375
+ const client = createDb({
376
+ d1: ctx.env.DB as D1Database,
377
+ schema: schema as unknown as Record<string, unknown>,
378
+ grants: ctx.auth.grants,
379
+ user: ctx.auth.user ? { id: ctx.auth.user.id } : null,
380
+ cache: false,
381
+ });
382
+ return { client };
383
+ },
384
+ });`);
385
+ } else {
386
+ pluginDefs.push(`
387
+ const dbPlugin = definePlugin({
388
+ name: "db",
389
+ setup(ctx) {
390
+ const client = createDb({
391
+ d1: ctx.env.DB as D1Database,
392
+ schema: schema as unknown as Record<string, unknown>,
393
+ grants: [],
394
+ user: null,
395
+ cache: false,
396
+ });
397
+ return { client };
398
+ },
399
+ });`);
400
+ }
401
+ useChain.push("dbPlugin");
402
+ }
403
+ const appLine = useChain.length > 0 ? `export const app = createApp({ env: envSchema, permissions })
404
+ ${useChain.map((p) => ` .use(${p})`).join("\n")};` : `export const app = createApp({ env: envSchema, permissions });`;
405
+ return `${imports.join("\n")}
406
+ ${pluginDefs.join("\n")}
407
+
408
+ ${appLine}
409
+ `;
410
+ }
411
+
412
+ // src/generators/vite-config.ts
413
+ function generateViteConfig(config) {
414
+ const optimizeDeps = [];
415
+ if (config.features.ui && config.uiLibrary === "joy") {
416
+ optimizeDeps.push(`"@cfast/ui/joy"`);
417
+ }
418
+ if (config.features.ui) {
419
+ optimizeDeps.push(`"@cfast/actions/client"`);
420
+ }
421
+ if (config.features.auth) {
422
+ optimizeDeps.push(`"@cfast/auth/client"`);
423
+ }
424
+ const optimizeDepsBlock = optimizeDeps.length > 0 ? `
425
+ optimizeDeps: {
426
+ include: [
427
+ ${optimizeDeps.join(",\n ")},
428
+ ],
429
+ },` : "";
430
+ return `import { reactRouter } from "@react-router/dev/vite";
431
+ import { cloudflare } from "@cloudflare/vite-plugin";
432
+ import { defineConfig } from "vite";
433
+
434
+ export default defineConfig({
435
+ resolve: {
436
+ tsconfigPaths: true,
437
+ },${optimizeDepsBlock}
438
+ plugins: [
439
+ cloudflare({ viteEnvironment: { name: "ssr" } }),
440
+ reactRouter(),
441
+ ],
442
+ });
443
+ `;
444
+ }
445
+
446
+ // src/generators/root-tsx.ts
447
+ function generateRootTsx(config) {
448
+ const imports = [
449
+ `import {`,
450
+ ` isRouteErrorResponse,`,
451
+ ` Links,`,
452
+ ` Meta,`,
453
+ ` Outlet,`,
454
+ ` Scripts,`,
455
+ ` ScrollRestoration,`,
456
+ `} from "react-router";`
457
+ ];
458
+ let beforeChildren = "";
459
+ let afterChildren = "";
460
+ let beforeOutlet = "";
461
+ let afterOutlet = "";
462
+ let pluginSetup = "";
463
+ let errorContent;
464
+ if (config.features.ui && config.uiLibrary === "joy") {
465
+ imports.push(`import { CssVarsProvider } from "@mui/joy/styles";`);
466
+ imports.push(`import CssBaseline from "@mui/joy/CssBaseline";`);
467
+ imports.push(`import Typography from "@mui/joy/Typography";`);
468
+ imports.push(`import Container from "@mui/joy/Container";`);
469
+ imports.push(
470
+ `import { createUIPlugin, UIPluginProvider, ConfirmProvider } from "@cfast/ui";`
471
+ );
472
+ imports.push(`import { ConfirmDialog } from "@cfast/ui/joy";`);
473
+ pluginSetup = `
474
+ const plugin = createUIPlugin({
475
+ components: { confirmDialog: ConfirmDialog },
476
+ });
477
+ `;
478
+ beforeChildren = ` <CssVarsProvider>
479
+ <CssBaseline />
480
+ <UIPluginProvider plugin={plugin}>
481
+ <ConfirmProvider>`;
482
+ afterChildren = ` </ConfirmProvider>
483
+ </UIPluginProvider>
484
+ </CssVarsProvider>`;
485
+ errorContent = ` <Container sx={{ pt: 8, p: 4 }}>
486
+ <Typography level="h1">{message}</Typography>
487
+ <Typography>{details}</Typography>
488
+ {stack && (
489
+ <pre style={{ width: "100%", padding: "16px", overflowX: "auto" }}>
490
+ <code>{stack}</code>
491
+ </pre>
492
+ )}
493
+ </Container>`;
494
+ } else {
495
+ errorContent = ` <div style={{ padding: "2rem" }}>
496
+ <h1>{message}</h1>
497
+ <p>{details}</p>
498
+ {stack && (
499
+ <pre style={{ width: "100%", padding: "16px", overflowX: "auto" }}>
500
+ <code>{stack}</code>
501
+ </pre>
502
+ )}
503
+ </div>`;
504
+ }
505
+ if (config.features.auth) {
506
+ imports.push(`import { AuthClientProvider } from "@cfast/auth/client";`);
507
+ imports.push(`import { authClient } from "~/auth.client";`);
508
+ beforeOutlet = ` <AuthClientProvider authClient={authClient}>`;
509
+ afterOutlet = ` </AuthClientProvider>`;
510
+ }
511
+ const childrenBlock = beforeChildren ? `${beforeChildren}
512
+ {children}
513
+ ${afterChildren}` : ` {children}`;
514
+ const outletBlock = beforeOutlet ? `${beforeOutlet}
515
+ <Outlet />
516
+ ${afterOutlet}` : ` <Outlet />`;
517
+ return `${imports.join("\n")}
518
+ ${pluginSetup}
519
+ export function Layout({ children }: { children: React.ReactNode }) {
520
+ return (
521
+ <html lang="en">
522
+ <head>
523
+ <meta charSet="utf-8" />
524
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
525
+ <Meta />
526
+ <Links />
527
+ </head>
528
+ <body>
529
+ ${childrenBlock}
530
+ <ScrollRestoration />
531
+ <Scripts />
532
+ </body>
533
+ </html>
534
+ );
535
+ }
536
+
537
+ export default function App() {
538
+ return (
539
+ ${outletBlock}
540
+ );
541
+ }
542
+
543
+ export function ErrorBoundary({ error }: { error: unknown }) {
544
+ let message = "Oops!";
545
+ let details = "An unexpected error occurred.";
546
+ let stack: string | undefined;
547
+
548
+ if (isRouteErrorResponse(error)) {
549
+ message = error.status === 404 ? "404" : "Error";
550
+ details =
551
+ error.status === 404
552
+ ? "The requested page could not be found."
553
+ : error.statusText || details;
554
+ } else if (import.meta.env.DEV && error && error instanceof Error) {
555
+ details = error.message;
556
+ stack = error.stack;
557
+ }
558
+
559
+ return (
560
+ ${errorContent}
561
+ );
562
+ }
563
+ `;
564
+ }
565
+
566
+ // src/generators/routes-ts.ts
567
+ function generateRoutesTs(config) {
568
+ const imports = [
569
+ `import { type RouteConfig, index, route } from "@react-router/dev/routes";`
570
+ ];
571
+ const routes = [` index("routes/_index.tsx"),`];
572
+ if (config.features.auth) {
573
+ routes.push(` route("login", "routes/login.tsx"),`);
574
+ routes.push(` route("api/auth/*", "routes/auth.$.tsx"),`);
575
+ }
576
+ if (config.features.admin) {
577
+ routes.push(` route("admin", "routes/admin.tsx"),`);
578
+ }
579
+ return `${imports.join("\n")}
580
+
581
+ export default [
582
+ ${routes.join("\n")}
583
+ ] satisfies RouteConfig;
584
+ `;
585
+ }
586
+
587
+ // src/generators/dev-vars.ts
588
+ function generateDevVars(config) {
589
+ const lines = [];
590
+ if (config.features.email) {
591
+ lines.push(`MAILGUN_API_KEY=test-key`);
592
+ }
593
+ if (lines.length === 0) return null;
594
+ return lines.join("\n") + "\n";
595
+ }
596
+
597
+ // src/scaffold.ts
598
+ function scaffold(config) {
599
+ const templatesDir = getTemplatesDir();
600
+ const targetDir = path2.resolve(process.cwd(), config.targetDir);
601
+ if (fs2.existsSync(targetDir) && fs2.readdirSync(targetDir).length > 0) {
602
+ throw new Error(`Directory "${config.targetDir}" already exists and is not empty.`);
603
+ }
604
+ copyDir(path2.join(templatesDir, "base"), targetDir);
605
+ const overlayOrder = ["db", "auth", "storage", "email", "ui", "admin"];
606
+ const enabledOverlays = overlayOrder.filter((f) => config.features[f]);
607
+ for (const overlay of enabledOverlays) {
608
+ const overlayDir = path2.join(templatesDir, overlay);
609
+ if (fs2.existsSync(overlayDir)) {
610
+ copyDir(overlayDir, targetDir);
611
+ }
612
+ }
613
+ const basePackageJson = readJsonFragment(path2.join(templatesDir, "base", "package.json"));
614
+ const overlayFragments = enabledOverlays.map((overlay) => readJsonFragment(path2.join(templatesDir, overlay, "package.json"))).filter((f) => Object.keys(f).length > 0);
615
+ const mergedPackageJson = mergePackageJsons(
616
+ basePackageJson,
617
+ overlayFragments
618
+ );
619
+ writeFile(path2.join(targetDir, "package.json"), stringifyPackageJson(mergedPackageJson));
620
+ writeFile(path2.join(targetDir, "wrangler.toml"), generateWranglerToml(config));
621
+ writeFile(path2.join(targetDir, "app", "env.ts"), generateEnv(config));
622
+ writeFile(path2.join(targetDir, "app", "cfast.server.ts"), generateCfastServer(config));
623
+ writeFile(path2.join(targetDir, "vite.config.ts"), generateViteConfig(config));
624
+ writeFile(path2.join(targetDir, "app", "root.tsx"), generateRootTsx(config));
625
+ writeFile(path2.join(targetDir, "app", "routes.ts"), generateRoutesTs(config));
626
+ const devVars = generateDevVars(config);
627
+ if (devVars) {
628
+ writeFile(path2.join(targetDir, ".dev.vars"), devVars);
629
+ }
630
+ replaceInDir(targetDir, {
631
+ projectName: config.projectName
632
+ });
633
+ }
634
+
635
+ // src/index.ts
636
+ function printNextSteps(config) {
637
+ console.log();
638
+ console.log(green2(`\u2714 Scaffolded ${bold(config.projectName)}/`));
639
+ console.log();
640
+ console.log(" Next steps:");
641
+ console.log(cyan(` cd ${config.projectName}`));
642
+ console.log(cyan(` pnpm install`));
643
+ console.log(cyan(` pnpm dev`));
644
+ if (config.features.db) {
645
+ console.log();
646
+ console.log(" Database (D1 + Drizzle):");
647
+ console.log(cyan(` pnpm db:generate`));
648
+ console.log(cyan(` pnpm db:migrate:local`));
649
+ }
650
+ if (config.features.email) {
651
+ console.log();
652
+ console.log(" Email:");
653
+ console.log(` ${yellow("Edit .dev.vars to set your MAILGUN_API_KEY")}`);
654
+ }
655
+ console.log();
656
+ console.log(" Deploy:");
657
+ console.log(cyan(` pnpm deploy:staging`));
658
+ console.log(cyan(` pnpm deploy:production`));
659
+ console.log();
660
+ }
661
+ async function main() {
662
+ console.log();
663
+ console.log(bold(" Welcome to cfast!"));
664
+ console.log();
665
+ const args = parseArgs(process.argv.slice(2));
666
+ if (args.help) {
667
+ printHelp();
668
+ return;
669
+ }
670
+ const config = await promptForConfig(args);
671
+ if (!config) {
672
+ console.log("Cancelled.");
673
+ return;
674
+ }
675
+ scaffold(config);
676
+ printNextSteps(config);
677
+ }
678
+ main().catch((err) => {
679
+ console.error(err);
680
+ process.exit(1);
681
+ });
package/package.json ADDED
@@ -0,0 +1,37 @@
1
+ {
2
+ "name": "create-cfast",
3
+ "version": "0.0.1",
4
+ "description": "Scaffold a fully wired Cloudflare Workers + React Router project with cfast",
5
+ "license": "MIT",
6
+ "repository": {
7
+ "type": "git",
8
+ "url": "https://github.com/DanielMSchmidt/cfast.git",
9
+ "directory": "packages/create-cfast"
10
+ },
11
+ "type": "module",
12
+ "bin": {
13
+ "create-cfast": "./dist/index.js"
14
+ },
15
+ "files": [
16
+ "dist",
17
+ "templates"
18
+ ],
19
+ "dependencies": {
20
+ "kolorist": "^1",
21
+ "prompts": "^2"
22
+ },
23
+ "devDependencies": {
24
+ "@types/node": "^22",
25
+ "@types/prompts": "^2",
26
+ "tsup": "^8",
27
+ "typescript": "^5.7",
28
+ "vitest": "^3"
29
+ },
30
+ "scripts": {
31
+ "build": "tsup src/index.ts --format esm",
32
+ "dev": "tsup src/index.ts --format esm --watch",
33
+ "typecheck": "tsc --noEmit",
34
+ "lint": "eslint src/",
35
+ "test": "vitest run"
36
+ }
37
+ }