@tanstack/cta-ui 0.10.0-alpha.19 → 0.10.0-alpha.21

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 (71) hide show
  1. package/lib/index.ts +16 -7
  2. package/lib-dist/index.d.ts +6 -1
  3. package/lib-dist/index.js +6 -3
  4. package/package.json +19 -7
  5. package/public/tailwind.svg +1 -0
  6. package/public/tanstack.png +0 -0
  7. package/public/typescript.svg +1 -0
  8. package/src/components/StatusList.tsx +22 -0
  9. package/src/components/add-on-info-dialog.tsx +39 -0
  10. package/src/components/cta-sidebar.tsx +55 -0
  11. package/src/components/custom-add-on-dialog.tsx +79 -0
  12. package/src/components/file-navigator.tsx +205 -0
  13. package/src/components/file-tree.tsx +18 -60
  14. package/src/components/file-viewer.tsx +11 -3
  15. package/src/components/sidebar-items/add-ons.tsx +91 -0
  16. package/src/components/sidebar-items/mode-selector.tsx +55 -0
  17. package/src/components/sidebar-items/project-name.tsx +29 -0
  18. package/src/components/sidebar-items/run-add-ons.tsx +71 -0
  19. package/src/components/sidebar-items/run-create-app.tsx +82 -0
  20. package/src/components/sidebar-items/starter.tsx +115 -0
  21. package/src/components/sidebar-items/typescript-switch.tsx +52 -0
  22. package/src/components/toaster.tsx +29 -0
  23. package/src/components/ui/button.tsx +21 -19
  24. package/src/components/ui/dialog.tsx +25 -20
  25. package/src/components/ui/dropdown-menu.tsx +255 -0
  26. package/src/components/ui/input.tsx +21 -0
  27. package/src/components/ui/label.tsx +22 -0
  28. package/src/components/ui/popover.tsx +46 -0
  29. package/src/components/ui/separator.tsx +28 -0
  30. package/src/components/ui/sheet.tsx +137 -0
  31. package/src/components/ui/sidebar.tsx +726 -0
  32. package/src/components/ui/skeleton.tsx +13 -0
  33. package/src/components/ui/sonner.tsx +23 -0
  34. package/src/components/ui/switch.tsx +29 -0
  35. package/src/components/ui/toggle-group.tsx +11 -11
  36. package/src/components/ui/toggle.tsx +15 -13
  37. package/src/components/ui/tooltip.tsx +61 -0
  38. package/src/components/ui/tree-view.tsx +17 -12
  39. package/src/engine-handling/add-to-app-wrapper.ts +114 -0
  40. package/src/engine-handling/create-app-wrapper.ts +107 -0
  41. package/src/engine-handling/file-helpers.ts +25 -0
  42. package/src/engine-handling/framework-registration.ts +11 -0
  43. package/src/engine-handling/generate-initial-payload.ts +93 -0
  44. package/src/engine-handling/server-environment.ts +13 -0
  45. package/src/file-classes.ts +54 -0
  46. package/src/hooks/use-mobile.ts +19 -0
  47. package/src/hooks/use-streaming-status.ts +70 -0
  48. package/src/lib/api.ts +90 -0
  49. package/src/routeTree.gen.ts +4 -27
  50. package/src/routes/__root.tsx +36 -7
  51. package/src/routes/api/add-to-app.ts +21 -0
  52. package/src/routes/api/create-app.ts +21 -0
  53. package/src/routes/api/dry-run-add-to-app.ts +16 -0
  54. package/src/routes/api/dry-run-create-app.ts +16 -0
  55. package/src/routes/api/initial-payload.ts +10 -0
  56. package/src/routes/api/load-remote-add-on.ts +42 -0
  57. package/src/routes/api/load-starter.ts +47 -0
  58. package/src/routes/api/shutdown.ts +11 -0
  59. package/src/routes/index.tsx +3 -210
  60. package/src/store/add-ons.ts +81 -0
  61. package/src/store/project.ts +268 -0
  62. package/src/styles.css +47 -0
  63. package/src/types.d.ts +87 -0
  64. package/tests/store/add-ons.test.ts +222 -0
  65. package/vitest.config.ts +6 -0
  66. package/.cursorrules +0 -7
  67. package/src/components/Header.tsx +0 -13
  68. package/src/components/applied-add-on.tsx +0 -149
  69. package/src/lib/server-fns.ts +0 -78
  70. package/src/routes/api.demo-names.ts +0 -11
  71. package/src/routes/demo.tanstack-query.tsx +0 -28
