create-visualbuild-app 0.1.0
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/README.md +123 -0
- package/package.json +85 -0
- package/scripts/create-builder-app.mjs +501 -0
- package/scripts/vb-dev.mjs +32 -0
- package/scripts/vb-generate.mjs +224 -0
- package/templates/default/app/README.md +21 -0
- package/templates/default/app/eslint.config.js +25 -0
- package/templates/default/app/index.html +15 -0
- package/templates/default/app/src/index.css +23 -0
- package/templates/default/app/src/main.tsx +10 -0
- package/templates/default/app/tsconfig.app.json +21 -0
- package/templates/default/app/tsconfig.json +7 -0
- package/templates/default/app/tsconfig.node.json +19 -0
- package/templates/default/app/visualbuild/generated-files.json +3 -0
- package/templates/default/app/visualbuild/pages.json +12 -0
- package/templates/default/app/vite.config.ts +7 -0
- package/templates/default/builder/README.md +21 -0
- package/templates/default/builder/eslint.config.js +25 -0
- package/templates/default/builder/tsconfig.app.json +21 -0
- package/templates/default/builder/tsconfig.json +7 -0
- package/templates/default/builder/tsconfig.node.json +22 -0
- package/visual-app-builder/index.html +15 -0
- package/visual-app-builder/server/parseReactPage.ts +571 -0
- package/visual-app-builder/shared/generateReactSource.d.mts +37 -0
- package/visual-app-builder/shared/generateReactSource.mjs +443 -0
- package/visual-app-builder/src/App.tsx +874 -0
- package/visual-app-builder/src/components/Canvas.tsx +1059 -0
- package/visual-app-builder/src/components/CodePanel.tsx +812 -0
- package/visual-app-builder/src/components/ComponentSidebar.tsx +302 -0
- package/visual-app-builder/src/components/PageSwitcher.tsx +161 -0
- package/visual-app-builder/src/components/ProjectExplorer.tsx +1054 -0
- package/visual-app-builder/src/components/PropertiesPanel.tsx +692 -0
- package/visual-app-builder/src/components/Topbar.tsx +128 -0
- package/visual-app-builder/src/data/componentRegistry.tsx +292 -0
- package/visual-app-builder/src/data/primitiveRegistry.ts +368 -0
- package/visual-app-builder/src/index.css +111 -0
- package/visual-app-builder/src/main.tsx +10 -0
- package/visual-app-builder/src/stores/useAppStore.ts +1265 -0
- package/visual-app-builder/src/tailwindGeneratedSafelist.ts +36 -0
- package/visual-app-builder/src/types/index.ts +261 -0
- package/visual-app-builder/src/utils/codegen.ts +66 -0
- package/visual-app-builder/src/utils/projectFiles.ts +146 -0
- package/visual-app-builder/src/utils/projectPersistence.ts +177 -0
- package/visual-app-builder/vite.config.ts +1479 -0
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
export const generatedStarterClassNames = `
|
|
2
|
+
min-h-screen bg-slate-950 text-white flex items-center justify-center px-6
|
|
3
|
+
w-full max-w-5xl rounded-2xl border border-white/10 bg-white/10 px-8 py-12 text-center shadow-2xl backdrop-blur
|
|
4
|
+
mb-4 text-xs font-semibold uppercase tracking-[0.28em] text-blue-300
|
|
5
|
+
text-4xl font-bold tracking-tight sm:text-5xl
|
|
6
|
+
mx-auto mt-5 max-w-2xl text-base text-slate-300
|
|
7
|
+
mt-8 flex-wrap gap-3
|
|
8
|
+
rounded-lg bg-blue-500 px-5 py-3 text-sm shadow-lg shadow-blue-500/25 hover:bg-blue-400
|
|
9
|
+
text-slate-400
|
|
10
|
+
`
|
|
11
|
+
|
|
12
|
+
export const editableCanvasClassNames = `
|
|
13
|
+
block inline inline-block inline-flex flex grid hidden contents
|
|
14
|
+
flex-row flex-col flex-wrap items-start items-center items-end items-stretch justify-start justify-center justify-end justify-between justify-around content-center self-start self-center self-end
|
|
15
|
+
gap-0 gap-1 gap-2 gap-3 gap-4 gap-5 gap-6 gap-8 gap-10 gap-12
|
|
16
|
+
grid-cols-1 grid-cols-2 grid-cols-3 grid-cols-4 grid-cols-5 grid-cols-6 col-span-1 col-span-2 col-span-3 col-span-full
|
|
17
|
+
w-full w-screen w-auto w-fit w-1/2 w-1/3 w-2/3 w-1/4 w-3/4 min-w-0 max-w-sm max-w-md max-w-lg max-w-xl max-w-2xl max-w-3xl max-w-4xl max-w-5xl max-w-6xl max-w-full
|
|
18
|
+
h-full h-screen h-auto min-h-0 min-h-6 min-h-8 min-h-10 min-h-12 min-h-16 min-h-24 min-h-32 min-h-48 min-h-screen
|
|
19
|
+
p-0 p-1 p-2 p-3 p-4 p-5 p-6 p-8 p-10 p-12
|
|
20
|
+
px-0 px-1 px-2 px-3 px-4 px-5 px-6 px-8 px-10 px-12
|
|
21
|
+
py-0 py-1 py-2 py-3 py-4 py-5 py-6 py-8 py-10 py-12
|
|
22
|
+
pt-0 pt-1 pt-2 pt-3 pt-4 pt-5 pt-6 pt-8 pb-0 pb-1 pb-2 pb-3 pb-4 pb-5 pb-6 pb-8
|
|
23
|
+
pl-0 pl-1 pl-2 pl-3 pl-4 pl-5 pl-6 pl-8 pr-0 pr-1 pr-2 pr-3 pr-4 pr-5 pr-6 pr-8
|
|
24
|
+
m-0 m-1 m-2 m-3 m-4 m-5 m-6 m-8 mx-auto mx-0 mx-1 mx-2 mx-3 mx-4 mx-5 mx-6 mx-8 my-0 my-1 my-2 my-3 my-4 my-5 my-6 my-8
|
|
25
|
+
mt-0 mt-1 mt-2 mt-3 mt-4 mt-5 mt-6 mt-8 mb-0 mb-1 mb-2 mb-3 mb-4 mb-5 mb-6 mb-8
|
|
26
|
+
space-y-1 space-y-2 space-y-3 space-y-4 space-y-5 space-y-6 space-x-1 space-x-2 space-x-3 space-x-4 space-x-5 space-x-6
|
|
27
|
+
text-left text-center text-right text-xs text-sm text-base text-lg text-xl text-2xl text-3xl text-4xl text-5xl font-thin font-light font-normal font-medium font-semibold font-bold font-extrabold italic not-italic underline line-through leading-none leading-tight leading-normal leading-relaxed tracking-tight tracking-normal tracking-wide tracking-widest uppercase lowercase capitalize
|
|
28
|
+
text-white text-black text-gray-50 text-gray-100 text-gray-200 text-gray-300 text-gray-400 text-gray-500 text-gray-600 text-gray-700 text-gray-800 text-gray-900 text-slate-300 text-slate-400 text-slate-950 text-red-500 text-orange-500 text-amber-500 text-yellow-500 text-green-500 text-emerald-500 text-teal-500 text-cyan-500 text-sky-500 text-blue-100 text-blue-300 text-blue-400 text-blue-500 text-blue-600 text-blue-700 text-indigo-500 text-indigo-600 text-indigo-700 text-violet-500 text-purple-500 text-pink-500
|
|
29
|
+
bg-white bg-black bg-transparent bg-slate-950 bg-gray-50 bg-gray-100 bg-gray-200 bg-gray-300 bg-gray-800 bg-gray-900 bg-gray-950 bg-red-500 bg-orange-500 bg-yellow-500 bg-green-500 bg-blue-500 bg-blue-600 bg-blue-700 bg-indigo-600 bg-indigo-700 bg-purple-600
|
|
30
|
+
bg-white/10 bg-white/20 bg-gray-950/25 bg-blue-100/20 bg-gradient-to-br from-blue-600 to-indigo-700
|
|
31
|
+
border border-0 border-2 border-t border-b border-l border-r border-transparent border-white border-black border-gray-200 border-gray-300 border-gray-700 border-blue-500 border-white/10 border-white/20
|
|
32
|
+
rounded rounded-sm rounded-md rounded-lg rounded-xl rounded-2xl rounded-full
|
|
33
|
+
shadow shadow-sm shadow-md shadow-lg shadow-xl shadow-2xl shadow-blue-500/25
|
|
34
|
+
overflow-hidden overflow-auto overflow-x-auto overflow-y-auto relative absolute fixed sticky top-0 right-0 bottom-0 left-0 z-10 z-20 z-50
|
|
35
|
+
opacity-0 opacity-50 opacity-75 opacity-100 cursor-pointer select-none transition transition-colors duration-150 duration-200 hover:bg-blue-400 hover:bg-gray-100 hover:bg-gray-900 hover:text-white hover:text-black focus:outline-none focus:ring-2 focus:ring-blue-500 backdrop-blur backdrop-blur-sm
|
|
36
|
+
`
|
|
@@ -0,0 +1,261 @@
|
|
|
1
|
+
import type { ReactNode } from 'react'
|
|
2
|
+
|
|
3
|
+
export type ComponentType =
|
|
4
|
+
| 'Navbar'
|
|
5
|
+
| 'Hero'
|
|
6
|
+
| 'Card'
|
|
7
|
+
| 'Button'
|
|
8
|
+
| 'Footer'
|
|
9
|
+
|
|
10
|
+
export type PrimitiveType =
|
|
11
|
+
| 'header'
|
|
12
|
+
| 'main'
|
|
13
|
+
| 'div'
|
|
14
|
+
| 'section'
|
|
15
|
+
| 'nav'
|
|
16
|
+
| 'article'
|
|
17
|
+
| 'aside'
|
|
18
|
+
| 'footer'
|
|
19
|
+
| 'address'
|
|
20
|
+
| 'hgroup'
|
|
21
|
+
| 'h1'
|
|
22
|
+
| 'h2'
|
|
23
|
+
| 'h3'
|
|
24
|
+
| 'h4'
|
|
25
|
+
| 'h5'
|
|
26
|
+
| 'h6'
|
|
27
|
+
| 'p'
|
|
28
|
+
| 'br'
|
|
29
|
+
| 'hr'
|
|
30
|
+
| 'pre'
|
|
31
|
+
| 'blockquote'
|
|
32
|
+
| 'span'
|
|
33
|
+
| 'strong'
|
|
34
|
+
| 'b'
|
|
35
|
+
| 'em'
|
|
36
|
+
| 'i'
|
|
37
|
+
| 'u'
|
|
38
|
+
| 's'
|
|
39
|
+
| 'small'
|
|
40
|
+
| 'mark'
|
|
41
|
+
| 'sub'
|
|
42
|
+
| 'sup'
|
|
43
|
+
| 'code'
|
|
44
|
+
| 'kbd'
|
|
45
|
+
| 'samp'
|
|
46
|
+
| 'var'
|
|
47
|
+
| 'abbr'
|
|
48
|
+
| 'cite'
|
|
49
|
+
| 'q'
|
|
50
|
+
| 'time'
|
|
51
|
+
| 'a'
|
|
52
|
+
| 'ul'
|
|
53
|
+
| 'ol'
|
|
54
|
+
| 'li'
|
|
55
|
+
| 'dl'
|
|
56
|
+
| 'dt'
|
|
57
|
+
| 'dd'
|
|
58
|
+
| 'menu'
|
|
59
|
+
| 'button'
|
|
60
|
+
| 'img'
|
|
61
|
+
| 'picture'
|
|
62
|
+
| 'source'
|
|
63
|
+
| 'figure'
|
|
64
|
+
| 'figcaption'
|
|
65
|
+
| 'video'
|
|
66
|
+
| 'audio'
|
|
67
|
+
| 'iframe'
|
|
68
|
+
| 'canvas'
|
|
69
|
+
| 'svg'
|
|
70
|
+
| 'form'
|
|
71
|
+
| 'label'
|
|
72
|
+
| 'input'
|
|
73
|
+
| 'textarea'
|
|
74
|
+
| 'select'
|
|
75
|
+
| 'option'
|
|
76
|
+
| 'optgroup'
|
|
77
|
+
| 'fieldset'
|
|
78
|
+
| 'legend'
|
|
79
|
+
| 'datalist'
|
|
80
|
+
| 'output'
|
|
81
|
+
| 'progress'
|
|
82
|
+
| 'meter'
|
|
83
|
+
| 'table'
|
|
84
|
+
| 'caption'
|
|
85
|
+
| 'thead'
|
|
86
|
+
| 'tbody'
|
|
87
|
+
| 'tfoot'
|
|
88
|
+
| 'tr'
|
|
89
|
+
| 'th'
|
|
90
|
+
| 'td'
|
|
91
|
+
| 'colgroup'
|
|
92
|
+
| 'col'
|
|
93
|
+
| 'details'
|
|
94
|
+
| 'summary'
|
|
95
|
+
| 'dialog'
|
|
96
|
+
|
|
97
|
+
export type AppShellSlot = 'header' | 'footer'
|
|
98
|
+
|
|
99
|
+
export type Viewport = 'mobile' | 'tablet' | 'desktop'
|
|
100
|
+
|
|
101
|
+
export type ProjectRefreshResult =
|
|
102
|
+
| 'updated'
|
|
103
|
+
| 'skipped-dirty'
|
|
104
|
+
| 'failed'
|
|
105
|
+
|
|
106
|
+
export type PropType =
|
|
107
|
+
| 'string'
|
|
108
|
+
| 'text'
|
|
109
|
+
| 'number'
|
|
110
|
+
| 'boolean'
|
|
111
|
+
| 'color'
|
|
112
|
+
| 'select'
|
|
113
|
+
| 'array'
|
|
114
|
+
|
|
115
|
+
export interface PropSchema {
|
|
116
|
+
type: PropType
|
|
117
|
+
label: string
|
|
118
|
+
default: unknown
|
|
119
|
+
options?: string[]
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
export interface ComponentDefinition {
|
|
123
|
+
type: ComponentType
|
|
124
|
+
label: string
|
|
125
|
+
category: string
|
|
126
|
+
icon: string
|
|
127
|
+
description: string
|
|
128
|
+
defaultProps: Record<string, unknown>
|
|
129
|
+
propSchema: Record<string, PropSchema>
|
|
130
|
+
renderer: (
|
|
131
|
+
props: Record<string, unknown>,
|
|
132
|
+
context: ComponentRenderContext
|
|
133
|
+
) => ReactNode
|
|
134
|
+
generator: (props: Record<string, unknown>) => string
|
|
135
|
+
source: () => string
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
export interface PrimitiveDefinition {
|
|
139
|
+
type: PrimitiveType
|
|
140
|
+
label: string
|
|
141
|
+
category: string
|
|
142
|
+
icon: string
|
|
143
|
+
description: string
|
|
144
|
+
defaultProps: Record<string, unknown>
|
|
145
|
+
propSchema: Record<string, PropSchema>
|
|
146
|
+
acceptsChildren: boolean
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
export interface ComponentRenderContext {
|
|
150
|
+
pages: Page[]
|
|
151
|
+
activePageId: string
|
|
152
|
+
setActivePage: (id: string) => void
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
export interface VisualNode {
|
|
156
|
+
id: string
|
|
157
|
+
type: PrimitiveType
|
|
158
|
+
label?: string
|
|
159
|
+
props: Record<string, unknown>
|
|
160
|
+
children: VisualNode[]
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
export interface Page {
|
|
164
|
+
id: string
|
|
165
|
+
name: string
|
|
166
|
+
route: string
|
|
167
|
+
sourcePath: string
|
|
168
|
+
root: VisualNode
|
|
169
|
+
components: VisualNode[]
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
export interface AppShell {
|
|
173
|
+
header: VisualNode | null
|
|
174
|
+
footer: VisualNode | null
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
export interface AppState {
|
|
178
|
+
projectName: string
|
|
179
|
+
appShell: AppShell
|
|
180
|
+
pages: Page[]
|
|
181
|
+
activePageId: string
|
|
182
|
+
selectedComponentId: string | null
|
|
183
|
+
viewport: Viewport
|
|
184
|
+
previewMode: boolean
|
|
185
|
+
isProjectLoaded: boolean
|
|
186
|
+
// Actions
|
|
187
|
+
loadProject: () => Promise<void>
|
|
188
|
+
refreshProjectFromDisk: (
|
|
189
|
+
force?: boolean
|
|
190
|
+
) => Promise<ProjectRefreshResult>
|
|
191
|
+
saveProject: () => Promise<void>
|
|
192
|
+
addPage: (name: string, route: string) => Page
|
|
193
|
+
registerPage: (page: Page) => void
|
|
194
|
+
deletePage: (id: string) => void
|
|
195
|
+
setActivePage: (id: string) => void
|
|
196
|
+
setSelectedComponent: (id: string | null) => void
|
|
197
|
+
setViewport: (viewport: Viewport) => void
|
|
198
|
+
togglePreviewMode: () => void
|
|
199
|
+
addComponent: (pageId: string, type: ComponentType) => void
|
|
200
|
+
addComponentAt: (
|
|
201
|
+
pageId: string,
|
|
202
|
+
type: ComponentType,
|
|
203
|
+
index: number
|
|
204
|
+
) => void
|
|
205
|
+
addPrimitive: (pageId: string, type: PrimitiveType) => void
|
|
206
|
+
addPrimitiveAt: (
|
|
207
|
+
pageId: string,
|
|
208
|
+
type: PrimitiveType,
|
|
209
|
+
index: number
|
|
210
|
+
) => void
|
|
211
|
+
addPrimitiveToNode: (
|
|
212
|
+
parentNodeId: string,
|
|
213
|
+
type: PrimitiveType
|
|
214
|
+
) => void
|
|
215
|
+
addPrimitiveToNodeAt: (
|
|
216
|
+
parentNodeId: string,
|
|
217
|
+
type: PrimitiveType,
|
|
218
|
+
index: number
|
|
219
|
+
) => void
|
|
220
|
+
addComponentToNode: (
|
|
221
|
+
parentNodeId: string,
|
|
222
|
+
type: ComponentType
|
|
223
|
+
) => void
|
|
224
|
+
addComponentToNodeAt: (
|
|
225
|
+
parentNodeId: string,
|
|
226
|
+
type: ComponentType,
|
|
227
|
+
index: number
|
|
228
|
+
) => void
|
|
229
|
+
removeNode: (nodeId: string) => void
|
|
230
|
+
moveNode: (
|
|
231
|
+
pageId: string,
|
|
232
|
+
nodeId: string,
|
|
233
|
+
targetParentNodeId: string | null,
|
|
234
|
+
targetIndex: number
|
|
235
|
+
) => void
|
|
236
|
+
moveComponent: (
|
|
237
|
+
pageId: string,
|
|
238
|
+
nodeId: string,
|
|
239
|
+
targetIndex: number
|
|
240
|
+
) => void
|
|
241
|
+
removeComponent: (pageId: string, nodeId: string) => void
|
|
242
|
+
setAppShellComponent: (slot: AppShellSlot, type: ComponentType) => void
|
|
243
|
+
removeAppShellComponent: (slot: AppShellSlot) => void
|
|
244
|
+
updateComponentProps: (
|
|
245
|
+
pageId: string,
|
|
246
|
+
nodeId: string,
|
|
247
|
+
props: Record<string, unknown>
|
|
248
|
+
) => void
|
|
249
|
+
updateNodeProps: (nodeId: string, props: Record<string, unknown>) => void
|
|
250
|
+
updatePageRoot: (
|
|
251
|
+
pageId: string,
|
|
252
|
+
updates: {
|
|
253
|
+
type?: PrimitiveType
|
|
254
|
+
props?: Record<string, unknown>
|
|
255
|
+
}
|
|
256
|
+
) => void
|
|
257
|
+
updateAppShellProps: (
|
|
258
|
+
slot: AppShellSlot,
|
|
259
|
+
props: Record<string, unknown>
|
|
260
|
+
) => void
|
|
261
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import {
|
|
2
|
+
createPageSourcePath as createAstPageSourcePath,
|
|
3
|
+
generateAppSource,
|
|
4
|
+
generateLayoutSource,
|
|
5
|
+
generateNodeSource,
|
|
6
|
+
generatePageSource,
|
|
7
|
+
getPageSourcePath as getAstPageSourcePath,
|
|
8
|
+
toPageComponentName as toAstPageComponentName,
|
|
9
|
+
} from '../../shared/generateReactSource.mjs'
|
|
10
|
+
import type { AppShell, ComponentType, Page, VisualNode } from '../types'
|
|
11
|
+
|
|
12
|
+
export function generatePageCode(page: Page): string {
|
|
13
|
+
return generatePageSource(page).trimEnd()
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function generateAppCode(pages: Page[], appShell: AppShell): string {
|
|
17
|
+
return generateAppSource(pages, appShell).trimEnd()
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function generateLayoutCode(appShell: AppShell): string {
|
|
21
|
+
return generateLayoutSource(appShell).trimEnd()
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function generateProjectSchema(
|
|
25
|
+
pages: Page[],
|
|
26
|
+
appShell: AppShell,
|
|
27
|
+
projectName = 'my-blog'
|
|
28
|
+
): string {
|
|
29
|
+
return JSON.stringify(
|
|
30
|
+
{
|
|
31
|
+
meta: {
|
|
32
|
+
projectName,
|
|
33
|
+
framework: 'react',
|
|
34
|
+
styling: 'tailwind',
|
|
35
|
+
},
|
|
36
|
+
appShell,
|
|
37
|
+
pages,
|
|
38
|
+
},
|
|
39
|
+
null,
|
|
40
|
+
2
|
|
41
|
+
)
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function generateComponentSource(type: ComponentType): string {
|
|
45
|
+
return `export function ${type}() {
|
|
46
|
+
return null
|
|
47
|
+
}`
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function toPageComponentName(name: string): string {
|
|
51
|
+
return toAstPageComponentName(name)
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export function getPageSourcePath(
|
|
55
|
+
page: Pick<Page, 'name' | 'sourcePath'>
|
|
56
|
+
): string {
|
|
57
|
+
return getAstPageSourcePath(page)
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export function createPageSourcePath(name: string): string {
|
|
61
|
+
return createAstPageSourcePath(name)
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export function generateNodeCode(node: VisualNode, indent: string): string {
|
|
65
|
+
return generateNodeSource(node, indent)
|
|
66
|
+
}
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
export type ProjectEntryKind = 'file' | 'directory' | 'symlink'
|
|
2
|
+
|
|
3
|
+
export interface ProjectEntry {
|
|
4
|
+
name: string
|
|
5
|
+
path: string
|
|
6
|
+
kind: ProjectEntryKind
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export interface ProjectDirectory {
|
|
10
|
+
rootName: string
|
|
11
|
+
path: string
|
|
12
|
+
entries: ProjectEntry[]
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export interface ProjectFile {
|
|
16
|
+
path: string
|
|
17
|
+
content: string
|
|
18
|
+
size: number
|
|
19
|
+
isBinary: boolean
|
|
20
|
+
isTooLarge: boolean
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export async function listProjectDirectory(path = '') {
|
|
24
|
+
return requestJson<ProjectDirectory>(
|
|
25
|
+
`/__visualbuild/files?path=${encodeURIComponent(path)}`
|
|
26
|
+
)
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const excludedMoveDirectoryNames = new Set(['node_modules'])
|
|
30
|
+
|
|
31
|
+
export async function listProjectDirectories() {
|
|
32
|
+
const root = await listProjectDirectory()
|
|
33
|
+
const directories: ProjectEntry[] = [
|
|
34
|
+
{ name: root.rootName, path: '', kind: 'directory' },
|
|
35
|
+
]
|
|
36
|
+
let pendingPaths = ['']
|
|
37
|
+
|
|
38
|
+
while (pendingPaths.length > 0) {
|
|
39
|
+
const results = await Promise.all(
|
|
40
|
+
pendingPaths.map((directoryPath) =>
|
|
41
|
+
listProjectDirectory(directoryPath)
|
|
42
|
+
)
|
|
43
|
+
)
|
|
44
|
+
const nextPaths: string[] = []
|
|
45
|
+
|
|
46
|
+
for (const result of results) {
|
|
47
|
+
for (const entry of result.entries) {
|
|
48
|
+
if (
|
|
49
|
+
entry.kind !== 'directory' ||
|
|
50
|
+
excludedMoveDirectoryNames.has(entry.name)
|
|
51
|
+
) {
|
|
52
|
+
continue
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
directories.push(entry)
|
|
56
|
+
nextPaths.push(entry.path)
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
pendingPaths = nextPaths
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
return directories.sort((left, right) =>
|
|
64
|
+
left.path.localeCompare(right.path)
|
|
65
|
+
)
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export async function createProjectEntry(
|
|
69
|
+
parentPath: string,
|
|
70
|
+
name: string,
|
|
71
|
+
kind: 'file' | 'directory'
|
|
72
|
+
) {
|
|
73
|
+
return requestJson<ProjectEntry>('/__visualbuild/files', {
|
|
74
|
+
method: 'POST',
|
|
75
|
+
headers: { 'Content-Type': 'application/json' },
|
|
76
|
+
body: JSON.stringify({ parentPath, name, kind }),
|
|
77
|
+
})
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export async function renameProjectEntry(path: string, name: string) {
|
|
81
|
+
return requestJson<ProjectEntry>('/__visualbuild/files', {
|
|
82
|
+
method: 'PATCH',
|
|
83
|
+
headers: { 'Content-Type': 'application/json' },
|
|
84
|
+
body: JSON.stringify({ path, name }),
|
|
85
|
+
})
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export async function moveProjectEntry(
|
|
89
|
+
path: string,
|
|
90
|
+
parentPath: string
|
|
91
|
+
) {
|
|
92
|
+
const name = path.split('/').pop()
|
|
93
|
+
|
|
94
|
+
if (!name) {
|
|
95
|
+
throw new Error('The project root cannot be moved')
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
return requestJson<ProjectEntry>('/__visualbuild/files', {
|
|
99
|
+
method: 'PATCH',
|
|
100
|
+
headers: { 'Content-Type': 'application/json' },
|
|
101
|
+
body: JSON.stringify({ path, name, parentPath }),
|
|
102
|
+
})
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export async function deleteProjectEntry(path: string) {
|
|
106
|
+
await request(`/__visualbuild/files?path=${encodeURIComponent(path)}`, {
|
|
107
|
+
method: 'DELETE',
|
|
108
|
+
})
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
export async function loadProjectFile(path: string) {
|
|
112
|
+
return requestJson<ProjectFile>(
|
|
113
|
+
`/__visualbuild/file?path=${encodeURIComponent(path)}`
|
|
114
|
+
)
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
export async function saveProjectFile(path: string, content: string) {
|
|
118
|
+
await request('/__visualbuild/file', {
|
|
119
|
+
method: 'PUT',
|
|
120
|
+
headers: { 'Content-Type': 'application/json' },
|
|
121
|
+
body: JSON.stringify({ path, content }),
|
|
122
|
+
})
|
|
123
|
+
notifyProjectFilesChanged()
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
export function notifyProjectFilesChanged() {
|
|
127
|
+
window.dispatchEvent(new Event('visualbuild:filesystem-changed'))
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
async function requestJson<T>(url: string, init?: RequestInit) {
|
|
131
|
+
const response = await request(url, init)
|
|
132
|
+
return (await response.json()) as T
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
async function request(url: string, init?: RequestInit) {
|
|
136
|
+
const response = await fetch(url, init)
|
|
137
|
+
|
|
138
|
+
if (!response.ok) {
|
|
139
|
+
const payload = (await response.json().catch(() => null)) as {
|
|
140
|
+
error?: string
|
|
141
|
+
} | null
|
|
142
|
+
throw new Error(payload?.error ?? `Request failed with ${response.status}`)
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
return response
|
|
146
|
+
}
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
import type { AppShell, Page } from '../types'
|
|
2
|
+
|
|
3
|
+
export interface ProjectSchema {
|
|
4
|
+
meta: {
|
|
5
|
+
projectName: string
|
|
6
|
+
framework: string
|
|
7
|
+
styling: string
|
|
8
|
+
}
|
|
9
|
+
appShell: AppShell
|
|
10
|
+
pages: Page[]
|
|
11
|
+
runtimeProjectId?: string
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export type ImportedProjectPage = Page
|
|
15
|
+
|
|
16
|
+
export interface GeneratorDiagnostic {
|
|
17
|
+
severity: 'error'
|
|
18
|
+
phase: 'generate'
|
|
19
|
+
code: string
|
|
20
|
+
title: string
|
|
21
|
+
message: string
|
|
22
|
+
path: string
|
|
23
|
+
suggestion?: string
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
class ProjectSaveError extends Error {
|
|
27
|
+
diagnostic?: GeneratorDiagnostic
|
|
28
|
+
|
|
29
|
+
constructor(message: string, diagnostic?: GeneratorDiagnostic) {
|
|
30
|
+
super(message)
|
|
31
|
+
this.name = 'ProjectSaveError'
|
|
32
|
+
this.diagnostic = diagnostic
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function getGeneratorDiagnostic(error: unknown) {
|
|
37
|
+
return error instanceof ProjectSaveError ? error.diagnostic : undefined
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function createProjectSchema(
|
|
41
|
+
pages: Page[],
|
|
42
|
+
appShell: AppShell,
|
|
43
|
+
projectName = 'my-blog'
|
|
44
|
+
): ProjectSchema {
|
|
45
|
+
return {
|
|
46
|
+
meta: {
|
|
47
|
+
projectName,
|
|
48
|
+
framework: 'react',
|
|
49
|
+
styling: 'tailwind',
|
|
50
|
+
},
|
|
51
|
+
appShell,
|
|
52
|
+
pages,
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export async function loadProjectSchema(): Promise<ProjectSchema | null> {
|
|
57
|
+
const response = await fetch('/__visualbuild/pages')
|
|
58
|
+
|
|
59
|
+
if (!response.ok) {
|
|
60
|
+
return null
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const schema = (await response.json()) as ProjectSchema
|
|
64
|
+
|
|
65
|
+
return {
|
|
66
|
+
...schema,
|
|
67
|
+
runtimeProjectId:
|
|
68
|
+
response.headers.get('X-VisualBuild-Project-Id') ?? undefined,
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export async function saveProjectSchema(
|
|
73
|
+
pages: Page[],
|
|
74
|
+
appShell: AppShell,
|
|
75
|
+
projectName: string
|
|
76
|
+
) {
|
|
77
|
+
const response = await fetch('/__visualbuild/pages', {
|
|
78
|
+
method: 'POST',
|
|
79
|
+
headers: {
|
|
80
|
+
'Content-Type': 'application/json',
|
|
81
|
+
},
|
|
82
|
+
body: JSON.stringify(createProjectSchema(pages, appShell, projectName), null, 2),
|
|
83
|
+
})
|
|
84
|
+
|
|
85
|
+
if (!response.ok) {
|
|
86
|
+
const responseBody = await response.text()
|
|
87
|
+
let payload: {
|
|
88
|
+
error?: string
|
|
89
|
+
diagnostic?: GeneratorDiagnostic
|
|
90
|
+
} | null = null
|
|
91
|
+
|
|
92
|
+
try {
|
|
93
|
+
payload = JSON.parse(responseBody) as {
|
|
94
|
+
error?: string
|
|
95
|
+
diagnostic?: GeneratorDiagnostic
|
|
96
|
+
}
|
|
97
|
+
} catch {
|
|
98
|
+
// Older servers may still return a plain error string.
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
throw new ProjectSaveError(
|
|
102
|
+
payload?.error ||
|
|
103
|
+
responseBody ||
|
|
104
|
+
'Failed to save VisualBuild project',
|
|
105
|
+
payload?.diagnostic
|
|
106
|
+
)
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
notifyProjectFilesChanged()
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
export async function importProjectPage(
|
|
113
|
+
path: string,
|
|
114
|
+
name: string,
|
|
115
|
+
route: string
|
|
116
|
+
) {
|
|
117
|
+
const response = await fetch('/__visualbuild/import-page', {
|
|
118
|
+
method: 'POST',
|
|
119
|
+
headers: {
|
|
120
|
+
'Content-Type': 'application/json',
|
|
121
|
+
},
|
|
122
|
+
body: JSON.stringify({ path, name, route }),
|
|
123
|
+
})
|
|
124
|
+
|
|
125
|
+
if (!response.ok) {
|
|
126
|
+
const payload = (await response.json().catch(() => null)) as {
|
|
127
|
+
error?: string
|
|
128
|
+
} | null
|
|
129
|
+
throw new Error(payload?.error ?? 'Failed to import page source')
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
return (await response.json()) as ImportedProjectPage
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
export async function syncTailwindClasses(pages: Page[], appShell: AppShell) {
|
|
136
|
+
const classNames = collectProjectClassNames(pages, appShell)
|
|
137
|
+
const response = await fetch('/__visualbuild/tailwind', {
|
|
138
|
+
method: 'POST',
|
|
139
|
+
headers: {
|
|
140
|
+
'Content-Type': 'text/plain',
|
|
141
|
+
},
|
|
142
|
+
body: classNames.join('\n'),
|
|
143
|
+
})
|
|
144
|
+
|
|
145
|
+
if (!response.ok) {
|
|
146
|
+
throw new Error(
|
|
147
|
+
(await response.text()) || 'Failed to compile Tailwind classes'
|
|
148
|
+
)
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function collectProjectClassNames(pages: Page[], appShell: AppShell) {
|
|
153
|
+
const classNames = new Set<string>()
|
|
154
|
+
const visitNode = (node: Page['root']) => {
|
|
155
|
+
const value = String(node.props.className ?? '').trim()
|
|
156
|
+
|
|
157
|
+
if (value) {
|
|
158
|
+
classNames.add(value)
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
node.children.forEach(visitNode)
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
if (appShell.header) visitNode(appShell.header)
|
|
165
|
+
if (appShell.footer) visitNode(appShell.footer)
|
|
166
|
+
|
|
167
|
+
pages.forEach((page) => {
|
|
168
|
+
visitNode(page.root)
|
|
169
|
+
page.components.forEach(visitNode)
|
|
170
|
+
})
|
|
171
|
+
|
|
172
|
+
return [...classNames]
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
function notifyProjectFilesChanged() {
|
|
176
|
+
window.dispatchEvent(new Event('visualbuild:filesystem-changed'))
|
|
177
|
+
}
|