Skip to content

Commit a7080af

Browse files
committed
A lot more developed add-on-developer
1 parent 9b80cd7 commit a7080af

File tree

8 files changed

+483
-61
lines changed

8 files changed

+483
-61
lines changed

cli/add-on-developer/package.json

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,10 @@
1111
"dev": "vinxi dev"
1212
},
1313
"dependencies": {
14+
"@codemirror/lang-css": "^6.3.1",
15+
"@codemirror/lang-html": "^6.4.9",
16+
"@codemirror/lang-javascript": "^6.2.3",
17+
"@codemirror/lang-json": "^6.0.1",
1418
"@radix-ui/react-accordion": "^1.2.3",
1519
"@radix-ui/react-dialog": "^1.1.6",
1620
"@radix-ui/react-slot": "^1.1.2",
@@ -24,6 +28,8 @@
2428
"@tanstack/react-router-with-query": "^1.114.3",
2529
"@tanstack/react-start": "^1.114.3",
2630
"@tanstack/router-plugin": "^1.114.3",
31+
"@uiw/codemirror-theme-okaidia": "^4.23.10",
32+
"@uiw/react-codemirror": "^4.23.10",
2733
"class-variance-authority": "^0.7.1",
2834
"clsx": "^2.1.1",
2935
"execa": "^9.5.2",

cli/add-on-developer/src/components/ui/tree-view.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ interface TreeDataItem {
2727
onClick?: () => void
2828
draggable?: boolean
2929
droppable?: boolean
30+
className?: string
3031
}
3132

3233
type TreeProps = React.HTMLAttributes<HTMLDivElement> & {
@@ -200,6 +201,7 @@ const TreeItem = React.forwardRef<HTMLDivElement, TreeItemProps>(
200201
handleDragStart={handleDragStart}
201202
handleDrop={handleDrop}
202203
draggedItem={draggedItem}
204+
className={item.className}
203205
/>
204206
)}
205207
</li>

cli/add-on-developer/src/routes/index.tsx

Lines changed: 153 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -2,99 +2,189 @@ import { useMemo, useState } from 'react'
22
import { createFileRoute } from '@tanstack/react-router'
33
import { FileText, Folder } from 'lucide-react'
44
import { createServerFn } from '@tanstack/react-start'
5+
import CodeMirror from '@uiw/react-codemirror'
56

6-
import type { TreeDataItem } from '@/components/ui/tree-view'
7+
import { javascript } from '@codemirror/lang-javascript'
8+
import { json } from '@codemirror/lang-json'
9+
import { css } from '@codemirror/lang-css'
10+
import { html } from '@codemirror/lang-html'
11+
12+
import { okaidia } from '@uiw/codemirror-theme-okaidia'
13+
import { readFileSync } from 'node:fs'
14+
import { basename, resolve } from 'node:path'
715

816
import {
917
getAllAddOns,
1018
createApp,
1119
createMemoryEnvironment,
20+
createAppOptionsFromPersisted,
1221
} from '@tanstack/cta-engine'
1322

1423
import { TreeView } from '@/components/ui/tree-view'
1524

25+
import type { TreeDataItem } from '@/components/ui/tree-view'
26+
import type { AddOn, PersistedOptions } from '@tanstack/cta-engine'
27+
1628
const getAddons = createServerFn({
1729
method: 'GET',
1830
}).handler(() => {
1931
return getAllAddOns('react', 'file-router')
2032
})
2133

34+
const getAddonInfo = createServerFn({
35+
method: 'GET',
36+
}).handler(async () => {
37+
const addOnInfo = readFileSync(
38+
resolve(process.env.PROJECT_PATH, 'add-on.json'),
39+
)
40+
return JSON.parse(addOnInfo.toString())
41+
})
42+
43+
const getOriginalOptions = createServerFn({
44+
method: 'GET',
45+
}).handler(async () => {
46+
const addOnInfo = readFileSync(resolve(process.env.PROJECT_PATH, '.cta.json'))
47+
return JSON.parse(addOnInfo.toString()) as PersistedOptions
48+
})
49+
2250
const runCreateApp = createServerFn({
2351
method: 'POST',
24-
}).handler(async () => {
25-
const { output, environment } = createMemoryEnvironment()
26-
await createApp(
27-
{
28-
addOns: false,
29-
framework: 'react',
30-
chosenAddOns: [],
31-
git: true,
32-
mode: 'code-router',
33-
packageManager: 'npm',
34-
projectName: 'foo',
35-
tailwind: false,
36-
toolchain: 'none',
37-
typescript: false,
38-
variableValues: {},
39-
},
40-
{
41-
silent: true,
42-
environment,
43-
cwd: process.env.PROJECT_PATH,
52+
})
53+
.validator((data: unknown) => {
54+
return data as { withAddOn: boolean; options: PersistedOptions }
55+
})
56+
.handler(
57+
async ({
58+
data: { withAddOn, options: persistedOptions },
59+
}: {
60+
data: { withAddOn: boolean; options: PersistedOptions }
61+
}) => {
62+
const { output, environment } = createMemoryEnvironment()
63+
const options = await createAppOptionsFromPersisted(persistedOptions)
64+
options.chosenAddOns = withAddOn
65+
? [...options.chosenAddOns, (await getAddonInfo()) as AddOn]
66+
: []
67+
await createApp(
68+
{
69+
...options,
70+
},
71+
{
72+
silent: true,
73+
environment,
74+
cwd: process.env.PROJECT_PATH,
75+
},
76+
)
77+
78+
output.files = Object.keys(output.files).reduce<Record<string, string>>(
79+
(acc, file) => {
80+
if (basename(file) !== '.cta.json') {
81+
acc[file] = output.files[file]
82+
}
83+
return acc
84+
},
85+
{},
86+
)
87+
88+
return output
4489
},
4590
)
46-
return output
47-
})
4891

4992
export const Route = createFileRoute('/')({
5093
component: App,
5194
loader: async () => {
95+
const originalOptions = await getOriginalOptions()
5296
return {
5397
addOns: await getAddons(),
5498
projectPath: process.env.PROJECT_PATH!,
55-
output: await runCreateApp(),
99+
output: await runCreateApp({
100+
data: { withAddOn: true, options: originalOptions },
101+
}),
102+
outputWithoutAddon: await runCreateApp({
103+
data: { withAddOn: false, options: originalOptions },
104+
}),
105+
addOnInfo: await getAddonInfo(),
106+
originalOptions,
56107
}
57108
},
58109
})
59110

60111
function App() {
61-
const { projectPath, output } = Route.useLoaderData()
112+
const {
113+
projectPath,
114+
output,
115+
addOnInfo,
116+
outputWithoutAddon,
117+
originalOptions,
118+
} = Route.useLoaderData()
62119
const [selectedFile, setSelectedFile] = useState<string | null>(null)
63120

64121
const tree = useMemo(() => {
65122
const treeData: Array<TreeDataItem> = []
66-
Object.keys(output.files).forEach((file) => {
67-
const parts = file.replace(`${projectPath}/`, '').split('/')
68-
69-
let currentLevel = treeData
70-
parts.forEach((part, index) => {
71-
const existingNode = currentLevel.find((node) => node.name === part)
72-
if (existingNode) {
73-
currentLevel = existingNode.children || []
74-
} else {
75-
const newNode: TreeDataItem = {
76-
id: index === parts.length - 1 ? file : `${file}-${index}`,
77-
name: part,
78-
children: index < parts.length - 1 ? [] : undefined,
79-
icon:
80-
index < parts.length - 1
81-
? () => <Folder className="w-4 h-4 mr-2" />
82-
: () => <FileText className="w-4 h-4 mr-2" />,
83-
onClick:
84-
index === parts.length - 1
85-
? () => {
86-
setSelectedFile(file)
87-
}
88-
: undefined,
123+
124+
function changed(file: string) {
125+
if (!outputWithoutAddon.files[file]) {
126+
return true
127+
}
128+
return output.files[file] !== outputWithoutAddon.files[file]
129+
}
130+
131+
Object.keys(output.files)
132+
.sort()
133+
.forEach((file) => {
134+
const parts = file.replace(`${projectPath}/`, '').split('/')
135+
136+
let currentLevel = treeData
137+
parts.forEach((part, index) => {
138+
const existingNode = currentLevel.find((node) => node.name === part)
139+
if (existingNode) {
140+
currentLevel = existingNode.children || []
141+
} else {
142+
const newNode: TreeDataItem = {
143+
id: index === parts.length - 1 ? file : `${file}-${index}`,
144+
name: part,
145+
children: index < parts.length - 1 ? [] : undefined,
146+
icon:
147+
index < parts.length - 1
148+
? () => <Folder className="w-4 h-4 mr-2" />
149+
: () => <FileText className="w-4 h-4 mr-2" />,
150+
onClick:
151+
index === parts.length - 1
152+
? () => {
153+
setSelectedFile(file)
154+
}
155+
: undefined,
156+
className:
157+
index === parts.length - 1 && changed(file)
158+
? 'text-green-300'
159+
: '',
160+
}
161+
currentLevel.push(newNode)
162+
currentLevel = newNode.children!
89163
}
90-
currentLevel.push(newNode)
91-
currentLevel = newNode.children!
92-
}
164+
})
93165
})
94-
})
95166
return treeData
96167
}, [projectPath, output])
97168

169+
function getLanguage(file: string) {
170+
if (file.endsWith('.js') || file.endsWith('.jsx')) {
171+
return javascript({ jsx: true })
172+
}
173+
if (file.endsWith('.ts') || file.endsWith('.tsx')) {
174+
return javascript({ typescript: true, jsx: true })
175+
}
176+
if (file.endsWith('.json')) {
177+
return json()
178+
}
179+
if (file.endsWith('.css')) {
180+
return css()
181+
}
182+
if (file.endsWith('.html')) {
183+
return html()
184+
}
185+
return javascript()
186+
}
187+
98188
return (
99189
<div className="p-5 flex flex-row">
100190
<TreeView
@@ -105,9 +195,17 @@ function App() {
105195
/>
106196
<div className="max-w-3/4 w-3/4 pl-2">
107197
<pre>
108-
{selectedFile
109-
? output.files[selectedFile] || 'Select a file to view its content'
110-
: null}
198+
{selectedFile && output.files[selectedFile] ? (
199+
<CodeMirror
200+
value={output.files[selectedFile]}
201+
theme={okaidia}
202+
height="100vh"
203+
width="100%"
204+
readOnly
205+
extensions={[getLanguage(selectedFile)]}
206+
className="text-lg"
207+
/>
208+
) : null}
111209
</pre>
112210
</div>
113211
</div>

example/mui-add-on/.add-on/info.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
{
2-
"name": "mui-add-on-add-on",
2+
"name": "mui-add-on",
33
"version": "0.0.1",
44
"description": "Add-on",
55
"author": "Jane Smith <[email protected]>",
@@ -20,6 +20,7 @@
2020
"variables": {},
2121
"phase": "add-on",
2222
"type": "add-on",
23+
"framework": "react",
2324
"packageAdditions": {
2425
"scripts": {},
2526
"dependencies": {

example/mui-add-on/add-on.json

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
{
2-
"name": "mui-add-on-add-on",
2+
"name": "mui-add-on",
33
"version": "0.0.1",
44
"description": "Add-on",
55
"author": "Jane Smith <[email protected]>",
@@ -20,14 +20,21 @@
2020
"variables": {},
2121
"phase": "add-on",
2222
"type": "add-on",
23+
"framework": "react",
2324
"packageAdditions": {
2425
"scripts": {},
25-
"dependencies": {},
26+
"dependencies": {
27+
"@emotion/styled": "^11.14.0",
28+
"@fontsource/roboto": "^5.2.5",
29+
"@mui/icons-material": "^7.0.1",
30+
"@mui/material": "^7.0.1",
31+
"@mui/styled-engine-sc": "^7.0.1",
32+
"styled-components": "^6.1.16"
33+
},
2634
"devDependencies": {}
2735
},
2836
"files": {
2937
"./src/routes/demo.mui.tsx.ejs": "import Button from \"@mui/material/Button\";\nimport Box from \"@mui/material/Box\";\nimport { createFileRoute } from \"@tanstack/react-router\";\n\n<% if (codeRouter) { %>\nimport type { RootRoute } from '@tanstack/react-router'\n<% } else { %>\nexport const Route = createFileRoute('/demo/mui')({\n component: MUIDemo,\n})\n<% } %>;\n\nfunction MUIDemo() {\n return (\n <Box sx={{ px: 2, py: 4 }}>\n <Button variant=\"contained\">Hello world</Button>\n </Box>\n );\n}\n\n<% if (codeRouter) { %>\nexport default (parentRoute: RootRoute) => createRoute({\n path: '/demo/mui',\n \n component: MUIDemo,\n\n getParentRoute: () => parentRoute,\n})\n<% } %>\n"
3038
},
31-
"framework": "react",
3239
"addDependencies": []
3340
}

packages/cta-engine/src/custom-add-on.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -102,7 +102,7 @@ export default (parentRoute: RootRoute) => createRoute({
102102
return { url: path, code, name }
103103
}
104104

105-
async function createOptions(
105+
export async function createAppOptionsFromPersisted(
106106
json: PersistedOptions,
107107
): Promise<Required<Options>> {
108108
return {
@@ -204,6 +204,7 @@ To create an add-on, the project must be created with TypeScript.`)
204204
link: `https://github.com/jane-smith/${persistedOptions.projectName}-${mode}`,
205205
command: {},
206206
shadcnComponents: [],
207+
framework: persistedOptions.framework,
207208
templates: [persistedOptions.mode],
208209
routes: [],
209210
warning: '',
@@ -220,7 +221,7 @@ To create an add-on, the project must be created with TypeScript.`)
220221
const compiledInfo = JSON.parse(JSON.stringify(info))
221222

222223
const originalOutput = await runCreateApp(
223-
await createOptions(persistedOptions),
224+
await createAppOptionsFromPersisted(persistedOptions),
224225
)
225226

226227
const originalPackageJson = JSON.parse(
@@ -302,6 +303,7 @@ To create an add-on, the project must be created with TypeScript.`)
302303
compiledInfo.routes = info.routes
303304
compiledInfo.framework = persistedOptions.framework
304305
compiledInfo.addDependencies = persistedOptions.existingAddOns
306+
compiledInfo.packageAdditions = info.packageAdditions
305307

306308
if (mode === 'overlay') {
307309
compiledInfo.mode = persistedOptions.mode

packages/cta-engine/src/index.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,11 @@
11
export { cli } from './cli.js'
22
export { getAllAddOns } from './add-ons.js'
33
export { createApp } from './create-app.js'
4+
export { createAppOptionsFromPersisted } from './custom-add-on.js'
45
export {
56
createMemoryEnvironment,
67
createDefaultEnvironment,
78
} from './environment.js'
9+
10+
export type { AddOn } from './types.js'
11+
export type { PersistedOptions } from './config-file.js'

0 commit comments

Comments
 (0)