@@ -0,0 +1,13 @@
1
+ import type { SerializedOptions } from '@tanstack/cta-engine'
2
+
3
+ export function getProjectPath(): string {
4
+ return process.env.CTA_PROJECT_PATH!
5
+ }
6
+
7
+ export function getApplicationMode(): 'add' | 'setup' {
8
+ return process.env.CTA_MODE as 'add' | 'setup'
9
+ }
10
+
11
+ export function getProjectOptions(): SerializedOptions {
12
+ return JSON.parse(process.env.CTA_OPTIONS!)
13
+ }
@@ -0,0 +1,54 @@
1
+ import type { FileClass } from '@/types.js'
2
+
3
+ export const twClasses: Record<FileClass, string> = {
4
+ unchanged: 'text-gray-500',
5
+ added: 'text-green-500 font-bold',
6
+ modified: 'text-blue-500 italic',
7
+ deleted: 'text-red-500 line-through',
8
+ overwritten: 'text-red-700 underline',
9
+ }
10
+
11
+ export type FileClassAndInfo = {
12
+ fileClass: FileClass
13
+ originalFile?: string
14
+ modifiedFile?: string
15
+ }
16
+
17
+ export const getFileClass = (
18
+ file: string,
19
+ tree: Record<string, string>,
20
+ originalTree: Record<string, string>,
21
+ localTree: Record<string, string>,
22
+ deletedFiles: Array<string>,
23
+ ): FileClassAndInfo => {
24
+ if (localTree[file]) {
25
+ if (deletedFiles.includes(file)) {
26
+ return { fileClass: 'deleted', originalFile: localTree[file] }
27
+ }
28
+ // We have a local file and it's in the new tree
29
+ if (tree[file]) {
30
+ // Our new tree has changed this file
31
+ if (localTree[file] !== tree[file]) {
32
+ // Was the local tree different from the original?
33
+ if (originalTree[file] && localTree[file] !== originalTree[file]) {
34
+ // Yes, it was overwritten
35
+ return {
36
+ fileClass: 'overwritten',
37
+ originalFile: localTree[file],
38
+ modifiedFile: tree[file],
39
+ }
40
+ } else {
41
+ // No, it just being modified
42
+ return {
43
+ fileClass: 'modified',
44
+ originalFile: localTree[file],
45
+ modifiedFile: tree[file],
46
+ }
47
+ }
48
+ }
49
+ }
50
+ return { fileClass: 'unchanged', modifiedFile: localTree[file] }
51
+ } else {
52
+ return { fileClass: 'added', modifiedFile: tree[file] }
53
+ }
54
+ }
@@ -0,0 +1,19 @@
1
+ import * as React from "react"
2
+
3
+ const MOBILE_BREAKPOINT = 768
4
+
5
+ export function useIsMobile() {
6
+ const [isMobile, setIsMobile] = React.useState<boolean | undefined>(undefined)
7
+
8
+ React.useEffect(() => {
9
+ const mql = window.matchMedia(`(max-width: ${MOBILE_BREAKPOINT - 1}px)`)
10
+ const onChange = () => {
11
+ setIsMobile(window.innerWidth < MOBILE_BREAKPOINT)
12
+ }
13
+ mql.addEventListener("change", onChange)
14
+ setIsMobile(window.innerWidth < MOBILE_BREAKPOINT)
15
+ return () => mql.removeEventListener("change", onChange)
16
+ }, [])
17
+
18
+ return !!isMobile
19
+ }
@@ -0,0 +1,70 @@
1
+ import { useCallback, useState } from 'react'
2
+
3
+ import {
4
+ InfoIcon,
5
+ MessageCircleCodeIcon,
6
+ PackageIcon,
7
+ SquarePenIcon,
8
+ TerminalIcon,
9
+ } from 'lucide-react'
10
+
11
+ import type { StatusStepType } from '@tanstack/cta-engine'
12
+
13
+ import type { StreamEvent, StreamItem } from '@/types'
14
+
15
+ const iconMap: Record<StatusStepType, typeof InfoIcon> = {
16
+ file: SquarePenIcon,
17
+ command: TerminalIcon,
18
+ 'package-manager': PackageIcon,
19
+ info: InfoIcon,
20
+ other: MessageCircleCodeIcon,
21
+ }
22
+
23
+ export default function useStreamingStatus() {
24
+ const [streamItems, setStreamItems] = useState<Array<StreamItem>>([])
25
+ const [finished, setFinished] = useState(false)
26
+
27
+ const monitorStream = useCallback(async (res: Response) => {
28
+ setFinished(false)
29
+ const reader = res.body?.getReader()
30
+ const decoder = new TextDecoder()
31
+
32
+ let rawStream = ''
33
+ while (true) {
34
+ const result = await reader?.read()
35
+ if (result?.done) break
36
+
37
+ rawStream += decoder.decode(result?.value)
38
+
39
+ let currentId: string | undefined
40
+ const newStreamItems: Array<StreamItem> = []
41
+ for (const line of rawStream.split('\n')) {
42
+ if (line.startsWith('{') && line.endsWith('}')) {
43
+ const item = JSON.parse(line) as StreamEvent
44
+ if (item.msgType === 'start') {
45
+ if (currentId === item.id) {
46
+ newStreamItems[newStreamItems.length - 1].message = item.message
47
+ } else {
48
+ currentId = item.id
49
+ newStreamItems.push({
50
+ id: currentId,
51
+ icon: iconMap[item.type],
52
+ message: item.message,
53
+ })
54
+ }
55
+ } else {
56
+ if (newStreamItems.length > 0) {
57
+ newStreamItems[newStreamItems.length - 1].message = item.message
58
+ }
59
+ currentId = undefined
60
+ }
61
+ }
62
+ }
63
+ setStreamItems(newStreamItems)
64
+ }
65
+ setFinished(true)
66
+ return rawStream
67
+ }, [])
68
+
69
+ return { finished, streamItems, monitorStream }
70
+ }
package/src/lib/api.ts ADDED
@@ -0,0 +1,90 @@
1
+ import type { SerializedOptions } from '@tanstack/cta-engine'
2
+
3
+ import type { AddOnInfo, DryRunOutput, InitialData, StarterInfo } from '@/types'
4
+
5
+ export async function createAppStreaming(
6
+ options: SerializedOptions,
7
+ chosenAddOns: Array<string>,
8
+ projectStarter?: StarterInfo,
9
+ ) {
10
+ return await fetch('/api/create-app', {
11
+ method: 'POST',
12
+ body: JSON.stringify({
13
+ options: {
14
+ ...options,
15
+ chosenAddOns,
16
+ starter: projectStarter?.url || undefined,
17
+ },
18
+ }),
19
+ headers: {
20
+ 'Content-Type': 'application/json',
21
+ },
22
+ })
23
+ }
24
+
25
+ export async function addToAppStreaming(chosenAddOns: Array<string>) {
26
+ return await fetch('/api/add-to-app', {
27
+ method: 'POST',
28
+ body: JSON.stringify({
29
+ addOns: chosenAddOns,
30
+ }),
31
+ headers: {
32
+ 'Content-Type': 'application/json',
33
+ },
34
+ })
35
+ }
36
+
37
+ export function shutdown() {
38
+ return fetch('/api/shutdown', {
39
+ method: 'POST',
40
+ })
41
+ }
42
+
43
+ export async function loadRemoteAddOn(url: string) {
44
+ const response = await fetch(`/api/load-remote-add-on?url=${url}`)
45
+ return (await response.json()) as AddOnInfo | { error: string }
46
+ }
47
+
48
+ export async function loadRemoteStarter(url: string) {
49
+ const response = await fetch(`/api/load-starter?url=${url}`)
50
+ return (await response.json()) as StarterInfo | { error: string }
51
+ }
52
+
53
+ export async function loadInitialData() {
54
+ const payloadReq = await fetch('/api/initial-payload')
55
+ return (await payloadReq.json()) as InitialData
56
+ }
57
+
58
+ export async function dryRunCreateApp(
59
+ options: SerializedOptions,
60
+ chosenAddOns: Array<string>,
61
+ projectStarter?: StarterInfo,
62
+ ) {
63
+ const outputReq = await fetch('/api/dry-run-create-app', {
64
+ method: 'POST',
65
+ headers: {
66
+ 'Content-Type': 'application/json',
67
+ },
68
+ body: JSON.stringify({
69
+ options: {
70
+ ...options,
71
+ chosenAddOns: chosenAddOns,
72
+ starter: projectStarter?.url,
73
+ },
74
+ }),
75
+ })
76
+ return outputReq.json() as Promise<DryRunOutput>
77
+ }
78
+
79
+ export async function dryRunAddToApp(addOns: Array<string>) {
80
+ const outputReq = await fetch('/api/dry-run-add-to-app', {
81
+ method: 'POST',
82
+ headers: {
83
+ 'Content-Type': 'application/json',
84
+ },
85
+ body: JSON.stringify({
86
+ addOns,
87
+ }),
88
+ })
89
+ return outputReq.json() as Promise<DryRunOutput>
90
+ }
@@ -12,7 +12,6 @@
12
12
 
