create-visualbuild-app 0.1.0 → 1.0.1

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 +919 -58
  2. package/docs/user-guide.md +759 -0
  3. package/package.json +92 -85
  4. package/scripts/create-builder-app.mjs +513 -501
  5. package/scripts/vb-dev.mjs +41 -32
  6. package/scripts/vb-generate.mjs +332 -224
  7. package/templates/default/app/.vercelignore +6 -0
  8. package/templates/default/app/README.md +56 -21
  9. package/templates/default/app/visualbuild/components.json +3 -0
  10. package/templates/default/app/visualbuild/config.d.ts +48 -0
  11. package/templates/default/app/visualbuild.config.ts +38 -0
  12. package/templates/default/builder/README.md +37 -21
  13. package/visual-app-builder/server/parseReactPage.ts +776 -571
  14. package/visual-app-builder/shared/createReactComponentSource.d.mts +2 -0
  15. package/visual-app-builder/shared/createReactComponentSource.mjs +45 -0
  16. package/visual-app-builder/shared/generateReactSource.d.mts +57 -37
  17. package/visual-app-builder/shared/generateReactSource.mjs +608 -443
  18. package/visual-app-builder/shared/npmBuildCommand.d.mts +17 -0
  19. package/visual-app-builder/shared/npmBuildCommand.mjs +26 -0
  20. package/visual-app-builder/shared/syncCustomComponentSource.d.mts +13 -0
  21. package/visual-app-builder/shared/syncCustomComponentSource.mjs +345 -0
  22. package/visual-app-builder/shared/vercelDeployment.d.mts +31 -0
  23. package/visual-app-builder/shared/vercelDeployment.mjs +266 -0
  24. package/visual-app-builder/shared/visualbuildConfig.d.mts +144 -0
  25. package/visual-app-builder/shared/visualbuildConfig.mjs +578 -0
  26. package/visual-app-builder/src/App.tsx +1090 -874
  27. package/visual-app-builder/src/components/Canvas.tsx +1116 -1059
  28. package/visual-app-builder/src/components/CodePanel.tsx +1147 -812
  29. package/visual-app-builder/src/components/ComponentSidebar.tsx +365 -302
  30. package/visual-app-builder/src/components/DeploymentModal.tsx +295 -0
  31. package/visual-app-builder/src/components/PageSwitcher.tsx +133 -133
  32. package/visual-app-builder/src/components/ProjectExplorer.tsx +1054 -1054
  33. package/visual-app-builder/src/components/PropertiesPanel.tsx +792 -692
  34. package/visual-app-builder/src/components/Topbar.tsx +257 -128
  35. package/visual-app-builder/src/data/componentRegistry.tsx +613 -292
  36. package/visual-app-builder/src/data/tailwindClassCatalog.ts +95 -0
  37. package/visual-app-builder/src/index.css +383 -111
  38. package/visual-app-builder/src/stores/useAppStore.ts +2385 -1265
  39. package/visual-app-builder/src/theme.ts +71 -0
  40. package/visual-app-builder/src/types/index.ts +350 -261
  41. package/visual-app-builder/src/utils/codegen.ts +90 -66
  42. package/visual-app-builder/src/utils/deployment.ts +54 -0
  43. package/visual-app-builder/src/utils/projectPersistence.ts +218 -177
  44. package/visual-app-builder/vite.config.ts +1946 -1479
