@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.
- package/lib/index.ts +16 -7
- package/lib-dist/index.d.ts +6 -1
- package/lib-dist/index.js +6 -3
- package/package.json +19 -7
- package/public/tailwind.svg +1 -0
- package/public/tanstack.png +0 -0
- package/public/typescript.svg +1 -0
- package/src/components/StatusList.tsx +22 -0
- package/src/components/add-on-info-dialog.tsx +39 -0
- package/src/components/cta-sidebar.tsx +55 -0
- package/src/components/custom-add-on-dialog.tsx +79 -0
- package/src/components/file-navigator.tsx +205 -0
- package/src/components/file-tree.tsx +18 -60
- package/src/components/file-viewer.tsx +11 -3
- package/src/components/sidebar-items/add-ons.tsx +91 -0
- package/src/components/sidebar-items/mode-selector.tsx +55 -0
- package/src/components/sidebar-items/project-name.tsx +29 -0
- package/src/components/sidebar-items/run-add-ons.tsx +71 -0
- package/src/components/sidebar-items/run-create-app.tsx +82 -0
- package/src/components/sidebar-items/starter.tsx +115 -0
- package/src/components/sidebar-items/typescript-switch.tsx +52 -0
- package/src/components/toaster.tsx +29 -0
- package/src/components/ui/button.tsx +21 -19
- package/src/components/ui/dialog.tsx +25 -20
- package/src/components/ui/dropdown-menu.tsx +255 -0
- package/src/components/ui/input.tsx +21 -0
- package/src/components/ui/label.tsx +22 -0
- package/src/components/ui/popover.tsx +46 -0
- package/src/components/ui/separator.tsx +28 -0
- package/src/components/ui/sheet.tsx +137 -0
- package/src/components/ui/sidebar.tsx +726 -0
- package/src/components/ui/skeleton.tsx +13 -0
- package/src/components/ui/sonner.tsx +23 -0
- package/src/components/ui/switch.tsx +29 -0
- package/src/components/ui/toggle-group.tsx +11 -11
- package/src/components/ui/toggle.tsx +15 -13
- package/src/components/ui/tooltip.tsx +61 -0
- package/src/components/ui/tree-view.tsx +17 -12
- package/src/engine-handling/add-to-app-wrapper.ts +114 -0
- package/src/engine-handling/create-app-wrapper.ts +107 -0
- package/src/engine-handling/file-helpers.ts +25 -0
- package/src/engine-handling/framework-registration.ts +11 -0
- package/src/engine-handling/generate-initial-payload.ts +93 -0
- package/src/engine-handling/server-environment.ts +13 -0
- package/src/file-classes.ts +54 -0
- package/src/hooks/use-mobile.ts +19 -0
- package/src/hooks/use-streaming-status.ts +70 -0
- package/src/lib/api.ts +90 -0
- package/src/routeTree.gen.ts +4 -27
- package/src/routes/__root.tsx +36 -7
- package/src/routes/api/add-to-app.ts +21 -0
- package/src/routes/api/create-app.ts +21 -0
- package/src/routes/api/dry-run-add-to-app.ts +16 -0
- package/src/routes/api/dry-run-create-app.ts +16 -0
- package/src/routes/api/initial-payload.ts +10 -0
- package/src/routes/api/load-remote-add-on.ts +42 -0
- package/src/routes/api/load-starter.ts +47 -0
- package/src/routes/api/shutdown.ts +11 -0
- package/src/routes/index.tsx +3 -210
- package/src/store/add-ons.ts +81 -0
- package/src/store/project.ts +268 -0
- package/src/styles.css +47 -0
- package/src/types.d.ts +87 -0
- package/tests/store/add-ons.test.ts +222 -0
- package/vitest.config.ts +6 -0
- package/.cursorrules +0 -7
- package/src/components/Header.tsx +0 -13
- package/src/components/applied-add-on.tsx +0 -149
- package/src/lib/server-fns.ts +0 -78
- package/src/routes/api.demo-names.ts +0 -11
- package/src/routes/demo.tanstack-query.tsx +0 -28
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { cn } from "@/lib/utils"
|
|
2
|
+
|
|
3
|
+
function Skeleton({ className, ...props }: React.ComponentProps<"div">) {
|
|
4
|
+
return (
|
|
5
|
+
<div
|
|
6
|
+
data-slot="skeleton"
|
|
7
|
+
className={cn("bg-accent animate-pulse rounded-md", className)}
|
|
8
|
+
{...props}
|
|
9
|
+
/>
|
|
10
|
+
)
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export { Skeleton }
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { useTheme } from "next-themes"
|
|
2
|
+
import { Toaster as Sonner, ToasterProps } from "sonner"
|
|
3
|
+
|
|
4
|
+
const Toaster = ({ ...props }: ToasterProps) => {
|
|
5
|
+
const { theme = "system" } = useTheme()
|
|
6
|
+
|
|
7
|
+
return (
|
|
8
|
+
<Sonner
|
|
9
|
+
theme={theme as ToasterProps["theme"]}
|
|
10
|
+
className="toaster group"
|
|
11
|
+
style={
|
|
12
|
+
{
|
|
13
|
+
"--normal-bg": "var(--popover)",
|
|
14
|
+
"--normal-text": "var(--popover-foreground)",
|
|
15
|
+
"--normal-border": "var(--border)",
|
|
16
|
+
} as React.CSSProperties
|
|
17
|
+
}
|
|
18
|
+
{...props}
|
|
19
|
+
/>
|
|
20
|
+
)
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export { Toaster }
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import * as React from "react"
|
|
2
|
+
import * as SwitchPrimitive from "@radix-ui/react-switch"
|
|
3
|
+
|
|
4
|
+
import { cn } from "@/lib/utils"
|
|
5
|
+
|
|
6
|
+
function Switch({
|
|
7
|
+
className,
|
|
8
|
+
...props
|
|
9
|
+
}: React.ComponentProps<typeof SwitchPrimitive.Root>) {
|
|
10
|
+
return (
|
|
11
|
+
<SwitchPrimitive.Root
|
|
12
|
+
data-slot="switch"
|
|
13
|
+
className={cn(
|
|
14
|
+
"peer data-[state=checked]:bg-primary data-[state=unchecked]:bg-input focus-visible:border-ring focus-visible:ring-ring/50 dark:data-[state=unchecked]:bg-input/80 inline-flex h-[1.15rem] w-8 shrink-0 items-center rounded-full border border-transparent shadow-xs transition-all outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50",
|
|
15
|
+
className
|
|
16
|
+
)}
|
|
17
|
+
{...props}
|
|
18
|
+
>
|
|
19
|
+
<SwitchPrimitive.Thumb
|
|
20
|
+
data-slot="switch-thumb"
|
|
21
|
+
className={cn(
|
|
22
|
+
"bg-background dark:data-[state=unchecked]:bg-foreground dark:data-[state=checked]:bg-primary-foreground pointer-events-none block size-4 rounded-full ring-0 transition-transform data-[state=checked]:translate-x-[calc(100%-2px)] data-[state=unchecked]:translate-x-0"
|
|
23
|
+
)}
|
|
24
|
+
/>
|
|
25
|
+
</SwitchPrimitive.Root>
|
|
26
|
+
)
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export { Switch }
|
|
@@ -1,15 +1,15 @@
|
|
|
1
|
-
import * as React from
|
|
2
|
-
import * as ToggleGroupPrimitive from
|
|
3
|
-
import {
|
|
1
|
+
import * as React from 'react'
|
|
2
|
+
import * as ToggleGroupPrimitive from '@radix-ui/react-toggle-group'
|
|
3
|
+
import type { VariantProps } from 'class-variance-authority'
|
|
4
4
|
|
|
5
|
-
import { cn } from
|
|
6
|
-
import { toggleVariants } from
|
|
5
|
+
import { cn } from '@/lib/utils'
|
|
6
|
+
import { toggleVariants } from '@/components/ui/toggle'
|
|
7
7
|
|
|
8
8
|
const ToggleGroupContext = React.createContext<
|
|
9
9
|
VariantProps<typeof toggleVariants>
|
|
10
10
|
>({
|
|
11
|
-
size:
|
|
12
|
-
variant:
|
|
11
|
+
size: 'default',
|
|
12
|
+
variant: 'default',
|
|
13
13
|
})
|
|
14
14
|
|
|
15
15
|
function ToggleGroup({
|
|
@@ -26,8 +26,8 @@ function ToggleGroup({
|
|
|
26
26
|
data-variant={variant}
|
|
27
27
|
data-size={size}
|
|
28
28
|
className={cn(
|
|
29
|
-
|
|
30
|
-
className
|
|
29
|
+
'group/toggle-group flex w-fit items-center rounded-md data-[variant=outline]:shadow-xs',
|
|
30
|
+
className,
|
|
31
31
|
)}
|
|
32
32
|
{...props}
|
|
33
33
|
>
|
|
@@ -58,8 +58,8 @@ function ToggleGroupItem({
|
|
|
58
58
|
variant: context.variant || variant,
|
|
59
59
|
size: context.size || size,
|
|
60
60
|
}),
|
|
61
|
-
|
|
62
|
-
className
|
|
61
|
+
'min-w-0 flex-1 shrink-0 rounded-none shadow-none first:rounded-l-md last:rounded-r-md focus:z-10 focus-visible:z-10 data-[variant=outline]:border-l-0 data-[variant=outline]:first:border-l',
|
|
62
|
+
className,
|
|
63
63
|
)}
|
|
64
64
|
{...props}
|
|
65
65
|
>
|
|
@@ -1,31 +1,33 @@
|
|
|
1
|
-
|
|
1
|
+
'use client'
|
|
2
2
|
|
|
3
|
-
import * as React from
|
|
4
|
-
import * as TogglePrimitive from
|
|
5
|
-
import { cva
|
|
3
|
+
import * as React from 'react'
|
|
4
|
+
import * as TogglePrimitive from '@radix-ui/react-toggle'
|
|
5
|
+
import { cva } from 'class-variance-authority'
|
|
6
6
|
|
|
7
|
-
import {
|
|
7
|
+
import type { VariantProps } from 'class-variance-authority'
|
|
8
|
+
|
|
9
|
+
import { cn } from '@/lib/utils'
|
|
8
10
|
|
|
9
11
|
const toggleVariants = cva(
|
|
10
12
|
"inline-flex items-center justify-center gap-2 rounded-md text-sm font-medium hover:bg-muted hover:text-muted-foreground disabled:pointer-events-none disabled:opacity-50 data-[state=on]:bg-accent data-[state=on]:text-accent-foreground [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 [&_svg]:shrink-0 focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] outline-none transition-[color,box-shadow] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive whitespace-nowrap",
|
|
11
13
|
{
|
|
12
14
|
variants: {
|
|
13
15
|
variant: {
|
|
14
|
-
default:
|
|
16
|
+
default: 'bg-transparent',
|
|
15
17
|
outline:
|
|
16
|
-
|
|
18
|
+
'border border-input bg-transparent shadow-xs hover:bg-accent hover:text-accent-foreground',
|
|
17
19
|
},
|
|
18
20
|
size: {
|
|
19
|
-
default:
|
|
20
|
-
sm:
|
|
21
|
-
lg:
|
|
21
|
+
default: 'h-9 px-2 min-w-9',
|
|
22
|
+
sm: 'h-8 px-1.5 min-w-8',
|
|
23
|
+
lg: 'h-10 px-2.5 min-w-10',
|
|
22
24
|
},
|
|
23
25
|
},
|
|
24
26
|
defaultVariants: {
|
|
25
|
-
variant:
|
|
26
|
-
size:
|
|
27
|
+
variant: 'default',
|
|
28
|
+
size: 'default',
|
|
27
29
|
},
|
|
28
|
-
}
|
|
30
|
+
},
|
|
29
31
|
)
|
|
30
32
|
|
|
31
33
|
function Toggle({
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import * as React from "react"
|
|
4
|
+
import * as TooltipPrimitive from "@radix-ui/react-tooltip"
|
|
5
|
+
|
|
6
|
+
import { cn } from "@/lib/utils"
|
|
7
|
+
|
|
8
|
+
function TooltipProvider({
|
|
9
|
+
delayDuration = 0,
|
|
10
|
+
...props
|
|
11
|
+
}: React.ComponentProps<typeof TooltipPrimitive.Provider>) {
|
|
12
|
+
return (
|
|
13
|
+
<TooltipPrimitive.Provider
|
|
14
|
+
data-slot="tooltip-provider"
|
|
15
|
+
delayDuration={delayDuration}
|
|
16
|
+
{...props}
|
|
17
|
+
/>
|
|
18
|
+
)
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function Tooltip({
|
|
22
|
+
...props
|
|
23
|
+
}: React.ComponentProps<typeof TooltipPrimitive.Root>) {
|
|
24
|
+
return (
|
|
25
|
+
<TooltipProvider>
|
|
26
|
+
<TooltipPrimitive.Root data-slot="tooltip" {...props} />
|
|
27
|
+
</TooltipProvider>
|
|
28
|
+
)
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function TooltipTrigger({
|
|
32
|
+
...props
|
|
33
|
+
}: React.ComponentProps<typeof TooltipPrimitive.Trigger>) {
|
|
34
|
+
return <TooltipPrimitive.Trigger data-slot="tooltip-trigger" {...props} />
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function TooltipContent({
|
|
38
|
+
className,
|
|
39
|
+
sideOffset = 0,
|
|
40
|
+
children,
|
|
41
|
+
...props
|
|
42
|
+
}: React.ComponentProps<typeof TooltipPrimitive.Content>) {
|
|
43
|
+
return (
|
|
44
|
+
<TooltipPrimitive.Portal>
|
|
45
|
+
<TooltipPrimitive.Content
|
|
46
|
+
data-slot="tooltip-content"
|
|
47
|
+
sideOffset={sideOffset}
|
|
48
|
+
className={cn(
|
|
49
|
+
"bg-primary text-primary-foreground animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 w-fit origin-(--radix-tooltip-content-transform-origin) rounded-md px-3 py-1.5 text-xs text-balance",
|
|
50
|
+
className
|
|
51
|
+
)}
|
|
52
|
+
{...props}
|
|
53
|
+
>
|
|
54
|
+
{children}
|
|
55
|
+
<TooltipPrimitive.Arrow className="bg-primary fill-primary z-50 size-2.5 translate-y-[calc(-50%_-_2px)] rotate-45 rounded-[2px]" />
|
|
56
|
+
</TooltipPrimitive.Content>
|
|
57
|
+
</TooltipPrimitive.Portal>
|
|
58
|
+
)
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider }
|
|
@@ -22,7 +22,7 @@ interface TreeDataItem {
|
|
|
22
22
|
icon?: any
|
|
23
23
|
selectedIcon?: any
|
|
24
24
|
openIcon?: any
|
|
25
|
-
children?: TreeDataItem
|
|
25
|
+
children?: Array<TreeDataItem>
|
|
26
26
|
actions?: React.ReactNode
|
|
27
27
|
onClick?: () => void
|
|
28
28
|
draggable?: boolean
|
|
@@ -31,10 +31,11 @@ interface TreeDataItem {
|
|
|
31
31
|
}
|
|
32
32
|
|
|
33
33
|
type TreeProps = React.HTMLAttributes<HTMLDivElement> & {
|
|
34
|
-
data: TreeDataItem
|
|
34
|
+
data: Array<TreeDataItem> | TreeDataItem
|
|
35
35
|
initialSelectedItemId?: string
|
|
36
36
|
onSelectChange?: (item: TreeDataItem | undefined) => void
|
|
37
37
|
expandAll?: boolean
|
|
38
|
+
initialExpandedItemIds?: Array<string>
|
|
38
39
|
defaultNodeIcon?: any
|
|
39
40
|
defaultLeafIcon?: any
|
|
40
41
|
onDocumentDrag?: (sourceItem: TreeDataItem, targetItem: TreeDataItem) => void
|
|
@@ -47,6 +48,7 @@ const TreeView = React.forwardRef<HTMLDivElement, TreeProps>(
|
|
|
47
48
|
initialSelectedItemId,
|
|
48
49
|
onSelectChange,
|
|
49
50
|
expandAll,
|
|
51
|
+
initialExpandedItemIds,
|
|
50
52
|
defaultLeafIcon,
|
|
51
53
|
defaultNodeIcon,
|
|
52
54
|
className,
|
|
@@ -88,14 +90,17 @@ const TreeView = React.forwardRef<HTMLDivElement, TreeProps>(
|
|
|
88
90
|
)
|
|
89
91
|
|
|
90
92
|
const expandedItemIds = React.useMemo(() => {
|
|
91
|
-
if (
|
|
92
|
-
return
|
|
93
|
+
if (initialExpandedItemIds) {
|
|
94
|
+
return initialExpandedItemIds
|
|
95
|
+
}
|
|
96
|
+
if (!initialSelectedItemId && !expandAll) {
|
|
97
|
+
return [] as Array<string>
|
|
93
98
|
}
|
|
94
99
|
|
|
95
|
-
const ids: string
|
|
100
|
+
const ids: Array<string> = []
|
|
96
101
|
|
|
97
102
|
function walkTreeItems(
|
|
98
|
-
items: TreeDataItem
|
|
103
|
+
items: Array<TreeDataItem> | TreeDataItem,
|
|
99
104
|
targetId: string,
|
|
100
105
|
) {
|
|
101
106
|
if (items instanceof Array) {
|
|
@@ -115,7 +120,7 @@ const TreeView = React.forwardRef<HTMLDivElement, TreeProps>(
|
|
|
115
120
|
|
|
116
121
|
walkTreeItems(data, initialSelectedItemId)
|
|
117
122
|
return ids
|
|
118
|
-
}, [data, expandAll, initialSelectedItemId])
|
|
123
|
+
}, [data, expandAll, initialSelectedItemId, initialExpandedItemIds])
|
|
119
124
|
|
|
120
125
|
return (
|
|
121
126
|
<div className={cn('overflow-hidden relative p-2', className)}>
|
|
@@ -147,7 +152,7 @@ TreeView.displayName = 'TreeView'
|
|
|
147
152
|
type TreeItemProps = TreeProps & {
|
|
148
153
|
selectedItemId?: string
|
|
149
154
|
handleSelectChange: (item: TreeDataItem | undefined) => void
|
|
150
|
-
expandedItemIds: string
|
|
155
|
+
expandedItemIds: Array<string>
|
|
151
156
|
defaultNodeIcon?: any
|
|
152
157
|
defaultLeafIcon?: any
|
|
153
158
|
handleDragStart?: (item: TreeDataItem) => void
|
|
@@ -226,7 +231,7 @@ const TreeNode = ({
|
|
|
226
231
|
}: {
|
|
227
232
|
item: TreeDataItem
|
|
228
233
|
handleSelectChange: (item: TreeDataItem | undefined) => void
|
|
229
|
-
expandedItemIds: string
|
|
234
|
+
expandedItemIds: Array<string>
|
|
230
235
|
selectedItemId?: string
|
|
231
236
|
defaultNodeIcon?: any
|
|
232
237
|
defaultLeafIcon?: any
|
|
@@ -294,7 +299,7 @@ const TreeNode = ({
|
|
|
294
299
|
isOpen={value.includes(item.id)}
|
|
295
300
|
default={defaultNodeIcon}
|
|
296
301
|
/>
|
|
297
|
-
<span className="text-
|
|
302
|
+
<span className="text-md truncate">{item.name}</span>
|
|
298
303
|
<TreeActions isSelected={selectedItemId === item.id}>
|
|
299
304
|
{item.actions}
|
|
300
305
|
</TreeActions>
|
|
@@ -401,7 +406,7 @@ const TreeLeaf = React.forwardRef<
|
|
|
401
406
|
isSelected={selectedItemId === item.id}
|
|
402
407
|
default={defaultLeafIcon}
|
|
403
408
|
/>
|
|
404
|
-
<span className="flex-grow text-
|
|
409
|
+
<span className="flex-grow text-md truncate">{item.name}</span>
|
|
405
410
|
<TreeActions isSelected={selectedItemId === item.id}>
|
|
406
411
|
{item.actions}
|
|
407
412
|
</TreeActions>
|
|
@@ -438,7 +443,7 @@ const AccordionContent = React.forwardRef<
|
|
|
438
443
|
<AccordionPrimitive.Content
|
|
439
444
|
ref={ref}
|
|
440
445
|
className={cn(
|
|
441
|
-
'overflow-hidden text-
|
|
446
|
+
'overflow-hidden text-md transition-all data-[state=closed]:animate-accordion-up data-[state=open]:animate-accordion-down',
|
|
442
447
|
className,
|
|
443
448
|
)}
|
|
444
449
|
{...props}
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import { readFileSync } from 'node:fs'
|
|
2
|
+
import { resolve } from 'node:path'
|
|
3
|
+
|
|
4
|
+
import {
|
|
5
|
+
addToApp,
|
|
6
|
+
createDefaultEnvironment,
|
|
7
|
+
createMemoryEnvironment,
|
|
8
|
+
recursivelyGatherFiles,
|
|
9
|
+
} from '@tanstack/cta-engine'
|
|
10
|
+
|
|
11
|
+
import { createAppWrapper } from './create-app-wrapper.js'
|
|
12
|
+
|
|
13
|
+
import { getProjectPath } from '@/engine-handling/server-environment.js'
|
|
14
|
+
import {
|
|
15
|
+
cleanUpFileArray,
|
|
16
|
+
cleanUpFiles,
|
|
17
|
+
} from '@/engine-handling/file-helpers.js'
|
|
18
|
+
|
|
19
|
+
export async function addToAppWrapper(
|
|
20
|
+
addOns: Array<string>,
|
|
21
|
+
opts: {
|
|
22
|
+
dryRun?: boolean
|
|
23
|
+
stream?: boolean
|
|
24
|
+
},
|
|
25
|
+
) {
|
|
26
|
+
const projectPath = getProjectPath()
|
|
27
|
+
|
|
28
|
+
const persistedOptions = JSON.parse(
|
|
29
|
+
readFileSync(resolve(projectPath, '.cta.json')),
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
const newAddons: Array<string> = []
|
|
33
|
+
for (const addOn of addOns) {
|
|
34
|
+
if (!persistedOptions.existingAddOns.includes(addOn)) {
|
|
35
|
+
newAddons.push(addOn)
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
if (newAddons.length === 0) {
|
|
40
|
+
return await createAppWrapper(persistedOptions, opts)
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
async function createEnvironment() {
|
|
44
|
+
if (opts.dryRun) {
|
|
45
|
+
const { environment, output } = createMemoryEnvironment(projectPath)
|
|
46
|
+
environment.writeFile(
|
|
47
|
+
resolve(projectPath, '.cta.json'),
|
|
48
|
+
JSON.stringify(persistedOptions, null, 2),
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
const localFiles = await cleanUpFiles(
|
|
52
|
+
await recursivelyGatherFiles(projectPath, false),
|
|
53
|
+
)
|
|
54
|
+
for (const file of Object.keys(localFiles)) {
|
|
55
|
+
environment.writeFile(resolve(projectPath, file), localFiles[file])
|
|
56
|
+
}
|
|
57
|
+
return { environment, output }
|
|
58
|
+
}
|
|
59
|
+
return {
|
|
60
|
+
environment: createDefaultEnvironment(),
|
|
61
|
+
output: { files: {}, deletedFiles: [], commands: [] },
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const { environment, output } = await createEnvironment()
|
|
66
|
+
|
|
67
|
+
if (opts.stream) {
|
|
68
|
+
return new ReadableStream({
|
|
69
|
+
start(controller) {
|
|
70
|
+
environment.startStep = ({ id, type, message }) => {
|
|
71
|
+
controller.enqueue(
|
|
72
|
+
new TextEncoder().encode(
|
|
73
|
+
JSON.stringify({
|
|
74
|
+
msgType: 'start',
|
|
75
|
+
id,
|
|
76
|
+
type,
|
|
77
|
+
message,
|
|
78
|
+
}) + '\n',
|
|
79
|
+
),
|
|
80
|
+
)
|
|
81
|
+
}
|
|
82
|
+
environment.finishStep = (id, message) => {
|
|
83
|
+
controller.enqueue(
|
|
84
|
+
new TextEncoder().encode(
|
|
85
|
+
JSON.stringify({
|
|
86
|
+
msgType: 'finish',
|
|
87
|
+
id,
|
|
88
|
+
message,
|
|
89
|
+
}) + '\n',
|
|
90
|
+
),
|
|
91
|
+
)
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
environment.startRun()
|
|
95
|
+
addToApp(environment, newAddons, projectPath, {
|
|
96
|
+
forced: true,
|
|
97
|
+
}).then(() => {
|
|
98
|
+
environment.finishRun()
|
|
99
|
+
controller.close()
|
|
100
|
+
})
|
|
101
|
+
},
|
|
102
|
+
})
|
|
103
|
+
} else {
|
|
104
|
+
environment.startRun()
|
|
105
|
+
await addToApp(environment, newAddons, projectPath, {
|
|
106
|
+
forced: true,
|
|
107
|
+
})
|
|
108
|
+
environment.finishRun()
|
|
109
|
+
|
|
110
|
+
output.files = cleanUpFiles(output.files, projectPath)
|
|
111
|
+
output.deletedFiles = cleanUpFileArray(output.deletedFiles, projectPath)
|
|
112
|
+
return output
|
|
113
|
+
}
|
|
114
|
+
}
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import { resolve } from 'node:path'
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
createApp,
|
|
5
|
+
createDefaultEnvironment,
|
|
6
|
+
createMemoryEnvironment,
|
|
7
|
+
finalizeAddOns,
|
|
8
|
+
getFrameworkById,
|
|
9
|
+
loadStarter,
|
|
10
|
+
} from '@tanstack/cta-engine'
|
|
11
|
+
|
|
12
|
+
import { registerFrameworks } from './framework-registration'
|
|
13
|
+
|
|
14
|
+
import { cleanUpFileArray, cleanUpFiles } from './file-helpers'
|
|
15
|
+
import { getApplicationMode, getProjectPath } from './server-environment'
|
|
16
|
+
|
|
17
|
+
import type { Options, SerializedOptions, Starter } from '@tanstack/cta-engine'
|
|
18
|
+
|
|
19
|
+
export async function createAppWrapper(
|
|
20
|
+
projectOptions: SerializedOptions,
|
|
21
|
+
opts: { dryRun?: boolean; stream?: boolean },
|
|
22
|
+
) {
|
|
23
|
+
registerFrameworks()
|
|
24
|
+
|
|
25
|
+
const framework = getFrameworkById(projectOptions.framework)!
|
|
26
|
+
|
|
27
|
+
let starter: Starter | undefined
|
|
28
|
+
const addOns: Array<string> = [...projectOptions.chosenAddOns]
|
|
29
|
+
if (projectOptions.starter) {
|
|
30
|
+
starter = await loadStarter(projectOptions.starter)
|
|
31
|
+
for (const addOn of starter.dependsOn ?? []) {
|
|
32
|
+
addOns.push(addOn)
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
const chosenAddOns = await finalizeAddOns(
|
|
36
|
+
framework,
|
|
37
|
+
projectOptions.mode,
|
|
38
|
+
addOns,
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
const projectPath = getProjectPath()
|
|
42
|
+
const targetDir =
|
|
43
|
+
getApplicationMode() === 'add'
|
|
44
|
+
? projectOptions.targetDir
|
|
45
|
+
: resolve(projectPath, projectOptions.projectName)
|
|
46
|
+
|
|
47
|
+
const options: Options = {
|
|
48
|
+
...projectOptions,
|
|
49
|
+
targetDir,
|
|
50
|
+
starter,
|
|
51
|
+
framework,
|
|
52
|
+
chosenAddOns,
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function createEnvironment() {
|
|
56
|
+
if (opts.dryRun) {
|
|
57
|
+
return createMemoryEnvironment(targetDir)
|
|
58
|
+
}
|
|
59
|
+
return {
|
|
60
|
+
environment: createDefaultEnvironment(),
|
|
61
|
+
output: { files: {}, deletedFiles: [], commands: [] },
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const { environment, output } = createEnvironment()
|
|
66
|
+
|
|
67
|
+
if (opts.stream) {
|
|
68
|
+
return new ReadableStream({
|
|
69
|
+
start(controller) {
|
|
70
|
+
environment.startStep = ({ id, type, message }) => {
|
|
71
|
+
controller.enqueue(
|
|
72
|
+
new TextEncoder().encode(
|
|
73
|
+
JSON.stringify({
|
|
74
|
+
msgType: 'start',
|
|
75
|
+
id,
|
|
76
|
+
type,
|
|
77
|
+
message,
|
|
78
|
+
}) + '\n',
|
|
79
|
+
),
|
|
80
|
+
)
|
|
81
|
+
}
|
|
82
|
+
environment.finishStep = (id, message) => {
|
|
83
|
+
controller.enqueue(
|
|
84
|
+
new TextEncoder().encode(
|
|
85
|
+
JSON.stringify({
|
|
86
|
+
msgType: 'finish',
|
|
87
|
+
id,
|
|
88
|
+
message,
|
|
89
|
+
}) + '\n',
|
|
90
|
+
),
|
|
91
|
+
)
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
createApp(environment, options).then(() => {
|
|
95
|
+
controller.close()
|
|
96
|
+
})
|
|
97
|
+
},
|
|
98
|
+
})
|
|
99
|
+
} else {
|
|
100
|
+
await createApp(environment, options)
|
|
101
|
+
|
|
102
|
+
output.files = cleanUpFiles(output.files, targetDir)
|
|
103
|
+
output.deletedFiles = cleanUpFileArray(output.deletedFiles, targetDir)
|
|
104
|
+
|
|
105
|
+
return output
|
|
106
|
+
}
|
|
107
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { basename } from 'node:path'
|
|
2
|
+
|
|
3
|
+
export function cleanUpFiles(
|
|
4
|
+
files: Record<string, string>,
|
|
5
|
+
targetDir?: string,
|
|
6
|
+
) {
|
|
7
|
+
return Object.keys(files).reduce<Record<string, string>>((acc, file) => {
|
|
8
|
+
const content = files[file].startsWith('base64::')
|
|
9
|
+
? '<binary file>'
|
|
10
|
+
: files[file]
|
|
11
|
+
if (basename(file) !== '.cta.json') {
|
|
12
|
+
acc[targetDir ? file.replace(targetDir, '.') : file] = content
|
|
13
|
+
}
|
|
14
|
+
return acc
|
|
15
|
+
}, {})
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function cleanUpFileArray(files: Array<string>, targetDir?: string) {
|
|
19
|
+
return files.reduce<Array<string>>((acc, file) => {
|
|
20
|
+
if (basename(file) !== '.cta.json') {
|
|
21
|
+
acc.push(targetDir ? file.replace(targetDir, '.') : file)
|
|
22
|
+
}
|
|
23
|
+
return acc
|
|
24
|
+
}, [])
|
|
25
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { register as registerReactCra } from '@tanstack/cta-framework-react-cra'
|
|
2
|
+
import { register as registerSolid } from '@tanstack/cta-framework-solid'
|
|
3
|
+
|
|
4
|
+
let registered = false
|
|
5
|
+
|
|
6
|
+
export function registerFrameworks() {
|
|
7
|
+
if (registered) return
|
|
8
|
+
registerReactCra()
|
|
9
|
+
registerSolid()
|
|
10
|
+
registered = true
|
|
11
|
+
}
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import { readFileSync } from 'node:fs'
|
|
2
|
+
import { basename, resolve } from 'node:path'
|
|
3
|
+
|
|
4
|
+
import {
|
|
5
|
+
createSerializedOptionsFromPersisted,
|
|
6
|
+
getAllAddOns,
|
|
7
|
+
getFrameworkById,
|
|
8
|
+
recursivelyGatherFiles,
|
|
9
|
+
} from '@tanstack/cta-engine'
|
|
10
|
+
|
|
11
|
+
import { cleanUpFiles } from './file-helpers.js'
|
|
12
|
+
import { createAppWrapper } from './create-app-wrapper.js'
|
|
13
|
+
import { registerFrameworks } from './framework-registration.js'
|
|
14
|
+
import {
|
|
15
|
+
getApplicationMode,
|
|
16
|
+
getProjectOptions,
|
|
17
|
+
getProjectPath,
|
|
18
|
+
} from './server-environment.js'
|
|
19
|
+
|
|
20
|
+
import type { SerializedOptions } from '@tanstack/cta-engine'
|
|
21
|
+
|
|
22
|
+
export async function generateInitialPayload() {
|
|
23
|
+
registerFrameworks()
|
|
24
|
+
|
|
25
|
+
const projectPath = getProjectPath()
|
|
26
|
+
const applicationMode = getApplicationMode()
|
|
27
|
+
|
|
28
|
+
const localFiles =
|
|
29
|
+
applicationMode === 'add'
|
|
30
|
+
? await cleanUpFiles(await recursivelyGatherFiles(projectPath, false))
|
|
31
|
+
: {}
|
|
32
|
+
|
|
33
|
+
function getSerializedOptions() {
|
|
34
|
+
if (applicationMode === 'setup') {
|
|
35
|
+
const projectOptions = getProjectOptions()
|
|
36
|
+
return {
|
|
37
|
+
...projectOptions,
|
|
38
|
+
framework: projectOptions.framework || 'react-cra',
|
|
39
|
+
projectName: projectOptions.projectName || basename(projectPath),
|
|
40
|
+
mode: projectOptions.mode || 'file-router',
|
|
41
|
+
typescript: projectOptions.typescript || true,
|
|
42
|
+
tailwind: projectOptions.tailwind || true,
|
|
43
|
+
git: projectOptions.git || true,
|
|
44
|
+
} as SerializedOptions
|
|
45
|
+
} else {
|
|
46
|
+
const persistedOptions = JSON.parse(
|
|
47
|
+
readFileSync(resolve(projectPath, '.cta.json')),
|
|
48
|
+
)
|
|
49
|
+
return createSerializedOptionsFromPersisted(persistedOptions)
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const serializedOptions = getSerializedOptions()
|
|
54
|
+
|
|
55
|
+
const output = await createAppWrapper(serializedOptions, {
|
|
56
|
+
dryRun: true,
|
|
57
|
+
})
|
|
58
|
+
|
|
59
|
+
const framework = await getFrameworkById(serializedOptions.framework)
|
|
60
|
+
|
|
61
|
+
const codeRouter = getAllAddOns(framework!, 'code-router').map((addOn) => ({
|
|
62
|
+
id: addOn.id,
|
|
63
|
+
name: addOn.name,
|
|
64
|
+
description: addOn.description,
|
|
65
|
+
type: addOn.type,
|
|
66
|
+
smallLogo: addOn.smallLogo,
|
|
67
|
+
logo: addOn.logo,
|
|
68
|
+
link: addOn.link,
|
|
69
|
+
dependsOn: addOn.dependsOn,
|
|
70
|
+
}))
|
|
71
|
+
|
|
72
|
+
const fileRouter = getAllAddOns(framework!, 'file-router').map((addOn) => ({
|
|
73
|
+
id: addOn.id,
|
|
74
|
+
name: addOn.name,
|
|
75
|
+
description: addOn.description,
|
|
76
|
+
type: addOn.type,
|
|
77
|
+
smallLogo: addOn.smallLogo,
|
|
78
|
+
logo: addOn.logo,
|
|
79
|
+
link: addOn.link,
|
|
80
|
+
dependsOn: addOn.dependsOn,
|
|
81
|
+
}))
|
|
82
|
+
|
|
83
|
+
return {
|
|
84
|
+
applicationMode,
|
|
85
|
+
localFiles,
|
|
86
|
+
addOns: {
|
|
87
|
+
'code-router': codeRouter,
|
|
88
|
+
'file-router': fileRouter,
|
|
89
|
+
},
|
|
90
|
+
options: serializedOptions,
|
|
91
|
+
output,
|
|
92
|
+
}
|
|
93
|
+
}
|