13
13
  import { Route as rootRoute } from './routes/__root'
14
14
  import { Route as IndexImport } from './routes/index'
15
- import { Route as DemoTanstackQueryImport } from './routes/demo.tanstack-query'
16
15
 
17
16
  // Create/Update Routes
18
17
 
@@ -22,12 +21,6 @@ const IndexRoute = IndexImport.update({
22
21
  getParentRoute: () => rootRoute,
23
22
  } as any)
24
23
 
25
- const DemoTanstackQueryRoute = DemoTanstackQueryImport.update({
26
- id: '/demo/tanstack-query',
27
- path: '/demo/tanstack-query',
28
- getParentRoute: () => rootRoute,
29
- } as any)
30
-
31
24
  // Populate the FileRoutesByPath interface
32
25
 
33
26
  declare module '@tanstack/react-router' {
@@ -39,13 +32,6 @@ declare module '@tanstack/react-router' {
39
32
  preLoaderRoute: typeof IndexImport
40
33
  parentRoute: typeof rootRoute
41
34
  }
42
- '/demo/tanstack-query': {
43
- id: '/demo/tanstack-query'
44
- path: '/demo/tanstack-query'
45
- fullPath: '/demo/tanstack-query'
46
- preLoaderRoute: typeof DemoTanstackQueryImport
47
- parentRoute: typeof rootRoute
48
- }
49
35
  }
