Skip to content

Commit 374c29a

Browse files
committed
chore: end-to-end add flow
1 parent b0a5f1c commit 374c29a

File tree

8 files changed

+182
-55
lines changed

8 files changed

+182
-55
lines changed

packages/cta-engine/src/add-to-app.ts

Lines changed: 40 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { mkdir, readFile, writeFile } from 'node:fs/promises'
22
import { existsSync, statSync } from 'node:fs'
33
import { basename, dirname, resolve } from 'node:path'
4-
import { execa, execaSync } from 'execa'
4+
import { execaSync } from 'execa'
55

66
import { CONFIG_FILE } from './constants.js'
77
import { finalizeAddOns } from './add-ons.js'
@@ -12,7 +12,8 @@ import {
1212
} from './environment.js'
1313
import { createApp } from './create-app.js'
1414
import { readConfigFile, writeConfigFile } from './config-file.js'
15-
import { sortObject } from './utils.js'
15+
import { formatCommand, sortObject } from './utils.js'
16+
import { packageManagerInstall } from './package-manager.js'
1617

1718
import type { Environment, Mode, Options } from './types.js'
1819
import type { PersistedOptions } from './config-file.js'
@@ -73,21 +74,22 @@ export async function addToApp(
7374
if (!silent) {
7475
environment.intro(`Adding ${addOns.join(', ')} to the project...`)
7576
}
76-
console.log('>>hasPendingGitChanges')
7777
if (await hasPendingGitChanges()) {
7878
environment.error(
7979
'You have pending git changes.',
8080
'Please commit or stash them before adding add-ons.',
8181
)
8282
return
8383
}
84-
console.log('>>hasPendingGitChanges')
84+
85+
environment.startStep('Processing new app setup...')
86+
8587
const newOptions = await createOptions(persistedOptions, addOns)
8688
const output = await runCreateApp(newOptions)
8789
const overwrittenFiles: Array<string> = []
8890
const changedFiles: Array<string> = []
8991
const contentMap = new Map<string, string>()
90-
console.log('>>hasPendingGitChanges')
92+
9193
for (const file of Object.keys(output.files)) {
9294
const relativeFile = file.replace(process.cwd(), '')
9395
if (existsSync(file)) {
@@ -106,6 +108,9 @@ export async function addToApp(
106108
contentMap.set(relativeFile, output.files[file])
107109
}
108110
}
111+
112+
environment.finishStep('App setup processed')
113+
109114
if (overwrittenFiles.length > 0 && !silent) {
110115
environment.warn(
111116
'The following will be overwritten:',
@@ -116,6 +121,9 @@ export async function addToApp(
116121
process.exit(0)
117122
}
118123
}
124+
125+
environment.startStep('Writing files...')
126+
119127
for (const file of [...changedFiles, ...overwrittenFiles]) {
120128
const targetFile = `.${file}`
121129
const fName = basename(file)
@@ -140,29 +148,52 @@ export async function addToApp(
140148
await writeFile(resolve(targetFile), contents)
141149
}
142150
}
151+
152+
environment.finishStep('Files written')
153+
143154
// Handle commands
155+
144156
const originalOutput = await runCreateApp(
145157
await createOptions(persistedOptions, []),
146158
)
159+
147160
const originalCommands = new Set(
148161
originalOutput.commands.map((c) => [c.command, ...c.args].join(' ')),
149162
)
163+
150164
for (const command of output.commands) {
151165
const commandString = [command.command, ...command.args].join(' ')
152166
if (!originalCommands.has(commandString)) {
153-
await execa(command.command, command.args)
167+
environment.startStep(
168+
`Running ${formatCommand({ command: command.command, args: command.args })}...`,
169+
)
170+
await environment.execute(
171+
command.command,
172+
command.args,
173+
newOptions.targetDir,
174+
)
175+
environment.finishStep(`${command.command} complete`)
154176
}
155177
}
178+
179+
environment.startStep('Writing config file...')
156180
const realEnvironment = createDefaultEnvironment()
157181
writeConfigFile(realEnvironment, process.cwd(), newOptions)
182+
environment.finishStep('Config file written')
183+
184+
environment.startStep(
185+
`Installing dependencies via ${newOptions.packageManager}...`,
186+
)
158187
const s = silent ? null : environment.spinner()
159188
s?.start(`Installing dependencies via ${newOptions.packageManager}...`)
160-
await realEnvironment.execute(
189+
await packageManagerInstall(
190+
realEnvironment,
191+
newOptions.targetDir,
161192
newOptions.packageManager,
162-
['install'],
163-
resolve(process.cwd()),
164193
)
165194
s?.stop(`Installed dependencies`)
195+
environment.finishStep('Installed dependencies')
196+
166197
if (!silent) {
167198
environment.outro('Add-ons added successfully!')
168199
}

packages/cta-engine/src/environment.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,10 @@ export function createDefaultEnvironment(): Environment {
5858
exists: (path: string) => existsSync(path),
5959

6060
appName: 'TanStack',
61+
62+
startStep: () => {},
63+
finishStep: () => {},
64+
6165
intro: () => {},
6266
outro: () => {},
6367
info: () => {},

packages/cta-engine/src/types.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -158,6 +158,9 @@ type FileEnvironment = {
158158
type UIEnvironment = {
159159
appName: string
160160

161+
startStep: (message: string) => void
162+
finishStep: (message: string) => void
163+
161164
intro: (message: string) => void
162165
outro: (message: string) => void
163166

Lines changed: 75 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,33 +1,88 @@
1+
import { useState } from 'react'
12
import { useStore } from '@tanstack/react-store'
23

34
import { Button } from '@/components/ui/button'
5+
import {
6+
Dialog,
7+
DialogClose,
8+
DialogContent,
9+
DialogFooter,
10+
DialogHeader,
11+
DialogTitle,
12+
} from '@/components/ui/dialog'
13+
414
import { selectedAddOns } from '@/store/project'
515

616
export default function RunAddOns() {
717
const currentlySelectedAddOns = useStore(selectedAddOns)
18+
const [isRunning, setIsRunning] = useState(false)
19+
const [output, setOutput] = useState('')
20+
const [finished, setFinished] = useState(false)
21+
22+
async function onAddToApp() {
23+
setIsRunning(true)
24+
setOutput('')
25+
26+
const streamingReq = await fetch('/api/add-to-app', {
27+
method: 'POST',
28+
body: JSON.stringify({
29+
addOns: selectedAddOns.state.map((addOn) => addOn.id),
30+
}),
31+
headers: {
32+
'Content-Type': 'application/json',
33+
},
34+
})
35+
const reader = streamingReq.body?.getReader()
36+
const decoder = new TextDecoder()
37+
38+
while (true) {
39+
const result = await reader?.read()
40+
if (result?.done) break
41+
setOutput((s) => s + decoder.decode(result?.value))
42+
}
43+
setFinished(true)
44+
}
845

946
return (
1047
<div>
11-
<Button
12-
variant="default"
13-
onClick={async () => {
14-
await fetch('/api/add-to-app', {
15-
method: 'POST',
16-
body: JSON.stringify({
17-
addOns: selectedAddOns.state.map((addOn) => addOn.id),
18-
}),
19-
headers: {
20-
'Content-Type': 'application/json',
21-
},
22-
})
23-
// await closeApp()
24-
// window.close()
25-
}}
26-
disabled={currentlySelectedAddOns.length === 0}
27-
className="w-full"
28-
>
29-
Run Add-Ons
30-
</Button>
48+
<Dialog open={isRunning}>
49+
<DialogContent
50+
className="sm:min-w-[425px] sm:max-w-fit"
51+
hideCloseButton
52+
>
53+
<DialogHeader>
54+
<DialogTitle>Adding Add-Ons</DialogTitle>
55+
</DialogHeader>
56+
<div className="grid gap-4 py-4">
57+
<pre>{output}</pre>
58+
</div>
59+
<DialogFooter>
60+
<Button
61+
variant="default"
62+
onClick={async () => {
63+
await fetch('/api/shutdown', {
64+
method: 'POST',
65+
})
66+
window.close()
67+
}}
68+
disabled={!finished}
69+
>
70+
Exit This Application
71+
</Button>
72+
</DialogFooter>
73+
</DialogContent>
74+
</Dialog>
75+
76+
<div className="flex flex-col gap-2">
77+
<Button
78+
variant="default"
79+
onClick={onAddToApp}
80+
disabled={currentlySelectedAddOns.length === 0 || isRunning}
81+
className="w-full"
82+
>
83+
Run Add-Ons
84+
</Button>
85+
</div>
3186
</div>
3287
)
3388
}

packages/cta-ui/src/components/ui/dialog.tsx

Lines changed: 25 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
1-
import * as React from "react"
2-
import * as DialogPrimitive from "@radix-ui/react-dialog"
3-
import { XIcon } from "lucide-react"
1+
import * as React from 'react'
2+
import * as DialogPrimitive from '@radix-ui/react-dialog'
3+
import { XIcon } from 'lucide-react'
44

5-
import { cn } from "@/lib/utils"
5+
import { cn } from '@/lib/utils'
66

77
function Dialog({
88
...props
@@ -36,8 +36,8 @@ function DialogOverlay({
3636
<DialogPrimitive.Overlay
3737
data-slot="dialog-overlay"
3838
className={cn(
39-
"data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/50",
40-
className
39+
'data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/50',
40+
className,
4141
)}
4242
{...props}
4343
/>
@@ -47,46 +47,51 @@ function DialogOverlay({
4747
function DialogContent({
4848
className,
4949
children,
50+
hideCloseButton,
5051
...props
51-
}: React.ComponentProps<typeof DialogPrimitive.Content>) {
52+
}: React.ComponentProps<typeof DialogPrimitive.Content> & {
53+
hideCloseButton?: boolean
54+
}) {
5255
return (
5356
<DialogPortal data-slot="dialog-portal">
5457
<DialogOverlay />
5558
<DialogPrimitive.Content
5659
data-slot="dialog-content"
5760
className={cn(
58-
"bg-background data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 fixed top-[50%] left-[50%] z-50 grid w-full max-w-[calc(100%-2rem)] translate-x-[-50%] translate-y-[-50%] gap-4 rounded-lg border p-6 shadow-lg duration-200 sm:max-w-lg",
59-
className
61+
'bg-background data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 fixed top-[50%] left-[50%] z-50 grid w-full max-w-[calc(100%-2rem)] translate-x-[-50%] translate-y-[-50%] gap-4 rounded-lg p-6 shadow-lg duration-200 sm:max-w-lg bg-opacity-70 backdrop-blur-md border-3 border-white/20',
62+
className,
6063
)}
6164
{...props}
6265
>
6366
{children}
64-
<DialogPrimitive.Close className="ring-offset-background focus:ring-ring data-[state=open]:bg-accent data-[state=open]:text-muted-foreground absolute top-4 right-4 rounded-xs opacity-70 transition-opacity hover:opacity-100 focus:ring-2 focus:ring-offset-2 focus:outline-hidden disabled:pointer-events-none [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4">
65-
<XIcon />
66-
<span className="sr-only">Close</span>
67-
</DialogPrimitive.Close>
67+
{!hideCloseButton && (
68+
<DialogPrimitive.Close className="ring-offset-background focus:ring-ring data-[state=open]:bg-accent data-[state=open]:text-muted-foreground absolute top-4 right-4 rounded-xs opacity-70 transition-opacity hover:opacity-100 focus:ring-2 focus:ring-offset-2 focus:outline-hidden disabled:pointer-events-none [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4">
69+
<XIcon />
70+
<span className="sr-only">Close</span>
71+
</DialogPrimitive.Close>
72+
)}
6873
</DialogPrimitive.Content>
6974
</DialogPortal>
7075
)
7176
}
7277

73-
function DialogHeader({ className, ...props }: React.ComponentProps<"div">) {
78+
function DialogHeader({ className, ...props }: React.ComponentProps<'div'>) {
7479
return (
7580
<div
7681
data-slot="dialog-header"
77-
className={cn("flex flex-col gap-2 text-center sm:text-left", className)}
82+
className={cn('flex flex-col gap-2 text-center sm:text-left', className)}
7883
{...props}
7984
/>
8085
)
8186
}
8287

83-
function DialogFooter({ className, ...props }: React.ComponentProps<"div">) {
88+
function DialogFooter({ className, ...props }: React.ComponentProps<'div'>) {
8489
return (
8590
<div
8691
data-slot="dialog-footer"
8792
className={cn(
88-
"flex flex-col-reverse gap-2 sm:flex-row sm:justify-end",
89-
className
93+
'flex flex-col-reverse gap-2 sm:flex-row sm:justify-end',
94+
className,
9095
)}
9196
{...props}
9297
/>
@@ -100,7 +105,7 @@ function DialogTitle({
100105
return (
101106
<DialogPrimitive.Title
102107
data-slot="dialog-title"
103-
className={cn("text-lg leading-none font-semibold", className)}
108+
className={cn('text-lg leading-none font-semibold', className)}
104109
{...props}
105110
/>
106111
)
@@ -113,7 +118,7 @@ function DialogDescription({
113118
return (
114119
<DialogPrimitive.Description
115120
data-slot="dialog-description"
116-
className={cn("text-muted-foreground text-sm", className)}
121+
className={cn('text-muted-foreground text-sm', className)}
117122
{...props}
118123
/>
119124
)

0 commit comments

Comments
 (0)