Skip to content

Commit 3695aff

Browse files
committed
chore: create end-to-end with UI
1 parent 374c29a commit 3695aff

File tree

15 files changed

+306
-61
lines changed

15 files changed

+306
-61
lines changed

packages/cta-cli/src/cli.ts

Lines changed: 10 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import {
99
compileAddOn,
1010
compileStarter,
1111
createApp,
12+
createSerializedOptions,
1213
getAllAddOns,
1314
getFrameworkById,
1415
getFrameworkByName,
@@ -123,15 +124,6 @@ export function cli({
123124
.action(async () => {
124125
await compileAddOn(environment)
125126
})
126-
addOnCommand
127-
.command('ui')
128-
.description('Show the add-on developer UI')
129-
.action(() => {
130-
launchUI({
131-
mode: 'add',
132-
addOns: [],
133-
})
134-
})
135127

136128
const starterCommand = program.command('starter')
137129
starterCommand
@@ -146,15 +138,6 @@ export function cli({
146138
.action(async () => {
147139
await compileStarter(environment)
148140
})
149-
starterCommand
150-
.command('ui')
151-
.description('Show the starter developer UI')
152-
.action(() => {
153-
launchUI({
154-
mode: 'starter',
155-
addOns: [],
156-
})
157-
})
158141

159142
program.argument('[project-name]', 'name of the project')
160143

@@ -245,6 +228,7 @@ export function cli({
245228
)
246229
.option('--mcp', 'run the MCP server', false)
247230
.option('--mcp-sse', 'run the MCP server in SSE mode', false)
231+
.option('--ui', 'Add with the UI')
248232

249233
program.action(async (projectName: string, options: CliOptions) => {
250234
if (options.listAddOns) {
@@ -302,7 +286,14 @@ export function cli({
302286
finalOptions.targetDir =
303287
options.targetDir || resolve(process.cwd(), finalOptions.projectName)
304288

305-
await createApp(environment, finalOptions)
289+
if (options.ui) {
290+
launchUI({
291+
mode: 'setup',
292+
options: createSerializedOptions(finalOptions),
293+
})
294+
} else {
295+
await createApp(environment, finalOptions)
296+
}
306297
} catch (error) {
307298
log.error(
308299
error instanceof Error ? error.message : 'An unknown error occurred',

packages/cta-cli/src/types.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,4 +17,5 @@ export interface CliOptions {
1717
starter?: string
1818
targetDir?: string
1919
interactive?: boolean
20+
ui?: boolean
2021
}

packages/cta-engine/src/create-app.ts

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,28 +38,38 @@ async function writeFiles(environment: Environment, options: Options) {
3838
}
3939
}
4040

41+
environment.startStep('Writing framework files...')
4142
await writeFileBundle(options.framework)
43+
environment.finishStep('Framework files written')
4244

4345
for (const type of ['add-on', 'example', 'toolchain']) {
4446
for (const phase of ['setup', 'add-on', 'example']) {
4547
for (const addOn of options.chosenAddOns.filter(
4648
(addOn) => addOn.phase === phase && addOn.type === type,
4749
)) {
50+
environment.startStep(`Writing ${addOn.name} files...`)
4851
await writeFileBundle(addOn)
52+
environment.finishStep(`${addOn.name} files written`)
4953
}
5054
}
5155
}
5256

5357
if (options.starter) {
58+
environment.startStep(`Writing starter files...`)
5459
await writeFileBundle(options.starter)
60+
environment.finishStep(`Starter files written`)
5561
}
5662

63+
environment.startStep(`Writing package.json...`)
5764
await environment.writeFile(
5865
resolve(options.targetDir, './package.json'),
5966
JSON.stringify(createPackageJSON(options), null, 2),
6067
)
68+
environment.finishStep(`package.json written`)
6169

70+
environment.startStep(`Writing config file...`)
6271
await writeConfigFile(environment, options.targetDir, options)
72+
environment.finishStep(`Config file written`)
6373
}
6474

6575
async function runCommandsAndInstallDependencies(
@@ -71,17 +81,25 @@ async function runCommandsAndInstallDependencies(
7181
// Setup git
7282
if (options.git) {
7383
s.start(`Initializing git repository...`)
84+
environment.startStep(`Initializing git repository...`)
85+
7486
await setupGit(environment, options.targetDir)
87+
88+
environment.finishStep(`Initialized git repository`)
7589
s.stop(`Initialized git repository`)
7690
}
7791

7892
// Install dependencies
7993
s.start(`Installing dependencies via ${options.packageManager}...`)
94+
environment.startStep(
95+
`Installing dependencies via ${options.packageManager}...`,
96+
)
8097
await packageManagerInstall(
8198
environment,
8299
options.targetDir,
83100
options.packageManager,
84101
)
102+
environment.finishStep(`Installed dependencies`)
85103
s.stop(`Installed dependencies`)
86104

87105
for (const phase of ['setup', 'add-on', 'example']) {
@@ -90,11 +108,13 @@ async function runCommandsAndInstallDependencies(
90108
addOn.phase === phase && addOn.command && addOn.command.command,
91109
)) {
92110
s.start(`Setting up ${addOn.name}...`)
111+
environment.startStep(`Setting up ${addOn.name}...`)
93112
await environment.execute(
94113
addOn.command!.command,
95114
addOn.command!.args || [],
96115
options.targetDir,
97116
)
117+
environment.finishStep(`${addOn.name} setup complete`)
98118
s.stop(`${addOn.name} setup complete`)
99119
}
100120
}
@@ -106,11 +126,15 @@ async function runCommandsAndInstallDependencies(
106126
options.starter.command.command
107127
) {
108128
s.start(`Setting up starter ${options.starter.name}...`)
129+
environment.startStep(`Setting up starter ${options.starter.name}...`)
130+
109131
await environment.execute(
110132
options.starter.command.command,
111133
options.starter.command.args || [],
112134
options.targetDir,
113135
)
136+
137+
environment.finishStep(`Starter ${options.starter.name} setup complete`)
114138
s.stop(`Starter ${options.starter.name} setup complete`)
115139
}
116140

packages/cta-engine/src/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,8 @@ export {
3939
recursivelyGatherFiles,
4040
} from './custom-add-ons/shared.js'
4141

42+
export { createSerializedOptions } from './options.js'
43+
4244
export type {
4345
AddOn,
4446
Environment,

packages/cta-engine/src/integrations/shadcn.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,13 +30,19 @@ export async function installShadcnComponents(
3030
s.start(
3131
`Installing shadcn components (${Array.from(shadcnComponents).join(', ')})...`,
3232
)
33+
environment.startStep(
34+
`Installing shadcn components (${Array.from(shadcnComponents).join(', ')})...`,
35+
)
36+
3337
await packageManagerExecute(
3438
environment,
3539
resolve(targetDir),
3640
options.packageManager,
3741
'shadcn@latest',
3842
['add', '--silent', '--yes', ...Array.from(shadcnComponents)],
3943
)
44+
45+
environment.finishStep(`Installed additional shadcn components`)
4046
s.stop(`Installed additional shadcn components`)
4147
}
4248
}

packages/cta-engine/src/options.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
import type { Options, SerializedOptions } from './types'
2+
3+
export const createSerializedOptions = (options: Options) => {
4+
const serializedOptions: SerializedOptions = {
5+
...options,
6+
chosenAddOns: options.chosenAddOns.map((addOn) => addOn.id),
7+
framework: options.framework.id,
8+
starter: options.starter?.id,
9+
}
10+
return serializedOptions
11+
}

packages/cta-ui/lib/index.ts

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,22 @@
11
import { dirname, resolve } from 'node:path'
22
import { fileURLToPath } from 'node:url'
33

4+
import type { SerializedOptions } from '@tanstack/cta-engine'
5+
46
export function launchUI({
57
mode,
68
addOns,
9+
options,
710
}: {
8-
mode: 'add' | 'add-on' | 'starter'
9-
addOns: Array<string>
11+
mode: 'add' | 'setup'
12+
addOns?: Array<string>
13+
options?: SerializedOptions
1014
}) {
1115
const projectPath = process.cwd()
1216

1317
process.env.CTA_PROJECT_PATH = projectPath
14-
process.env.CTA_ADD_ONS = addOns.join(',')
18+
process.env.CTA_ADD_ONS = addOns?.join(',') || ''
19+
process.env.CTA_OPTIONS = options ? JSON.stringify(options) : ''
1520
process.env.CTA_MODE = mode
1621

1722
const developerPath = resolve(dirname(fileURLToPath(import.meta.url)), '..')

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import {
88

99
import { SelectedAddOns } from '@/components/sidebar-items/add-ons'
1010
import RunAddOns from '@/components/sidebar-items/run-add-ons'
11+
import RunCreateApp from '@/components/sidebar-items/run-create-app'
1112

1213
export function AppSidebar() {
1314
return (
@@ -21,6 +22,7 @@ export function AppSidebar() {
2122
</SidebarContent>
2223
<SidebarFooter>
2324
<RunAddOns />
25+
<RunCreateApp />
2426
</SidebarFooter>
2527
</Sidebar>
2628
)

packages/cta-ui/src/components/sidebar-items/run-add-ons.tsx

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,21 +4,26 @@ import { useStore } from '@tanstack/react-store'
44
import { Button } from '@/components/ui/button'
55
import {
66
Dialog,
7-
DialogClose,
87
DialogContent,
98
DialogFooter,
109
DialogHeader,
1110
DialogTitle,
1211
} from '@/components/ui/dialog'
1312

14-
import { selectedAddOns } from '@/store/project'
13+
import { applicationMode, selectedAddOns } from '@/store/project'
1514

1615
export default function RunAddOns() {
1716
const currentlySelectedAddOns = useStore(selectedAddOns)
1817
const [isRunning, setIsRunning] = useState(false)
1918
const [output, setOutput] = useState('')
2019
const [finished, setFinished] = useState(false)
2120

21+
const mode = useStore(applicationMode)
22+
23+
if (mode !== 'add') {
24+
return null
25+
}
26+
2227
async function onAddToApp() {
2328
setIsRunning(true)
2429
setOutput('')
Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
import { useState } from 'react'
2+
import { useStore } from '@tanstack/react-store'
3+
4+
import { Button } from '@/components/ui/button'
5+
import {
6+
Dialog,
7+
DialogContent,
8+
DialogFooter,
9+
DialogHeader,
10+
DialogTitle,
11+
} from '@/components/ui/dialog'
12+
13+
import {
14+
applicationMode,
15+
projectOptions,
16+
selectedAddOns,
17+
} from '@/store/project'
18+
19+
export default function RunCreateApp() {
20+
const currentlySelectedAddOns = useStore(selectedAddOns)
21+
const [isRunning, setIsRunning] = useState(false)
22+
const [output, setOutput] = useState('')
23+
const [finished, setFinished] = useState(false)
24+
25+
const mode = useStore(applicationMode)
26+
const options = useStore(projectOptions)
27+
28+
if (mode !== 'setup') {
29+
return null
30+
}
31+
32+
async function onAddToApp() {
33+
setIsRunning(true)
34+
setOutput('')
35+
36+
const streamingReq = await fetch('/api/create-app', {
37+
method: 'POST',
38+
body: JSON.stringify({
39+
options: {
40+
...options,
41+
chosenAddOns: selectedAddOns.state.map((addOn) => addOn.id),
42+
},
43+
}),
44+
headers: {
45+
'Content-Type': 'application/json',
46+
},
47+
})
48+
const reader = streamingReq.body?.getReader()
49+
const decoder = new TextDecoder()
50+
51+
while (true) {
52+
const result = await reader?.read()
53+
if (result?.done) break
54+
setOutput((s) => s + decoder.decode(result?.value))
55+
}
56+
setFinished(true)
57+
}
58+
59+
return (
60+
<div>
61+
<Dialog open={isRunning}>
62+
<DialogContent
63+
className="sm:min-w-[425px] sm:max-w-fit"
64+
hideCloseButton
65+
>
66+
<DialogHeader>
67+
<DialogTitle>Creating Your Application</DialogTitle>
68+
</DialogHeader>
69+
<div className="grid gap-4 py-4">
70+
<pre>{output}</pre>
71+
</div>
72+
<DialogFooter>
73+
<Button
74+
variant="default"
75+
onClick={async () => {
76+
await fetch('/api/shutdown', {
77+
method: 'POST',
78+
})
79+
window.close()
80+
}}
81+
disabled={!finished}
82+
>
83+
Exit This Application
84+
</Button>
85+
</DialogFooter>
86+
</DialogContent>
87+
</Dialog>
88+
89+
<div className="flex flex-col gap-2">
90+
<Button
91+
variant="default"
92+
onClick={onAddToApp}
93+
disabled={currentlySelectedAddOns.length === 0 || isRunning}
94+
className="w-full"
95+
>
96+
Run Create App
97+
</Button>
98+
</div>
99+
</div>
100+
)
101+
}

0 commit comments

Comments
 (0)