50
36
  }
51
37
 
@@ -53,37 +39,32 @@ declare module '@tanstack/react-router' {
53
39
 
54
40
  export interface FileRoutesByFullPath {
55
41
  '/': typeof IndexRoute
56
- '/demo/tanstack-query': typeof DemoTanstackQueryRoute
57
42
  }
58
43
 
59
44
  export interface FileRoutesByTo {
60
45
  '/': typeof IndexRoute
61
- '/demo/tanstack-query': typeof DemoTanstackQueryRoute
62
46
  }
63
47
 
64
48
  export interface FileRoutesById {
65
49
  __root__: typeof rootRoute
66
50
  '/': typeof IndexRoute
67
- '/demo/tanstack-query': typeof DemoTanstackQueryRoute
68
51
  }
69
52
 
70
53
  export interface FileRouteTypes {
71
54
  fileRoutesByFullPath: FileRoutesByFullPath
72
- fullPaths: '/' | '/demo/tanstack-query'
55
+ fullPaths: '/'
73
56
  fileRoutesByTo: FileRoutesByTo
74
- to: '/' | '/demo/tanstack-query'
75
- id: '__root__' | '/' | '/demo/tanstack-query'
57
+ to: '/'
58
+ id: '__root__' | '/'
76
59
  fileRoutesById: FileRoutesById
77
60
  }
78
61
 
79
62
  export interface RootRouteChildren {
80
63
  IndexRoute: typeof IndexRoute
81
- DemoTanstackQueryRoute: typeof DemoTanstackQueryRoute
82
64
  }
83
65
 
84
66
  const rootRouteChildren: RootRouteChildren = {
85
67
  IndexRoute: IndexRoute,
86
- DemoTanstackQueryRoute: DemoTanstackQueryRoute,
87
68
  }
88
69
 
89
70
  export const routeTree = rootRoute
@@ -96,15 +77,11 @@ export const routeTree = rootRoute
96
77
  "__root__": {
97
78
  "filePath": "__root.tsx",
98
79
  "children": [
99
- "/",
100
- "/demo/tanstack-query"
80
+ "/"
101
81
  ]
102
82
  },
103
83
  "/": {
104
84
  "filePath": "index.tsx"
105
- },
106
- "/demo/tanstack-query": {
107
- "filePath": "demo.tanstack-query.tsx"
108
85
  }
109
86
  }
