create-swift-rust 0.7.1 → 0.8.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 (30) hide show
  1. package/dist/index.js +1041 -39
  2. package/dist/index.js.map +1 -1
  3. package/package.json +1 -1
  4. package/templates/default/src/app/about/page.tsx +6 -6
  5. package/templates/default/src/app/about/team/page.tsx +6 -6
  6. package/templates/default/src/app/blog/[slug]/page.tsx +24 -16
  7. package/templates/default/src/app/blog/page.tsx +23 -19
  8. package/templates/default/src/app/blog/tag/[tag]/page.tsx +8 -8
  9. package/templates/default/src/app/contact/page.tsx +8 -8
  10. package/templates/default/src/app/dashboard/analytics/page.tsx +12 -12
  11. package/templates/default/src/app/dashboard/loading.tsx +8 -8
  12. package/templates/default/src/app/dashboard/page.tsx +36 -27
  13. package/templates/default/src/app/dashboard/posts/[id]/not-found.tsx +2 -2
  14. package/templates/default/src/app/dashboard/posts/[id]/page.tsx +5 -5
  15. package/templates/default/src/app/dashboard/posts/new/page.tsx +15 -10
  16. package/templates/default/src/app/dashboard/posts/page.tsx +7 -7
  17. package/templates/default/src/app/dashboard/settings/page.tsx +39 -17
  18. package/templates/default/src/app/error.tsx +2 -2
  19. package/templates/default/src/app/fonts/page.tsx +39 -39
  20. package/templates/default/src/app/images/page.tsx +53 -15
  21. package/templates/default/src/app/layout.tsx +1 -1
  22. package/templates/default/src/app/loading.tsx +5 -5
  23. package/templates/default/src/app/not-found.tsx +2 -2
  24. package/templates/default/src/app/page.tsx +114 -72
  25. package/templates/default/src/app/videos/page.tsx +22 -22
  26. package/templates/default/src/components/font-studio.tsx +37 -37
  27. package/templates/default/src/components/footer.tsx +5 -5
  28. package/templates/default/src/components/logo.tsx +2 -2
  29. package/templates/default/src/components/nav.tsx +28 -10
  30. package/templates/default/src/components/sidebar.tsx +10 -10
package/dist/index.js CHANGED
@@ -5,7 +5,7 @@ import { basename, dirname, join, resolve } from "node:path";
5
5
  import { fileURLToPath } from "node:url";
6
6
  import * as p from "@clack/prompts";
7
7
  import pc from "picocolors";
