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.
Files changed (44) hide show
  1. package/README.md +123 -0
  2. package/package.json +85 -0
  3. package/scripts/create-builder-app.mjs +501 -0
  4. package/scripts/vb-dev.mjs +32 -0
  5. package/scripts/vb-generate.mjs +224 -0
  6. package/templates/default/app/README.md +21 -0
  7. package/templates/default/app/eslint.config.js +25 -0
  8. package/templates/default/app/index.html +15 -0
  9. package/templates/default/app/src/index.css +23 -0
  10. package/templates/default/app/src/main.tsx +10 -0
  11. package/templates/default/app/tsconfig.app.json +21 -0
  12. package/templates/default/app/tsconfig.json +7 -0
  13. package/templates/default/app/tsconfig.node.json +19 -0
  14. package/templates/default/app/visualbuild/generated-files.json +3 -0
  15. package/templates/default/app/visualbuild/pages.json +12 -0
  16. package/templates/default/app/vite.config.ts +7 -0
  17. package/templates/default/builder/README.md +21 -0
  18. package/templates/default/builder/eslint.config.js +25 -0
  19. package/templates/default/builder/tsconfig.app.json +21 -0
  20. package/templates/default/builder/tsconfig.json +7 -0
  21. package/templates/default/builder/tsconfig.node.json +22 -0
  22. package/visual-app-builder/index.html +15 -0
  23. package/visual-app-builder/server/parseReactPage.ts +571 -0
  24. package/visual-app-builder/shared/generateReactSource.d.mts +37 -0
  25. package/visual-app-builder/shared/generateReactSource.mjs +443 -0
  26. package/visual-app-builder/src/App.tsx +874 -0
  27. package/visual-app-builder/src/components/Canvas.tsx +1059 -0
  28. package/visual-app-builder/src/components/CodePanel.tsx +812 -0
  29. package/visual-app-builder/src/components/ComponentSidebar.tsx +302 -0
  30. package/visual-app-builder/src/components/PageSwitcher.tsx +161 -0
  31. package/visual-app-builder/src/components/ProjectExplorer.tsx +1054 -0
  32. package/visual-app-builder/src/components/PropertiesPanel.tsx +692 -0
  33. package/visual-app-builder/src/components/Topbar.tsx +128 -0
  34. package/visual-app-builder/src/data/componentRegistry.tsx +292 -0
  35. package/visual-app-builder/src/data/primitiveRegistry.ts +368 -0
  36. package/visual-app-builder/src/index.css +111 -0
  37. package/visual-app-builder/src/main.tsx +10 -0
  38. package/visual-app-builder/src/stores/useAppStore.ts +1265 -0
  39. package/visual-app-builder/src/tailwindGeneratedSafelist.ts +36 -0
  40. package/visual-app-builder/src/types/index.ts +261 -0
  41. package/visual-app-builder/src/utils/codegen.ts +66 -0
  42. package/visual-app-builder/src/utils/projectFiles.ts +146 -0
  43. package/visual-app-builder/src/utils/projectPersistence.ts +177 -0
  44. package/visual-app-builder/vite.config.ts +1479 -0