110
87
  }
@@ -1,6 +1,6 @@
1
1
  import {
2
- Outlet,
3
2
  HeadContent,
3
+ Outlet,
4
4
  Scripts,
5
5
  createRootRouteWithContext,
6
6
  } from '@tanstack/react-router'
@@ -9,10 +9,33 @@ import appCss from '../styles.css?url'
9
9
 
10
10
  import type { QueryClient } from '@tanstack/react-query'
11
11
 
12
+ import {
13
+ SidebarProvider,
14
+ SidebarTrigger,
15
+ useSidebar,
16
+ } from '@/components/ui/sidebar'
17
+ import { Toaster } from '@/components/toaster'
18
+
19
+ import { AppSidebar } from '@/components/cta-sidebar'
20
+
12
21
  interface MyRouterContext {
13
22
  queryClient: QueryClient
14
23
  }
15
24
 
25
+ function Content() {
26
+ const { open } = useSidebar()
27
+
28
+ return (
29
+ <main
30
+ className={
31
+ open ? 'w-full max-w-[calc(100%-370px)]' : 'w-full max-w-[100%]'
32
+ }
33
+ >
34
+ <SidebarTrigger className="m-2" />
35
+ <Outlet />
36
+ </main>
37
+ )
38
+ }
16
39
  export const Route = createRootRouteWithContext<MyRouterContext>()({
17
40
  head: () => ({
18
41
  meta: [
@@ -24,7 +47,7 @@ export const Route = createRootRouteWithContext<MyRouterContext>()({
24
47
  content: 'width=device-width, initial-scale=1',
25
48
  },
26
49
  {
27
- title: 'Add-On Debugger',
50
+ title: 'TanStack CTA',
28
51
  },
29
52
  ],
30
53
  links: [
@@ -35,11 +58,17 @@ export const Route = createRootRouteWithContext<MyRouterContext>()({
35
58
  ],
36
59
  }),
37
60
 
38
- component: () => (
39
- <RootDocument>
40
- <Outlet />
41
- </RootDocument>
42
- ),
61
+ component: () => {
62
+ return (
63
+ <RootDocument>
64
+ <SidebarProvider>
65
+ <AppSidebar />
66
+ <Content />
67
+ <Toaster />
68
+ </SidebarProvider>
69
+ </RootDocument>
70
+ )
71
+ },
43
72
  })
44
73
 
45
74
  function RootDocument({ children }: { children: React.ReactNode }) {
@@ -0,0 +1,21 @@
1
+ import { createAPIFileRoute } from '@tanstack/react-start/api'
2
+
3
+ import { addToAppWrapper } from '@/engine-handling/add-to-app-wrapper'
4
+
5
+ export const APIRoute = createAPIFileRoute('/api/add-to-app')({
6
+ POST: async ({ request }) => {
7
+ const { addOns } = await request.json()
8
+
9
+ const stream = await addToAppWrapper(addOns, {
10
+ stream: true,
11
+ })
12
+
13
+ return new Response(stream as ReadableStream, {
14
+ headers: {
15
+ 'Content-Type': 'text/event-stream',
16
+ 'Cache-Control': 'no-cache',
17
+ Connection: 'keep-alive',
18
+ },
19
+ })
20
+ },
21
+ })
@@ -0,0 +1,21 @@
1
+ import { createAPIFileRoute } from '@tanstack/react-start/api'
2
+
3
+ import { createAppWrapper } from '@/engine-handling/create-app-wrapper'
4
+
5
+ export const APIRoute = createAPIFileRoute('/api/create-app')({
6
+ POST: async ({ request }) => {
7
+ const { options: serializedOptions } = await request.json()
8
+
9
+ const stream = await createAppWrapper(serializedOptions, {
10
+ stream: true,
11
+ })
12
+
13
+ return new Response(stream as ReadableStream, {
14
+ headers: {
15
+ 'Content-Type': 'text/event-stream',
16
+ 'Cache-Control': 'no-cache',
17
+ Connection: 'keep-alive',
18
+ },
19
+ })
20
+ },
21
+ })
@@ -0,0 +1,16 @@
1
+ import { json } from '@tanstack/react-start'
2
+ import { createAPIFileRoute } from '@tanstack/react-start/api'
3
+
4
+ import { addToAppWrapper } from '@/engine-handling/add-to-app-wrapper'
5
+
6
+ export const APIRoute = createAPIFileRoute('/api/dry-run-add-to-app')({
7
+ POST: async ({ request }) => {
8
+ const { addOns } = await request.json()
9
+
10
+ return json(
11
+ await addToAppWrapper(addOns, {
12
+ dryRun: true,
13
+ }),
14
+ )
15
+ },
16
+ })
@@ -0,0 +1,16 @@
1
+ import { json } from '@tanstack/react-start'
2
+ import { createAPIFileRoute } from '@tanstack/react-start/api'
3
+
4
+ import { createAppWrapper } from '@/engine-handling/create-app-wrapper'
5
+
6
+ export const APIRoute = createAPIFileRoute('/api/dry-run-create-app')({
7
+ POST: async ({ request }) => {
8
+ const { options: serializedOptions } = await request.json()
9
+
10
+ return json(
11
+ await createAppWrapper(serializedOptions, {
12
+ dryRun: true,
13
+ }),
14
+ )
15
+ },
16
+ })
@@ -0,0 +1,10 @@
1
+ import { json } from '@tanstack/react-start'
2
+ import { createAPIFileRoute } from '@tanstack/react-start/api'
3
+
4
+ import { generateInitialPayload } from '@/engine-handling/generate-initial-payload'
5
+
6
+ export const APIRoute = createAPIFileRoute('/api/initial-payload')({
7
+ GET: async () => {
8
+ return json(await generateInitialPayload())
9
+ },
10
+ })
@@ -0,0 +1,42 @@
1
+ import { json } from '@tanstack/react-start'
2
+ import { createAPIFileRoute } from '@tanstack/react-start/api'
3
+
4
+ import { AddOnCompiledSchema } from '@tanstack/cta-engine'
5
+
6
+ export const APIRoute = createAPIFileRoute('/api/load-remote-add-on')({
7
+ GET: async ({ request }) => {
8
+ const incomingUrl = new URL(request.url)
9
+ const url = incomingUrl.searchParams.get('url')
10
+
11
+ if (!url) {
12
+ return json({ error: 'url is required' }, { status: 400 })
13
+ }
14
+
15
+ try {
16
+ const response = await fetch(url)
17
+ const data = await response.json()
18
+
19
+ const parsed = AddOnCompiledSchema.safeParse(data)
20
+
21
+ if (!parsed.success) {
22
+ return json({ error: 'Invalid add-on data' }, { status: 400 })
23
+ }
24
+
25
+ return json({
26
+ id: url,
27
+ name: parsed.data.name,
28
+ description: parsed.data.description,
29
+ version: parsed.data.version,
30
+ author: parsed.data.author,
31
+ license: parsed.data.license,
32
+ link: parsed.data.link,
33
+ smallLogo: parsed.data.smallLogo,
34
+ logo: parsed.data.logo,
35
+ type: parsed.data.type,
36
+ modes: parsed.data.modes,
37
+ })
38
+ } catch {
39
+ return json({ error: 'Failed to load add-on' }, { status: 500 })
40
+ }
41
+ },
42
+ })
@@ -0,0 +1,47 @@
1
+ import { json } from '@tanstack/react-start'
2
+ import { createAPIFileRoute } from '@tanstack/react-start/api'
3
+
4
+ import { StarterCompiledSchema } from '@tanstack/cta-engine'
5
+
6
+ export const APIRoute = createAPIFileRoute('/api/load-starter')({
7
+ GET: async ({ request }) => {
8
+ const incomingUrl = new URL(request.url)
9
+ const url = incomingUrl.searchParams.get('url')
10
+
11
+ if (!url) {
12
+ return json({ error: 'url is required' }, { status: 400 })
13
+ }
14
+
15
+ try {
16
+ const response = await fetch(url)
17
+ const data = await response.json()
18
+
19
+ const parsed = StarterCompiledSchema.safeParse(data)
20
+
21
+ if (!parsed.success) {
22
+ return json({ error: 'Invalid starter data' }, { status: 400 })
23
+ }
24
+
25
+ return json({
26
+ url,
27
+
28
+ id: parsed.data.id,
29
+ name: parsed.data.name,
30
+ description: parsed.data.description,
31
+ version: parsed.data.version,
32
+ author: parsed.data.author,
33
+ license: parsed.data.license,
34
+ dependsOn: parsed.data.dependsOn,
35
+
36
+ mode: parsed.data.mode,
37
+ typescript: parsed.data.typescript,
38
+ tailwind: parsed.data.tailwind,
39
+ banner: parsed.data.banner
40
+ ? url.replace('starter.json', parsed.data.banner)
41
+ : undefined,
42
+ })
43
+ } catch {
44
+ return json({ error: 'Failed to load starter' }, { status: 500 })
45
+ }
46
+ },
47
+ })
@@ -0,0 +1,11 @@
1
+ import { json } from '@tanstack/react-start'
2
+ import { createAPIFileRoute } from '@tanstack/react-start/api'
3
+
4
+ export const APIRoute = createAPIFileRoute('/api/shutdown')({
5
+ POST: () => {
6
+ setTimeout(() => {
7
+ process.exit(0)
8
+ }, 50)
9
+ return json({ shutdown: true })
10
+ },
11
+ })