@helloleo/plugins 0.1.13 → 0.1.14
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +24 -769
- package/package.json +1 -1
- package/src/index.ts +0 -1
- package/src/tools/scaffold.ts +49 -934
- package/src/tools/start-scaffold.ts +0 -1343
package/src/tools/scaffold.ts
CHANGED
|
@@ -76,7 +76,7 @@ export default defineConfig({
|
|
|
76
76
|
})`;
|
|
77
77
|
}
|
|
78
78
|
|
|
79
|
-
|
|
79
|
+
function tailwindConfig() {
|
|
80
80
|
return `export default {
|
|
81
81
|
darkMode: ["class"],
|
|
82
82
|
content: ["./index.html", "./src/**/*.{js,ts,jsx,tsx}"],
|
|
@@ -119,7 +119,7 @@ export function tailwindConfig() {
|
|
|
119
119
|
}`;
|
|
120
120
|
}
|
|
121
121
|
|
|
122
|
-
|
|
122
|
+
function postcssConfig() {
|
|
123
123
|
return `export default {
|
|
124
124
|
plugins: {
|
|
125
125
|
tailwindcss: {},
|
|
@@ -128,7 +128,7 @@ export function postcssConfig() {
|
|
|
128
128
|
}`;
|
|
129
129
|
}
|
|
130
130
|
|
|
131
|
-
|
|
131
|
+
function indexCss(
|
|
132
132
|
primaryColor: string,
|
|
133
133
|
fontFamily: string,
|
|
134
134
|
borderRadius: string,
|
|
@@ -152,9 +152,8 @@ export function indexCss(
|
|
|
152
152
|
* --warning each have a paired -foreground for text on that surface.
|
|
153
153
|
* - --radius drives lg/md/sm via calc() in tailwind.config.js. Edit here,
|
|
154
154
|
* not in component classes.
|
|
155
|
-
* - --font-sans for body, --font-heading for headings.
|
|
156
|
-
*
|
|
157
|
-
* (SSR). Keep the family here in sync with that <link>.
|
|
155
|
+
* - --font-sans for body, --font-heading for headings. Both load from the
|
|
156
|
+
* Google Fonts <link> in index.html.
|
|
158
157
|
* - .dark variants mirror :root one-for-one. When adding a token, add it
|
|
159
158
|
* to BOTH blocks or dark mode breaks.
|
|
160
159
|
*
|
|
@@ -234,7 +233,7 @@ body {
|
|
|
234
233
|
}`;
|
|
235
234
|
}
|
|
236
235
|
|
|
237
|
-
|
|
236
|
+
function utilsTs() {
|
|
238
237
|
return `import { clsx, type ClassValue } from 'clsx'
|
|
239
238
|
import { twMerge } from 'tailwind-merge'
|
|
240
239
|
|
|
@@ -243,7 +242,18 @@ export function cn(...inputs: ClassValue[]) {
|
|
|
243
242
|
}`;
|
|
244
243
|
}
|
|
245
244
|
|
|
246
|
-
|
|
245
|
+
/**
|
|
246
|
+
* The project workspace directory is named after the project UUID -- the
|
|
247
|
+
* webapp generates `<slugified-name>-<uuidv4>` (actions.ts createProject)
|
|
248
|
+
* and leo-core mounts the project at /workspace/<uuid>. Baking it into
|
|
249
|
+
* proxy.ts at scaffold time removes the VITE_PROJECT_UUID env var, which
|
|
250
|
+
* nothing in the infrastructure ever set and which Leo kept mis-generating.
|
|
251
|
+
*/
|
|
252
|
+
export function projectUuidFromDir(dir: string): string {
|
|
253
|
+
return path.basename(dir);
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
function proxyTs(projectUuid: string) {
|
|
247
257
|
return `/*
|
|
248
258
|
* HelloLeo Integration Proxy -- runtime helper for connected integrations.
|
|
249
259
|
*
|
|
@@ -285,10 +295,8 @@ export function proxyTs() {
|
|
|
285
295
|
* from a Worker. Never embed secrets in frontend code.
|
|
286
296
|
*/
|
|
287
297
|
|
|
288
|
-
const PROJECT_UUID = import.meta.env.VITE_PROJECT_UUID ?? '';
|
|
289
298
|
export const INTEGRATION_PROXY_URL =
|
|
290
|
-
|
|
291
|
-
\`https://api.app.helloleo.dev/api/integration-proxy/\${PROJECT_UUID}\`;
|
|
299
|
+
'https://api.app.helloleo.dev/api/integration-proxy/${projectUuid}';
|
|
292
300
|
|
|
293
301
|
export interface ProxyRequest {
|
|
294
302
|
integration: string;
|
|
@@ -367,7 +375,7 @@ function hexToHslTriplet(hex: string): string {
|
|
|
367
375
|
return `${Math.round(hue)} ${Math.round(s * 100)}% ${Math.round(l * 100)}%`;
|
|
368
376
|
}
|
|
369
377
|
|
|
370
|
-
|
|
378
|
+
function normalizePrimaryColor(value: string): string {
|
|
371
379
|
const trimmed = value.trim();
|
|
372
380
|
if (/^(#|[0-9a-fA-F]{3}$|[0-9a-fA-F]{6}$)/.test(trimmed)) {
|
|
373
381
|
const hsl = hexToHslTriplet(trimmed);
|
|
@@ -422,7 +430,7 @@ export async function writeBaseScaffold(
|
|
|
422
430
|
["postcss.config.js", postcssConfig()],
|
|
423
431
|
[path.join("src", "index.css"), indexCss(primary, font, radius)],
|
|
424
432
|
[path.join("src", "lib", "utils.ts"), utilsTs()],
|
|
425
|
-
[path.join("src", "lib", "proxy.ts"), proxyTs()],
|
|
433
|
+
[path.join("src", "lib", "proxy.ts"), proxyTs(projectUuidFromDir(dir))],
|
|
426
434
|
[path.join("src", "main.tsx"), mainTsx()],
|
|
427
435
|
];
|
|
428
436
|
|
|
@@ -434,953 +442,60 @@ export async function writeBaseScaffold(
|
|
|
434
442
|
return created;
|
|
435
443
|
}
|
|
436
444
|
|
|
437
|
-
|
|
438
|
-
// React+Vite base, still consumed by `setup_netsuite`. The `scaffold` TOOL below
|
|
439
|
-
// is the current TanStack scaffolder (spa/ssr) — it replaced the old React tool.
|
|
440
|
-
|
|
441
|
-
/*
|
|
442
|
-
* scaffold_app -- next-gen HelloLeo scaffold.
|
|
443
|
-
*
|
|
444
|
-
* One tool, two modes (both TanStack, both Vite, same design system + tooling):
|
|
445
|
-
* - mode "spa" -> TanStack Router SPA (client-only). Runs `vite dev`; deploys
|
|
446
|
-
* as a static Vite app on the preview sandbox, like today.
|
|
447
|
-
* - mode "ssr" -> TanStack Start (Node server target, validated by
|
|
448
|
-
* cf-sandbox-tanstack-poc). SSR loaders + server functions +
|
|
449
|
-
* Drizzle. Runs `vite dev`; on the preview sandbox the Node
|
|
450
|
-
* server runs inside the CF Sandbox container (the PM2
|
|
451
|
-
* replacement). DB adapter swaps libsql (dev) -> D1 (prod);
|
|
452
|
-
* wrangler.jsonc carries the Workers-for-Platforms prod target.
|
|
453
|
-
* (Executing SSR *on workerd itself* needs a newer TanStack/CF
|
|
454
|
-
* integration than the pinned 1.121.x — tracked as follow-up.)
|
|
455
|
-
*
|
|
456
|
-
* Shared across both:
|
|
457
|
-
* - Tailwind v3 + the HelloLeo design-system tokens (src/styles/app.css) so
|
|
458
|
-
* the no-code Design panel round-trip keeps working (same tokens as the
|
|
459
|
-
* legacy `scaffold`).
|
|
460
|
-
* - ShadcnUI primitives (components.json + src/components/ui/*) consuming the
|
|
461
|
-
* design tokens -- never hardcoded colors.
|
|
462
|
-
* - Biome (biome.json) for format + lint. No ESLint/Prettier.
|
|
463
|
-
* - Strict TypeScript (tsconfig.json: strict, bundler resolution, @/* paths)
|
|
464
|
-
* so the editor / OpenCode LSP gives full type info.
|
|
465
|
-
*
|
|
466
|
-
* Version pins mirror the validated cf-sandbox-tanstack-poc:
|
|
467
|
-
* TanStack 1.121.x, React 19, Vite 6, @cloudflare/* current.
|
|
468
|
-
*/
|
|
469
|
-
|
|
470
|
-
const REACT = "^19.0.0";
|
|
471
|
-
const VITE = "^6.0.5";
|
|
472
|
-
const TS = "^5.6.3";
|
|
473
|
-
// Pin EXACT (no caret): @tanstack/react-router and @tanstack/react-start must
|
|
474
|
-
// share one @tanstack/router-core. react-router floats ahead of react-start's
|
|
475
|
-
// release line, so a caret skews router-core and SSR breaks (missing
|
|
476
|
-
// processRouteTree export). 1.131.50 is the matched set react-start ships with.
|
|
477
|
-
const TANSTACK = "1.131.50";
|
|
478
|
-
const BIOME = "2.4.8";
|
|
479
|
-
|
|
480
|
-
// ---------- shared tooling files ----------
|
|
481
|
-
|
|
482
|
-
function tsconfig(): string {
|
|
483
|
-
return JSON.stringify(
|
|
484
|
-
{
|
|
485
|
-
compilerOptions: {
|
|
486
|
-
target: "ES2022",
|
|
487
|
-
module: "ESNext",
|
|
488
|
-
moduleResolution: "Bundler",
|
|
489
|
-
lib: ["ES2022", "DOM", "DOM.Iterable"],
|
|
490
|
-
jsx: "react-jsx",
|
|
491
|
-
strict: true,
|
|
492
|
-
noUncheckedIndexedAccess: true,
|
|
493
|
-
noUnusedLocals: true,
|
|
494
|
-
noUnusedParameters: true,
|
|
495
|
-
verbatimModuleSyntax: true,
|
|
496
|
-
skipLibCheck: true,
|
|
497
|
-
noEmit: true,
|
|
498
|
-
baseUrl: ".",
|
|
499
|
-
paths: { "@/*": ["./src/*"] },
|
|
500
|
-
types: ["vite/client"],
|
|
501
|
-
},
|
|
502
|
-
include: ["src", "*.config.ts"],
|
|
503
|
-
},
|
|
504
|
-
null,
|
|
505
|
-
2,
|
|
506
|
-
);
|
|
507
|
-
}
|
|
508
|
-
|
|
509
|
-
function biomeJson(): string {
|
|
510
|
-
return JSON.stringify(
|
|
511
|
-
{
|
|
512
|
-
$schema: `https://biomejs.dev/schemas/${BIOME}/schema.json`,
|
|
513
|
-
vcs: { enabled: true, clientKind: "git", useIgnoreFile: true },
|
|
514
|
-
files: { ignoreUnknown: true },
|
|
515
|
-
formatter: { enabled: true, indentStyle: "tab", lineWidth: 80 },
|
|
516
|
-
linter: { enabled: true, rules: { recommended: true } },
|
|
517
|
-
javascript: { formatter: { quoteStyle: "double" } },
|
|
518
|
-
assist: { actions: { source: { organizeImports: "on" } } },
|
|
519
|
-
},
|
|
520
|
-
null,
|
|
521
|
-
2,
|
|
522
|
-
);
|
|
523
|
-
}
|
|
524
|
-
|
|
525
|
-
function componentsJson(): string {
|
|
526
|
-
// shadcn/ui config -- "new-york" style on Tailwind v3 with our token names.
|
|
527
|
-
return JSON.stringify(
|
|
528
|
-
{
|
|
529
|
-
$schema: "https://ui.shadcn.com/schema.json",
|
|
530
|
-
style: "new-york",
|
|
531
|
-
rsc: false,
|
|
532
|
-
tsx: true,
|
|
533
|
-
tailwind: {
|
|
534
|
-
config: "tailwind.config.js",
|
|
535
|
-
css: "src/styles/app.css",
|
|
536
|
-
baseColor: "slate",
|
|
537
|
-
cssVariables: true,
|
|
538
|
-
prefix: "",
|
|
539
|
-
},
|
|
540
|
-
aliases: {
|
|
541
|
-
components: "@/components",
|
|
542
|
-
utils: "@/lib/utils",
|
|
543
|
-
ui: "@/components/ui",
|
|
544
|
-
lib: "@/lib",
|
|
545
|
-
hooks: "@/hooks",
|
|
546
|
-
},
|
|
547
|
-
iconLibrary: "lucide",
|
|
548
|
-
},
|
|
549
|
-
null,
|
|
550
|
-
2,
|
|
551
|
-
);
|
|
552
|
-
}
|
|
553
|
-
|
|
554
|
-
// ---------- shadcn primitives (token-driven) ----------
|
|
555
|
-
|
|
556
|
-
function uiButton(): string {
|
|
557
|
-
return `import * as React from "react"
|
|
558
|
-
import { Slot } from "@radix-ui/react-slot"
|
|
559
|
-
import { cva, type VariantProps } from "class-variance-authority"
|
|
560
|
-
import { cn } from "@/lib/utils"
|
|
561
|
-
|
|
562
|
-
const buttonVariants = cva(
|
|
563
|
-
"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 [&_svg]:size-4 [&_svg]:shrink-0",
|
|
564
|
-
{
|
|
565
|
-
variants: {
|
|
566
|
-
variant: {
|
|
567
|
-
default: "bg-primary text-primary-foreground shadow hover:bg-primary/90",
|
|
568
|
-
destructive: "bg-destructive text-destructive-foreground shadow-sm hover:bg-destructive/90",
|
|
569
|
-
outline: "border border-input bg-background shadow-sm hover:bg-accent hover:text-accent-foreground",
|
|
570
|
-
secondary: "bg-secondary text-secondary-foreground shadow-sm hover:bg-secondary/80",
|
|
571
|
-
ghost: "hover:bg-accent hover:text-accent-foreground",
|
|
572
|
-
link: "text-primary underline-offset-4 hover:underline",
|
|
573
|
-
},
|
|
574
|
-
size: {
|
|
575
|
-
default: "h-9 px-4 py-2",
|
|
576
|
-
sm: "h-8 rounded-md px-3 text-xs",
|
|
577
|
-
lg: "h-10 rounded-md px-8",
|
|
578
|
-
icon: "h-9 w-9",
|
|
579
|
-
},
|
|
580
|
-
},
|
|
581
|
-
defaultVariants: { variant: "default", size: "default" },
|
|
582
|
-
},
|
|
583
|
-
)
|
|
584
|
-
|
|
585
|
-
export interface ButtonProps
|
|
586
|
-
extends React.ButtonHTMLAttributes<HTMLButtonElement>,
|
|
587
|
-
VariantProps<typeof buttonVariants> {
|
|
588
|
-
asChild?: boolean
|
|
589
|
-
}
|
|
590
|
-
|
|
591
|
-
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
|
|
592
|
-
({ className, variant, size, asChild = false, ...props }, ref) => {
|
|
593
|
-
const Comp = asChild ? Slot : "button"
|
|
594
|
-
return (
|
|
595
|
-
<Comp className={cn(buttonVariants({ variant, size, className }))} ref={ref} {...props} />
|
|
596
|
-
)
|
|
597
|
-
},
|
|
598
|
-
)
|
|
599
|
-
Button.displayName = "Button"
|
|
600
|
-
|
|
601
|
-
export { Button, buttonVariants }
|
|
602
|
-
`;
|
|
603
|
-
}
|
|
604
|
-
|
|
605
|
-
function uiCard(): string {
|
|
606
|
-
return `import * as React from "react"
|
|
607
|
-
import { cn } from "@/lib/utils"
|
|
608
|
-
|
|
609
|
-
const Card = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
|
|
610
|
-
({ className, ...props }, ref) => (
|
|
611
|
-
<div ref={ref} className={cn("rounded-lg border bg-card text-card-foreground shadow", className)} {...props} />
|
|
612
|
-
),
|
|
613
|
-
)
|
|
614
|
-
Card.displayName = "Card"
|
|
615
|
-
|
|
616
|
-
const CardHeader = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
|
|
617
|
-
({ className, ...props }, ref) => (
|
|
618
|
-
<div ref={ref} className={cn("flex flex-col space-y-1.5 p-6", className)} {...props} />
|
|
619
|
-
),
|
|
620
|
-
)
|
|
621
|
-
CardHeader.displayName = "CardHeader"
|
|
622
|
-
|
|
623
|
-
const CardTitle = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
|
|
624
|
-
({ className, ...props }, ref) => (
|
|
625
|
-
<div ref={ref} className={cn("font-heading font-semibold leading-none tracking-tight", className)} {...props} />
|
|
626
|
-
),
|
|
627
|
-
)
|
|
628
|
-
CardTitle.displayName = "CardTitle"
|
|
629
|
-
|
|
630
|
-
const CardDescription = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
|
|
631
|
-
({ className, ...props }, ref) => (
|
|
632
|
-
<div ref={ref} className={cn("text-sm text-muted-foreground", className)} {...props} />
|
|
633
|
-
),
|
|
634
|
-
)
|
|
635
|
-
CardDescription.displayName = "CardDescription"
|
|
636
|
-
|
|
637
|
-
const CardContent = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
|
|
638
|
-
({ className, ...props }, ref) => (
|
|
639
|
-
<div ref={ref} className={cn("p-6 pt-0", className)} {...props} />
|
|
640
|
-
),
|
|
641
|
-
)
|
|
642
|
-
CardContent.displayName = "CardContent"
|
|
643
|
-
|
|
644
|
-
export { Card, CardHeader, CardTitle, CardDescription, CardContent }
|
|
645
|
-
`;
|
|
646
|
-
}
|
|
647
|
-
|
|
648
|
-
function uiInput(): string {
|
|
649
|
-
return `import * as React from "react"
|
|
650
|
-
import { cn } from "@/lib/utils"
|
|
651
|
-
|
|
652
|
-
const Input = React.forwardRef<HTMLInputElement, React.InputHTMLAttributes<HTMLInputElement>>(
|
|
653
|
-
({ className, type, ...props }, ref) => (
|
|
654
|
-
<input
|
|
655
|
-
type={type}
|
|
656
|
-
ref={ref}
|
|
657
|
-
className={cn(
|
|
658
|
-
"flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-sm shadow-sm transition-colors placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50",
|
|
659
|
-
className,
|
|
660
|
-
)}
|
|
661
|
-
{...props}
|
|
662
|
-
/>
|
|
663
|
-
),
|
|
664
|
-
)
|
|
665
|
-
Input.displayName = "Input"
|
|
666
|
-
|
|
667
|
-
export { Input }
|
|
668
|
-
`;
|
|
669
|
-
}
|
|
670
|
-
|
|
671
|
-
function uiTextarea(): string {
|
|
672
|
-
return `import * as React from "react"
|
|
673
|
-
import { cn } from "@/lib/utils"
|
|
674
|
-
|
|
675
|
-
const Textarea = React.forwardRef<HTMLTextAreaElement, React.TextareaHTMLAttributes<HTMLTextAreaElement>>(
|
|
676
|
-
({ className, ...props }, ref) => (
|
|
677
|
-
<textarea
|
|
678
|
-
ref={ref}
|
|
679
|
-
className={cn(
|
|
680
|
-
"flex min-h-[60px] w-full rounded-md border border-input bg-transparent px-3 py-2 text-sm shadow-sm placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50",
|
|
681
|
-
className,
|
|
682
|
-
)}
|
|
683
|
-
{...props}
|
|
684
|
-
/>
|
|
685
|
-
),
|
|
686
|
-
)
|
|
687
|
-
Textarea.displayName = "Textarea"
|
|
688
|
-
|
|
689
|
-
export { Textarea }
|
|
690
|
-
`;
|
|
691
|
-
}
|
|
692
|
-
|
|
693
|
-
function uiLabel(): string {
|
|
694
|
-
return `import * as React from "react"
|
|
695
|
-
import { cn } from "@/lib/utils"
|
|
696
|
-
|
|
697
|
-
const Label = React.forwardRef<HTMLLabelElement, React.LabelHTMLAttributes<HTMLLabelElement>>(
|
|
698
|
-
({ className, ...props }, ref) => (
|
|
699
|
-
<label
|
|
700
|
-
ref={ref}
|
|
701
|
-
className={cn(
|
|
702
|
-
"text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70",
|
|
703
|
-
className,
|
|
704
|
-
)}
|
|
705
|
-
{...props}
|
|
706
|
-
/>
|
|
707
|
-
),
|
|
708
|
-
)
|
|
709
|
-
Label.displayName = "Label"
|
|
710
|
-
|
|
711
|
-
export { Label }
|
|
712
|
-
`;
|
|
713
|
-
}
|
|
714
|
-
|
|
715
|
-
function uiBadge(): string {
|
|
716
|
-
return `import * as React from "react"
|
|
717
|
-
import { cva, type VariantProps } from "class-variance-authority"
|
|
718
|
-
import { cn } from "@/lib/utils"
|
|
719
|
-
|
|
720
|
-
const badgeVariants = cva(
|
|
721
|
-
"inline-flex items-center rounded-md border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2",
|
|
722
|
-
{
|
|
723
|
-
variants: {
|
|
724
|
-
variant: {
|
|
725
|
-
default: "border-transparent bg-primary text-primary-foreground shadow hover:bg-primary/80",
|
|
726
|
-
secondary: "border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80",
|
|
727
|
-
destructive: "border-transparent bg-destructive text-destructive-foreground shadow hover:bg-destructive/80",
|
|
728
|
-
outline: "text-foreground",
|
|
729
|
-
},
|
|
730
|
-
},
|
|
731
|
-
defaultVariants: { variant: "default" },
|
|
732
|
-
},
|
|
733
|
-
)
|
|
734
|
-
|
|
735
|
-
export interface BadgeProps
|
|
736
|
-
extends React.HTMLAttributes<HTMLDivElement>,
|
|
737
|
-
VariantProps<typeof badgeVariants> {}
|
|
738
|
-
|
|
739
|
-
function Badge({ className, variant, ...props }: BadgeProps) {
|
|
740
|
-
return <div className={cn(badgeVariants({ variant }), className)} {...props} />
|
|
741
|
-
}
|
|
742
|
-
|
|
743
|
-
export { Badge, badgeVariants }
|
|
744
|
-
`;
|
|
745
|
-
}
|
|
746
|
-
|
|
747
|
-
// ---------- mode: SPA (TanStack Router) ----------
|
|
748
|
-
|
|
749
|
-
function spaPackageJson(name: string): string {
|
|
750
|
-
return JSON.stringify(
|
|
751
|
-
{
|
|
752
|
-
name,
|
|
753
|
-
private: true,
|
|
754
|
-
version: "0.0.0",
|
|
755
|
-
type: "module",
|
|
756
|
-
scripts: {
|
|
757
|
-
dev: "vite dev --host",
|
|
758
|
-
build: "vite build",
|
|
759
|
-
preview: "vite preview",
|
|
760
|
-
lint: "biome check --write .",
|
|
761
|
-
},
|
|
762
|
-
dependencies: {
|
|
763
|
-
react: REACT,
|
|
764
|
-
"react-dom": REACT,
|
|
765
|
-
"@tanstack/react-router": TANSTACK,
|
|
766
|
-
"class-variance-authority": "^0.7.1",
|
|
767
|
-
clsx: "^2.1.1",
|
|
768
|
-
"tailwind-merge": "^2.6.0",
|
|
769
|
-
"lucide-react": "^0.468.0",
|
|
770
|
-
"@radix-ui/react-slot": "^1.1.1",
|
|
771
|
-
},
|
|
772
|
-
devDependencies: {
|
|
773
|
-
"@tanstack/router-plugin": TANSTACK,
|
|
774
|
-
"@types/react": "^19.0.0",
|
|
775
|
-
"@types/react-dom": "^19.0.0",
|
|
776
|
-
"@vitejs/plugin-react": "^4.3.4",
|
|
777
|
-
"@biomejs/biome": BIOME,
|
|
778
|
-
autoprefixer: "^10.4.20",
|
|
779
|
-
postcss: "^8.4.49",
|
|
780
|
-
tailwindcss: "^3.4.17",
|
|
781
|
-
typescript: TS,
|
|
782
|
-
vite: VITE,
|
|
783
|
-
},
|
|
784
|
-
},
|
|
785
|
-
null,
|
|
786
|
-
2,
|
|
787
|
-
);
|
|
788
|
-
}
|
|
789
|
-
|
|
790
|
-
function spaViteConfig(): string {
|
|
791
|
-
return `import { defineConfig } from "vite"
|
|
792
|
-
import react from "@vitejs/plugin-react"
|
|
793
|
-
import { TanStackRouterVite } from "@tanstack/router-plugin/vite"
|
|
794
|
-
import path from "node:path"
|
|
795
|
-
|
|
796
|
-
export default defineConfig({
|
|
797
|
-
// File-based routing: routes live in src/routes, routeTree.gen.ts is generated.
|
|
798
|
-
plugins: [TanStackRouterVite({ target: "react", autoCodeSplitting: true }), react()],
|
|
799
|
-
// allowedHosts:true is REQUIRED for the HelloLeo preview sandbox (proxied host).
|
|
800
|
-
server: { allowedHosts: true },
|
|
801
|
-
resolve: { alias: { "@": path.resolve(__dirname, "./src") } },
|
|
802
|
-
})
|
|
803
|
-
`;
|
|
804
|
-
}
|
|
805
|
-
|
|
806
|
-
function spaIndexHtml(fontFamily: string): string {
|
|
807
|
-
const fontParam = fontFamily.replace(/\s+/g, "+");
|
|
808
|
-
return `<!DOCTYPE html>
|
|
809
|
-
<html lang="en">
|
|
810
|
-
<head>
|
|
811
|
-
<meta charset="UTF-8" />
|
|
812
|
-
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
813
|
-
<link rel="preconnect" href="https://fonts.googleapis.com">
|
|
814
|
-
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
|
815
|
-
<link href="https://fonts.googleapis.com/css2?family=${fontParam}:wght@400;500;600;700&display=swap" rel="stylesheet">
|
|
816
|
-
<title>App</title>
|
|
817
|
-
</head>
|
|
818
|
-
<body>
|
|
819
|
-
<div id="root"></div>
|
|
820
|
-
<script type="module" src="/src/main.tsx"></script>
|
|
821
|
-
</body>
|
|
822
|
-
</html>
|
|
823
|
-
`;
|
|
824
|
-
}
|
|
825
|
-
|
|
826
|
-
function spaMainTsx(): string {
|
|
827
|
-
return `import React from "react"
|
|
828
|
-
import ReactDOM from "react-dom/client"
|
|
829
|
-
import { RouterProvider, createRouter } from "@tanstack/react-router"
|
|
830
|
-
import { routeTree } from "./routeTree.gen"
|
|
831
|
-
import "./styles/app.css"
|
|
832
|
-
|
|
833
|
-
const router = createRouter({ routeTree, defaultPreload: "intent", scrollRestoration: true })
|
|
834
|
-
|
|
835
|
-
declare module "@tanstack/react-router" {
|
|
836
|
-
interface Register {
|
|
837
|
-
router: typeof router
|
|
838
|
-
}
|
|
839
|
-
}
|
|
840
|
-
|
|
841
|
-
ReactDOM.createRoot(document.getElementById("root") as HTMLElement).render(
|
|
842
|
-
<React.StrictMode>
|
|
843
|
-
<RouterProvider router={router} />
|
|
844
|
-
</React.StrictMode>,
|
|
845
|
-
)
|
|
846
|
-
`;
|
|
847
|
-
}
|
|
848
|
-
|
|
849
|
-
function spaRootRoute(): string {
|
|
850
|
-
return `import { Outlet, createRootRoute } from "@tanstack/react-router"
|
|
851
|
-
|
|
852
|
-
export const Route = createRootRoute({
|
|
853
|
-
errorComponent: ({ error }) => (
|
|
854
|
-
<main className="mx-auto flex min-h-screen max-w-2xl flex-col items-center justify-center gap-3 p-8">
|
|
855
|
-
<h1 className="text-2xl font-heading font-semibold text-destructive">Something went wrong</h1>
|
|
856
|
-
<pre className="max-w-full overflow-auto rounded-md bg-muted p-4 text-left text-xs text-muted-foreground">{error instanceof Error ? error.message : String(error)}</pre>
|
|
857
|
-
</main>
|
|
858
|
-
),
|
|
859
|
-
notFoundComponent: () => (
|
|
860
|
-
<main className="mx-auto flex min-h-screen max-w-2xl flex-col items-center justify-center gap-2 p-8">
|
|
861
|
-
<h1 className="text-2xl font-heading font-semibold">404</h1>
|
|
862
|
-
<p className="text-muted-foreground">This page could not be found.</p>
|
|
863
|
-
</main>
|
|
864
|
-
),
|
|
865
|
-
component: () => (
|
|
866
|
-
<div className="min-h-screen bg-background text-foreground">
|
|
867
|
-
<Outlet />
|
|
868
|
-
</div>
|
|
869
|
-
),
|
|
870
|
-
})
|
|
871
|
-
`;
|
|
872
|
-
}
|
|
873
|
-
|
|
874
|
-
function spaIndexRoute(): string {
|
|
875
|
-
return `import { createFileRoute } from "@tanstack/react-router"
|
|
876
|
-
import { Button } from "@/components/ui/button"
|
|
877
|
-
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"
|
|
878
|
-
|
|
879
|
-
export const Route = createFileRoute("/")({ component: Home })
|
|
880
|
-
|
|
881
|
-
function Home() {
|
|
882
|
-
return (
|
|
883
|
-
<main className="mx-auto flex min-h-screen max-w-2xl flex-col items-center justify-center gap-6 p-8">
|
|
884
|
-
<Card className="w-full">
|
|
885
|
-
<CardHeader>
|
|
886
|
-
<CardTitle className="text-2xl">Welcome to your app</CardTitle>
|
|
887
|
-
<CardDescription>TanStack Router SPA · Tailwind · shadcn/ui · Biome</CardDescription>
|
|
888
|
-
</CardHeader>
|
|
889
|
-
<CardContent className="flex gap-3">
|
|
890
|
-
<Button>Get started</Button>
|
|
891
|
-
<Button variant="outline">Learn more</Button>
|
|
892
|
-
</CardContent>
|
|
893
|
-
</Card>
|
|
894
|
-
</main>
|
|
895
|
-
)
|
|
896
|
-
}
|
|
897
|
-
`;
|
|
898
|
-
}
|
|
899
|
-
|
|
900
|
-
// ---------- mode: SSR (TanStack Start on Cloudflare workerd) ----------
|
|
901
|
-
|
|
902
|
-
function ssrPackageJson(name: string): string {
|
|
903
|
-
return JSON.stringify(
|
|
904
|
-
{
|
|
905
|
-
name,
|
|
906
|
-
private: true,
|
|
907
|
-
version: "0.0.0",
|
|
908
|
-
type: "module",
|
|
909
|
-
scripts: {
|
|
910
|
-
// Node-target SSR (Nitro). vite dev for local; build emits
|
|
911
|
-
// .output/server/index.mjs (PORT-aware) which the CF Sandbox runs.
|
|
912
|
-
// `predev` runs automatically on `npm run dev` (the sandbox's launch
|
|
913
|
-
// command) → creates the DB tables before the server starts, so a
|
|
914
|
-
// DB query in a route loader never hits "no such table".
|
|
915
|
-
predev: "drizzle-kit push --force",
|
|
916
|
-
dev: "vite dev --host",
|
|
917
|
-
build: "vite build",
|
|
918
|
-
// `preview` is what HelloLeo's PM2 publish runs for vite projects to
|
|
919
|
-
// serve the production build. For node-SSR that's the Nitro server
|
|
920
|
-
// (honours PORT). `prepreview` runs first → ensures DB tables exist.
|
|
921
|
-
prepreview: "drizzle-kit push --force",
|
|
922
|
-
preview: "node .output/server/index.mjs",
|
|
923
|
-
start: "node .output/server/index.mjs",
|
|
924
|
-
"db:generate": "drizzle-kit generate",
|
|
925
|
-
"db:push": "drizzle-kit push --force",
|
|
926
|
-
deploy: "wrangler deploy",
|
|
927
|
-
lint: "biome check --write .",
|
|
928
|
-
},
|
|
929
|
-
dependencies: {
|
|
930
|
-
react: REACT,
|
|
931
|
-
"react-dom": REACT,
|
|
932
|
-
"@tanstack/react-router": TANSTACK,
|
|
933
|
-
"@tanstack/react-start": TANSTACK,
|
|
934
|
-
"drizzle-orm": "^0.36.4",
|
|
935
|
-
"@libsql/client": "^0.14.0",
|
|
936
|
-
"class-variance-authority": "^0.7.1",
|
|
937
|
-
clsx: "^2.1.1",
|
|
938
|
-
"tailwind-merge": "^2.6.0",
|
|
939
|
-
"lucide-react": "^0.468.0",
|
|
940
|
-
"@radix-ui/react-slot": "^1.1.1",
|
|
941
|
-
},
|
|
942
|
-
devDependencies: {
|
|
943
|
-
wrangler: "^4.20.0",
|
|
944
|
-
"@cloudflare/workers-types": "^4.20250601.0",
|
|
945
|
-
"drizzle-kit": "^0.28.1",
|
|
946
|
-
"@types/node": "^22.0.0",
|
|
947
|
-
"@types/react": "^19.0.0",
|
|
948
|
-
"@types/react-dom": "^19.0.0",
|
|
949
|
-
"@vitejs/plugin-react": "^4.3.4",
|
|
950
|
-
"@biomejs/biome": BIOME,
|
|
951
|
-
autoprefixer: "^10.4.20",
|
|
952
|
-
postcss: "^8.4.49",
|
|
953
|
-
tailwindcss: "^3.4.17",
|
|
954
|
-
typescript: TS,
|
|
955
|
-
vite: VITE,
|
|
956
|
-
},
|
|
957
|
-
},
|
|
958
|
-
null,
|
|
959
|
-
2,
|
|
960
|
-
);
|
|
961
|
-
}
|
|
962
|
-
|
|
963
|
-
function ssrViteConfig(): string {
|
|
964
|
-
return `import { defineConfig } from "vite"
|
|
965
|
-
import react from "@vitejs/plugin-react"
|
|
966
|
-
import { tanstackStart } from "@tanstack/react-start/plugin/vite"
|
|
967
|
-
import path from "node:path"
|
|
968
|
-
|
|
969
|
-
// TanStack Start default (Node server) target -> dist/server/server.js + dist/client.
|
|
970
|
-
// The CF Sandbox container runs that server (PORT-aware), proven by the POC.
|
|
971
|
-
export default defineConfig({
|
|
972
|
-
plugins: [tanstackStart({ customViteReactPlugin: true }), react()],
|
|
973
|
-
// allowedHosts:true is REQUIRED for the HelloLeo preview sandbox (proxied host).
|
|
974
|
-
server: { allowedHosts: true },
|
|
975
|
-
// Pre-bundle shared deps at boot so vite doesn't re-optimize + restart the dev
|
|
976
|
-
// server mid-request (which 500s the first SSR load with "transport disconnected").
|
|
977
|
-
optimizeDeps: {
|
|
978
|
-
include: ["clsx", "tailwind-merge", "class-variance-authority", "lucide-react"],
|
|
979
|
-
},
|
|
980
|
-
resolve: { alias: { "@": path.resolve(__dirname, "./src") } },
|
|
981
|
-
// __WORKERD__ tells src/db which runtime it's building for: true only for the
|
|
982
|
-
// prod cloudflare-module build (NITRO_PRESET set by the publish flow), false
|
|
983
|
-
// for dev and the Node preview build. Lets getDb dead-code-eliminate the wrong
|
|
984
|
-
// DB driver per target.
|
|
985
|
-
define: {
|
|
986
|
-
__WORKERD__: JSON.stringify(process.env.NITRO_PRESET === "cloudflare-module"),
|
|
987
|
-
},
|
|
988
|
-
// "cloudflare:workers" is a workerd-only module (src/db reads the D1 binding
|
|
989
|
-
// from it in prod). vite's SSR pass can't resolve it — externalize so the
|
|
990
|
-
// workerd build keeps it as an import; on other targets it's DCE'd away.
|
|
991
|
-
build: { rollupOptions: { external: ["cloudflare:workers"] } },
|
|
992
|
-
})
|
|
993
|
-
`;
|
|
994
|
-
}
|
|
995
|
-
|
|
996
|
-
function ssrWranglerJsonc(name: string): string {
|
|
997
|
-
return `{
|
|
998
|
-
// PROD target (Workers for Platforms). Dev/preview runs the Node server in the
|
|
999
|
-
// CF Sandbox container and does NOT use this file.
|
|
1000
|
-
//
|
|
1001
|
-
// NOTE: HelloLeo's publish flow REGENERATES this file at deploy time — it
|
|
1002
|
-
// auto-creates the D1 database, fills in database_id, and pins its own
|
|
1003
|
-
// compatibility_date. This file is the local-dev / manual \`wrangler deploy\`
|
|
1004
|
-
// copy; for a manual deploy, run \`wrangler d1 create ${name}\` and paste the
|
|
1005
|
-
// id below first. compatibility_date is kept at/under the runtime baseline so
|
|
1006
|
-
// \`wrangler dev\` works on the pinned workerd binary.
|
|
1007
|
-
"$schema": "node_modules/wrangler/config-schema.json",
|
|
1008
|
-
"name": "${name}",
|
|
1009
|
-
"compatibility_date": "2026-04-01",
|
|
1010
|
-
"compatibility_flags": ["nodejs_compat"],
|
|
1011
|
-
"observability": { "enabled": true },
|
|
1012
|
-
"assets": { "directory": ".output/public", "binding": "ASSETS" },
|
|
1013
|
-
"d1_databases": [
|
|
1014
|
-
{ "binding": "DB", "database_name": "${name}", "database_id": "" }
|
|
1015
|
-
]
|
|
1016
|
-
}
|
|
1017
|
-
`;
|
|
1018
|
-
}
|
|
1019
|
-
|
|
1020
|
-
function ssrRouter(): string {
|
|
1021
|
-
return `import { createRouter as createTanStackRouter } from "@tanstack/react-router"
|
|
1022
|
-
import { routeTree } from "./routeTree.gen"
|
|
1023
|
-
|
|
1024
|
-
export function createRouter() {
|
|
1025
|
-
return createTanStackRouter({
|
|
1026
|
-
routeTree,
|
|
1027
|
-
defaultPreload: "intent",
|
|
1028
|
-
scrollRestoration: true,
|
|
1029
|
-
})
|
|
1030
|
-
}
|
|
1031
|
-
|
|
1032
|
-
declare module "@tanstack/react-router" {
|
|
1033
|
-
interface Register {
|
|
1034
|
-
router: ReturnType<typeof createRouter>
|
|
1035
|
-
}
|
|
1036
|
-
}
|
|
1037
|
-
`;
|
|
1038
|
-
}
|
|
1039
|
-
|
|
1040
|
-
function ssrRootRoute(fontFamily: string): string {
|
|
1041
|
-
const fontParam = fontFamily.replace(/\s+/g, "+");
|
|
1042
|
-
return `import { Outlet, createRootRoute, HeadContent, Scripts } from "@tanstack/react-router"
|
|
1043
|
-
import appCss from "../styles/app.css?url"
|
|
1044
|
-
|
|
1045
|
-
export const Route = createRootRoute({
|
|
1046
|
-
head: () => ({
|
|
1047
|
-
meta: [
|
|
1048
|
-
{ charSet: "utf-8" },
|
|
1049
|
-
{ name: "viewport", content: "width=device-width, initial-scale=1" },
|
|
1050
|
-
{ title: "App" },
|
|
1051
|
-
],
|
|
1052
|
-
// The design-system font is loaded here (SSR has no index.html). Keep the
|
|
1053
|
-
// family in sync with --font-sans / --font-heading in src/styles/app.css.
|
|
1054
|
-
links: [
|
|
1055
|
-
{ rel: "preconnect", href: "https://fonts.googleapis.com" },
|
|
1056
|
-
{ rel: "preconnect", href: "https://fonts.gstatic.com", crossOrigin: "anonymous" },
|
|
1057
|
-
{ rel: "stylesheet", href: "https://fonts.googleapis.com/css2?family=${fontParam}:wght@400;500;600;700&display=swap" },
|
|
1058
|
-
{ rel: "stylesheet", href: appCss },
|
|
1059
|
-
],
|
|
1060
|
-
}),
|
|
1061
|
-
errorComponent: ({ error }) => (
|
|
1062
|
-
<main className="mx-auto flex min-h-screen max-w-2xl flex-col items-center justify-center gap-3 p-8">
|
|
1063
|
-
<h1 className="text-2xl font-heading font-semibold text-destructive">Something went wrong</h1>
|
|
1064
|
-
<pre className="max-w-full overflow-auto rounded-md bg-muted p-4 text-left text-xs text-muted-foreground">{error instanceof Error ? error.message : String(error)}</pre>
|
|
1065
|
-
</main>
|
|
1066
|
-
),
|
|
1067
|
-
notFoundComponent: () => (
|
|
1068
|
-
<main className="mx-auto flex min-h-screen max-w-2xl flex-col items-center justify-center gap-2 p-8">
|
|
1069
|
-
<h1 className="text-2xl font-heading font-semibold">404</h1>
|
|
1070
|
-
<p className="text-muted-foreground">This page could not be found.</p>
|
|
1071
|
-
</main>
|
|
1072
|
-
),
|
|
1073
|
-
component: RootComponent,
|
|
1074
|
-
})
|
|
1075
|
-
|
|
1076
|
-
function RootComponent() {
|
|
1077
|
-
return (
|
|
1078
|
-
<html lang="en">
|
|
1079
|
-
<head>
|
|
1080
|
-
<HeadContent />
|
|
1081
|
-
</head>
|
|
1082
|
-
<body className="min-h-screen bg-background text-foreground font-sans">
|
|
1083
|
-
<Outlet />
|
|
1084
|
-
<Scripts />
|
|
1085
|
-
</body>
|
|
1086
|
-
</html>
|
|
1087
|
-
)
|
|
1088
|
-
}
|
|
1089
|
-
`;
|
|
1090
|
-
}
|
|
1091
|
-
|
|
1092
|
-
function ssrIndexRoute(): string {
|
|
1093
|
-
return `import { createFileRoute } from "@tanstack/react-router"
|
|
1094
|
-
import { createServerFn } from "@tanstack/react-start"
|
|
1095
|
-
import { Button } from "@/components/ui/button"
|
|
1096
|
-
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"
|
|
1097
|
-
|
|
1098
|
-
// Server functions (TanStack Start 1.131.x API — IMPORTANT, do not invent
|
|
1099
|
-
// methods):
|
|
1100
|
-
// - No input: createServerFn({ method: "GET" }).handler(async () => {...})
|
|
1101
|
-
// - With input: createServerFn({ method: "POST" })
|
|
1102
|
-
// .validator((d: { title: string }) => d) // NOT .inputType()
|
|
1103
|
-
// .handler(async ({ data }) => { data.title }) // input is { data }
|
|
1104
|
-
// Call it as: addTask({ data: { title } }) // NOT addTask({ title })
|
|
1105
|
-
// - DB access: \`const db = await getDb()\` (async). Resolves D1 in prod,
|
|
1106
|
-
// libsql in dev. Safe in server fns AND route loaders — just
|
|
1107
|
-
// never at module top. Prefer server functions for mutations.
|
|
1108
|
-
// Example DB-backed server fn (dev=libsql, prod=D1, same code):
|
|
1109
|
-
// const getItems = createServerFn({ method: "GET" }).handler(async () => {
|
|
1110
|
-
// const db = await getDb()
|
|
1111
|
-
// return await db.select().from(items)
|
|
1112
|
-
// })
|
|
1113
|
-
const getServerInfo = createServerFn({ method: "GET" }).handler(async () => {
|
|
1114
|
-
return { renderedAt: new Date().toISOString() }
|
|
1115
|
-
})
|
|
1116
|
-
|
|
1117
|
-
export const Route = createFileRoute("/")({
|
|
1118
|
-
loader: () => getServerInfo(),
|
|
1119
|
-
component: Home,
|
|
1120
|
-
})
|
|
1121
|
-
|
|
1122
|
-
function Home() {
|
|
1123
|
-
const info = Route.useLoaderData()
|
|
1124
|
-
return (
|
|
1125
|
-
<main className="mx-auto flex min-h-screen max-w-2xl flex-col items-center justify-center gap-6 p-8">
|
|
1126
|
-
<Card className="w-full">
|
|
1127
|
-
<CardHeader>
|
|
1128
|
-
<CardTitle className="text-2xl">Welcome to your app</CardTitle>
|
|
1129
|
-
<CardDescription>TanStack Start SSR (workerd) · Tailwind · shadcn/ui · Biome · Drizzle/D1</CardDescription>
|
|
1130
|
-
</CardHeader>
|
|
1131
|
-
<CardContent className="flex flex-col gap-3">
|
|
1132
|
-
<p className="text-sm text-muted-foreground">Server-rendered at {info.renderedAt}</p>
|
|
1133
|
-
<div className="flex gap-3">
|
|
1134
|
-
<Button>Get started</Button>
|
|
1135
|
-
<Button variant="outline">Learn more</Button>
|
|
1136
|
-
</div>
|
|
1137
|
-
</CardContent>
|
|
1138
|
-
</Card>
|
|
1139
|
-
</main>
|
|
1140
|
-
)
|
|
1141
|
-
}
|
|
1142
|
-
`;
|
|
1143
|
-
}
|
|
1144
|
-
|
|
1145
|
-
function ssrDbSchema(): string {
|
|
1146
|
-
return `import { sqliteTable, text, integer } from "drizzle-orm/sqlite-core"
|
|
1147
|
-
|
|
1148
|
-
// Example table. dev uses local D1 (miniflare), prod uses the D1 binding "DB".
|
|
1149
|
-
export const items = sqliteTable("items", {
|
|
1150
|
-
id: integer("id").primaryKey({ autoIncrement: true }),
|
|
1151
|
-
title: text("title").notNull(),
|
|
1152
|
-
createdAt: integer("created_at", { mode: "timestamp" }).$defaultFn(() => new Date()),
|
|
1153
|
-
})
|
|
1154
|
-
`;
|
|
1155
|
-
}
|
|
1156
|
-
|
|
1157
|
-
function ssrDbIndex(): string {
|
|
1158
|
-
return `/// <reference types="@cloudflare/workers-types" />
|
|
1159
|
-
import { drizzle as drizzleD1 } from "drizzle-orm/d1"
|
|
1160
|
-
import * as schema from "./schema"
|
|
1161
|
-
|
|
1162
|
-
// Resolve the DB for the CURRENT request. ASYNC — always \`await getDb()\`.
|
|
1163
|
-
// Works from ANYWHERE on the server (route loaders, server functions,
|
|
1164
|
-
// beforeLoad) because it does NOT depend on AsyncLocalStorage / getEvent (which
|
|
1165
|
-
// is only populated inside server functions under workerd — calling getDb in a
|
|
1166
|
-
// loader the old way threw "No HTTPEvent found in AsyncLocalStorage").
|
|
1167
|
-
// - PROD (workerd): the D1 binding read straight from the per-request env.
|
|
1168
|
-
// - DEV (node SSR): a local libsql file, imported lazily and behind the DEV
|
|
1169
|
-
// guard so neither @libsql/client nor node:stream is ever bundled into the
|
|
1170
|
-
// workerd build (which would break it).
|
|
1171
|
-
// Same sqlite schema both ways.
|
|
1172
|
-
// __WORKERD__ is replaced at build time by vite (see vite.config.ts): true only
|
|
1173
|
-
// for the prod cloudflare-module build, false for dev + the Node preview build.
|
|
1174
|
-
// Using it (not import.meta.env.DEV) is what lets the Node preview — also a
|
|
1175
|
-
// production build — still take the libsql path. It's a literal, so the unused
|
|
1176
|
-
// branch (and its driver) is dead-code-eliminated from each bundle.
|
|
1177
|
-
declare const __WORKERD__: boolean
|
|
1178
|
-
|
|
1179
|
-
export async function getDb() {
|
|
1180
|
-
if (__WORKERD__) {
|
|
1181
|
-
const { env } = await import("cloudflare:workers")
|
|
1182
|
-
return drizzleD1((env as unknown as { DB: D1Database }).DB, { schema })
|
|
1183
|
-
}
|
|
1184
|
-
const { drizzle } = await import("drizzle-orm/libsql")
|
|
1185
|
-
const { createClient } = await import("@libsql/client")
|
|
1186
|
-
return drizzle(
|
|
1187
|
-
createClient({ url: process.env.DATABASE_URL ?? "file:./local.db" }),
|
|
1188
|
-
{ schema },
|
|
1189
|
-
)
|
|
1190
|
-
}
|
|
1191
|
-
`;
|
|
1192
|
-
}
|
|
1193
|
-
|
|
1194
|
-
function ssrDrizzleConfig(): string {
|
|
1195
|
-
return `import { defineConfig } from "drizzle-kit"
|
|
1196
|
-
|
|
1197
|
-
// Dev/preview: push the schema straight into the local libsql file (no D1 creds
|
|
1198
|
-
// needed) — this is what \`predev\` runs so tables exist before the dev server.
|
|
1199
|
-
// Prod (D1) uses generated migrations applied via wrangler, not this push.
|
|
1200
|
-
export default defineConfig({
|
|
1201
|
-
dialect: "sqlite",
|
|
1202
|
-
schema: "./src/db/schema.ts",
|
|
1203
|
-
out: "./drizzle",
|
|
1204
|
-
dbCredentials: { url: process.env.DATABASE_URL ?? "file:./local.db" },
|
|
1205
|
-
})
|
|
1206
|
-
`;
|
|
1207
|
-
}
|
|
1208
|
-
|
|
1209
|
-
function gitignore(mode: string): string {
|
|
1210
|
-
const base = `node_modules
|
|
1211
|
-
dist
|
|
1212
|
-
.output
|
|
1213
|
-
.nitro
|
|
1214
|
-
.tanstack
|
|
1215
|
-
.vite
|
|
1216
|
-
local.db
|
|
1217
|
-
*.local
|
|
1218
|
-
.DS_Store
|
|
1219
|
-
|
|
1220
|
-
# Generated by the TanStack Router/Start vite plugin
|
|
1221
|
-
src/routeTree.gen.ts
|
|
1222
|
-
`;
|
|
1223
|
-
return mode === "ssr" ? `${base}.wrangler\n.dev.vars\n` : base;
|
|
1224
|
-
}
|
|
1225
|
-
|
|
1226
|
-
// ---------- writer ----------
|
|
1227
|
-
|
|
1228
|
-
interface ScaffoldAppOptions {
|
|
1229
|
-
name?: string;
|
|
1230
|
-
mode?: "spa" | "ssr";
|
|
1231
|
-
primaryColor?: string;
|
|
1232
|
-
fontFamily?: string;
|
|
1233
|
-
borderRadius?: string;
|
|
1234
|
-
}
|
|
1235
|
-
|
|
1236
|
-
async function writeAppScaffold(
|
|
1237
|
-
opts: ScaffoldAppOptions,
|
|
1238
|
-
dir: string,
|
|
1239
|
-
): Promise<string[]> {
|
|
1240
|
-
const name = opts.name ?? "leo-app";
|
|
1241
|
-
const mode = opts.mode ?? "spa";
|
|
1242
|
-
const primary = normalizePrimaryColor(
|
|
1243
|
-
opts.primaryColor ?? "221.2 83.2% 53.3%",
|
|
1244
|
-
);
|
|
1245
|
-
const font = opts.fontFamily ?? "Inter";
|
|
1246
|
-
const radius = opts.borderRadius ?? "0.5rem";
|
|
1247
|
-
|
|
1248
|
-
await mkdir(path.join(dir, "src", "lib"), { recursive: true });
|
|
1249
|
-
await mkdir(path.join(dir, "src", "components", "ui"), { recursive: true });
|
|
1250
|
-
await mkdir(path.join(dir, "src", "routes"), { recursive: true });
|
|
1251
|
-
await mkdir(path.join(dir, "src", "styles"), { recursive: true });
|
|
1252
|
-
|
|
1253
|
-
// Files common to both modes.
|
|
1254
|
-
const files: [string, string][] = [
|
|
1255
|
-
["tsconfig.json", tsconfig()],
|
|
1256
|
-
["biome.json", biomeJson()],
|
|
1257
|
-
["components.json", componentsJson()],
|
|
1258
|
-
["tailwind.config.js", tailwindConfig()],
|
|
1259
|
-
["postcss.config.js", postcssConfig()],
|
|
1260
|
-
[".gitignore", gitignore(mode)],
|
|
1261
|
-
[path.join("src", "styles", "app.css"), indexCss(primary, font, radius)],
|
|
1262
|
-
[path.join("src", "lib", "utils.ts"), utilsTs()],
|
|
1263
|
-
[path.join("src", "lib", "proxy.ts"), proxyTs()],
|
|
1264
|
-
[path.join("src", "components", "ui", "button.tsx"), uiButton()],
|
|
1265
|
-
[path.join("src", "components", "ui", "card.tsx"), uiCard()],
|
|
1266
|
-
[path.join("src", "components", "ui", "input.tsx"), uiInput()],
|
|
1267
|
-
[path.join("src", "components", "ui", "textarea.tsx"), uiTextarea()],
|
|
1268
|
-
[path.join("src", "components", "ui", "label.tsx"), uiLabel()],
|
|
1269
|
-
[path.join("src", "components", "ui", "badge.tsx"), uiBadge()],
|
|
1270
|
-
];
|
|
1271
|
-
|
|
1272
|
-
if (mode === "spa") {
|
|
1273
|
-
files.push(
|
|
1274
|
-
["package.json", spaPackageJson(name)],
|
|
1275
|
-
["vite.config.ts", spaViteConfig()],
|
|
1276
|
-
["index.html", spaIndexHtml(font)],
|
|
1277
|
-
[path.join("src", "main.tsx"), spaMainTsx()],
|
|
1278
|
-
[path.join("src", "routes", "__root.tsx"), spaRootRoute()],
|
|
1279
|
-
[path.join("src", "routes", "index.tsx"), spaIndexRoute()],
|
|
1280
|
-
);
|
|
1281
|
-
} else {
|
|
1282
|
-
await mkdir(path.join(dir, "src", "db"), { recursive: true });
|
|
1283
|
-
files.push(
|
|
1284
|
-
["package.json", ssrPackageJson(name)],
|
|
1285
|
-
["vite.config.ts", ssrViteConfig()],
|
|
1286
|
-
["wrangler.jsonc", ssrWranglerJsonc(name)],
|
|
1287
|
-
["drizzle.config.ts", ssrDrizzleConfig()],
|
|
1288
|
-
[path.join("src", "router.tsx"), ssrRouter()],
|
|
1289
|
-
[path.join("src", "routes", "__root.tsx"), ssrRootRoute(font)],
|
|
1290
|
-
[path.join("src", "routes", "index.tsx"), ssrIndexRoute()],
|
|
1291
|
-
[path.join("src", "db", "schema.ts"), ssrDbSchema()],
|
|
1292
|
-
[path.join("src", "db", "index.ts"), ssrDbIndex()],
|
|
1293
|
-
);
|
|
1294
|
-
}
|
|
1295
|
-
|
|
1296
|
-
const created: string[] = [];
|
|
1297
|
-
for (const [filePath, content] of files) {
|
|
1298
|
-
await Bun.write(path.join(dir, filePath), content);
|
|
1299
|
-
created.push(filePath);
|
|
1300
|
-
}
|
|
1301
|
-
return created.sort();
|
|
1302
|
-
}
|
|
1303
|
-
|
|
1304
|
-
export const scaffoldApp = tool({
|
|
445
|
+
export const scaffold = tool({
|
|
1305
446
|
description:
|
|
1306
|
-
"Scaffold a
|
|
1307
|
-
"
|
|
1308
|
-
"
|
|
1309
|
-
"
|
|
1310
|
-
"
|
|
1311
|
-
"
|
|
1312
|
-
"
|
|
1313
|
-
"src/styles/app.css (design tokens, light+dark), src/lib/utils.ts + proxy.ts, " +
|
|
1314
|
-
"shadcn primitives in src/components/ui (Button, Card, Input, Textarea, " +
|
|
1315
|
-
"Label, Badge), and TanStack routes in src/routes. " +
|
|
1316
|
-
"Call this FIRST for any new project before writing feature routes/components. " +
|
|
1317
|
-
"For NetSuite projects use `setup_netsuite` instead.",
|
|
447
|
+
"Scaffold a new React + Vite + Tailwind CSS project with the HelloLeo design system. " +
|
|
448
|
+
"Creates all mandatory boilerplate: package.json, index.html (with Google Fonts), " +
|
|
449
|
+
"vite.config.js, tailwind.config.js, postcss.config.js, src/index.css (CSS variables " +
|
|
450
|
+
"light+dark), src/lib/utils.ts (cn helper), src/lib/proxy.ts, and src/main.tsx. " +
|
|
451
|
+
"Call this FIRST for any new project before writing App.tsx and components. " +
|
|
452
|
+
"For NetSuite projects (NetSuite MCP in the live list), call `setup_netsuite` " +
|
|
453
|
+
"INSTEAD -- it bundles base scaffold + OAuth wiring in one call. Do not call both.",
|
|
1318
454
|
args: {
|
|
1319
455
|
name: tool.schema
|
|
1320
456
|
.string()
|
|
1321
457
|
.optional()
|
|
1322
|
-
.describe("Project name for package.json (default: leo-
|
|
1323
|
-
mode: tool.schema
|
|
1324
|
-
.enum(["spa", "ssr"])
|
|
1325
|
-
.optional()
|
|
1326
|
-
.describe(
|
|
1327
|
-
"'spa' = TanStack Router client-only (default). 'ssr' = TanStack Start " +
|
|
1328
|
-
"on Cloudflare workerd with SSR + D1/Drizzle. Pick 'ssr' when the app " +
|
|
1329
|
-
"needs a backend, server functions, auth, or a database.",
|
|
1330
|
-
),
|
|
458
|
+
.describe("Project name for package.json (default: leo-project)"),
|
|
1331
459
|
primaryColor: tool.schema
|
|
1332
460
|
.string()
|
|
1333
461
|
.optional()
|
|
1334
462
|
.describe(
|
|
1335
|
-
|
|
1336
|
-
'Hex like "#3b82f6" is
|
|
463
|
+
"Primary brand color. Prefer raw HSL triplet (no hsl() wrapper, no # prefix), " +
|
|
464
|
+
'e.g. "221.2 83.2% 53.3%". Hex like "#3b82f6" is also accepted and will ' +
|
|
465
|
+
"be auto-converted. Default: blue.",
|
|
1337
466
|
),
|
|
1338
467
|
fontFamily: tool.schema
|
|
1339
468
|
.string()
|
|
1340
469
|
.optional()
|
|
1341
|
-
.describe(
|
|
470
|
+
.describe(
|
|
471
|
+
'Google Font family name, e.g. "Inter", "Poppins" (default: Inter)',
|
|
472
|
+
),
|
|
1342
473
|
borderRadius: tool.schema
|
|
1343
474
|
.string()
|
|
1344
475
|
.optional()
|
|
1345
|
-
.describe(
|
|
476
|
+
.describe(
|
|
477
|
+
'Base border radius, e.g. "0.5rem", "0.75rem" (default: 0.5rem)',
|
|
478
|
+
),
|
|
1346
479
|
},
|
|
1347
480
|
async execute(args, context) {
|
|
1348
|
-
const
|
|
1349
|
-
const created = await writeAppScaffold(args, context.directory);
|
|
1350
|
-
const next =
|
|
1351
|
-
mode === "ssr"
|
|
1352
|
-
? [
|
|
1353
|
-
"Mode: SSR (TanStack Start, Node server — runs in the CF Sandbox container).",
|
|
1354
|
-
"Dev server: `npm run dev`.",
|
|
1355
|
-
"Add routes in src/routes/*.tsx; server logic via createServerFn.",
|
|
1356
|
-
"Database: edit src/db/schema.ts, `npm run db:push`, query via `db` from src/db.",
|
|
1357
|
-
" (dev = local libsql file; prod = D1, see wrangler.jsonc).",
|
|
1358
|
-
]
|
|
1359
|
-
: [
|
|
1360
|
-
"Mode: SPA (TanStack Router, client-only).",
|
|
1361
|
-
"Dev server: `npm run dev`.",
|
|
1362
|
-
"Add routes in src/routes/*.tsx (file-based; routeTree.gen.ts is generated).",
|
|
1363
|
-
];
|
|
481
|
+
const created = await writeBaseScaffold(args, context.directory);
|
|
1364
482
|
return [
|
|
1365
|
-
`Scaffolded ${created.length} files
|
|
483
|
+
`Scaffolded ${created.length} files:`,
|
|
1366
484
|
...created.map((f) => ` - ${f}`),
|
|
1367
485
|
"",
|
|
1368
|
-
|
|
1369
|
-
"",
|
|
1370
|
-
"
|
|
1371
|
-
"
|
|
1372
|
-
"
|
|
1373
|
-
"
|
|
1374
|
-
"
|
|
1375
|
-
"Lint/format: `npm run lint` (Biome).",
|
|
486
|
+
"Next: create src/App.tsx with your application code.",
|
|
487
|
+
"Design system reference: read the header comment in src/index.css",
|
|
488
|
+
" before adding tokens or styling primitives.",
|
|
489
|
+
"Integration calls: use callIntegration() from src/lib/proxy.ts.",
|
|
490
|
+
" Header comment in that file documents slugs, response unwrapping,",
|
|
491
|
+
" pagination, and frontend-only vs HelloLeo Cloud routing.",
|
|
492
|
+
"Update package.json with any extra packages you need after scaffolding.",
|
|
1376
493
|
].join("\n");
|
|
1377
494
|
},
|
|
1378
495
|
});
|
|
1379
496
|
|
|
1380
|
-
// Registered as `scaffold` — this is THE scaffold tool (replaced the legacy
|
|
1381
|
-
// React+Vite `scaffold`). The coder UI keys its scaffold events off this name.
|
|
1382
497
|
export const ScaffoldPlugin: Plugin = async () => {
|
|
1383
498
|
return {
|
|
1384
|
-
tool: { scaffold:
|
|
499
|
+
tool: { scaffold: scaffold },
|
|
1385
500
|
};
|
|
1386
501
|
};
|