8
- /** Locate the bundled template dir, both from src/ (dev) and dist/ (published). */
8
+ /** Locate the bundled full-demo template (src/ in dev, dist/ when published). */
9
9
  function templateDir() {
10
10
  const here = dirname(fileURLToPath(import.meta.url));
11
11
  for (const candidate of [
@@ -17,10 +17,16 @@ function templateDir() {
17
17
  }
18
18
  return resolve(here, "..", "templates", "default");
19
19
  }
20
- /**
21
- * Copy the full-demo template into `target`, restoring underscore-prefixed
22
- * dotfiles and substituting the project name placeholder.
23
- */
20
+ async function writeVscodeConfig(target) {
21
+ await mkdir(join(target, ".vscode"), { recursive: true });
22
+ await writeFile(join(target, ".vscode", "settings.json"), `${JSON.stringify({
23
+ "css.lint.unknownAtRules": "ignore",
24
+ "scss.lint.unknownAtRules": "ignore",
25
+ "less.lint.unknownAtRules": "ignore",
26
+ }, null, 2)}\n`);
27
+ await writeFile(join(target, ".vscode", "extensions.json"), `${JSON.stringify({ recommendations: ["bradlc.vscode-tailwindcss", "biomejs.biome"] }, null, 2)}\n`);
28
+ }
29
+ /** Copy the full-demo template into `target` (restores dotfiles, substitutes name). */
24
30
  async function copyTemplate(target, projectName) {
25
31
  const src = templateDir();
26
32
  if (!existsSync(src)) {
@@ -28,18 +34,15 @@ async function copyTemplate(target, projectName) {
28
34
  }
29
35
  await mkdir(target, { recursive: true });
30
36
  await cp(src, target, { recursive: true });
31
- // Restore real dotfile names (npm mangles dotfiles inside packages).
32
- const dotfiles = [
37
+ for (const [from, to] of [
33
38
  ["_gitignore", ".gitignore"],
34
39
  ["_env.example", ".env.example"],
35
40
  ["_env.local", ".env.local"],
36
- ];
37
- for (const [from, to] of dotfiles) {
41
+ ]) {
38
42
  const fromPath = join(target, from);
39
43
  if (existsSync(fromPath))
40
44
  await rename(fromPath, join(target, to));
41
45
  }
42
- // Substitute the project name in package.json + README.
43
46
  const displayName = projectName === "." ? basename(resolve(target)) : projectName;
44
47
  for (const file of ["package.json", "README.md"]) {
45
48
  const path = join(target, file);
@@ -48,6 +51,7 @@ async function copyTemplate(target, projectName) {
48
51
  const contents = await readFile(path, "utf8");
49
52
  await writeFile(path, contents.split("__PROJECT_NAME__").join(displayName));
50
53
  }
54
+ await writeVscodeConfig(target);
51
55
  }
52
56
  async function pathExists(path) {
53
57
  try {
@@ -73,16 +77,33 @@ function normalizeProjectName(name) {
73
77
  return ".";
74
78
  return t.replace(/\/+$/, "");
75
79
  }
80
+ function isValidAlias(alias) {
81
+ return /^[a-zA-Z@][a-zA-Z0-9_/*-]*$/.test(alias);
82
+ }
76
83
  const HELP = `
77
84
  ${pc.bold("create-swift-rust")} - scaffold a new swift-rust project
78
85
 
79
86
  ${pc.bold("Usage")}
80
87
  ${pc.cyan("bun create swift-rust@latest")} [project-name] [options]
81
88
 
82
- Scaffolds the full-demo starter: blog, dashboard, API routes, and the
83
- Image / PDF / Video / Font components — TypeScript, Tailwind, Biome, src/app.
84
-
85
89
  ${pc.bold("Options")}
90
+ ${pc.yellow("--full")} Scaffold the full demo (default; opinionated)
91
+ ${pc.yellow("--minimal")} Scaffold a minimal app (configurable below)
92
+ ${pc.yellow("--ts, --typescript")} Use TypeScript (default)
93
+ ${pc.yellow("--js, --javascript")} Use JavaScript
94
+ ${pc.yellow("--tailwind")} Install Tailwind CSS
95
+ ${pc.yellow("--no-tailwind")} Skip Tailwind CSS
96
+ ${pc.yellow("--shadcn")} Install shadcn-style UI components (default)
97
+ ${pc.yellow("--no-shadcn")} Skip shadcn UI components
98
+ ${pc.yellow("--src-dir")} Use a src/ directory (src/app) (default)
99
+ ${pc.yellow("--no-src-dir")} Use top-level app/
100
+ ${pc.yellow("--biome")} Use Biome for linting (default)
101
+ ${pc.yellow("--eslint")} Use ESLint for linting
102
+ ${pc.yellow("--import-alias <prefix>")} Set import alias (default "@/*")
103
+ ${pc.yellow("--ssr")} Server-side rendering
104
+ ${pc.yellow("--ssr-wasm")} SSR with WASM hydration (default)
105
+ ${pc.yellow("--ssr-htmx")} SSR with HTMX
106
+ ${pc.yellow("--wasm")} Full WASM SPA
86
107
  ${pc.yellow("--install")} Run bun install after scaffolding
87
108
  ${pc.yellow("--no-install")} Skip bun install (default)
88
109
  ${pc.yellow("--yes, -y")} Skip prompts, use defaults
@@ -90,8 +111,8 @@ ${pc.bold("Options")}
90
111
 
91
112
  ${pc.bold("Examples")}
92
113
  ${pc.cyan("bun create swift-rust@latest")}
93
- ${pc.cyan("bun create swift-rust@latest my-app --install")}
94
- ${pc.cyan("bun create swift-rust@latest ./")}
114
+ ${pc.cyan("bun create swift-rust@latest my-app --ts --tailwind --src-dir")}
115
+ ${pc.cyan("bun create swift-rust@latest ./ --tailwind --eslint")}
95
116
  `;
96
117
  function parseFlags(args) {
97
118
  const flags = {};
@@ -103,6 +124,12 @@ function parseFlags(args) {
103
124
  flags.language = "ts";
104
125
  else if (a === "--js" || a === "--javascript")
105
126
  flags.language = "js";
127
+ else if (a === "--full" || a === "--full-demo")
128
+ flags.template = "full";
129
+ else if (a === "--minimal")
130
+ flags.template = "minimal";
131
+ else if (a === "--template")
132
+ flags.template = args[++i];
106
133
  else if (a === "--tailwind")
107
134
  flags.tailwind = true;
108
135
  else if (a === "--no-tailwind")
@@ -144,6 +171,24 @@ function parseFlags(args) {
144
171
  }
145
172
  return flags;
146
173
  }
174
+ const RENDERER_OPTIONS = [
175
+ {
176
+ value: "ssr-wasm",
177
+ label: "SSR + WASM hydration",
178
+ hint: "Default. Server-rendered HTML, WASM-hydrated interactive islands.",
179
+ },
180
+ { value: "ssr", label: "SSR only", hint: "Server-rendered HTML, no client JavaScript at all." },
181
+ {
182
+ value: "ssr-htmx",
183
+ label: "SSR + HTMX",
184
+ hint: "Progressive enhancement with HTMX-style interactions.",
185
+ },
186
+ {
187
+ value: "wasm",
188
+ label: "Full WASM SPA",
189
+ hint: "Single-page app compiled entirely to WebAssembly.",
190
+ },
191
+ ];
147
192
  async function askQuestions(flags, positional) {
148
193
  const yesMode = flags.yes === true;
149
194
  const projectName = positional ??
@@ -165,8 +210,201 @@ async function askQuestions(flags, positional) {
165
210
  p.cancel("Aborted.");
166
211
  return null;
167
212
  }
168
- // The scaffold is the full-demo template (TypeScript + src/ + Tailwind +
169
- // Biome). The only remaining choice is whether to install dependencies.
213
+ // Template selection. The full demo is opinionated (TS + Tailwind + Biome +
214
+ // src/app), so picking it skips the configuration prompts.
215
+ let templateAnswer;
216
+ if (flags.template) {
217
+ templateAnswer = flags.template;
218
+ }
219
+ else if (yesMode) {
220
+ templateAnswer = "full";
221
+ }
222
+ else {
223
+ templateAnswer = (await p.select({
224
+ message: "Which template would you like?",
225
+ options: [
226
+ {
227
+ value: "full",
228
+ label: "Full demo",
229
+ hint: "blog, dashboard, API, Image/PDF/Video/Font — TS + Tailwind + Biome",
230
+ },
231
+ { value: "minimal", label: "Minimal", hint: "a clean starter you configure below" },
232
+ ],
233
+ initialValue: "full",
234
+ }));
235
+ }
236
+ if (p.isCancel(templateAnswer)) {
237
+ p.cancel("Aborted.");
238
+ return null;
239
+ }
240
+ const template = templateAnswer;
241
+ const askInstall = async () => {
242
+ const a = typeof flags.install === "boolean"
243
+ ? flags.install
244
+ : yesMode
245
+ ? false
246
+ : ((await p.confirm({
247
+ message: "Would you like to install dependencies with `bun install`?",
248
+ initialValue: true,
249
+ })) ?? false);
250
+ if (p.isCancel(a)) {
251
+ p.cancel("Aborted.");
252
+ return null;
253
+ }
254
+ return a === true;
255
+ };
256
+ if (template === "full") {
257
+ const install = await askInstall();
258
+ if (install === null)
259
+ return null;
260
+ return {
261
+ projectName: normalizeProjectName(String(projectName)),
262
+ template: "full",
263
+ language: "ts",
264
+ renderer: "ssr-wasm",
265
+ linter: "biome",
266
+ tailwind: true,
267
+ srcDir: true,
268
+ importAlias: "@/*",
269
+ useShadcn: false,
270
+ install,
271
+ };
272
+ }
273
+ let languageAnswer;
274
+ if (flags.language) {
275
+ languageAnswer = flags.language;
276
+ }
277
+ else if (yesMode) {
278
+ languageAnswer = "ts";
279
+ }
280
+ else {
281
+ const languageOptions = [
282
+ { value: "ts", label: "TypeScript", hint: "Type-safe, recommended" },
283
+ { value: "js", label: "JavaScript", hint: "No type checking" },
284
+ ];
285
+ const result = await p.select({
286
+ message: "Which language would you like to use?",
287
+ options: languageOptions,
288
+ initialValue: "ts",
289
+ });
290
+ languageAnswer = result;
291
+ }
292
+ if (p.isCancel(languageAnswer)) {
293
+ p.cancel("Aborted.");
294
+ return null;
295
+ }
296
+ const language = languageAnswer;
297
+ let renderer = flags.renderer ?? "ssr-wasm";
298
+ if (!flags.renderer && !yesMode) {
299
+ const r = (await p.select({
300
+ message: "Which rendering mode do you want?",
301
+ options: RENDERER_OPTIONS,
302
+ initialValue: "ssr-wasm",
303
+ }));
304
+ if (p.isCancel(r)) {
305
+ p.cancel("Aborted.");
306
+ return null;
307
+ }
308
+ renderer = r;
309
+ }
310
+ let linterAnswer;
311
+ if (flags.linter) {
312
+ linterAnswer = flags.linter;
313
+ }
314
+ else if (yesMode) {
315
+ linterAnswer = "biome";
316
+ }
317
+ else {
318
+ const linterOptions = [
319
+ { value: "biome", label: "Biome", hint: "Fast Rust-based all-in-one tool (recommended)" },
320
+ { value: "eslint", label: "ESLint", hint: "The classic, more configurable" },
321
+ ];
322
+ const result = await p.select({
323
+ message: "Which linter would you like to use?",
324
+ options: linterOptions,
325
+ initialValue: "biome",
326
+ });
327
+ linterAnswer = result;
328
+ }
329
+ if (p.isCancel(linterAnswer)) {
330
+ p.cancel("Aborted.");
331
+ return null;
332
+ }
333
+ const linter = linterAnswer;
334
+ const tailwindAnswer = typeof flags.tailwind === "boolean"
335
+ ? flags.tailwind
336
+ : yesMode
337
+ ? false
338
+ : ((await p.confirm({
339
+ message: "Would you like to use Tailwind CSS?",
340
+ initialValue: true,
341
+ })) ?? false);
342
+ if (p.isCancel(tailwindAnswer)) {
343
+ p.cancel("Aborted.");
344
+ return null;
345
+ }
346
+ const tailwind = tailwindAnswer === true;
347
+ const useShadcnAnswer = typeof flags.useShadcn === "boolean"
348
+ ? flags.useShadcn
349
+ : yesMode
350
+ ? true
351
+ : ((await p.confirm({
352
+ message: "Would you like to use shadcn-style UI components?",
353
+ initialValue: true,
354
+ })) ?? false);
355
+ if (p.isCancel(useShadcnAnswer)) {
356
+ p.cancel("Aborted.");
357
+ return null;
358
+ }
359
+ const useShadcn = useShadcnAnswer === true;
360
+ const srcDirAnswer = typeof flags.srcDir === "boolean"
361
+ ? flags.srcDir
362
+ : yesMode
363
+ ? true
364
+ : ((await p.confirm({
365
+ message: "Would you like to use a `src/` directory for your app code?",
366
+ initialValue: true,
367
+ })) ?? true);
368
+ if (p.isCancel(srcDirAnswer)) {
369
+ p.cancel("Aborted.");
370
+ return null;
371
+ }
372
+ const srcDir = srcDirAnswer === true;
373
+ // Import alias: default @/*. Ask a yes/no first (like Next.js) so the common
374
+ // case is a single Enter; only prompt for a value if the user opts in.
375
+ let importAlias = "@/*";
376
+ if (typeof flags.importAlias === "string") {
377
+ importAlias = flags.importAlias;
378
+ }
379
+ else if (!yesMode) {
380
+ const customize = await p.confirm({
381
+ message: "Would you like to customize the import alias (@/* by default)?",
382
+ initialValue: false,
383
+ });
384
+ if (p.isCancel(customize)) {
385
+ p.cancel("Aborted.");
386
+ return null;
387
+ }
388
+ if (customize) {
389
+ const aliasAnswer = await p.text({
390
+ message: "What import alias would you like configured?",
391
+ placeholder: "@/*",
392
+ defaultValue: "@/*",
393
+ validate: (v) => {
394
+ if (!v)
395
+ return undefined; // empty → keep the default
396
+ if (!isValidAlias(v))
397
+ return "Invalid alias. Use letters, numbers, @, /, *, -, _.";
398
+ return undefined;
399
+ },
400
+ });
401
+ if (p.isCancel(aliasAnswer)) {
402
+ p.cancel("Aborted.");
403
+ return null;
404
+ }
405
+ importAlias = aliasAnswer || "@/*";
406
+ }
407
+ }
170
408
  const installAnswer = typeof flags.install === "boolean"
171
409
  ? flags.install
172
410
  : yesMode
@@ -179,24 +417,757 @@ async function askQuestions(flags, positional) {
179
417
  p.cancel("Aborted.");
180
418
  return null;
181
419
  }
420
+ const install = installAnswer === true;
182
421
  return {
183
422
  projectName: normalizeProjectName(String(projectName)),
184
- language: "ts",
185
- renderer: "ssr-wasm",
186
- linter: "biome",
187
- tailwind: true,
188
- srcDir: true,
189
- importAlias: "@/*",
190
- useShadcn: false,
191
- install: installAnswer === true,
423
+ template: "minimal",
424
+ language,
425
+ renderer,
426
+ linter,
427
+ tailwind,
428
+ srcDir,
429
+ importAlias,
430
+ useShadcn,
431
+ install,
192
432
  };
193
433
  }
434
+ async function writeProjectFiles(target, answers) {
435
+ const { projectName: rawName, language, renderer, linter, tailwind, srcDir, importAlias, useShadcn, } = answers;
436
+ // When scaffolding into the current directory ("."), name the package after
437
+ // the directory itself (like create-next-app), since "." is not a valid name.
438
+ const projectName = rawName === "." ? basename(resolve(target)) : rawName;
439
+ const appDir = join(target, srcDir ? "src/app" : "app");
440
+ const componentsDir = join(target, srcDir ? "src/components" : "components");
441
+ const libDir = join(target, srcDir ? "src/lib" : "lib");
442
+ const uiDir = join(componentsDir, "ui");
443
+ await mkdir(target, { recursive: true });
444
+ await mkdir(appDir, { recursive: true });
445
+ await mkdir(componentsDir, { recursive: true });
446
+ await mkdir(libDir, { recursive: true });
447
+ await mkdir(join(target, "public"), { recursive: true });
448
+ if (useShadcn) {
449
+ await mkdir(uiDir, { recursive: true });
450
+ }
451
+ const fileExt = (lang) => (lang === "ts" ? "ts" : "js");
452
+ const componentExt = (lang) => (lang === "ts" ? "tsx" : "jsx");
453
+ const pkg = {
454
+ name: projectName,
455
+ version: "0.1.0",
456
+ private: true,
457
+ type: "module",
458
+ scripts: {
459
+ dev: "swift-rust dev",
460
+ build: "swift-rust build",
461
+ start: "swift-rust start",
462
+ lint: linter === "biome" ? "biome check ." : "eslint .",
463
+ test: "bun test",
464
+ ...(language === "ts" ? { typecheck: "tsc --noEmit" } : {}),
465
+ format: linter === "biome" ? "biome format --write ." : "prettier --write .",
466
+ },
467
+ dependencies: {
468
+ "swift-rust": "^1.0.0",
469
+ react: "^19.0.0",
470
+ "react-dom": "^19.0.0",
471
+ ...(useShadcn
472
+ ? {
473
+ clsx: "^2.1.1",
474
+ "tailwind-merge": "^2.5.5",
475
+ "class-variance-authority": "^0.7.1",
476
+ "lucide-react": "^0.460.0",
477
+ "tw-animate-css": "^1.0.0",
478
+ }
479
+ : {}),
480
+ },
481
+ devDependencies: {
482
+ ...(language === "ts"
483
+ ? { typescript: "^6.0.0", "@types/react": "^19.0.0", "@types/react-dom": "^19.0.0" }
484
+ : {}),
485
+ ...(linter === "biome" ? { "@biomejs/biome": "^2.4.16" } : { eslint: "^9.0.0" }),
486
+ ...(tailwind
487
+ ? { tailwindcss: "^4.0.0", "@tailwindcss/postcss": "^4.0.0", postcss: "^8.4.0" }
488
+ : {}),
489
+ ...(useShadcn ? { shadcn: "^4.0.0" } : {}),
490
+ },
491
+ };
492
+ await writeFile(join(target, "package.json"), `${JSON.stringify(pkg, null, 2)}\n`);
493
+ const config = {
494
+ rendering: renderer,
495
+ image: { domains: [], formats: ["image/avif", "image/webp"] },
496
+ font: { subsets: ["latin"], display: "swap" },
497
+ pdf: { defaultPageSize: "A4" },
498
+ };
499
+ await writeFile(join(target, "swift-rust.config.json"), `${JSON.stringify(config, null, 2)}\n`);
500
+ if (language === "ts") {
501
+ const tsconfig = {
502
+ extends: "swift-rust/tsconfig.base.json",
503
+ compilerOptions: {
504
+ jsx: "preserve",
505
+ paths: { [importAlias]: [srcDir ? "./src/*" : "./*"] },
506
+ },
507
+ include: srcDir
508
+ ? ["src/**/*", "globals.d.ts", ".swift-rust/types/**/*"]
509
+ : ["app/**/*", "components/**/*", "lib/**/*", "globals.d.ts", ".swift-rust/types/**/*"],
510
+ exclude: ["node_modules", "dist", ".swift-rust", ".turbo"],
511
+ };
512
+ await writeFile(join(target, "tsconfig.json"), `${JSON.stringify(tsconfig, null, 2)}\n`);
513
+ }
514
+ else {
515
+ const jsconfig = {
516
+ compilerOptions: {
517
+ paths: { [importAlias]: [srcDir ? "./src/*" : "./*"] },
518
+ },
519
+ include: srcDir ? ["src/**/*"] : ["app/**/*", "components/**/*", "lib/**/*"],
520
+ };
521
+ await writeFile(join(target, "jsconfig.json"), `${JSON.stringify(jsconfig, null, 2)}\n`);
522
+ }
523
+ const gitignore = [
524
+ "node_modules/",
525
+ ".swift-rust/",
526
+ ".vercel/",
527
+ "dist/",
528
+ ".turbo/",
529
+ "target/",
530
+ "*.log",
531
+ ".DS_Store",
532
+ "",
533
+ "# env files — ignore everything except the example",
534
+ ".env",
535
+ ".env.*",
536
+ "!.env.example",
537
+ ].join("\n");
538
+ await writeFile(join(target, ".gitignore"), `${gitignore}\n`);
539
+ const vercelConfig = {
540
+ $schema: "https://openapi.vercel.sh/vercel.json",
541
+ buildCommand: "bun run build",
542
+ installCommand: "bun install --frozen-lockfile",
543
+ outputDirectory: ".vercel/output",
544
+ framework: null,
545
+ trailingSlash: false,
546
+ cleanUrls: true,
547
+ headers: [
548
+ {
549
+ source: "/_swift-rust/static/(.*)",
550
+ headers: [{ key: "Cache-Control", value: "public, max-age=31536000, immutable" }],
551
+ },
552
+ {
553
+ source: "/fonts/(.*)",
554
+ headers: [{ key: "Cache-Control", value: "public, max-age=31536000, immutable" }],
555
+ },
556
+ ],
557
+ };
558
+ await writeFile(join(target, "vercel.json"), `${JSON.stringify(vercelConfig, null, 2)}\n`);
559
+ if (tailwind) {
560
+ const postcss = `const config = {
561
+ plugins: {
562
+ "@tailwindcss/postcss": {},
563
+ },
564
+ };
565
+
566
+ export default config;
567
+ `;
568
+ await writeFile(join(target, "postcss.config.mjs"), postcss);
569
+ const shadcnVars = useShadcn
570
+ ? `
571
+
572
+ /* shadcn design tokens (https://ui.shadcn.com/docs/theming) */
573
+ :root {
574
+ --ui-bg: #ffffff;
575
+ --ui-fg: #09090b;
576
+ --ui-card: #ffffff;
577
+ --ui-card-fg: #09090b;
578
+ --ui-popover: #ffffff;
579
+ --ui-popover-fg: #09090b;
580
+ --ui-primary: #18181b;
581
+ --ui-primary-fg: #fafafa;
582
+ --ui-secondary: #f4f4f5;
583
+ --ui-secondary-fg: #18181b;
584
+ --ui-muted: #f4f4f5;
585
+ --ui-muted-fg: #71717a;
586
+ --ui-accent: #f4f4f5;
587
+ --ui-accent-fg: #18181b;
588
+ --ui-destructive: #ef4444;
589
+ --ui-destructive-fg: #fafafa;
590
+ --ui-border: #e4e4e7;
591
+ --ui-input: #e4e4e7;
592
+ --ui-ring: #18181b;
593
+ --ui-radius: 0.5rem;
594
+ }
595
+
596
+ .dark {
597
+ --ui-bg: #09090b;
598
+ --ui-fg: #fafafa;
599
+ --ui-card: #09090b;
600
+ --ui-card-fg: #fafafa;
601
+ --ui-popover: #09090b;
602
+ --ui-popover-fg: #fafafa;
603
+ --ui-primary: #fafafa;
604
+ --ui-primary-fg: #18181b;
605
+ --ui-secondary: #27272a;
606
+ --ui-secondary-fg: #fafafa;
607
+ --ui-muted: #27272a;
608
+ --ui-muted-fg: #a1a1aa;
609
+ --ui-accent: #27272a;
610
+ --ui-accent-fg: #fafafa;
611
+ --ui-destructive: #7f1d1d;
612
+ --ui-destructive-fg: #fafafa;
613
+ --ui-border: #27272a;
614
+ --ui-input: #27272a;
615
+ --ui-ring: #d4d4d8;
616
+ }
617
+
618
+ @media (prefers-color-scheme: dark) {
619
+ :root:not(.light) {
620
+ --ui-bg: #09090b;
621
+ --ui-fg: #fafafa;
622
+ --ui-card: #09090b;
623
+ --ui-card-fg: #fafafa;
624
+ --ui-popover: #09090b;
625
+ --ui-popover-fg: #fafafa;
626
+ --ui-primary: #fafafa;
627
+ --ui-primary-fg: #18181b;
628
+ --ui-secondary: #27272a;
629
+ --ui-secondary-fg: #fafafa;
630
+ --ui-muted: #27272a;
631
+ --ui-muted-fg: #a1a1aa;
632
+ --ui-accent: #27272a;
633
+ --ui-accent-fg: #fafafa;
634
+ --ui-destructive: #7f1d1d;
635
+ --ui-destructive-fg: #fafafa;
636
+ --ui-border: #27272a;
637
+ --ui-input: #27272a;
638
+ --ui-ring: #d4d4d8;
639
+ }
640
+ }
641
+
642
+ @theme inline {
643
+ --color-background: var(--ui-bg);
644
+ --color-foreground: var(--ui-fg);
645
+ --color-card: var(--ui-card);
646
+ --color-card-foreground: var(--ui-card-fg);
647
+ --color-popover: var(--ui-popover);
648
+ --color-popover-foreground: var(--ui-popover-fg);
649
+ --color-primary: var(--ui-primary);
650
+ --color-primary-foreground: var(--ui-primary-fg);
651
+ --color-secondary: var(--ui-secondary);
652
+ --color-secondary-foreground: var(--ui-secondary-fg);
653
+ --color-muted: var(--ui-muted);
654
+ --color-muted-foreground: var(--ui-muted-fg);
655
+ --color-accent: var(--ui-accent);
656
+ --color-accent-foreground: var(--ui-accent-fg);
657
+ --color-destructive: var(--ui-destructive);
658
+ --color-destructive-foreground: var(--ui-destructive-fg);
659
+ --color-border: var(--ui-border);
660
+ --color-input: var(--ui-input);
661
+ --color-ring: var(--ui-ring);
662
+ --radius: var(--ui-radius);
663
+ }
664
+ `
665
+ : "";
666
+ const css = `@import "tailwindcss";
667
+ ${useShadcn ? '@import "tw-animate-css";' : ""}
668
+
669
+ @theme {
670
+ --color-bg: #ffffff;
671
+ --color-fg: #09090b;
672
+ --color-accent: ${useShadcn ? "#f97316" : "#0070f3"};
673
+ --font-sans: var(--font-geist-sans), system-ui, sans-serif;
674
+ --font-mono: var(--font-geist-mono), ui-monospace, monospace;
675
+ }
676
+
677
+ @variant dark (&:where(.dark, .dark *));
678
+
679
+ @media (prefers-color-scheme: dark) {
680
+ :root:not(.light) {
681
+ --color-bg: #09090b;
682
+ --color-fg: #fafafa;
683
+ }
684
+ }
685
+
686
+ body {
687
+ background: var(--color-bg);
688
+ color: var(--color-fg);
689
+ font-family: var(--font-sans);
690
+ }
691
+ ${shadcnVars}`;
692
+ await writeFile(join(appDir, `globals.${fileExt(language)}`), language === "ts" ? `import "./globals.css";\n` : `import "./globals.css";\n`);
693
+ await writeFile(join(appDir, "globals.css"), css);
694
+ }
695
+ const layoutImports = `import type { ReactNode } from "react";
696
+ import { Geist, GeistMono } from "swift-rust/font/google";
697
+ ${tailwind ? `import "./globals.css";\n` : ""}
698
+ const geistSans = Geist({ subsets: ["latin"], display: "swap", variable: true });
699
+ const geistMono = GeistMono({ subsets: ["latin"], display: "swap", variable: true });
700
+
701
+ export const metadata = {
702
+ title: {
703
+ template: "%s | ${projectName}",
704
+ default: "${projectName}",
705
+ },
706
+ description: "Built with swift-rust — the React framework powered with Rust + Bun. 10x faster than Next.js.",
707
+ };
708
+
709
+ export default function RootLayout({ children }: { children: ReactNode }) {
710
+ return (
711
+ <html
712
+ lang="en"
713
+ className={\`\${geistSans.variable} \${geistMono.variable} h-full antialiased\`}
714
+ >
715
+ <head>
716
+ <link rel="preconnect" href="https://fonts.googleapis.com" />
717
+ <link rel="preconnect" href="https://fonts.gstatic.com" crossOrigin="anonymous" />
718
+ </head>
719
+ <body className="${tailwind ? "min-h-full flex flex-col" : ""}">${tailwind ? "{children}" : "\\n {children}\\n "}</body>
720
+ </html>
721
+ );
722
+ }
723
+ `;
724
+ await writeFile(join(appDir, `layout.${componentExt(language)}`), layoutImports);
725
+ const homePage = tailwind
726
+ ? useShadcn
727
+ ? `import { buttonVariants } from "@/components/ui/button";
728
+ import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
729
+ import { Badge } from "@/components/ui/badge";
730
+
731
+ export default function Home() {
732
+ return (
733
+ <main className="min-h-screen flex items-center justify-center px-6 py-12">
734
+ <Card className="max-w-2xl w-full">
735
+ <CardHeader>
736
+ <Badge variant="secondary" className="w-fit mb-2">Welcome</Badge>
737
+ <CardTitle className="text-3xl sm:text-5xl">${projectName}</CardTitle>
738
+ <CardDescription>
739
+ Get started by editing <code>${srcDir ? "src/app/" : "app/"}page.${componentExt(language)}</code>.
740
+ </CardDescription>
741
+ </CardHeader>
742
+ <CardContent className="flex gap-2">
743
+ <a
744
+ href="https://swift-rust.dev/docs"
745
+ className={buttonVariants({ variant: "default" })}
746
+ >
747
+ Read the docs →
748
+ </a>
749
+ <a
750
+ href="https://github.com/swift-rust/swift-rust"
751
+ className={buttonVariants({ variant: "outline" })}
752
+ >
753
+ GitHub
754
+ </a>
755
+ </CardContent>
756
+ </Card>
757
+ </main>
758
+ );
759
+ }
760
+ `
761
+ : `export default function Home() {
762
+ return (
763
+ <main className="min-h-screen flex items-center justify-center px-6">
764
+ <div className="max-w-2xl text-center">
765
+ <div className="inline-block mb-4 px-3 py-1 text-xs font-medium rounded-full bg-accent/10 text-accent">
766
+ Welcome
767
+ </div>
768
+ <h1 className="text-5xl sm:text-7xl font-bold tracking-tight mb-6">
769
+ ${projectName}
770
+ </h1>
771
+ <p className="text-lg text-fg-secondary mb-8">
772
+ Get started by editing <code>${srcDir ? "src/app/" : "app/"}page.${componentExt(language)}</code>.
773
+ </p>
774
+ <a
775
+ href="https://swift-rust.dev/docs"
776
+ className="inline-flex items-center gap-2 px-5 py-2.5 rounded-lg bg-fg text-bg font-medium text-sm hover:opacity-90 transition-opacity"
777
+ >
778
+ Read the docs →
779
+ </a>
780
+ </div>
781
+ </main>
782
+ );
783
+ }
784
+ `
785
+ : `export default function Home() {
786
+ return (
787
+ <main style={{ minHeight: "100vh", display: "flex", alignItems: "center", justifyContent: "center", padding: "2rem" }}>
788
+ <div style={{ maxWidth: "32rem", textAlign: "center" }}>
789
+ <h1>${projectName}</h1>
790
+ <p>Get started by editing <code>${srcDir ? "src/app/" : "app/"}page.${componentExt(language)}</code>.</p>
791
+ <a href="https://swift-rust.dev/docs">Read the docs →</a>
792
+ </div>
793
+ </main>
794
+ );
795
+ }
796
+ `;
797
+ await writeFile(join(appDir, `page.${componentExt(language)}`), homePage);
798
+ const notFoundPage = tailwind
799
+ ? `export const metadata = { title: "Not found" };
800
+
801
+ export default function NotFound() {
802
+ return (
803
+ <main className="min-h-screen flex items-center justify-center px-6">
804
+ <div className="text-center">
805
+ <h1 className="text-6xl font-bold mb-4">404</h1>
806
+ <p className="text-fg-secondary">Page not found</p>
807
+ <a href="/" className="text-accent mt-6 inline-block">← Go home</a>
808
+ </div>
809
+ </main>
810
+ );
811
+ }
812
+ `
813
+ : `export default function NotFound() {
814
+ return (
815
+ <main>
816
+ <h1>404</h1>
817
+ <p>Page not found</p>
818
+ <a href="/">← Go home</a>
819
+ </main>
820
+ );
821
+ }
822
+ `;
823
+ await writeFile(join(appDir, `not-found.${componentExt(language)}`), notFoundPage);
824
+ if (linter === "biome") {
825
+ const biome = {
826
+ $schema: "https://biomejs.dev/schemas/2.4.16/schema.json",
827
+ vcs: { enabled: true, clientKind: "git", useIgnoreFile: true },
828
+ files: {
829
+ includes: ["**", "!.swift-rust", "!node_modules", "!dist", "!.turbo"],
830
+ },
831
+ formatter: { enabled: true, indentStyle: "space", indentWidth: 2 },
832
+ assist: { actions: { source: { organizeImports: "on" } } },
833
+ linter: {
834
+ enabled: true,
835
+ rules: { recommended: true, a11y: { noSvgWithoutTitle: "off" } },
836
+ },
837
+ javascript: { formatter: { quoteStyle: "double", semicolons: "always" } },
838
+ };
839
+ await writeFile(join(target, "biome.json"), `${JSON.stringify(biome, null, 2)}\n`);
840
+ }
841
+ else {
842
+ const eslint = `import swiftRust from "eslint-config-swift-rust";
843
+
844
+ export default [
845
+ ...swiftRust,
846
+ {
847
+ ignores: [".swift-rust/**", "node_modules/**", "dist/**", ".turbo/**"],
848
+ },
849
+ ];
850
+ `;
851
+ await writeFile(join(target, "eslint.config.mjs"), eslint);
852
+ }
853
+ const readme = `# ${projectName}
854
+
855
+ Built with [swift-rust](https://swift-rust.dev) — the React framework powered with Rust + Bun. 10x faster than Next.js.
856
+
857
+ ## Scripts
858
+
859
+ - \`bun run dev\` — start the dev server
860
+ - \`bun run build\` — build for production
861
+ - \`bun run start\` — start the production server
862
+ - \`bun run lint\` — run the linter
863
+ - \`bun run typecheck\` — run TypeScript checks (if TS)
864
+ - \`bun run test\` — run tests
865
+ - \`bun run format\` — format the code
866
+
867
+ ## Project structure
868
+
869
+ ${srcDir
870
+ ? `\`\`\`\nsrc/\n app/\n layout.${componentExt(language)}\n page.${componentExt(language)}\n not-found.${componentExt(language)}\n components/\n lib/\n\`\`\``
871
+ : `\`\`\`\napp/\n layout.${componentExt(language)}\n page.${componentExt(language)}\n not-found.${componentExt(language)}\ncomponents/\nlib/\n\`\`\``}
872
+
873
+ ## Learn more
874
+
875
+ - [Documentation](https://swift-rust.dev/docs)
876
+ - [Examples](https://github.com/swift-rust/swift-rust/tree/main/examples)
877
+ - [Discord](https://discord.gg/swift-rust)
878
+
879
+ ## Deploy to Vercel
880
+
881
+ The fastest way to deploy is to push to GitHub and import the repo on Vercel:
882
+
883
+ \`\`\`bash
884
+ git init && git add -A && git commit -m "init"
885
+ git remote add origin https://github.com/you/${projectName}.git
886
+ git push -u origin main
887
+ \`\`\`
888
+
889
+ Then on [vercel.com/new](https://vercel.com/new), import the repo. No configuration needed — \`vercel.json\` is included. Your site will be live at \`https://${projectName}.vercel.app\`.
890
+
891
+ For custom domains and ISR / serverless functions, see the [deploy guide](https://swift-rust.dev/docs/guides/deploying).
892
+ `;
893
+ await writeFile(join(target, "README.md"), readme);
894
+ const envExample = `# Example environment variables. Copy values you need into .env.local.
895
+ # .env.local is git-ignored; .env.example is committed.
896
+ # SWIFT_RUST_RUNTIME=/path/to/bun
897
+ `;
898
+ await writeFile(join(target, ".env.example"), envExample);
899
+ // Local env file (git-ignored) so the app works out of the box.
900
+ const envLocal = `# Local environment variables (git-ignored). Not committed.
901
+ # SWIFT_RUST_RUNTIME=/path/to/bun
902
+ `;
903
+ await writeFile(join(target, ".env.local"), envLocal);
904
+ const favicon = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32"><rect width="32" height="32" rx="7" fill="#0a0a0a"/><g stroke="#ffffff" stroke-width="1.9" stroke-linecap="round" fill="none"><circle cx="16" cy="16" r="6.4"/><path d="M16 4.4v2.6M16 25v2.6M4.4 16h2.6M25 16h2.6M7.9 7.9l1.8 1.8M22.3 22.3l1.8 1.8M7.9 24.1l1.8-1.8M22.3 9.7l1.8-1.8"/></g><path d="M17.8 9.3 L11.6 17 H15.2 L14.2 22.8 L20.4 15 H16.8 Z" fill="#fb923c"/></svg>
905
+ `;
906
+ // Metadata icons live in the app directory (Next.js style): swift-rust
907
+ // serves app/favicon.* from the site root and auto-links them in <head>.
908
+ await writeFile(join(appDir, "favicon.svg"), favicon);
909
+ // 32x32 favicon.ico (PNG-encoded) matching favicon.svg, so browsers that
910
+ // request /favicon.ico get a real icon instead of a 404.
911
+ const faviconIco = "AAABAAEAICAAAAEAIAAZBQAAFgAAAIlQTkcNChoKAAAADUlIRFIAAAAgAAAAIAgGAAAAc3p69AAAAAZiS0dEAP8A/wD/oL2nkwAABM5JREFUWIXFl19sU2UYxn/f6cbsoTsFoqywPw5BMzodpGNxhUIINMREgssEg5ELI7tUs/HnYiExRI1wwRKWXRBiMGEmphqykKl3JYOOBLMy3U7nthDI6LqRzii6ullit75eYJtNtnWWoU/yJc15z/s8z/f2y/ueTzEDNpttdTKZfB94FXgeWM7SYBK4BXyraVrLxMTET6mASv2wWq0HgAtKqfwlEp0TIvI78E48Hr+UNmC1Wg8opb6caegJQ0TkjXg8fknZbLbV09PTt7PZeU1NDQCXL1/OxkRM07QNStf1j4ET2TCMjIwAUFRUlE06wEc5wN5ss3Nzc7NNTWGvBqx/XJZsISIbNMA2V1DTNPr6+vD7/RiGMSdBR0cHHR0dc8YMw8Dv99PX14emaXO+o5TKnzsCJJNJotEobreb9vb2WSaUUpSXl9Pa2srFixdxOp2PiLe3t+N2u4lGoySTyflkQNd1mW85HA7p6uoSEZGWlhax2+1y7NgxiUQiMhPJ+2EJh8Ny5MgRsdvt0tLSIiIiXV1d4nA45uXXdV2Urusyv72Hu2lsbOTKlSscP34cj8dDMpmku7ubUCjEyulfeHlljDWvf4hSis7OTs6cOcPu3bs5deoUsVhsIfqFK5BadrtdOjs7RUSkp6dHqqurRdd1KXo6X26frJL3dj0rbrdbTNMUEZFAICCGYWTk1XVd5j0DM1FXV4fH46G3t5c9e/Zgmia5FsXnh9bz7Ko8+qNxent78Xq9hEIhtm/fzuHDhxdDTca/QCnFwMAAhYWFbNu2DdM0AWh6rZg69zMAlJzsZfzBNACbN2/m+vXrRCIRnE4nIgvSk7ECGzdupLi4mJs3b6bF33StSouP/PZnWhygp6eH7u5uSkpKKCsry0Q/20BNTQ2RSITR0VFGR0fx+XyUlpYCpMWrSpbTXFuSzilasYzx0y7GT7u4deIl1hq56XdLS0vx+Xxpvkgkkp4fi65AqoSaplGQn0vroefIy3k0LTEtvP3FEPdiiXTjyVR+WMQZcDqdBINBgsEgO3funBXrb3yRQvsyABouD/PZdz8DEAgEqKyspLKyksHBwQUNZKzAwMAAw8PDbNmyhU2bNqWfG09ZWGs8FPf9cD8t7nK5cLlchMPhjOKLMiAiNDc3o5Ti/Pnz2GwPR0e5w4pSYN77g/q2YQBsNhvnzp1DKcXZs2czigOLb0SBQEBEREzTlK1bt8q7u0rk3ifV8sLaFaLrung8HgmFQiIicvXq1UU3on/Vio8ePcqOHTsQEcbaPuD7X62MWdZQUVGBy+VCKcW1a9doamrC6/U+fiv+5zAyDEMaGhokHA5Lcmxw1kC6e/eu1NfXi2EYSzeM/H4/brebYDDIvn37Zu2mrKyMdevWATA0NDTrwKXGcVVVFTdu3MDr9c5bgHkNaJqGaZpEo1Fqa2vnLKXP5wPg4MGDj8QMw6CtrQ2Hw0FFRcW83wTKarXGsr0LjI2NAVBQUJBNOkBMU0rdyTZ7CXBHA77JNjuRSJBIJB7HwNf/58Vk3GKxbEhdzfYrpb7iv72a7Y/H420WgKmpqf6cnJwflVKvAHlPWHxcRN6Kx+NtAJbU06mpqf68vLwLIvIAyP97LVsi0QmgD/jUYrEcmpyc7E4F/gIrAoFO2P2bCQAAAABJRU5ErkJggg==";
912
+ await writeFile(join(appDir, "favicon.ico"), Buffer.from(faviconIco, "base64"));
913
+ if (useShadcn) {
914
+ await scaffoldShadcn({ target, uiDir, libDir, componentExt: componentExt(language), srcDir });
915
+ }
916
+ await writeVscodeConfig(target);
917
+ }
918
+ async function scaffoldShadcn(options) {
919
+ const { target, uiDir, libDir, componentExt, srcDir } = options;
920
+ const utilsContent = `import { clsx, type ClassValue } from "clsx";
921
+ import { twMerge } from "tailwind-merge";
922
+
923
+ export function cn(...inputs: ClassValue[]) {
924
+ return twMerge(clsx(inputs));
925
+ }
926
+ `;
927
+ const buttonContent = `import * as React from "react";
928
+ import { cva, type VariantProps } from "class-variance-authority";
929
+ import { cn } from "@/lib/utils";
930
+
931
+ const buttonVariants = cva(
932
+ "inline-flex items-center justify-center gap-2 rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--ui-ring)] focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
933
+ {
934
+ variants: {
935
+ variant: {
936
+ default: "bg-[var(--ui-primary)] text-[var(--ui-primary-fg)] hover:opacity-90",
937
+ destructive: "bg-[var(--ui-destructive)] text-white hover:opacity-90",
938
+ outline:
939
+ "border border-[var(--ui-border)] bg-[var(--ui-bg)] hover:bg-[var(--ui-accent)] hover:text-[var(--ui-accent-fg)]",
940
+ secondary: "bg-[var(--ui-secondary)] text-[var(--ui-secondary-fg)] hover:opacity-90",
941
+ ghost: "hover:bg-[var(--ui-accent)] hover:text-[var(--ui-accent-fg)]",
942
+ link: "text-[var(--ui-primary)] underline-offset-4 hover:underline",
943
+ },
944
+ size: {
945
+ default: "h-9 px-4 py-2",
946
+ sm: "h-8 rounded-md px-3 text-xs",
947
+ lg: "h-10 rounded-md px-8",
948
+ icon: "h-9 w-9",
949
+ },
950
+ },
951
+ defaultVariants: {
952
+ variant: "default",
953
+ size: "default",
954
+ },
955
+ },
956
+ );
957
+
958
+ export interface ButtonProps
959
+ extends React.ButtonHTMLAttributes<HTMLButtonElement>,
960
+ VariantProps<typeof buttonVariants> {}
961
+
962
+ export const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
963
+ ({ className, variant, size, ...props }, ref) => (
964
+ <button ref={ref} className={cn(buttonVariants({ variant, size, className }))} {...props} />
965
+ ),
966
+ );
967
+ Button.displayName = "Button";
968
+
969
+ export { buttonVariants };
970
+ `;
971
+ const cardContent = `import * as React from "react";
972
+ import { cn } from "@/lib/utils";
973
+
974
+ export const Card = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
975
+ ({ className, ...props }, ref) => (
976
+ <div
977
+ ref={ref}
978
+ className={cn(
979
+ "rounded-xl border border-[var(--ui-border)] bg-[var(--ui-card)] text-[var(--ui-card-fg)] shadow-sm",
980
+ className,
981
+ )}
982
+ {...props}
983
+ />
984
+ ),
985
+ );
986
+ Card.displayName = "Card";
987
+
988
+ export const CardHeader = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
989
+ ({ className, ...props }, ref) => (
990
+ <div ref={ref} className={cn("flex flex-col space-y-1.5 p-6", className)} {...props} />
991
+ ),
992
+ );
993
+ CardHeader.displayName = "CardHeader";
994
+
995
+ export const CardTitle = React.forwardRef<
996
+ HTMLDivElement,
997
+ React.HTMLAttributes<HTMLDivElement>
998
+ >(({ className, ...props }, ref) => (
999
+ <div
1000
+ ref={ref}
1001
+ className={cn("font-semibold leading-none tracking-tight", className)}
1002
+ {...props}
1003
+ />
1004
+ ));
1005
+ CardTitle.displayName = "CardTitle";
1006
+
1007
+ export const CardDescription = React.forwardRef<
1008
+ HTMLDivElement,
1009
+ React.HTMLAttributes<HTMLDivElement>
1010
+ >(({ className, ...props }, ref) => (
1011
+ <div
1012
+ ref={ref}
1013
+ className={cn("text-sm text-[var(--ui-muted-fg)]", className)}
1014
+ {...props}
1015
+ />
1016
+ ));
1017
+ CardDescription.displayName = "CardDescription";
1018
+
1019
+ export const CardContent = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
1020
+ ({ className, ...props }, ref) => (
1021
+ <div ref={ref} className={cn("p-6 pt-0", className)} {...props} />
1022
+ ),
1023
+ );
1024
+ CardContent.displayName = "CardContent";
1025
+
1026
+ export const CardFooter = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
1027
+ ({ className, ...props }, ref) => (
1028
+ <div ref={ref} className={cn("flex items-center p-6 pt-0", className)} {...props} />
1029
+ ),
1030
+ );
1031
+ CardFooter.displayName = "CardFooter";
1032
+ `;
1033
+ const inputContent = `import * as React from "react";
1034
+ import { cn } from "@/lib/utils";
1035
+
1036
+ export type InputProps = React.InputHTMLAttributes<HTMLInputElement>;
1037
+
1038
+ export const Input = React.forwardRef<HTMLInputElement, InputProps>(
1039
+ ({ className, type, ...props }, ref) => (
1040
+ <input
1041
+ type={type}
1042
+ ref={ref}
1043
+ className={cn(
1044
+ "flex h-9 w-full rounded-md border border-[var(--ui-border)] bg-transparent px-3 py-1 text-sm shadow-sm transition-colors",
1045
+ "file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-[var(--ui-fg)]",
1046
+ "placeholder:text-[var(--ui-muted-fg)] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--ui-ring)] focus-visible:ring-offset-1",
1047
+ "disabled:cursor-not-allowed disabled:opacity-50",
1048
+ className,
1049
+ )}
1050
+ {...props}
1051
+ />
1052
+ ),
1053
+ );
1054
+ Input.displayName = "Input";
1055
+ `;
1056
+ const labelContent = `import * as React from "react";
1057
+ import { cn } from "@/lib/utils";
1058
+
1059
+ export const Label = React.forwardRef<
1060
+ HTMLLabelElement,
1061
+ React.LabelHTMLAttributes<HTMLLabelElement>
1062
+ >(({ className, ...props }, ref) => (
1063
+ <label
1064
+ ref={ref}
1065
+ className={cn(
1066
+ "text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70",
1067
+ className,
1068
+ )}
1069
+ {...props}
1070
+ />
1071
+ ));
1072
+ Label.displayName = "Label";
1073
+ `;
1074
+ const badgeContent = `import * as React from "react";
1075
+ import { cva, type VariantProps } from "class-variance-authority";
1076
+ import { cn } from "@/lib/utils";
1077
+
1078
+ const badgeVariants = cva(
1079
+ "inline-flex items-center rounded-full border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-[var(--ui-ring)] focus:ring-offset-2",
1080
+ {
1081
+ variants: {
1082
+ variant: {
1083
+ default:
1084
+ "border-transparent bg-[var(--ui-primary)] text-[var(--ui-primary-fg)] hover:opacity-90",
1085
+ secondary:
1086
+ "border-transparent bg-[var(--ui-secondary)] text-[var(--ui-secondary-fg)] hover:opacity-90",
1087
+ destructive:
1088
+ "border-transparent bg-[var(--ui-destructive)] text-white hover:opacity-90",
1089
+ outline: "text-[var(--ui-fg)] border-[var(--ui-border)]",
1090
+ },
1091
+ },
1092
+ defaultVariants: {
1093
+ variant: "default",
1094
+ },
1095
+ },
1096
+ );
1097
+
1098
+ export interface BadgeProps
1099
+ extends React.HTMLAttributes<HTMLDivElement>,
1100
+ VariantProps<typeof badgeVariants> {}
1101
+
1102
+ export function Badge({ className, variant, ...props }: BadgeProps) {
1103
+ return <div className={cn(badgeVariants({ variant }), className)} {...props} />;
1104
+ }
1105
+
1106
+ export { badgeVariants };
1107
+ `;
1108
+ const componentsJson = `{
1109
+ "$schema": "https://ui.shadcn.com/schema.json",
1110
+ "style": "new-york",
1111
+ "rsc": true,
1112
+ "tsx": ${componentExt === "tsx"},
1113
+ "tailwind": {
1114
+ "config": "",
1115
+ "css": "${srcDir ? "src/app/globals.css" : "app/globals.css"}",
1116
+ "baseColor": "zinc",
1117
+ "cssVariables": true,
1118
+ "prefix": ""
1119
+ },
1120
+ "aliases": {
1121
+ "components": "@/components",
1122
+ "utils": "@/lib/utils",
1123
+ "ui": "@/components/ui",
1124
+ "lib": "@/lib",
1125
+ "hooks": "@/hooks"
1126
+ },
1127
+ "iconLibrary": "lucide"
1128
+ }
1129
+ `;
1130
+ await writeFile(join(libDir, "utils.ts"), utilsContent);
1131
+ await writeFile(join(uiDir, `button.${componentExt}`), buttonContent);
1132
+ await writeFile(join(uiDir, `card.${componentExt}`), cardContent);
1133
+ await writeFile(join(uiDir, `input.${componentExt}`), inputContent);
1134
+ await writeFile(join(uiDir, `label.${componentExt}`), labelContent);
1135
+ await writeFile(join(uiDir, `badge.${componentExt}`), badgeContent);
1136
+ await writeFile(join(target, "components.json"), `${componentsJson}\n`);
1137
+ }
194
1138
  async function runInstall(target) {
195
1139
  const { spawn } = await import("node:child_process");
196
- await new Promise((resolvePromise, reject) => {
197
- const child = spawn("bun", ["install"], { cwd: target, stdio: "inherit" });
198
- child.on("error", reject);
199
- child.on("close", (code) => code === 0 ? resolvePromise() : reject(new Error(`bun install exited with code ${code}`)));
1140
+ return new Promise((resolve, reject) => {
1141
+ const proc = spawn("bun", ["install"], { cwd: target, stdio: "inherit" });
1142
+ proc.on("exit", (code) => {
1143
+ if (code === 0)
1144
+ resolve();
1145
+ else
1146
+ reject(new Error(`bun install exited with code ${code}`));
1147
+ });
1148
+ proc.on("error", reject);
1149
+ });
1150
+ }
1151
+ const SHADCN_DEFAULT_COMPONENTS = ["button", "card", "input", "label", "badge"];
1152
+ async function runShadcnAdd(target, components) {
1153
+ const { spawn } = await import("node:child_process");
1154
+ return new Promise((resolve) => {
1155
+ const args = [
1156
+ "--bun",
1157
+ "shadcn@latest",
1158
+ "add",
1159
+ ...components,
1160
+ "--yes",
1161
+ "--overwrite",
1162
+ "--cwd",
1163
+ target,
1164
+ "--silent",
1165
+ ];
1166
+ const proc = spawn("bunx", args, { cwd: target, stdio: "pipe" });
1167
+ proc.on("exit", (code) => {
1168
+ resolve(code === 0);
1169
+ });
1170
+ proc.on("error", () => resolve(false));
200
1171
  });
201
1172
  }
202
1173
  async function main() {
@@ -218,7 +1189,7 @@ async function main() {
218
1189
  const answers = await askQuestions(flags, positional);
219
1190
  if (!answers)
220
1191
  process.exit(1);
221
- const { projectName, install } = answers;
1192
+ const { projectName, tailwind, linter, srcDir, install } = answers;
222
1193
  const target = resolve(process.cwd(), projectName);
223
1194
  if (projectName === ".") {
224
1195
  if ((await listDir(target)).length > 0) {
@@ -248,14 +1219,27 @@ async function main() {
248
1219
  process.exit(1);
249
1220
  }
250
1221
  }
251
- const summary = [
252
- `${pc.cyan("•")} Project: ${pc.bold(projectName === "." ? resolve(target) : projectName)}`,
253
- `${pc.cyan("•")} Template: Full demo (blog, dashboard, API, Image/PDF/Video/Font)`,
254
- `${pc.cyan("•")} Language: TypeScript`,
255
- `${pc.cyan("•")} Styling: Tailwind CSS`,
256
- `${pc.cyan("•")} Linter: Biome`,
257
- `${pc.cyan("•")} Install: ${install ? "Yes" : "No"}`,
258
- ].join("\n");
1222
+ const summary = (answers.template === "full"
1223
+ ? [
1224
+ `${pc.cyan("•")} Project: ${pc.bold(projectName === "." ? resolve(target) : projectName)}`,
1225
+ `${pc.cyan("•")} Template: Full demo (blog, dashboard, API, Image/PDF/Video/Font)`,
1226
+ `${pc.cyan("•")} Language: TypeScript`,
1227
+ `${pc.cyan("•")} Styling: Tailwind CSS`,
1228
+ `${pc.cyan("•")} Linter: Biome`,
1229
+ `${pc.cyan("•")} Install: ${install ? "Yes" : "No"}`,
1230
+ ]
1231
+ : [
1232
+ `${pc.cyan("•")} Project: ${pc.bold(projectName === "." ? resolve(target) : projectName)}`,
1233
+ `${pc.cyan("•")} Template: Minimal`,
1234
+ `${pc.cyan("•")} Language: ${answers.language === "ts" ? "TypeScript" : "JavaScript"}`,
1235
+ `${pc.cyan("•")} Renderer: ${answers.renderer}`,
1236
+ `${pc.cyan("•")} Linter: ${linter === "biome" ? "Biome" : "ESLint"}`,
1237
+ `${pc.cyan("•")} Tailwind: ${tailwind ? "Yes" : "No"}`,
1238
+ `${pc.cyan("•")} shadcn UI: ${answers.useShadcn ? "Yes" : "No"}`,
1239
+ `${pc.cyan("•")} src/ dir: ${srcDir ? "Yes (src/app/)" : "No"}`,
1240
+ `${pc.cyan("•")} Import as: ${answers.importAlias}`,
1241
+ `${pc.cyan("•")} Install: ${install ? "Yes" : "No"}`,
1242
+ ]).join("\n");
259
1243
  if (quiet) {
260
1244
  console.log(`Configuration:\n${summary}`);
261
1245
  }
@@ -272,18 +1256,36 @@ async function main() {
272
1256
  }
273
1257
  console.log(`Scaffolding ${pc.cyan(projectName === "." ? "." : projectName)}…`);
274
1258
  try {
275
- await copyTemplate(target, projectName);
1259
+ if (answers.template === "full") {
1260
+ await copyTemplate(target, projectName);
1261
+ }
1262
+ else {
1263
+ await writeProjectFiles(target, answers);
1264
+ }
276
1265
  console.log(`✓ Scaffolded project into ${pc.green(target)}`);
1266
+ let installSucceeded = false;
277
1267
  if (install) {
278
1268
  console.log("Running bun install…");
279
1269
  try {
280
1270
  await runInstall(target);
281
1271
  console.log("✓ Installed dependencies");
1272
+ installSucceeded = true;
282
1273
  }
283
1274
  catch (_err) {
284
1275
  console.error("Failed to install dependencies. Run `bun install` manually.");
285
1276
  }
286
1277
  }
1278
+ if (answers.useShadcn && installSucceeded) {
1279
+ console.log("Fetching canonical shadcn components…");
1280
+ const ok = await runShadcnAdd(target, SHADCN_DEFAULT_COMPONENTS);
1281
+ if (ok) {
1282
+ console.log(`✓ Added ${SHADCN_DEFAULT_COMPONENTS.length} shadcn components from the registry`);
1283
+ }
1284
+ else {
1285
+ const cwdArg = projectName === "." ? "." : `./${projectName}`;
1286
+ console.log(`${pc.yellow("!")} Could not fetch shadcn components from the registry. Using bundled shadcn-style components as a fallback. You can retry later with: ${pc.cyan(`bunx --bun shadcn@latest add ${SHADCN_DEFAULT_COMPONENTS.join(" ")} --cwd ${cwdArg}`)}`);
1287
+ }
1288
+ }
287
1289
  const next = projectName === "." ? "bun run dev" : `cd ${projectName}\nbun run dev`;
288
1290
  console.log(`\n${pc.green("Done!")} Next steps:\n${pc.cyan(next)}`);
289
1291
  }