@@ -0,0 +1,443 @@
1
+ import { generate } from '@babel/generator'
2
+ import * as t from '@babel/types'
3
+
4
+ const voidElements = new Set(['br', 'hr', 'img', 'input', 'source', 'col'])
5
+
6
+ export function generatePageSource(page) {
7
+ const root = {
8
+ ...(page.root ?? {
9
+ id: `${page.id}:root`,
10
+ type: 'div',
11
+ props: { className: 'min-h-screen' },
12
+ children: [],
13
+ }),
14
+ children: page.components ?? [],
15
+ }
16
+ const declaration = t.functionDeclaration(
17
+ t.identifier(toPageComponentName(page.name)),
18
+ [],
19
+ t.blockStatement([
20
+ t.returnStatement(createJsxElement(root, 1)),
21
+ ])
22
+ )
23
+
24
+ return printProgram([t.exportDefaultDeclaration(declaration)])
25
+ }
26
+
27
+ export function generateAppSource(
28
+ pages,
29
+ appShell = { header: null, footer: null }
30
+ ) {
31
+ const hasLayout = Boolean(appShell.header || appShell.footer)
32
+ const statements = pages.map((page) =>
33
+ t.importDeclaration(
34
+ [
35
+ t.importDefaultSpecifier(
36
+ t.identifier(toPageComponentName(page.name))
37
+ ),
38
+ ],
39
+ t.stringLiteral(toPageImportPath(page))
40
+ )
41
+ )
42
+
43
+ if (hasLayout) {
44
+ statements.push(
45
+ t.importDeclaration(
46
+ [t.importDefaultSpecifier(t.identifier('AppLayout'))],
47
+ t.stringLiteral('./layouts/AppLayout')
48
+ )
49
+ )
50
+ }
51
+
52
+ statements.push(
53
+ t.variableDeclaration('const', [
54
+ t.variableDeclarator(
55
+ t.identifier('routes'),
56
+ t.arrayExpression(
57
+ pages.map((page) =>
58
+ t.objectExpression([
59
+ t.objectProperty(
60
+ t.identifier('path'),
61
+ t.stringLiteral(page.route)
62
+ ),
63
+ t.objectProperty(
64
+ t.identifier('component'),
65
+ t.identifier(toPageComponentName(page.name))
66
+ ),
67
+ ])
68
+ )
69
+ )
70
+ ),
71
+ ])
72
+ )
73
+
74
+ const fallbackPage = t.identifier(
75
+ toPageComponentName(pages[0]?.name ?? 'Home')
76
+ )
77
+ const appBody = [
78
+ t.variableDeclaration('const', [
79
+ t.variableDeclarator(
80
+ t.identifier('path'),
81
+ t.memberExpression(
82
+ t.memberExpression(
83
+ t.identifier('window'),
84
+ t.identifier('location')
85
+ ),
86
+ t.identifier('pathname')
87
+ )
88
+ ),
89
+ ]),
90
+ t.variableDeclaration('const', [
91
+ t.variableDeclarator(
92
+ t.identifier('match'),
93
+ t.callExpression(
94
+ t.memberExpression(
95
+ t.identifier('routes'),
96
+ t.identifier('find')
97
+ ),
98
+ [
99
+ t.arrowFunctionExpression(
100
+ [t.identifier('route')],
101
+ t.binaryExpression(
102
+ '===',
103
+ t.memberExpression(
104
+ t.identifier('route'),
105
+ t.identifier('path')
106
+ ),
107
+ t.identifier('path')
108
+ )
109
+ ),
110
+ ]
111
+ )
112
+ ),
113
+ ]),
114
+ t.variableDeclaration('const', [
115
+ t.variableDeclarator(
116
+ t.identifier('Page'),
117
+ t.logicalExpression(
118
+ '??',
119
+ t.optionalMemberExpression(
120
+ t.identifier('match'),
121
+ t.identifier('component'),
122
+ false,
123
+ true
124
+ ),
125
+ fallbackPage
126
+ )
127
+ ),
128
+ ]),
129
+ t.returnStatement(
130
+ hasLayout
131
+ ? t.jsxElement(
132
+ t.jsxOpeningElement(t.jsxIdentifier('AppLayout'), [], false),
133
+ t.jsxClosingElement(t.jsxIdentifier('AppLayout')),
134
+ withLineBreaks([
135
+ t.jsxElement(
136
+ t.jsxOpeningElement(t.jsxIdentifier('Page'), [], true),
137
+ null,
138
+ [],
139
+ true
140
+ ),
141
+ ], 1),
142
+ false
143
+ )
144
+ : t.jsxElement(
145
+ t.jsxOpeningElement(t.jsxIdentifier('Page'), [], true),
146
+ null,
147
+ [],
148
+ true
149
+ )
150
+ ),
151
+ ]
152
+ const appDeclaration = t.functionDeclaration(
153
+ t.identifier('App'),
154
+ [],
155
+ t.blockStatement(appBody)
156
+ )
157
+
158
+ statements.push(t.exportDefaultDeclaration(appDeclaration))
159
+ return printProgram(statements)
160
+ }
161
+
162
+ export function generateLayoutSource(appShell) {
163
+ const reactImport = t.importDeclaration(
164
+ [
165
+ t.importSpecifier(
166
+ t.identifier('ReactNode'),
167
+ t.identifier('ReactNode')
168
+ ),
169
+ ],
170
+ t.stringLiteral('react')
171
+ )
172
+ reactImport.importKind = 'type'
173
+
174
+ const childrenProperty = t.objectProperty(
175
+ t.identifier('children'),
176
+ t.identifier('children'),
177
+ false,
178
+ true
179
+ )
180
+ const childrenPattern = t.objectPattern([childrenProperty])
181
+ const childrenType = t.tsPropertySignature(
182
+ t.identifier('children'),
183
+ t.tsTypeAnnotation(
184
+ t.tsTypeReference(t.identifier('ReactNode'))
185
+ )
186
+ )
187
+ childrenPattern.typeAnnotation = t.tsTypeAnnotation(
188
+ t.tsTypeLiteral([childrenType])
189
+ )
190
+
191
+ const wrapperChildren = []
192
+
193
+ if (appShell.header) {
194
+ wrapperChildren.push(createJsxElement(appShell.header, 2))
195
+ }
196
+
197
+ wrapperChildren.push(
198
+ t.jsxExpressionContainer(t.identifier('children'))
199
+ )
200
+
201
+ if (appShell.footer) {
202
+ wrapperChildren.push(createJsxElement(appShell.footer, 2))
203
+ }
204
+
205
+ const declaration = t.functionDeclaration(
206
+ t.identifier('AppLayout'),
207
+ [childrenPattern],
208
+ t.blockStatement([
209
+ t.returnStatement(
210
+ t.jsxElement(
211
+ t.jsxOpeningElement(t.jsxIdentifier('div'), [], false),
212
+ t.jsxClosingElement(t.jsxIdentifier('div')),
213
+ withLineBreaks(wrapperChildren, 1),
214
+ false
215
+ )
216
+ ),
217
+ ])
218
+ )
219
+
220
+ return printProgram([
221
+ reactImport,
222
+ t.exportDefaultDeclaration(declaration),
223
+ ])
224
+ }
225
+
226
+ export function generateNodeSource(node, indent = '') {
227
+ const source = generate(createJsxElement(node), generatorOptions).code
228
+ return source
229
+ .split('\n')
230
+ .map((line) => `${indent}${line}`)
231
+ .join('\n')
232
+ }
233
+
234
+ export function toPageComponentName(name) {
235
+ const pascalName = String(name)
236
+ .split(/[^a-zA-Z0-9]+/)
237
+ .filter(Boolean)
238
+ .map((part) => part.charAt(0).toUpperCase() + part.slice(1))
239
+ .join('')
240
+
241
+ return `${pascalName || 'Untitled'}Page`
242
+ }
243
+
244
+ export function getPageSourcePath(page) {
245
+ return normalizeProjectPath(
246
+ page.sourcePath || `src/pages/${toPageComponentName(page.name)}.tsx`
247
+ )
248
+ }
249
+
250
+ export function createPageSourcePath(name) {
251
+ return `src/pages/${toPageComponentName(name)}.tsx`
252
+ }
253
+
254
+ export function normalizeProjectPath(value) {
255
+ return String(value).replace(/\\/g, '/').replace(/^\.\/+/, '')
256
+ }
257
+
258
+ function createJsxElement(node, depth = 0) {
259
+ const tagName = String(node.type)
260
+ const selfClosing = voidElements.has(tagName) || tagName === 'textarea'
261
+ const opening = t.jsxOpeningElement(
262
+ t.jsxIdentifier(tagName),
263
+ createJsxAttributes(node),
264
+ selfClosing
265
+ )
266
+
267
+ if (selfClosing) {
268
+ return t.jsxElement(opening, null, [], true)
269
+ }
270
+
271
+ const children = Array.isArray(node.children)
272
+ ? node.children.map((child) => createJsxElement(child, depth + 1))
273
+ : []
274
+ const text = String(node.props?.text ?? '')
275
+
276
+ if (children.length === 0 && text) {
277
+ children.push(t.jsxText(escapeJsxText(text)))
278
+ }
279
+
280
+ return t.jsxElement(
281
+ opening,
282
+ t.jsxClosingElement(t.jsxIdentifier(tagName)),
283
+ children.length > 0 ? withLineBreaks(children, depth) : children,
284
+ false
285
+ )
286
+ }
287
+
288
+ function withLineBreaks(children, depth) {
289
+ const childIndent = ' '.repeat(depth + 1)
290
+ const closingIndent = ' '.repeat(depth)
291
+
292
+ return [
293
+ t.jsxText(`\n${childIndent}`),
294
+ ...children.flatMap((child, index) => [
295
+ child,
296
+ t.jsxText(
297
+ index === children.length - 1
298
+ ? `\n${closingIndent}`
299
+ : `\n${childIndent}`
300
+ ),
301
+ ]),
302
+ ]
303
+ }
304
+
305
+ function createJsxAttributes(node) {
306
+ const props = node.props ?? {}
307
+ const attributes = [
308
+ props.id ? createValueAttribute('id', String(props.id)) : null,
309
+ props.className
310
+ ? createValueAttribute('className', String(props.className))
311
+ : null,
312
+ node.type === 'a'
313
+ ? createValueAttribute('href', String(props.href ?? '#'))
314
+ : null,
315
+ node.type === 'a'
316
+ ? createValueAttribute('target', String(props.target ?? '_self'))
317
+ : null,
318
+ node.type === 'button'
319
+ ? createValueAttribute('type', String(props.type ?? 'button'))
320
+ : null,
321
+ node.type === 'img'
322
+ ? createValueAttribute('src', String(props.src ?? ''))
323
+ : null,
324
+ node.type === 'img'
325
+ ? createValueAttribute('alt', String(props.alt ?? ''))
326
+ : null,
327
+ node.type === 'input'
328
+ ? createValueAttribute('type', String(props.type ?? 'text'))
329
+ : null,
330
+ node.type === 'input' || node.type === 'textarea'
331
+ ? props.name
332
+ ? createValueAttribute('name', String(props.name))
333
+ : null
334
+ : null,
335
+ node.type === 'input' || node.type === 'textarea'
336
+ ? props.placeholder
337
+ ? createValueAttribute('placeholder', String(props.placeholder))
338
+ : null
339
+ : null,
340
+ node.type === 'textarea'
341
+ ? createValueAttribute('defaultValue', String(props.text ?? ''))
342
+ : null,
343
+ node.type === 'label' && props.htmlFor
344
+ ? createValueAttribute('htmlFor', String(props.htmlFor))
345
+ : null,
346
+ node.type === 'iframe'
347
+ ? createValueAttribute('src', String(props.src ?? ''))
348
+ : null,
349
+ node.type === 'iframe'
350
+ ? createValueAttribute(
351
+ 'title',
352
+ String(props.title ?? 'Embedded content')
353
+ )
354
+ : null,
355
+ node.type === 'source'
356
+ ? createValueAttribute('src', String(props.src ?? ''))
357
+ : null,
358
+ node.type === 'source' && props.type
359
+ ? createValueAttribute('type', String(props.type))
360
+ : null,
361
+ node.type === 'video' || node.type === 'audio'
362
+ ? props.src
363
+ ? createValueAttribute('src', String(props.src))
364
+ : null
365
+ : null,
366
+ node.type === 'video' || node.type === 'audio'
367
+ ? props.controls !== false
368
+ ? createBooleanAttribute('controls')
369
+ : null
370
+ : null,
371
+ node.type === 'option' && props.value
372
+ ? createValueAttribute('value', String(props.value))
373
+ : null,
374
+ node.type === 'optgroup'
375
+ ? createValueAttribute('label', String(props.label ?? 'Group'))
376
+ : null,
377
+ node.type === 'progress' || node.type === 'meter'
378
+ ? props.value !== undefined
379
+ ? createValueAttribute('value', Number(props.value))
380
+ : null
381
+ : null,
382
+ node.type === 'meter'
383
+ ? props.min !== undefined
384
+ ? createValueAttribute('min', Number(props.min))
385
+ : null
386
+ : null,
387
+ node.type === 'progress' || node.type === 'meter'
388
+ ? props.max !== undefined
389
+ ? createValueAttribute('max', Number(props.max))
390
+ : null
391
+ : null,
392
+ ]
393
+
394
+ return attributes.filter(Boolean)
395
+ }
396
+
397
+ function createValueAttribute(name, value) {
398
+ return t.jsxAttribute(
399
+ t.jsxIdentifier(name),
400
+ t.jsxExpressionContainer(
401
+ typeof value === 'number'
402
+ ? Number.isFinite(value)
403
+ ? t.numericLiteral(value)
404
+ : t.nullLiteral()
405
+ : t.stringLiteral(value)
406
+ )
407
+ )
408
+ }
409
+
410
+ function createBooleanAttribute(name) {
411
+ return t.jsxAttribute(t.jsxIdentifier(name), null)
412
+ }
413
+
414
+ function toPageImportPath(page) {
415
+ const sourcePath = getPageSourcePath(page)
416
+ const sourceRelativePath = sourcePath.startsWith('src/')
417
+ ? sourcePath.slice(4)
418
+ : sourcePath
419
+ const withoutExtension = sourceRelativePath.replace(/\.[^./]+$/, '')
420
+
421
+ return withoutExtension.startsWith('.')
422
+ ? withoutExtension
423
+ : `./${withoutExtension}`
424
+ }
425
+
426
+ function escapeJsxText(value) {
427
+ return value
428
+ .replace(/&/g, '&')
429
+ .replace(/</g, '&lt;')
430
+ .replace(/>/g, '&gt;')
431
+ }
432
+
433
+ function printProgram(statements) {
434
+ const file = t.file(t.program(statements))
435
+ return `${generate(file, generatorOptions).code}\n`
436
+ }
437
+
438
+ const generatorOptions = {
439
+ comments: false,
440
+ compact: false,
441
+ concise: false,
442
+ jsescOption: { minimal: true },
443
+ }