@helloleo/plugins 0.2.4 → 0.2.5
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 +32 -13
- package/dist/templates/tanstack-router-2026-06-15/index.html +4 -0
- package/dist/templates/tanstack-router-2026-06-15/src/components/error-component.tsx +47 -0
- package/dist/templates/tanstack-router-2026-06-15/src/main.tsx +4 -0
- package/dist/templates/tanstack-router-2026-06-15/src/routes/README.md +85 -0
- package/dist/templates/tanstack-router-2026-06-15/src/routes/__root.tsx +2 -0
- package/package.json +1 -1
- package/src/templates/tanstack-router-2026-06-15/index.html +4 -0
- package/src/templates/tanstack-router-2026-06-15/src/components/error-component.tsx +47 -0
- package/src/templates/tanstack-router-2026-06-15/src/main.tsx +4 -0
- package/src/templates/tanstack-router-2026-06-15/src/routes/README.md +85 -0
- package/src/templates/tanstack-router-2026-06-15/src/routes/__root.tsx +2 -0
- package/src/tools/scaffold.ts +71 -23
package/dist/index.js
CHANGED
|
@@ -24900,19 +24900,25 @@ async function renderTemplate(templateId, vars, destDir) {
|
|
|
24900
24900
|
}
|
|
24901
24901
|
|
|
24902
24902
|
// src/tools/scaffold.ts
|
|
24903
|
-
var
|
|
24903
|
+
var SCAFFOLD_TEMPLATES = {
|
|
24904
|
+
"tanstack-start": "tanstack-start-2026-06-18",
|
|
24905
|
+
"tanstack-router": "tanstack-router-2026-06-15"
|
|
24906
|
+
};
|
|
24907
|
+
var SCAFFOLD_VERSION = SCAFFOLD_TEMPLATES["tanstack-start"];
|
|
24904
24908
|
function projectUuidFromDir(dir) {
|
|
24905
24909
|
return path3.basename(dir);
|
|
24906
24910
|
}
|
|
24907
24911
|
var scaffoldOptionsSchema = exports_external2.object({
|
|
24908
24912
|
name: exports_external2.string().describe("Human-readable name for SEO, e.g. 'Shipping Platform'"),
|
|
24909
|
-
description: exports_external2.string().describe("One sentence describing the app. Used as the meta description.")
|
|
24913
|
+
description: exports_external2.string().describe("One sentence describing the app. Used as the meta description."),
|
|
24914
|
+
type: exports_external2.enum(["tanstack-start", "tanstack-router"]).optional().describe("Project type. 'tanstack-router': " + "client-only SPA with no server and no database \u2014 use it only for " + "pure frontends that talk to external APIs." + "'tanstack-start': SSR app with API routes and a " + "database, KV and bucket \u2014 use it whenever the app " + "needs to persist data or run server-side code (experimental, " + "use only if the user expresses a really complexe application idea).")
|
|
24910
24915
|
});
|
|
24911
24916
|
function slugify(name) {
|
|
24912
24917
|
return name.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "") || "leo-project";
|
|
24913
24918
|
}
|
|
24914
|
-
async function writeBaseScaffold({ name, description }, dir) {
|
|
24915
|
-
const
|
|
24919
|
+
async function writeBaseScaffold({ name, description, type }, dir) {
|
|
24920
|
+
const templateId = SCAFFOLD_TEMPLATES[type ?? "tanstack-start"];
|
|
24921
|
+
const created = await renderTemplate(templateId, {
|
|
24916
24922
|
LEO_NAME: name,
|
|
24917
24923
|
LEO_SLUG: slugify(name),
|
|
24918
24924
|
LEO_DESCRIPTION: description,
|
|
@@ -24920,36 +24926,49 @@ async function writeBaseScaffold({ name, description }, dir) {
|
|
|
24920
24926
|
}, dir);
|
|
24921
24927
|
await mkdir(path3.join(dir, ".leo"), { recursive: true });
|
|
24922
24928
|
const leoPath = path3.join(".leo", "project.json");
|
|
24923
|
-
await Bun.write(path3.join(dir, leoPath), JSON.stringify({ $schema: 1, scaffold:
|
|
24929
|
+
await Bun.write(path3.join(dir, leoPath), JSON.stringify({ $schema: 1, scaffold: templateId }, null, 2));
|
|
24924
24930
|
return [...created, leoPath];
|
|
24925
24931
|
}
|
|
24926
24932
|
var scaffold = tool({
|
|
24927
|
-
description: "Scaffold a new
|
|
24933
|
+
description: "Scaffold a new project with React 19, file-based routing (TanStack), " + "Tailwind v4, shadcn/ui, and react-query. Two types: 'tanstack-start' " + "(SSR, API routes, and a database, KV and bucket via @helloleo/runtime) " + "or 'tanstack-router' (client-only SPA, no server, no database). Call " + "this first for any new project, before writing routes or components.",
|
|
24928
24934
|
args: {
|
|
24929
24935
|
name: scaffoldOptionsSchema.shape.name,
|
|
24930
|
-
description: scaffoldOptionsSchema.shape.description
|
|
24936
|
+
description: scaffoldOptionsSchema.shape.description,
|
|
24937
|
+
type: scaffoldOptionsSchema.shape.type
|
|
24931
24938
|
},
|
|
24932
24939
|
async execute(args, context) {
|
|
24940
|
+
const type = args.type ?? "tanstack-start";
|
|
24933
24941
|
const created = await writeBaseScaffold(args, context.directory);
|
|
24934
24942
|
const uiComponents = created.filter((f) => f.startsWith("src/components/ui/"));
|
|
24935
24943
|
const rest = created.filter((f) => !f.startsWith("src/components/ui/"));
|
|
24944
|
+
const typeNotes = type === "tanstack-start" ? [
|
|
24945
|
+
"- This is a TanStack Start project. The server entry is src/server.ts. There is no index.html or client entrypoint.",
|
|
24946
|
+
"- For JSON/CRUD APIs, add server routes under src/routes/api/. Read src/routes/api/health.ts first, add routes as needed.",
|
|
24947
|
+
"- The database schema lives in src/db/schema.ts. Migrations run automatically."
|
|
24948
|
+
] : [
|
|
24949
|
+
"- This is a TanStack Router SPA. The client entry is index.html -> src/main.tsx. There is no server: no API routes, no server functions, no database.",
|
|
24950
|
+
"- Fetch data from external APIs only. If the app needs its own persistence or server-side code, it should have been scaffolded as tanstack-start."
|
|
24951
|
+
];
|
|
24952
|
+
const nextSteps = type === "tanstack-start" ? [
|
|
24953
|
+
"Next: read src/routes/index.tsx and continue with the user's request.",
|
|
24954
|
+
"For authentication (password and/or OAuth), call setup_authentication.",
|
|
24955
|
+
"For NetSuite projects, call setup_netsuite."
|
|
24956
|
+
] : [
|
|
24957
|
+
"Next: read src/routes/index.tsx and continue with the user's request."
|
|
24958
|
+
];
|
|
24936
24959
|
return [
|
|
24937
24960
|
`Scaffolded ${created.length} files:`,
|
|
24938
24961
|
...rest.map((f) => ` - ${f}`),
|
|
24939
24962
|
` - src/components/ui/ (${uiComponents.length} shadcn/ui components)`,
|
|
24940
24963
|
"",
|
|
24941
24964
|
"Notes:",
|
|
24942
|
-
|
|
24965
|
+
...typeNotes,
|
|
24943
24966
|
"- Add routes as files in src/routes/. Do not default export route components. Routing and layout conventions are documented in src/routes/README.md, read it before adding routes.",
|
|
24944
|
-
"- For JSON/CRUD APIs, add server routes under src/routes/api/. Read src/routes/api/health.ts first, add routes as needed.",
|
|
24945
24967
|
"- shadcn/ui components are already in src/components/ui.",
|
|
24946
24968
|
"- Design system: style exclusively with the token utilities, never hardcoded colors. To change the look, edit the token values in src/styles.css (light and dark blocks).",
|
|
24947
|
-
"- The database schema lives in src/db/schema.ts. Migrations run automatically.",
|
|
24948
24969
|
"- Always fetch data with react-query (useQuery and useMutation).",
|
|
24949
24970
|
"",
|
|
24950
|
-
|
|
24951
|
-
"For authentication (password and/or OAuth), call setup_authentication.",
|
|
24952
|
-
"For NetSuite projects, call setup_netsuite."
|
|
24971
|
+
...nextSteps
|
|
24953
24972
|
].join(`
|
|
24954
24973
|
`);
|
|
24955
24974
|
}
|
|
@@ -5,6 +5,10 @@
|
|
|
5
5
|
<meta charset="UTF-8" />
|
|
6
6
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
7
7
|
<title>{{LEO_NAME}}</title>
|
|
8
|
+
<meta name="description" content="{{LEO_DESCRIPTION}}" />
|
|
9
|
+
<meta property="og:title" content="{{LEO_NAME}}" />
|
|
10
|
+
<meta property="og:description" content="{{LEO_DESCRIPTION}}" />
|
|
11
|
+
<meta property="og:type" content="website" />
|
|
8
12
|
<link rel="preconnect" href="https://fonts.googleapis.com">
|
|
9
13
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
|
10
14
|
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=Space+Grotesk:wght@400;500;600;700&display=swap" rel="stylesheet">
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { useState } from 'react'
|
|
2
|
+
import { useRouterState } from '@tanstack/react-router'
|
|
3
|
+
import { Button } from '@/components/ui/button'
|
|
4
|
+
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
|
|
5
|
+
|
|
6
|
+
// Rendered by the router's defaultErrorComponent for any uncaught render or
|
|
7
|
+
// loader error. Per-route `errorComponent` options override it. Keep this
|
|
8
|
+
// component free of app dependencies (toasts, auth, etc.) -- it must still
|
|
9
|
+
// render when the rest of the app is broken.
|
|
10
|
+
export function DefaultErrorComponent({ error }: { error: Error }) {
|
|
11
|
+
const pathname = useRouterState({ select: (s) => s.location.pathname })
|
|
12
|
+
const [copied, setCopied] = useState(false)
|
|
13
|
+
|
|
14
|
+
async function copyForLeo() {
|
|
15
|
+
// Blank lines + fenced stack so the report keeps its structure when
|
|
16
|
+
// pasted into a markdown-rendering chat.
|
|
17
|
+
const report = [
|
|
18
|
+
'My app threw an error, please fix it:',
|
|
19
|
+
`Page: ${pathname}`,
|
|
20
|
+
`Error: ${error.message}`,
|
|
21
|
+
['Stack:', '```', error.stack ?? '(no stack trace)', '```'].join('\n'),
|
|
22
|
+
].join('\n\n')
|
|
23
|
+
|
|
24
|
+
await navigator.clipboard.writeText(report)
|
|
25
|
+
setCopied(true)
|
|
26
|
+
setTimeout(() => setCopied(false), 2000)
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
return (
|
|
30
|
+
<div className="min-h-screen flex items-center justify-center bg-background p-4">
|
|
31
|
+
<Card className="w-full max-w-lg">
|
|
32
|
+
<CardHeader>
|
|
33
|
+
<CardTitle>Something went wrong</CardTitle>
|
|
34
|
+
</CardHeader>
|
|
35
|
+
<CardContent className="space-y-4">
|
|
36
|
+
<pre className="text-sm text-destructive whitespace-pre-wrap break-words rounded-md bg-muted p-3">
|
|
37
|
+
{error.message}
|
|
38
|
+
</pre>
|
|
39
|
+
<Button onClick={copyForLeo}>{copied ? 'Copied!' : 'Copy error'}</Button>
|
|
40
|
+
<p className="text-sm text-muted-foreground">
|
|
41
|
+
Paste the copied error to Leo and it will help you fix it.
|
|
42
|
+
</p>
|
|
43
|
+
</CardContent>
|
|
44
|
+
</Card>
|
|
45
|
+
</div>
|
|
46
|
+
)
|
|
47
|
+
}
|
|
@@ -2,6 +2,7 @@ import ReactDOM from 'react-dom/client'
|
|
|
2
2
|
import { RouterProvider, createRouter } from '@tanstack/react-router'
|
|
3
3
|
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
|
|
4
4
|
import { routeTree } from './routeTree.gen'
|
|
5
|
+
import { DefaultErrorComponent } from '@/components/error-component'
|
|
5
6
|
|
|
6
7
|
const queryClient = new QueryClient()
|
|
7
8
|
|
|
@@ -12,6 +13,9 @@ const router = createRouter({
|
|
|
12
13
|
// on every preload/visit and React Query owns caching from there.
|
|
13
14
|
defaultPreloadStaleTime: 0,
|
|
14
15
|
scrollRestoration: true,
|
|
16
|
+
// Catches uncaught render/loader errors on any route. Routes can still
|
|
17
|
+
// set their own `errorComponent` to override this.
|
|
18
|
+
defaultErrorComponent: DefaultErrorComponent,
|
|
15
19
|
context: { queryClient },
|
|
16
20
|
})
|
|
17
21
|
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
# Routes
|
|
2
|
+
|
|
3
|
+
TanStack Router uses file-based routing. Every `.ts` and `.tsx` file in this directory is a route. Do not create `src/pages/`, `app/layout.tsx`, or similar, those are Next.js / Remix conventions. The only root layout is `src/routes/__root.tsx`.
|
|
4
|
+
|
|
5
|
+
## Conventions
|
|
6
|
+
|
|
7
|
+
| File | URL |
|
|
8
|
+
| --- | --- |
|
|
9
|
+
| `index.tsx` | `/` |
|
|
10
|
+
| `about.tsx` | `/about` |
|
|
11
|
+
| `users/index.tsx` | `/users` |
|
|
12
|
+
| `users/$id.tsx` | `/users/:id` (dynamic — bare `$`, no curly braces) |
|
|
13
|
+
| `posts.$postId.tsx` | `/posts/:postId` (dots nest without folders) |
|
|
14
|
+
| `admin/route.tsx` | layout for every `/admin/*` page; must render `<Outlet />` |
|
|
15
|
+
| `admin_.login.tsx` | `/admin/login`, but outside the admin layout (trailing `_` escapes it) |
|
|
16
|
+
| `_auth.tsx` | pathless layout; wraps `_auth/*` files without adding a URL segment |
|
|
17
|
+
| `__root.tsx` | app shell, wraps every page; preserve `<Outlet />` |
|
|
18
|
+
|
|
19
|
+
## Rules
|
|
20
|
+
|
|
21
|
+
- A layout route without `<Outlet />` renders its children nowhere.
|
|
22
|
+
- Do not `export default` route components, it breaks code splitting. Use a named function passed as `component:`.
|
|
23
|
+
- `routeTree.gen.ts` is auto-generated. Don't edit it by hand.
|
|
24
|
+
- This is a client-only SPA — there are no server routes or server functions. Fetch data from external APIs with React Query.
|
|
25
|
+
|
|
26
|
+
## Authenticated routes
|
|
27
|
+
|
|
28
|
+
Put protected pages inside a pathless `_authed` layout and keep login/signup
|
|
29
|
+
at the top level, outside it. Never special-case the login pathname inside
|
|
30
|
+
the layout guard.
|
|
31
|
+
|
|
32
|
+
```
|
|
33
|
+
routes/
|
|
34
|
+
__root.tsx loads the session into router context (context.user)
|
|
35
|
+
_authed.tsx pathless guard — no URL segment of its own
|
|
36
|
+
_authed/
|
|
37
|
+
dashboard.tsx /dashboard (protected)
|
|
38
|
+
posts.tsx /posts (protected)
|
|
39
|
+
index.tsx / (public)
|
|
40
|
+
login.tsx /login (public)
|
|
41
|
+
logout.tsx /logout
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
`_authed.tsx` does the guarding, all children inherit it:
|
|
45
|
+
|
|
46
|
+
```tsx _authed.tsx
|
|
47
|
+
export const Route = createFileRoute('/_authed')({
|
|
48
|
+
beforeLoad: ({ context }) => {
|
|
49
|
+
if (!context.user) {
|
|
50
|
+
throw redirect({ to: '/login' })
|
|
51
|
+
}
|
|
52
|
+
},
|
|
53
|
+
})
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
The session is loaded once in `__root.tsx`'s `beforeLoad` and returned into
|
|
57
|
+
the router context so every route can read `context.user`. `getUser()` is
|
|
58
|
+
whatever your auth uses (e.g. verify a JWT from localStorage) and resolves to
|
|
59
|
+
the user or null:
|
|
60
|
+
|
|
61
|
+
```tsx __root.tsx
|
|
62
|
+
// (keep the existing component, add this)
|
|
63
|
+
export const Route = createRootRouteWithContext<{ queryClient: QueryClient }>()({
|
|
64
|
+
beforeLoad: async () => {
|
|
65
|
+
const user = await getUser()
|
|
66
|
+
return { user }
|
|
67
|
+
},
|
|
68
|
+
// ...component unchanged
|
|
69
|
+
})
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
Components read it with `Route.useRouteContext()` (e.g. to swap a Login link
|
|
73
|
+
for a Logout button in the shell):
|
|
74
|
+
|
|
75
|
+
```tsx
|
|
76
|
+
function Route() {
|
|
77
|
+
const { user } = Route.useRouteContext()
|
|
78
|
+
|
|
79
|
+
return (
|
|
80
|
+
<nav>
|
|
81
|
+
{user ? <Link to="/logout">Logout</Link> : <Link to="/login">Login</Link>}
|
|
82
|
+
</nav>
|
|
83
|
+
)
|
|
84
|
+
}
|
|
85
|
+
```
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { Outlet, createRootRouteWithContext } from '@tanstack/react-router'
|
|
2
2
|
import type { QueryClient } from '@tanstack/react-query'
|
|
3
|
+
import { Toaster } from '@/components/ui/sonner'
|
|
3
4
|
|
|
4
5
|
import '../styles.css'
|
|
5
6
|
|
|
@@ -11,6 +12,7 @@ function RootComponent() {
|
|
|
11
12
|
return (
|
|
12
13
|
<>
|
|
13
14
|
<Outlet />
|
|
15
|
+
<Toaster richColors position="top-right" />
|
|
14
16
|
</>
|
|
15
17
|
)
|
|
16
18
|
}
|
package/package.json
CHANGED
|
@@ -5,6 +5,10 @@
|
|
|
5
5
|
<meta charset="UTF-8" />
|
|
6
6
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
7
7
|
<title>{{LEO_NAME}}</title>
|
|
8
|
+
<meta name="description" content="{{LEO_DESCRIPTION}}" />
|
|
9
|
+
<meta property="og:title" content="{{LEO_NAME}}" />
|
|
10
|
+
<meta property="og:description" content="{{LEO_DESCRIPTION}}" />
|
|
11
|
+
<meta property="og:type" content="website" />
|
|
8
12
|
<link rel="preconnect" href="https://fonts.googleapis.com">
|
|
9
13
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
|
10
14
|
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=Space+Grotesk:wght@400;500;600;700&display=swap" rel="stylesheet">
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { useState } from 'react'
|
|
2
|
+
import { useRouterState } from '@tanstack/react-router'
|
|
3
|
+
import { Button } from '@/components/ui/button'
|
|
4
|
+
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
|
|
5
|
+
|
|
6
|
+
// Rendered by the router's defaultErrorComponent for any uncaught render or
|
|
7
|
+
// loader error. Per-route `errorComponent` options override it. Keep this
|
|
8
|
+
// component free of app dependencies (toasts, auth, etc.) -- it must still
|
|
9
|
+
// render when the rest of the app is broken.
|
|
10
|
+
export function DefaultErrorComponent({ error }: { error: Error }) {
|
|
11
|
+
const pathname = useRouterState({ select: (s) => s.location.pathname })
|
|
12
|
+
const [copied, setCopied] = useState(false)
|
|
13
|
+
|
|
14
|
+
async function copyForLeo() {
|
|
15
|
+
// Blank lines + fenced stack so the report keeps its structure when
|
|
16
|
+
// pasted into a markdown-rendering chat.
|
|
17
|
+
const report = [
|
|
18
|
+
'My app threw an error, please fix it:',
|
|
19
|
+
`Page: ${pathname}`,
|
|
20
|
+
`Error: ${error.message}`,
|
|
21
|
+
['Stack:', '```', error.stack ?? '(no stack trace)', '```'].join('\n'),
|
|
22
|
+
].join('\n\n')
|
|
23
|
+
|
|
24
|
+
await navigator.clipboard.writeText(report)
|
|
25
|
+
setCopied(true)
|
|
26
|
+
setTimeout(() => setCopied(false), 2000)
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
return (
|
|
30
|
+
<div className="min-h-screen flex items-center justify-center bg-background p-4">
|
|
31
|
+
<Card className="w-full max-w-lg">
|
|
32
|
+
<CardHeader>
|
|
33
|
+
<CardTitle>Something went wrong</CardTitle>
|
|
34
|
+
</CardHeader>
|
|
35
|
+
<CardContent className="space-y-4">
|
|
36
|
+
<pre className="text-sm text-destructive whitespace-pre-wrap break-words rounded-md bg-muted p-3">
|
|
37
|
+
{error.message}
|
|
38
|
+
</pre>
|
|
39
|
+
<Button onClick={copyForLeo}>{copied ? 'Copied!' : 'Copy error'}</Button>
|
|
40
|
+
<p className="text-sm text-muted-foreground">
|
|
41
|
+
Paste the copied error to Leo and it will help you fix it.
|
|
42
|
+
</p>
|
|
43
|
+
</CardContent>
|
|
44
|
+
</Card>
|
|
45
|
+
</div>
|
|
46
|
+
)
|
|
47
|
+
}
|
|
@@ -2,6 +2,7 @@ import ReactDOM from 'react-dom/client'
|
|
|
2
2
|
import { RouterProvider, createRouter } from '@tanstack/react-router'
|
|
3
3
|
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
|
|
4
4
|
import { routeTree } from './routeTree.gen'
|
|
5
|
+
import { DefaultErrorComponent } from '@/components/error-component'
|
|
5
6
|
|
|
6
7
|
const queryClient = new QueryClient()
|
|
7
8
|
|
|
@@ -12,6 +13,9 @@ const router = createRouter({
|
|
|
12
13
|
// on every preload/visit and React Query owns caching from there.
|
|
13
14
|
defaultPreloadStaleTime: 0,
|
|
14
15
|
scrollRestoration: true,
|
|
16
|
+
// Catches uncaught render/loader errors on any route. Routes can still
|
|
17
|
+
// set their own `errorComponent` to override this.
|
|
18
|
+
defaultErrorComponent: DefaultErrorComponent,
|
|
15
19
|
context: { queryClient },
|
|
16
20
|
})
|
|
17
21
|
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
# Routes
|
|
2
|
+
|
|
3
|
+
TanStack Router uses file-based routing. Every `.ts` and `.tsx` file in this directory is a route. Do not create `src/pages/`, `app/layout.tsx`, or similar, those are Next.js / Remix conventions. The only root layout is `src/routes/__root.tsx`.
|
|
4
|
+
|
|
5
|
+
## Conventions
|
|
6
|
+
|
|
7
|
+
| File | URL |
|
|
8
|
+
| --- | --- |
|
|
9
|
+
| `index.tsx` | `/` |
|
|
10
|
+
| `about.tsx` | `/about` |
|
|
11
|
+
| `users/index.tsx` | `/users` |
|
|
12
|
+
| `users/$id.tsx` | `/users/:id` (dynamic — bare `$`, no curly braces) |
|
|
13
|
+
| `posts.$postId.tsx` | `/posts/:postId` (dots nest without folders) |
|
|
14
|
+
| `admin/route.tsx` | layout for every `/admin/*` page; must render `<Outlet />` |
|
|
15
|
+
| `admin_.login.tsx` | `/admin/login`, but outside the admin layout (trailing `_` escapes it) |
|
|
16
|
+
| `_auth.tsx` | pathless layout; wraps `_auth/*` files without adding a URL segment |
|
|
17
|
+
| `__root.tsx` | app shell, wraps every page; preserve `<Outlet />` |
|
|
18
|
+
|
|
19
|
+
## Rules
|
|
20
|
+
|
|
21
|
+
- A layout route without `<Outlet />` renders its children nowhere.
|
|
22
|
+
- Do not `export default` route components, it breaks code splitting. Use a named function passed as `component:`.
|
|
23
|
+
- `routeTree.gen.ts` is auto-generated. Don't edit it by hand.
|
|
24
|
+
- This is a client-only SPA — there are no server routes or server functions. Fetch data from external APIs with React Query.
|
|
25
|
+
|
|
26
|
+
## Authenticated routes
|
|
27
|
+
|
|
28
|
+
Put protected pages inside a pathless `_authed` layout and keep login/signup
|
|
29
|
+
at the top level, outside it. Never special-case the login pathname inside
|
|
30
|
+
the layout guard.
|
|
31
|
+
|
|
32
|
+
```
|
|
33
|
+
routes/
|
|
34
|
+
__root.tsx loads the session into router context (context.user)
|
|
35
|
+
_authed.tsx pathless guard — no URL segment of its own
|
|
36
|
+
_authed/
|
|
37
|
+
dashboard.tsx /dashboard (protected)
|
|
38
|
+
posts.tsx /posts (protected)
|
|
39
|
+
index.tsx / (public)
|
|
40
|
+
login.tsx /login (public)
|
|
41
|
+
logout.tsx /logout
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
`_authed.tsx` does the guarding, all children inherit it:
|
|
45
|
+
|
|
46
|
+
```tsx _authed.tsx
|
|
47
|
+
export const Route = createFileRoute('/_authed')({
|
|
48
|
+
beforeLoad: ({ context }) => {
|
|
49
|
+
if (!context.user) {
|
|
50
|
+
throw redirect({ to: '/login' })
|
|
51
|
+
}
|
|
52
|
+
},
|
|
53
|
+
})
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
The session is loaded once in `__root.tsx`'s `beforeLoad` and returned into
|
|
57
|
+
the router context so every route can read `context.user`. `getUser()` is
|
|
58
|
+
whatever your auth uses (e.g. verify a JWT from localStorage) and resolves to
|
|
59
|
+
the user or null:
|
|
60
|
+
|
|
61
|
+
```tsx __root.tsx
|
|
62
|
+
// (keep the existing component, add this)
|
|
63
|
+
export const Route = createRootRouteWithContext<{ queryClient: QueryClient }>()({
|
|
64
|
+
beforeLoad: async () => {
|
|
65
|
+
const user = await getUser()
|
|
66
|
+
return { user }
|
|
67
|
+
},
|
|
68
|
+
// ...component unchanged
|
|
69
|
+
})
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
Components read it with `Route.useRouteContext()` (e.g. to swap a Login link
|
|
73
|
+
for a Logout button in the shell):
|
|
74
|
+
|
|
75
|
+
```tsx
|
|
76
|
+
function Route() {
|
|
77
|
+
const { user } = Route.useRouteContext()
|
|
78
|
+
|
|
79
|
+
return (
|
|
80
|
+
<nav>
|
|
81
|
+
{user ? <Link to="/logout">Logout</Link> : <Link to="/login">Login</Link>}
|
|
82
|
+
</nav>
|
|
83
|
+
)
|
|
84
|
+
}
|
|
85
|
+
```
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { Outlet, createRootRouteWithContext } from '@tanstack/react-router'
|
|
2
2
|
import type { QueryClient } from '@tanstack/react-query'
|
|
3
|
+
import { Toaster } from '@/components/ui/sonner'
|
|
3
4
|
|
|
4
5
|
import '../styles.css'
|
|
5
6
|
|
|
@@ -11,6 +12,7 @@ function RootComponent() {
|
|
|
11
12
|
return (
|
|
12
13
|
<>
|
|
13
14
|
<Outlet />
|
|
15
|
+
<Toaster richColors position="top-right" />
|
|
14
16
|
</>
|
|
15
17
|
)
|
|
16
18
|
}
|
package/src/tools/scaffold.ts
CHANGED
|
@@ -6,14 +6,26 @@ import { z } from "zod/v4";
|
|
|
6
6
|
import { renderTemplate } from "../template-render.ts";
|
|
7
7
|
|
|
8
8
|
/**
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
* conventions
|
|
13
|
-
*
|
|
14
|
-
*
|
|
9
|
+
* Scaffold template identifiers, keyed by project type. Each value is the
|
|
10
|
+
* directory name under src/templates/ and is written to .leo/project.json so
|
|
11
|
+
* the webapp / leo-core can tell which template (and therefore which
|
|
12
|
+
* conventions) a workspace was bootstrapped with. Bump a value (and add a new
|
|
13
|
+
* templates/<id>/ dir) when that scaffold's shape changes.
|
|
14
|
+
*
|
|
15
|
+
* - tanstack-start: SSR Worker with file routing, API routes, and a database,
|
|
16
|
+
* KV and bucket via @helloleo/runtime.
|
|
17
|
+
* - tanstack-router: client-only SPA (Vite + index.html entry), no server and
|
|
18
|
+
* no database.
|
|
15
19
|
*/
|
|
16
|
-
export const
|
|
20
|
+
export const SCAFFOLD_TEMPLATES = {
|
|
21
|
+
"tanstack-start": "tanstack-start-2026-06-18",
|
|
22
|
+
"tanstack-router": "tanstack-router-2026-06-15",
|
|
23
|
+
} as const;
|
|
24
|
+
|
|
25
|
+
export type ScaffoldType = keyof typeof SCAFFOLD_TEMPLATES;
|
|
26
|
+
|
|
27
|
+
/** Default template id, kept for callers that predate the `type` param. */
|
|
28
|
+
export const SCAFFOLD_VERSION = SCAFFOLD_TEMPLATES["tanstack-start"];
|
|
17
29
|
|
|
18
30
|
/**
|
|
19
31
|
* The project workspace directory is named after the project UUID -- the
|
|
@@ -33,6 +45,18 @@ const scaffoldOptionsSchema = z.object({
|
|
|
33
45
|
description: z
|
|
34
46
|
.string()
|
|
35
47
|
.describe("One sentence describing the app. Used as the meta description."),
|
|
48
|
+
type: z
|
|
49
|
+
.enum(["tanstack-start", "tanstack-router"])
|
|
50
|
+
.optional()
|
|
51
|
+
.describe(
|
|
52
|
+
"Project type. 'tanstack-router': " +
|
|
53
|
+
"client-only SPA with no server and no database — use it only for " +
|
|
54
|
+
"pure frontends that talk to external APIs." +
|
|
55
|
+
"'tanstack-start': SSR app with API routes and a " +
|
|
56
|
+
"database, KV and bucket — use it whenever the app " +
|
|
57
|
+
"needs to persist data or run server-side code (experimental, " +
|
|
58
|
+
"use only if the user expresses a really complexe application idea).",
|
|
59
|
+
),
|
|
36
60
|
});
|
|
37
61
|
|
|
38
62
|
/** Kebab-case slug of the product name, for package.json. */
|
|
@@ -46,16 +70,17 @@ function slugify(name: string): string {
|
|
|
46
70
|
}
|
|
47
71
|
|
|
48
72
|
/**
|
|
49
|
-
* Write the base scaffold by rendering the
|
|
50
|
-
* then adding .leo/project.json (template metadata the tool owns,
|
|
51
|
-
* the template tree). Returns the relative paths written.
|
|
73
|
+
* Write the base scaffold by rendering the template for the requested type
|
|
74
|
+
* into dir, then adding .leo/project.json (template metadata the tool owns,
|
|
75
|
+
* not part of the template tree). Returns the relative paths written.
|
|
52
76
|
*/
|
|
53
77
|
export async function writeBaseScaffold(
|
|
54
|
-
{ name, description }: z.infer<typeof scaffoldOptionsSchema>,
|
|
78
|
+
{ name, description, type }: z.infer<typeof scaffoldOptionsSchema>,
|
|
55
79
|
dir: string,
|
|
56
80
|
): Promise<string[]> {
|
|
81
|
+
const templateId = SCAFFOLD_TEMPLATES[type ?? "tanstack-start"];
|
|
57
82
|
const created = await renderTemplate(
|
|
58
|
-
|
|
83
|
+
templateId,
|
|
59
84
|
{
|
|
60
85
|
LEO_NAME: name,
|
|
61
86
|
LEO_SLUG: slugify(name),
|
|
@@ -69,7 +94,7 @@ export async function writeBaseScaffold(
|
|
|
69
94
|
const leoPath = path.join(".leo", "project.json");
|
|
70
95
|
await Bun.write(
|
|
71
96
|
path.join(dir, leoPath),
|
|
72
|
-
JSON.stringify({ $schema: 1, scaffold:
|
|
97
|
+
JSON.stringify({ $schema: 1, scaffold: templateId }, null, 2),
|
|
73
98
|
);
|
|
74
99
|
|
|
75
100
|
return [...created, leoPath];
|
|
@@ -77,37 +102,60 @@ export async function writeBaseScaffold(
|
|
|
77
102
|
|
|
78
103
|
export const scaffold = tool({
|
|
79
104
|
description:
|
|
80
|
-
"Scaffold a new
|
|
81
|
-
"
|
|
82
|
-
"
|
|
83
|
-
"
|
|
105
|
+
"Scaffold a new project with React 19, file-based routing (TanStack), " +
|
|
106
|
+
"Tailwind v4, shadcn/ui, and react-query. Two types: 'tanstack-start' " +
|
|
107
|
+
"(SSR, API routes, and a database, KV and bucket via @helloleo/runtime) " +
|
|
108
|
+
"or 'tanstack-router' (client-only SPA, no server, no database). Call " +
|
|
109
|
+
"this first for any new project, before writing routes or components.",
|
|
84
110
|
args: {
|
|
85
111
|
name: scaffoldOptionsSchema.shape.name,
|
|
86
112
|
description: scaffoldOptionsSchema.shape.description,
|
|
113
|
+
type: scaffoldOptionsSchema.shape.type,
|
|
87
114
|
},
|
|
88
115
|
async execute(args, context) {
|
|
116
|
+
const type: ScaffoldType = args.type ?? "tanstack-start";
|
|
89
117
|
const created = await writeBaseScaffold(args, context.directory);
|
|
90
118
|
const uiComponents = created.filter((f) =>
|
|
91
119
|
f.startsWith("src/components/ui/"),
|
|
92
120
|
);
|
|
93
121
|
const rest = created.filter((f) => !f.startsWith("src/components/ui/"));
|
|
122
|
+
|
|
123
|
+
const typeNotes =
|
|
124
|
+
type === "tanstack-start"
|
|
125
|
+
? [
|
|
126
|
+
"- This is a TanStack Start project. The server entry is src/server.ts. There is no index.html or client entrypoint.",
|
|
127
|
+
"- For JSON/CRUD APIs, add server routes under src/routes/api/. Read src/routes/api/health.ts first, add routes as needed.",
|
|
128
|
+
"- The database schema lives in src/db/schema.ts. Migrations run automatically.",
|
|
129
|
+
]
|
|
130
|
+
: [
|
|
131
|
+
"- This is a TanStack Router SPA. The client entry is index.html -> src/main.tsx. There is no server: no API routes, no server functions, no database.",
|
|
132
|
+
"- Fetch data from external APIs only. If the app needs its own persistence or server-side code, it should have been scaffolded as tanstack-start.",
|
|
133
|
+
];
|
|
134
|
+
|
|
135
|
+
const nextSteps =
|
|
136
|
+
type === "tanstack-start"
|
|
137
|
+
? [
|
|
138
|
+
"Next: read src/routes/index.tsx and continue with the user's request.",
|
|
139
|
+
"For authentication (password and/or OAuth), call setup_authentication.",
|
|
140
|
+
"For NetSuite projects, call setup_netsuite.",
|
|
141
|
+
]
|
|
142
|
+
: [
|
|
143
|
+
"Next: read src/routes/index.tsx and continue with the user's request.",
|
|
144
|
+
];
|
|
145
|
+
|
|
94
146
|
return [
|
|
95
147
|
`Scaffolded ${created.length} files:`,
|
|
96
148
|
...rest.map((f) => ` - ${f}`),
|
|
97
149
|
` - src/components/ui/ (${uiComponents.length} shadcn/ui components)`,
|
|
98
150
|
"",
|
|
99
151
|
"Notes:",
|
|
100
|
-
|
|
152
|
+
...typeNotes,
|
|
101
153
|
"- Add routes as files in src/routes/. Do not default export route components. Routing and layout conventions are documented in src/routes/README.md, read it before adding routes.",
|
|
102
|
-
"- For JSON/CRUD APIs, add server routes under src/routes/api/. Read src/routes/api/health.ts first, add routes as needed.",
|
|
103
154
|
"- shadcn/ui components are already in src/components/ui.",
|
|
104
155
|
"- Design system: style exclusively with the token utilities, never hardcoded colors. To change the look, edit the token values in src/styles.css (light and dark blocks).",
|
|
105
|
-
"- The database schema lives in src/db/schema.ts. Migrations run automatically.",
|
|
106
156
|
"- Always fetch data with react-query (useQuery and useMutation).",
|
|
107
157
|
"",
|
|
108
|
-
|
|
109
|
-
"For authentication (password and/or OAuth), call setup_authentication.",
|
|
110
|
-
"For NetSuite projects, call setup_netsuite.",
|
|
158
|
+
...nextSteps,
|
|
111
159
|
].join("\n");
|
|
112
160
|
},
|
|
113
161
|
});
|