Skip to content

Commit 28ba2c9

Browse files
committed
chore: more UI updates and support for ts-router to start add mode
1 parent f454a39 commit 28ba2c9

File tree

17 files changed

+194
-78
lines changed

17 files changed

+194
-78
lines changed

frameworks/react-cra/add-ons/start/info.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,5 +19,6 @@
1919
"path": "src/routes/demo.start.api-request.tsx",
2020
"jsName": "StartApiRequestDemo"
2121
}
22-
]
22+
],
23+
"deletedFiles": ["./vite.config.js", "./vite.config.ts", "./index.html"]
2324
}

packages/cta-cli/src/cli.ts

Lines changed: 15 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -89,20 +89,29 @@ export function cli({
8989
program
9090
.command('add')
9191
.argument(
92-
'[add-on]',
92+
'[add-on...]',
9393
'Name of the add-ons (or add-ons separated by spaces or commas)',
9494
)
9595
.option('--ui', 'Add with the UI')
96-
.action(async (addOn: string, options: { ui: boolean }) => {
97-
const addOns = (addOn || '').split(',').map((addon) => addon.trim())
98-
if (options.ui) {
96+
.action(async (addOns: Array<string>) => {
97+
const parsedAddOns: Array<string> = []
98+
for (const addOn of addOns) {
99+
if (addOn.includes(',') || addOn.includes(' ')) {
100+
parsedAddOns.push(
101+
...addOn.split(/[\s,]+/).map((addon) => addon.trim()),
102+
)
103+
} else {
104+
parsedAddOns.push(addOn.trim())
105+
}
106+
}
107+
if (program.opts().ui) {
99108
launchUI({
100109
mode: 'add',
101-
addOns,
110+
addOns: parsedAddOns,
102111
})
103112
} else {
104113
await addToApp(
105-
addOns,
114+
parsedAddOns,
106115
{
107116
silent: false,
108117
},

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

Lines changed: 20 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { mkdir, readFile, writeFile } from 'node:fs/promises'
1+
import { mkdir, readFile, unlink, writeFile } from 'node:fs/promises'
22
import { existsSync, statSync } from 'node:fs'
33
import { basename, dirname, resolve } from 'node:path'
44
import { execaSync } from 'execa'
@@ -14,6 +14,7 @@ import { createApp } from './create-app.js'
1414
import { readConfigFile, writeConfigFile } from './config-file.js'
1515
import { formatCommand, sortObject } from './utils.js'
1616
import { packageManagerInstall } from './package-manager.js'
17+
import { getBinaryFile, isBase64, readFileHelper } from './file-helpers.js'
1718

1819
import type { Environment, Mode, Options } from './types.js'
1920
import type { PersistedOptions } from './config-file.js'
@@ -94,7 +95,7 @@ export async function addToApp(
9495
const relativeFile = file.replace(process.cwd(), '')
9596
if (existsSync(file)) {
9697
if (!isDirectory(file)) {
97-
const contents = (await readFile(file)).toString()
98+
const contents = readFileHelper(file)
9899
if (
99100
['package.json', CONFIG_FILE].includes(basename(file)) ||
100101
contents !== output.files[file]
@@ -109,12 +110,17 @@ export async function addToApp(
109110
}
110111
}
111112

113+
const deletedFiles: Array<string> = []
114+
for (const file of output.deletedFiles) {
115+
deletedFiles.push(file.replace(process.cwd(), ''))
116+
}
117+
112118
environment.finishStep('App setup processed')
113119

114120
if (overwrittenFiles.length > 0 && !silent) {
115121
environment.warn(
116122
'The following will be overwritten:',
117-
overwrittenFiles.join('\n'),
123+
[...overwrittenFiles, ...deletedFiles].join('\n'),
118124
)
119125
const shouldContinue = await environment.confirm('Do you want to continue?')
120126
if (!shouldContinue) {
@@ -124,6 +130,12 @@ export async function addToApp(
124130

125131
environment.startStep('Writing files...')
126132

133+
for (const file of output.deletedFiles) {
134+
if (existsSync(file)) {
135+
await unlink(file)
136+
}
137+
}
138+
127139
for (const file of [...changedFiles, ...overwrittenFiles]) {
128140
const targetFile = `.${file}`
129141
const fName = basename(file)
@@ -145,7 +157,11 @@ export async function addToApp(
145157
await writeFile(targetFile, JSON.stringify(currentJson, null, 2))
146158
} else if (fName !== CONFIG_FILE) {
147159
await mkdir(resolve(dirname(targetFile)), { recursive: true })
148-
await writeFile(resolve(targetFile), contents)
160+
if (isBase64(contents)) {
161+
await writeFile(resolve(targetFile), getBinaryFile(contents)!)
162+
} else {
163+
await writeFile(resolve(targetFile), contents)
164+
}
149165
}
150166
}
151167

packages/cta-engine/src/environment.ts

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,9 @@ export function createDefaultEnvironment(): Environment {
5252
}
5353
},
5454
deleteFile: async (path: string) => {
55-
await unlink(path)
55+
if (existsSync(path)) {
56+
await unlink(path)
57+
}
5658
},
5759

5860
exists: (path: string) => existsSync(path),
@@ -80,13 +82,15 @@ export function createMemoryEnvironment() {
8082

8183
const output: {
8284
files: Record<string, string>
85+
deletedFiles: Array<string>
8386
commands: Array<{
8487
command: string
8588
args: Array<string>
8689
}>
8790
} = {
8891
files: {},
8992
commands: [],
93+
deletedFiles: [],
9094
}
9195

9296
const { fs, vol } = memfs({})
@@ -127,7 +131,10 @@ export function createMemoryEnvironment() {
127131
await fs.writeFileSync(path, contents)
128132
}
129133
environment.deleteFile = async (path: string) => {
130-
await fs.unlinkSync(path)
134+
output.deletedFiles.push(path)
135+
if (fs.existsSync(path)) {
136+
await fs.unlinkSync(path)
137+
}
131138
}
132139
environment.exists = (path: string) => {
133140
if (isTemplatePath(path)) {

packages/cta-engine/src/frameworks.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -57,10 +57,9 @@ function getAddOns(framework: FrameworkDefinition) {
5757
packageAdditions,
5858
readme,
5959
files,
60-
deletedFiles: [],
6160
getFiles,
6261
getFileContents,
63-
getDeletedFiles: () => Promise.resolve([]),
62+
getDeletedFiles: () => Promise.resolve(info.deletedFiles ?? []),
6463
})
6564
}
6665
}

packages/cta-engine/src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@ export type {
5757
Mode,
5858
Options,
5959
SerializedOptions,
60+
Starter,
6061
StarterCompiled,
6162
} from './types.js'
6263
export type { PersistedOptions } from './config-file.js'

packages/cta-ui/src/components/custom-add-on-dialog.tsx

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { useState } from 'react'
22
import { useStore } from '@tanstack/react-store'
33
import { toast } from 'sonner'
4+
import { TicketPlusIcon } from 'lucide-react'
45

56
import { Button } from '@/components/ui/button'
67
import { Input } from '@/components/ui/input'
@@ -47,6 +48,7 @@ export default function CustomAddOnDialog() {
4748
setOpen(true)
4849
}}
4950
>
51+
<TicketPlusIcon className="w-4 h-4" />
5052
Import Custom Add-On
5153
</Button>
5254
<Dialog modal open={open}>
@@ -60,6 +62,11 @@ export default function CustomAddOnDialog() {
6062
onChange={(e) => setUrl(e.target.value)}
6163
placeholder="https://github.com/myorg/myproject/add-on.json"
6264
className="min-w-lg w-full"
65+
onKeyDown={(e) => {
66+
if (e.key === 'Enter') {
67+
onImport()
68+
}
69+
}}
6370
/>
6471
</div>
6572
<DialogFooter>

packages/cta-ui/src/components/file-navigator.tsx

Lines changed: 38 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { useState } from 'react'
1+
import { useMemo, useState } from 'react'
22
import { useStore } from '@tanstack/react-store'
33
import { FilterIcon } from 'lucide-react'
44

@@ -50,42 +50,60 @@ export function DropdownMenuDemo() {
5050
}
5151

5252
export default function FileNavigator() {
53-
const [selectedFile, setSelectedFile] = useState<string | null>(null)
54-
const [originalFileContents, setOriginalFileContents] = useState<string>()
55-
const [modifiedFileContents, setModifiedFileContents] = useState<string>()
53+
const [selectedFile, setSelectedFile] = useState<string | null>(
54+
'./package.json',
55+
)
5656

5757
const { output, originalOutput } = useStore(projectFiles)
5858
const localFiles = useStore(projectLocalFiles)
5959

6060
const mode = useStore(applicationMode)
6161

62+
const { originalFileContents, modifiedFileContents } = useMemo(() => {
63+
if (!selectedFile) {
64+
return {
65+
originalFileContents: undefined,
66+
modifiedFileContents: undefined,
67+
}
68+
}
69+
if (mode === 'add') {
70+
if (localFiles[selectedFile]) {
71+
if (!output.files[selectedFile]) {
72+
return {
73+
originalFileContents: undefined,
74+
modifiedFileContents: localFiles[selectedFile],
75+
}
76+
} else {
77+
return {
78+
originalFileContents: localFiles[selectedFile],
79+
modifiedFileContents: output.files[selectedFile],
80+
}
81+
}
82+
} else {
83+
return {
84+
originalFileContents: originalOutput.files[selectedFile],
85+
modifiedFileContents: output.files[selectedFile],
86+
}
87+
}
88+
} else {
89+
return {
90+
modifiedFileContents: output.files[selectedFile],
91+
}
92+
}
93+
}, [mode, selectedFile, output.files, originalOutput.files, localFiles])
94+
6295
return (
6396
<div className="flex flex-row w-[calc(100vw-450px)]">
6497
<div className="w-1/4 max-w-1/4 pr-2">
6598
<DropdownMenuDemo />
6699
<FileTree
100+
selectedFile={selectedFile}
67101
prefix="./"
68102
tree={output.files}
69103
originalTree={mode === 'setup' ? output.files : originalOutput.files}
70104
localTree={localFiles}
71105
onFileSelected={(file) => {
72106
setSelectedFile(file)
73-
if (mode === 'add') {
74-
if (localFiles[file]) {
75-
if (!output.files[file]) {
76-
setOriginalFileContents(undefined)
77-
setModifiedFileContents(localFiles[file])
78-
} else {
79-
setOriginalFileContents(localFiles[file])
80-
setModifiedFileContents(output.files[file])
81-
}
82-
} else {
83-
setOriginalFileContents(originalOutput.files[file])
84-
setModifiedFileContents(output.files[file])
85-
}
86-
} else {
87-
setModifiedFileContents(output.files[file])
88-
}
89107
}}
90108
/>
91109
</div>

packages/cta-ui/src/components/file-tree.tsx

Lines changed: 21 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -7,30 +7,26 @@ import { TreeView } from '@/components/ui/tree-view'
77

88
type FileTreeItem = TreeDataItem & {
99
contents: string
10+
fullPath: string
1011
}
1112

1213
export default function FileTree({
1314
prefix,
1415
tree,
1516
originalTree,
1617
localTree,
18+
selectedFile,
1719
onFileSelected,
1820
}: {
1921
prefix: string
2022
tree: Record<string, string>
2123
originalTree: Record<string, string>
2224
localTree: Record<string, string>
25+
selectedFile: string | null
2326
onFileSelected: (file: string) => void
2427
}) {
2528
const computedTree = useMemo(() => {
26-
const treeData: Array<TreeDataItem> = [
27-
{
28-
id: 'root',
29-
name: '.',
30-
children: [],
31-
icon: () => <Folder className="w-4 h-4 mr-2" />,
32-
},
33-
]
29+
const treeData: Array<TreeDataItem> = []
3430

3531
function changed(file: string) {
3632
if (!originalTree[file]) {
@@ -48,7 +44,8 @@ export default function FileTree({
4844
)
4945

5046
allFileSet.sort().forEach((file) => {
51-
const parts = file.split('/')
47+
const strippedFile = file.replace('./', '')
48+
const parts = strippedFile.split('/')
5249

5350
let currentLevel = treeData
5451
parts.forEach((part, index) => {
@@ -79,8 +76,9 @@ export default function FileTree({
7976
}
8077

8178
const newNode: FileTreeItem = {
82-
id: index === parts.length - 1 ? file : `${file}-${index}`,
79+
id: parts.slice(0, index + 1).join('/'),
8380
name: part,
81+
fullPath: strippedFile,
8482
children: index < parts.length - 1 ? [] : undefined,
8583
icon:
8684
index < parts.length - 1
@@ -103,8 +101,21 @@ export default function FileTree({
103101
return treeData
104102
}, [prefix, tree, originalTree, localTree])
105103

104+
const initialExpandedItemIds = useMemo(
105+
() => [
106+
'src',
107+
'src/routes',
108+
'src/components',
109+
'src/components/ui',
110+
'src/lib',
111+
],
112+
[],
113+
)
114+
106115
return (
107116
<TreeView
117+
initialSelectedItemId={selectedFile?.replace('./', '') ?? undefined}
118+
initialExpandedItemIds={initialExpandedItemIds}
108119
data={computedTree}
109120
defaultNodeIcon={() => <Folder className="w-4 h-4 mr-2" />}
110121
defaultLeafIcon={() => <FileText className="w-4 h-4 mr-2" />}

packages/cta-ui/src/components/sidebar-items/mode-selector.tsx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { useStore } from '@tanstack/react-store'
2+
import { CodeIcon, FileIcon } from 'lucide-react'
23

34
import type { Mode } from '@tanstack/cta-engine'
45

@@ -39,13 +40,15 @@ export default function ModeSelector() {
3940
className="px-8"
4041
disabled={!enableMode}
4142
>
43+
<CodeIcon className="w-4 h-4" />
4244
Code Router
4345
</ToggleGroupItem>
4446
<ToggleGroupItem
4547
value="file-router"
4648
className="px-4"
4749
disabled={!enableMode}
4850
>
51+
<FileIcon className="w-4 h-4" />
4952
File Router
5053
</ToggleGroupItem>
5154
</ToggleGroup>

0 commit comments

Comments
 (0)