@@ -1,501 +1,513 @@
1
- #!/usr/bin/env node
2
-
3
- import { execFile } from 'node:child_process'
4
- import fs from 'node:fs/promises'
5
- import path from 'node:path'
6
- import { fileURLToPath } from 'node:url'
7
- import { promisify } from 'node:util'
8
-
9
- const execFileAsync = promisify(execFile)
10
- const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..')
11
- const DEFAULT_PROJECT_NAME = 'my-app'
12
- const BUILDER_FOLDER_NAME = 'visual-builder'
13
- const templateRoot = path.join(repoRoot, 'templates', 'default')
14
- const appTemplateDir = path.join(templateRoot, 'app')
15
- const builderTemplateDir = path.join(templateRoot, 'builder')
16
- const editorSourceDir = path.join(repoRoot, 'visual-app-builder')
17
- const scriptFilesToCopy = ['vb-dev.mjs', 'vb-generate.mjs']
18
-
19
- const appPackageDependencies = ['react', 'react-dom']
20
- const appPackageDevDependencies = [
21
- '@eslint/js',
22
- '@tailwindcss/vite',
23
- '@types/node',
24
- '@types/react',
25
- '@types/react-dom',
26
- '@vitejs/plugin-react',
27
- 'eslint',
28
- 'eslint-plugin-react-hooks',
29
- 'eslint-plugin-react-refresh',
30
- 'globals',
31
- 'tailwindcss',
32
- 'typescript',
33
- 'typescript-eslint',
34
- 'vite',
35
- ]
36
-
37
- const builderPackageDependencies = [
38
- '@babel/generator',
39
- '@babel/parser',
40
- '@babel/types',
41
- '@dnd-kit/core',
42
- '@dnd-kit/sortable',
43
- '@dnd-kit/utilities',
44
- '@tailwindcss/vite',
45
- 'chokidar',
46
- 'react',
47
- 'react-dom',
48
- 'uuid',
49
- 'zustand',
50
- ]
51
-
52
- const builderPackageDevDependencies = [
53
- '@eslint/js',
54
- '@types/node',
55
- '@types/react',
56
- '@types/react-dom',
57
- '@vitejs/plugin-react',
58
- 'eslint',
59
- 'eslint-plugin-react-hooks',
60
- 'eslint-plugin-react-refresh',
61
- 'globals',
62
- 'tailwindcss',
63
- 'typescript',
64
- 'typescript-eslint',
65
- 'vite',
66
- ]
67
-
68
- function getTargetBaseDir() {
69
- return path.resolve(process.env.INIT_CWD || process.cwd())
70
- }
71
-
72
- function toProjectSlug(input) {
73
- const slug = String(input || DEFAULT_PROJECT_NAME)
74
- .trim()
75
- .toLowerCase()
76
- .replace(/['"]/g, '')
77
- .replace(/[^a-z0-9]+/g, '-')
78
- .replace(/^-+|-+$/g, '')
79
-
80
- return slug || DEFAULT_PROJECT_NAME
81
- }
82
-
83
- function toDisplayName(slug) {
84
- return slug
85
- .split(/[-_]+/)
86
- .filter(Boolean)
87
- .map((part) => part.charAt(0).toUpperCase() + part.slice(1))
88
- .join(' ')
89
- }
90
-
91
- async function pathExists(filePath) {
92
- try {
93
- await fs.access(filePath)
94
- return true
95
- } catch {
96
- return false
97
- }
98
- }
99
-
100
- async function assertTargetIsWritable(targetDir) {
101
- if (!(await pathExists(targetDir))) {
102
- return
103
- }
104
-
105
- const entries = await fs.readdir(targetDir)
106
-
107
- if (entries.length > 0) {
108
- throw new Error(
109
- `Target folder "${targetDir}" already exists and is not empty.`
110
- )
111
- }
112
- }
113
-
114
- async function assertScaffoldTargetsAreWritable(appDir, builderDir) {
115
- await assertTargetIsWritable(appDir)
116
- await assertTargetIsWritable(builderDir)
117
- }
118
-
119
- async function assertScaffoldAssetsExist() {
120
- const requiredAssets = [
121
- appTemplateDir,
122
- builderTemplateDir,
123
- editorSourceDir,
124
- ...scriptFilesToCopy.map((file) => path.join(repoRoot, 'scripts', file)),
125
- ]
126
-
127
- for (const assetPath of requiredAssets) {
128
- if (!(await pathExists(assetPath))) {
129
- throw new Error(
130
- `Required scaffold asset is missing: ${path.relative(repoRoot, assetPath)}`
131
- )
132
- }
133
- }
134
- }
135
-
136
- async function copyAppTemplate(appDir) {
137
- await fs.cp(appTemplateDir, appDir, { recursive: true })
138
- }
139
-
140
- async function copyBuilderTemplate(builderDir) {
141
- await fs.cp(builderTemplateDir, builderDir, { recursive: true })
142
- await fs.cp(
143
- editorSourceDir,
144
- path.join(builderDir, 'visual-app-builder'),
145
- { recursive: true }
146
- )
147
-
148
- await fs.mkdir(path.join(builderDir, 'scripts'), { recursive: true })
149
-
150
- for (const file of scriptFilesToCopy) {
151
- await fs.copyFile(
152
- path.join(repoRoot, 'scripts', file),
153
- path.join(builderDir, 'scripts', file)
154
- )
155
- }
156
- }
157
-
158
- async function rewriteJsonFile(filePath, update) {
159
- const contents = await fs.readFile(filePath, 'utf8')
160
- const json = JSON.parse(contents)
161
- const nextJson = update(json)
162
- await fs.writeFile(filePath, `${JSON.stringify(nextJson, null, 2)}\n`)
163
- }
164
-
165
- async function replaceInFile(filePath, replacements) {
166
- if (!(await pathExists(filePath))) {
167
- return
168
- }
169
-
170
- let contents = await fs.readFile(filePath, 'utf8')
171
-
172
- for (const [from, to] of replacements) {
173
- contents = contents.split(from).join(to)
174
- }
175
-
176
- await fs.writeFile(filePath, contents)
177
- }
178
-
179
- async function seedProject(appDir, builderDir, projectName, displayName) {
180
- const appPackageJson = await readRootPackageJson()
181
- const builderPackageJson = await readRootPackageJson()
182
- const relativeAppDir = toPosixPath(path.relative(builderDir, appDir))
183
-
184
- await fs.writeFile(
185
- path.join(appDir, 'package.json'),
186
- `${JSON.stringify(
187
- createAppPackageJson(appPackageJson, projectName),
188
- null,
189
- 2
190
- )}\n`
191
- )
192
-
193
- await fs.writeFile(
194
- path.join(builderDir, 'package.json'),
195
- `${JSON.stringify(
196
- createBuilderPackageJson(
197
- builderPackageJson,
198
- relativeAppDir
199
- ),
200
- null,
201
- 2
202
- )}\n`
203
- )
204
-
205
- await rewriteJsonFile(
206
- path.join(appDir, 'visualbuild', 'pages.json'),
207
- (schema) => seedProjectSchema(schema, projectName, displayName)
208
- )
209
-
210
- const templateReplacements = [
211
- ['__DISPLAY_NAME__', displayName],
212
- ['__PROJECT_NAME__', projectName],
213
- ]
214
-
215
- await replaceInFile(path.join(appDir, 'index.html'), templateReplacements)
216
- await replaceInFile(path.join(appDir, 'README.md'), templateReplacements)
217
- await replaceInFile(path.join(builderDir, 'README.md'), templateReplacements)
218
-
219
- const sourceReplacements = [
220
- ['my-blog', projectName],
221
- ['My Blog', displayName],
222
- ['Welcome to My Blog', `Welcome to ${displayName}`],
223
- ]
224
-
225
- await replaceInFile(
226
- path.join(builderDir, 'visual-app-builder', 'src', 'stores', 'useAppStore.ts'),
227
- sourceReplacements
228
- )
229
- await replaceInFile(
230
- path.join(
231
- builderDir,
232
- 'visual-app-builder',
233
- 'src',
234
- 'utils',
235
- 'projectPersistence.ts'
236
- ),
237
- sourceReplacements
238
- )
239
- await replaceInFile(
240
- path.join(
241
- builderDir,
242
- 'visual-app-builder',
243
- 'src',
244
- 'data',
245
- 'componentRegistry.tsx'
246
- ),
247
- sourceReplacements
248
- )
249
- }
250
-
251
- async function readRootPackageJson() {
252
- return JSON.parse(await fs.readFile(path.join(repoRoot, 'package.json'), 'utf8'))
253
- }
254
-
255
- function createAppPackageJson(packageJson, projectName) {
256
- const allDependencies = mergeDependencies(packageJson)
257
-
258
- return {
259
- name: projectName,
260
- private: true,
261
- version: packageJson.version,
262
- type: 'module',
263
- scripts: {
264
- dev: 'vite',
265
- build: 'tsc -b && vite build',
266
- lint: 'eslint .',
267
- preview: 'vite preview',
268
- },
269
- dependencies: pickDependencies(allDependencies, appPackageDependencies),
270
- devDependencies: pickDependencies(
271
- allDependencies,
272
- appPackageDevDependencies
273
- ),
274
- }
275
- }
276
-
277
- function createBuilderPackageJson(packageJson, relativeAppDir) {
278
- const allDependencies = mergeDependencies(packageJson)
279
-
280
- return {
281
- name: BUILDER_FOLDER_NAME,
282
- private: true,
283
- version: packageJson.version,
284
- type: 'module',
285
- scripts: {
286
- 'visual-builder': `node scripts/vb-dev.mjs ${JSON.stringify(relativeAppDir)}`,
287
- 'vb:generate': `node scripts/vb-generate.mjs ${JSON.stringify(relativeAppDir)}`,
288
- build: 'vite build --config visual-app-builder/vite.config.ts',
289
- lint: 'eslint .',
290
- },
291
- dependencies: pickDependencies(
292
- allDependencies,
293
- builderPackageDependencies
294
- ),
295
- devDependencies: pickDependencies(
296
- allDependencies,
297
- builderPackageDevDependencies
298
- ),
299
- }
300
- }
301
-
302
- function mergeDependencies(packageJson) {
303
- return {
304
- ...(packageJson.dependencies ?? {}),
305
- ...(packageJson.devDependencies ?? {}),
306
- }
307
- }
308
-
309
- function pickDependencies(source, names) {
310
- return Object.fromEntries(
311
- names
312
- .filter((name) => source?.[name])
313
- .map((name) => [name, source[name]])
314
- )
315
- }
316
-
317
- function toPosixPath(filePath) {
318
- return filePath.split(path.sep).join('/')
319
- }
320
-
321
- function seedProjectSchema(schema, projectName, displayName) {
322
- return {
323
- ...schema,
324
- meta: {
325
- ...schema.meta,
326
- projectName,
327
- },
328
- appShell: {
329
- header: null,
330
- footer: null,
331
- },
332
- pages: [createStarterHomePage(displayName)],
333
- }
334
- }
335
-
336
- function createStarterHomePage(displayName) {
337
- return {
338
- id: 'page-home',
339
- name: 'Home',
340
- route: '/',
341
- sourcePath: 'src/pages/HomePage.tsx',
342
- root: {
343
- id: 'page-home:root',
344
- type: 'main',
345
- label: 'Page root',
346
- props: {
347
- className:
348
- 'min-h-screen bg-slate-950 text-white flex items-center justify-center px-6',
349
- },
350
- children: [],
351
- },
352
- components: [
353
- {
354
- id: 'starter-shell',
355
- type: 'section',
356
- label: 'Starter',
357
- props: {
358
- className:
359
- 'w-full max-w-5xl rounded-2xl border border-white/10 bg-white/10 px-8 py-12 text-center shadow-2xl backdrop-blur',
360
- },
361
- children: [
362
- {
363
- id: 'starter-eyebrow',
364
- type: 'p',
365
- props: {
366
- text: 'VisualBuild starter',
367
- className:
368
- 'mb-4 text-xs font-semibold uppercase tracking-[0.28em] text-blue-300',
369
- },
370
- children: [],
371
- },
372
- {
373
- id: 'starter-title',
374
- type: 'h1',
375
- props: {
376
- text: `Welcome to ${displayName}`,
377
- className: 'text-4xl font-bold tracking-tight sm:text-5xl',
378
- },
379
- children: [],
380
- },
381
- {
382
- id: 'starter-subtitle',
383
- type: 'p',
384
- props: {
385
- text: 'Build your app visually with drag and drop, then keep the real React code.',
386
- className: 'mx-auto mt-5 max-w-2xl text-base text-slate-300',
387
- },
388
- children: [],
389
- },
390
- {
391
- id: 'starter-actions',
392
- type: 'div',
393
- props: {
394
- className:
395
- 'mt-8 flex flex-wrap items-center justify-center gap-3',
396
- },
397
- children: [
398
- {
399
- id: 'starter-primary-action',
400
- type: 'a',
401
- props: {
402
- text: 'Open Visual Builder',
403
- href: 'http://127.0.0.1:7371',
404
- target: '_blank',
405
- className:
406
- 'rounded-lg bg-blue-500 px-5 py-3 text-sm font-semibold text-white shadow-lg shadow-blue-500/25 hover:bg-blue-400',
407
- },
408
- children: [],
409
- },
410
- {
411
- id: 'starter-secondary-action',
412
- type: 'span',
413
- props: {
414
- text: 'Edit this page from visual-builder',
415
- className: 'text-sm text-slate-400',
416
- },
417
- children: [],
418
- },
419
- ],
420
- },
421
- ],
422
- },
423
- ],
424
- }
425
- }
426
-
427
- async function regenerateSource(appDir) {
428
- await execFileAsync(
429
- process.execPath,
430
- [path.join(repoRoot, 'scripts', 'vb-generate.mjs'), appDir],
431
- {
432
- cwd: repoRoot,
433
- }
434
- )
435
- }
436
-
437
- async function writeWorkspaceReadme(targetBaseDir, projectName, displayName) {
438
- const readmePath = path.join(targetBaseDir, `${projectName}-README.md`)
439
- const contents = `# ${displayName} VisualBuild Workspace
440
-
441
- This workspace contains two separate projects:
442
-
443
- - \`${projectName}\`: deployable React app source.
444
- - \`${BUILDER_FOLDER_NAME}\`: VisualBuild editor and generator tooling.
445
-
446
- Install and run the app:
447
-
448
- \`\`\`powershell
449
- cd ${projectName}
450
- npm.cmd install
451
- npm.cmd run dev
452
- \`\`\`
453
-
454
- Install and run the visual builder:
455
-
456
- \`\`\`powershell
457
- cd ..\\${BUILDER_FOLDER_NAME}
458
- npm.cmd install
459
- npm.cmd run visual-builder
460
- \`\`\`
461
-
462
- The builder writes to \`${projectName}/visualbuild/pages.json\` and regenerates \`${projectName}/src\`.
463
- The React app package does not include builder-only drag-and-drop dependencies.
464
- `
465
-
466
- await fs.writeFile(readmePath, contents)
467
- }
468
-
469
- async function main() {
470
- const projectName = toProjectSlug(process.argv[2])
471
- const displayName = toDisplayName(projectName)
472
- const targetBaseDir = getTargetBaseDir()
473
- const appDir = path.resolve(targetBaseDir, projectName)
474
- const builderDir = path.resolve(targetBaseDir, BUILDER_FOLDER_NAME)
475
-
476
- await assertScaffoldAssetsExist()
477
- await assertScaffoldTargetsAreWritable(appDir, builderDir)
478
- await copyAppTemplate(appDir)
479
- await copyBuilderTemplate(builderDir)
480
- await seedProject(appDir, builderDir, projectName, displayName)
481
- await regenerateSource(appDir)
482
- await writeWorkspaceReadme(targetBaseDir, projectName, displayName)
483
-
484
- console.log(`Created VisualBuild app "${displayName}" in ${appDir}`)
485
- console.log(`Created VisualBuild editor in ${builderDir}`)
486
- console.log('')
487
- console.log('Next steps:')
488
- console.log(` cd ${projectName}`)
489
- console.log(' npm install')
490
- console.log(' npm.cmd run dev')
491
- console.log('')
492
- console.log('Run the visual builder:')
493
- console.log(` cd ..\\${BUILDER_FOLDER_NAME}`)
494
- console.log(' npm install')
495
- console.log(' npm.cmd run visual-builder')
496
- }
497
-
498
- main().catch((error) => {
499
- console.error(error.message)
500
- process.exit(1)
501
- })
1
+ #!/usr/bin/env node
2
+
3
+ import { execFile } from 'node:child_process'
4
+ import fs from 'node:fs/promises'
5
+ import path from 'node:path'
6
+ import { fileURLToPath } from 'node:url'
7
+ import { promisify } from 'node:util'
8
+
9
+ const execFileAsync = promisify(execFile)
10
+ const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..')
11
+ const DEFAULT_PROJECT_NAME = 'my-app'
12
+ const BUILDER_FOLDER_NAME = 'visual-builder'
13
+ const templateRoot = path.join(repoRoot, 'templates', 'default')
14
+ const appTemplateDir = path.join(templateRoot, 'app')
15
+ const builderTemplateDir = path.join(templateRoot, 'builder')
16
+ const editorSourceDir = path.join(repoRoot, 'visual-app-builder')
17
+ const userGuidePath = path.join(repoRoot, 'docs', 'user-guide.md')
18
+ const scriptFilesToCopy = ['vb-dev.mjs', 'vb-generate.mjs']
19
+
20
+ const appPackageDependencies = ['react', 'react-dom']
21
+ const appPackageDevDependencies = [
22
+ '@eslint/js',
23
+ '@tailwindcss/vite',
24
+ '@types/node',
25
+ '@types/react',
26
+ '@types/react-dom',
27
+ '@vitejs/plugin-react',
28
+ 'eslint',
29
+ 'eslint-plugin-react-hooks',
30
+ 'eslint-plugin-react-refresh',
31
+ 'globals',
32
+ 'tailwindcss',
33
+ 'typescript',
34
+ 'typescript-eslint',
35
+ 'vite',
36
+ ]
37
+
38
+ const builderPackageDependencies = [
39
+ '@babel/generator',
40
+ '@babel/parser',
41
+ '@babel/types',
42
+ '@dnd-kit/core',
43
+ '@dnd-kit/sortable',
44
+ '@dnd-kit/utilities',
45
+ '@tailwindcss/vite',
46
+ 'chokidar',
47
+ 'react',
48
+ 'react-dom',
49
+ 'uuid',
50
+ 'zustand',
51
+ ]
52
+
53
+ const builderPackageDevDependencies = [
54
+ '@eslint/js',
55
+ '@types/node',
56
+ '@types/react',
57
+ '@types/react-dom',
58
+ '@vitejs/plugin-react',
59
+ 'eslint',
60
+ 'eslint-plugin-react-hooks',
61
+ 'eslint-plugin-react-refresh',
62
+ 'globals',
63
+ 'tailwindcss',
64
+ 'typescript',
65
+ 'typescript-eslint',
66
+ 'vite',
67
+ ]
68
+
69
+ function getTargetBaseDir() {
70
+ return path.resolve(process.env.INIT_CWD || process.cwd())
71
+ }
72
+
73
+ function toProjectSlug(input) {
74
+ const slug = String(input || DEFAULT_PROJECT_NAME)
75
+ .trim()
76
+ .toLowerCase()
77
+ .replace(/['"]/g, '')
78
+ .replace(/[^a-z0-9]+/g, '-')
79
+ .replace(/^-+|-+$/g, '')
80
+
81
+ return slug || DEFAULT_PROJECT_NAME
82
+ }
83
+
84
+ function toDisplayName(slug) {
85
+ return slug
86
+ .split(/[-_]+/)
87
+ .filter(Boolean)
88
+ .map((part) => part.charAt(0).toUpperCase() + part.slice(1))
89
+ .join(' ')
90
+ }
91
+
92
+ async function pathExists(filePath) {
93
+ try {
94
+ await fs.access(filePath)
95
+ return true
96
+ } catch {
97
+ return false
98
+ }
99
+ }
100
+
101
+ async function assertTargetIsWritable(targetDir) {
102
+ if (!(await pathExists(targetDir))) {
103
+ return
104
+ }
105
+
106
+ const entries = await fs.readdir(targetDir)
107
+
108
+ if (entries.length > 0) {
109
+ throw new Error(
110
+ `Target folder "${targetDir}" already exists and is not empty.`
111
+ )
112
+ }
113
+ }
114
+
115
+ async function assertScaffoldTargetsAreWritable(appDir, builderDir) {
116
+ await assertTargetIsWritable(appDir)
117
+ await assertTargetIsWritable(builderDir)
118
+ }
119
+
120
+ async function assertScaffoldAssetsExist() {
121
+ const requiredAssets = [
122
+ appTemplateDir,
123
+ builderTemplateDir,
124
+ editorSourceDir,
125
+ userGuidePath,
126
+ ...scriptFilesToCopy.map((file) => path.join(repoRoot, 'scripts', file)),
127
+ ]
128
+
129
+ for (const assetPath of requiredAssets) {
130
+ if (!(await pathExists(assetPath))) {
131
+ throw new Error(
132
+ `Required scaffold asset is missing: ${path.relative(repoRoot, assetPath)}`
133
+ )
134
+ }
135
+ }
136
+ }
137
+
138
+ async function copyAppTemplate(appDir) {
139
+ await fs.cp(appTemplateDir, appDir, { recursive: true })
140
+ }
141
+
142
+ async function copyBuilderTemplate(builderDir) {
143
+ await fs.cp(builderTemplateDir, builderDir, { recursive: true })
144
+ await fs.cp(
145
+ editorSourceDir,
146
+ path.join(builderDir, 'visual-app-builder'),
147
+ { recursive: true }
148
+ )
149
+
150
+ await fs.mkdir(path.join(builderDir, 'scripts'), { recursive: true })
151
+
152
+ for (const file of scriptFilesToCopy) {
153
+ await fs.copyFile(
154
+ path.join(repoRoot, 'scripts', file),
155
+ path.join(builderDir, 'scripts', file)
156
+ )
157
+ }
158
+ }
159
+
160
+ async function rewriteJsonFile(filePath, update) {
161
+ const contents = await fs.readFile(filePath, 'utf8')
162
+ const json = JSON.parse(contents)
163
+ const nextJson = update(json)
164
+ await fs.writeFile(filePath, `${JSON.stringify(nextJson, null, 2)}\n`)
165
+ }
166
+
167
+ async function replaceInFile(filePath, replacements) {
168
+ if (!(await pathExists(filePath))) {
169
+ return
170
+ }
171
+
172
+ let contents = await fs.readFile(filePath, 'utf8')
173
+
174
+ for (const [from, to] of replacements) {
175
+ contents = contents.split(from).join(to)
176
+ }
177
+
178
+ await fs.writeFile(filePath, contents)
179
+ }
180
+
181
+ async function seedProject(appDir, builderDir, projectName, displayName) {
182
+ const appPackageJson = await readRootPackageJson()
183
+ const builderPackageJson = await readRootPackageJson()
184
+ const relativeAppDir = toPosixPath(path.relative(builderDir, appDir))
185
+
186
+ await fs.writeFile(
187
+ path.join(appDir, 'package.json'),
188
+ `${JSON.stringify(
189
+ createAppPackageJson(appPackageJson, projectName),
190
+ null,
191
+ 2
192
+ )}\n`
193
+ )
194
+
195
+ await fs.writeFile(
196
+ path.join(builderDir, 'package.json'),
197
+ `${JSON.stringify(
198
+ createBuilderPackageJson(
199
+ builderPackageJson,
200
+ relativeAppDir
201
+ ),
202
+ null,
203
+ 2
204
+ )}\n`
205
+ )
206
+
207
+ await rewriteJsonFile(
208
+ path.join(appDir, 'visualbuild', 'pages.json'),
209
+ (schema) => seedProjectSchema(schema, projectName, displayName)
210
+ )
211
+
212
+ const templateReplacements = [
213
+ ['__DISPLAY_NAME__', displayName],
214
+ ['__PROJECT_NAME__', projectName],
215
+ ]
216
+
217
+ await replaceInFile(path.join(appDir, 'index.html'), templateReplacements)
218
+ await replaceInFile(path.join(appDir, 'README.md'), templateReplacements)
219
+ await replaceInFile(path.join(builderDir, 'README.md'), templateReplacements)
220
+
221
+ const sourceReplacements = [
222
+ ['Welcome to My App', `Welcome to ${displayName}`],
223
+ ['My App', displayName],
224
+ ['my-app', projectName],
225
+ ]
226
+
227
+ await replaceInFile(
228
+ path.join(builderDir, 'visual-app-builder', 'src', 'stores', 'useAppStore.ts'),
229
+ sourceReplacements
230
+ )
231
+ await replaceInFile(
232
+ path.join(
233
+ builderDir,
234
+ 'visual-app-builder',
235
+ 'src',
236
+ 'utils',
237
+ 'projectPersistence.ts'
238
+ ),
239
+ sourceReplacements
240
+ )
241
+ await replaceInFile(
242
+ path.join(
243
+ builderDir,
244
+ 'visual-app-builder',
245
+ 'src',
246
+ 'data',
247
+ 'componentRegistry.tsx'
248
+ ),
249
+ sourceReplacements
250
+ )
251
+ }
252
+
253
+ async function readRootPackageJson() {
254
+ return JSON.parse(await fs.readFile(path.join(repoRoot, 'package.json'), 'utf8'))
255
+ }
256
+
257
+ function createAppPackageJson(packageJson, projectName) {
258
+ const allDependencies = mergeDependencies(packageJson)
259
+
260
+ return {
261
+ name: projectName,
262
+ private: true,
263
+ version: packageJson.version,
264
+ type: 'module',
265
+ scripts: {
266
+ dev: 'vite',
267
+ build: 'tsc -b && vite build',
268
+ lint: 'eslint .',
269
+ preview: 'vite preview',
270
+ },
271
+ dependencies: pickDependencies(allDependencies, appPackageDependencies),
272
+ devDependencies: pickDependencies(
273
+ allDependencies,
274
+ appPackageDevDependencies
275
+ ),
276
+ }
277
+ }
278
+
279
+ function createBuilderPackageJson(packageJson, relativeAppDir) {
280
+ const allDependencies = mergeDependencies(packageJson)
281
+
282
+ return {
283
+ name: BUILDER_FOLDER_NAME,
284
+ private: true,
285
+ version: packageJson.version,
286
+ type: 'module',
287
+ scripts: {
288
+ 'visual-builder': `node scripts/vb-dev.mjs ${JSON.stringify(relativeAppDir)}`,
289
+ 'vb:generate': `node scripts/vb-generate.mjs ${JSON.stringify(relativeAppDir)}`,
290
+ build: 'vite build --config visual-app-builder/vite.config.ts',
291
+ lint: 'eslint .',
292
+ },
293
+ dependencies: pickDependencies(
294
+ allDependencies,
295
+ builderPackageDependencies
296
+ ),
297
+ devDependencies: pickDependencies(
298
+ allDependencies,
299
+ builderPackageDevDependencies
300
+ ),
301
+ }
302
+ }
303
+
304
+ function mergeDependencies(packageJson) {
305
+ return {
306
+ ...(packageJson.dependencies ?? {}),
307
+ ...(packageJson.devDependencies ?? {}),
308
+ }
309
+ }
310
+
311
+ function pickDependencies(source, names) {
312
+ return Object.fromEntries(
313
+ names
314
+ .filter((name) => source?.[name])
315
+ .map((name) => [name, source[name]])
316
+ )
317
+ }
318
+
319
+ function toPosixPath(filePath) {
320
+ return filePath.split(path.sep).join('/')
321
+ }
322
+
323
+ function seedProjectSchema(schema, projectName, displayName) {
324
+ return {
325
+ ...schema,
326
+ meta: {
327
+ ...schema.meta,
328
+ projectName,
329
+ },
330
+ appShell: {
331
+ header: null,
332
+ footer: null,
333
+ },
334
+ pages: [createStarterHomePage(displayName)],
335
+ }
336
+ }
337
+
338
+ function createStarterHomePage(displayName) {
339
+ return {
340
+ id: 'page-home',
341
+ name: 'Home',
342
+ route: '/',
343
+ sourcePath: 'src/pages/HomePage.tsx',
344
+ root: {
345
+ id: 'page-home:root',
346
+ type: 'main',
347
+ label: 'Page root',
348
+ props: {
349
+ className:
350
+ 'min-h-screen bg-slate-950 text-white flex items-center justify-center px-6',
351
+ },
352
+ children: [],
353
+ },
354
+ components: [
355
+ {
356
+ id: 'starter-shell',
357
+ type: 'section',
358
+ label: 'Starter',
359
+ props: {
360
+ className:
361
+ 'w-full max-w-5xl rounded-2xl border border-white/10 bg-white/10 px-8 py-12 text-center shadow-2xl backdrop-blur',
362
+ },
363
+ children: [
364
+ {
365
+ id: 'starter-eyebrow',
366
+ type: 'p',
367
+ props: {
368
+ text: 'VisualBuild starter',
369
+ className:
370
+ 'mb-4 text-xs font-semibold uppercase tracking-[0.28em] text-blue-300',
371
+ },
372
+ children: [],
373
+ },
374
+ {
375
+ id: 'starter-title',
376
+ type: 'h1',
377
+ props: {
378
+ text: `Welcome to ${displayName}`,
379
+ className: 'text-4xl font-bold tracking-tight sm:text-5xl',
380
+ },
381
+ children: [],
382
+ },
383
+ {
384
+ id: 'starter-subtitle',
385
+ type: 'p',
386
+ props: {
387
+ text: 'Build your app visually with drag and drop, then keep the real React code.',
388
+ className: 'mx-auto mt-5 max-w-2xl text-base text-slate-300',
389
+ },
390
+ children: [],
391
+ },
392
+ {
393
+ id: 'starter-actions',
394
+ type: 'div',
395
+ props: {
396
+ className:
397
+ 'mt-8 flex flex-wrap items-center justify-center gap-3',
398
+ },
399
+ children: [
400
+ {
401
+ id: 'starter-primary-action',
402
+ type: 'a',
403
+ props: {
404
+ text: 'Open Visual Builder',
405
+ href: 'http://127.0.0.1:7371',
406
+ target: '_blank',
407
+ className:
408
+ 'rounded-lg bg-blue-500 px-5 py-3 text-sm font-semibold text-white shadow-lg shadow-blue-500/25 hover:bg-blue-400',
409
+ },
410
+ children: [],
411
+ },
412
+ {
413
+ id: 'starter-secondary-action',
414
+ type: 'span',
415
+ props: {
416
+ text: 'Edit this page from visual-builder',
417
+ className: 'text-sm text-slate-400',
418
+ },
419
+ children: [],
420
+ },
421
+ ],
422
+ },
423
+ ],
424
+ },
425
+ ],
426
+ }
427
+ }
428
+
429
+ async function regenerateSource(appDir) {
430
+ await execFileAsync(
431
+ process.execPath,
432
+ [path.join(repoRoot, 'scripts', 'vb-generate.mjs'), appDir],
433
+ {
434
+ cwd: repoRoot,
435
+ }
436
+ )
437
+ }
438
+
439
+ async function writeWorkspaceReadme(targetBaseDir, projectName, displayName) {
440
+ const readmePath = path.join(targetBaseDir, `${projectName}-README.md`)
441
+ const workspaceGuidePath = path.join(targetBaseDir, 'VISUALBUILD-GUIDE.md')
442
+ const contents = `# ${displayName} VisualBuild Workspace
443
+
444
+ This workspace contains two separate projects:
445
+
446
+ - \`${projectName}\`: deployable React app source.
447
+ - \`${BUILDER_FOLDER_NAME}\`: VisualBuild editor and generator tooling.
448
+
449
+ Read [VISUALBUILD-GUIDE.md](./VISUALBUILD-GUIDE.md) for the complete workflow,
450
+ including visual editing, page and component registration, the larger code
451
+ workspace, source ownership, two-way sync limits, configuration, deployment,
452
+ and error recovery.
453
+
454
+ Install and run the app:
455
+
456
+ \`\`\`powershell
457
+ cd ${projectName}
458
+ npm.cmd install
459
+ npm.cmd run dev
460
+ \`\`\`
461
+
462
+ In another terminal opened in this workspace, install and run the visual builder:
463
+
464
+ \`\`\`powershell
465
+ cd ${BUILDER_FOLDER_NAME}
466
+ npm.cmd install
467
+ npm.cmd run visual-builder
468
+ \`\`\`
469
+
470
+ If that terminal starts inside \`${projectName}\`, use
471
+ \`cd ..\\${BUILDER_FOLDER_NAME}\` instead.
472
+
473
+ The builder writes to \`${projectName}/visualbuild/pages.json\` and regenerates \`${projectName}/src\`.
474
+ The React app package does not include builder-only drag-and-drop dependencies.
475
+ `
476
+
477
+ await fs.copyFile(userGuidePath, workspaceGuidePath)
478
+ await fs.writeFile(readmePath, contents)
479
+ }
480
+
481
+ async function main() {
482
+ const projectName = toProjectSlug(process.argv[2])
483
+ const displayName = toDisplayName(projectName)
484
+ const targetBaseDir = getTargetBaseDir()
485
+ const appDir = path.resolve(targetBaseDir, projectName)
486
+ const builderDir = path.resolve(targetBaseDir, BUILDER_FOLDER_NAME)
487
+
488
+ await assertScaffoldAssetsExist()
489
+ await assertScaffoldTargetsAreWritable(appDir, builderDir)
490
+ await copyAppTemplate(appDir)
491
+ await copyBuilderTemplate(builderDir)
492
+ await seedProject(appDir, builderDir, projectName, displayName)
493
+ await regenerateSource(appDir)
494
+ await writeWorkspaceReadme(targetBaseDir, projectName, displayName)
495
+
496
+ console.log(`Created VisualBuild app "${displayName}" in ${appDir}`)
497
+ console.log(`Created VisualBuild editor in ${builderDir}`)
498
+ console.log('')
499
+ console.log('Next steps:')
500
+ console.log(` cd ${projectName}`)
501
+ console.log(' npm install')
502
+ console.log(' npm.cmd run dev')
503
+ console.log('')
504
+ console.log('In another terminal opened in the workspace, run the visual builder:')
505
+ console.log(` cd ${BUILDER_FOLDER_NAME}`)
506
+ console.log(' npm install')
507
+ console.log(' npm.cmd run visual-builder')
508
+ }
509
+
510
+ main().catch((error) => {
511
+ console.error(error.message)
512
+ process.exit(1)
513
+ })