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,32 +1,41 @@
1
- #!/usr/bin/env node
2
-
3
- import { spawn } from 'node:child_process'
4
- import path from 'node:path'
5
- import { fileURLToPath } from 'node:url'
6
-
7
- const packageRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..')
8
- const targetAppDir = path.resolve(process.argv[2] ?? packageRoot)
9
- const viteBin = path.join(packageRoot, 'node_modules', 'vite', 'bin', 'vite.js')
10
- const viteConfig = path.join(packageRoot, 'visual-app-builder', 'vite.config.ts')
11
-
12
- const child = spawn(
13
- process.execPath,
14
- [viteBin, '--config', viteConfig, '--host', '127.0.0.1', '--port', '7371'],
15
- {
16
- cwd: packageRoot,
17
- env: {
18
- ...process.env,
19
- VISUALBUILD_APP_DIR: targetAppDir,
20
- },
21
- stdio: 'inherit',
22
- }
23
- )
24
-
25
- child.on('exit', (code, signal) => {
26
- if (signal) {
27
- process.kill(process.pid, signal)
28
- return
29
- }
30
-
31
- process.exit(code ?? 0)
32
- })
1
+ #!/usr/bin/env node
2
+
3
+ import { spawn } from 'node:child_process'
4
+ import path from 'node:path'
5
+ import { fileURLToPath } from 'node:url'
6
+
7
+ const packageRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..')
8
+ const targetAppDir = path.resolve(process.argv[2] ?? packageRoot)
9
+ const viteBin = path.join(packageRoot, 'node_modules', 'vite', 'bin', 'vite.js')
10
+ const viteConfig = path.join(packageRoot, 'visual-app-builder', 'vite.config.ts')
11
+
12
+ const child = spawn(
13
+ process.execPath,
14
+ [
15
+ viteBin,
16
+ '--config',
17
+ viteConfig,
18
+ '--host',
19
+ '127.0.0.1',
20
+ '--port',
21
+ '7371',
22
+ '--strictPort',
23
+ ],
24
+ {
25
+ cwd: packageRoot,
26
+ env: {
27
+ ...process.env,
28
+ VISUALBUILD_APP_DIR: targetAppDir,
29
+ },
30
+ stdio: 'inherit',
31
+ }
32
+ )
33
+
34
+ child.on('exit', (code, signal) => {
35
+ if (signal) {
36
+ process.kill(process.pid, signal)
37
+ return
38
+ }
39
+
40
+ process.exit(code ?? 0)
41
+ })
@@ -1,224 +1,332 @@
1
- import fs from 'node:fs/promises'
2
- import path from 'node:path'
3
- import {
4
- generateAppSource,
5
- generateLayoutSource,
6
- generatePageSource,
7
- getPageSourcePath,
8
- normalizeProjectPath,
9
- } from '../visual-app-builder/shared/generateReactSource.mjs'
10
-
11
- const root = path.resolve(process.argv[2] ?? process.cwd())
12
- const schemaPath = path.join(root, 'visualbuild', 'pages.json')
13
- const outputRoot = path.join(root, 'src')
14
- const manifestPath = path.join(root, 'visualbuild', 'generated-files.json')
15
- const diagnosticMarker = 'VISUALBUILD_DIAGNOSTIC:'
16
-
17
- class GeneratorDiagnosticError extends Error {
18
- constructor(code, message, suggestion) {
19
- super(message)
20
- this.name = 'GeneratorDiagnosticError'
21
- this.code = code
22
- this.suggestion = suggestion
23
- }
24
- }
25
-
26
- function validatePages(pages) {
27
- const seenRoutes = new Set()
28
- const seenSourcePaths = new Set()
29
-
30
- for (const page of pages) {
31
- if (!page.name || typeof page.name !== 'string') {
32
- throw new GeneratorDiagnosticError(
33
- 'INVALID_PAGE_NAME',
34
- 'Every page needs a name.',
35
- 'Give each page a non-empty name in the page manager.'
36
- )
37
- }
38
-
39
- if (!page.route || typeof page.route !== 'string') {
40
- throw new GeneratorDiagnosticError(
41
- 'MISSING_PAGE_ROUTE',
42
- `Page "${page.name}" needs a route.`,
43
- 'Add a route in the page manager before saving.'
44
- )
45
- }
46
-
47
- if (!page.route.startsWith('/')) {
48
- throw new GeneratorDiagnosticError(
49
- 'INVALID_PAGE_ROUTE',
50
- `Route "${page.route}" must start with "/".`,
51
- `Change the route to "/${page.route.replace(/^\/+/, '')}".`
52
- )
53
- }
54
-
55
- if (seenRoutes.has(page.route)) {
56
- throw new GeneratorDiagnosticError(
57
- 'DUPLICATE_PAGE_ROUTE',
58
- `Duplicate route found: ${page.route}`,
59
- 'Give every registered page a unique route.'
60
- )
61
- }
62
-
63
- const sourcePath = getPageSourcePath(page)
64
-
65
- if (
66
- !sourcePath.startsWith('src/') ||
67
- !/\.(jsx|tsx)$/i.test(sourcePath) ||
68
- sourcePath.includes('/../') ||
69
- sourcePath.endsWith('/..')
70
- ) {
71
- throw new GeneratorDiagnosticError(
72
- 'INVALID_PAGE_SOURCE_PATH',
73
- `Page "${page.name}" needs a .jsx or .tsx sourcePath inside src.`,
74
- 'Move the page source under src and use a .jsx or .tsx extension.'
75
- )
76
- }
77
-
78
- if (
79
- sourcePath === 'src/App.tsx' ||
80
- sourcePath === 'src/layouts/AppLayout.tsx'
81
- ) {
82
- throw new GeneratorDiagnosticError(
83
- 'RESERVED_PAGE_SOURCE_PATH',
84
- `Page source path is reserved: ${sourcePath}`,
85
- 'Choose a page file that does not replace App.tsx or AppLayout.tsx.'
86
- )
87
- }
88
-
89
- if (seenSourcePaths.has(sourcePath)) {
90
- throw new GeneratorDiagnosticError(
91
- 'DUPLICATE_PAGE_SOURCE_PATH',
92
- `Duplicate page source path found: ${sourcePath}`,
93
- 'Give every registered page its own source file.'
94
- )
95
- }
96
-
97
- seenRoutes.add(page.route)
98
- seenSourcePaths.add(sourcePath)
99
- }
100
- }
101
-
102
- async function writeFile(filePath, contents) {
103
- await fs.mkdir(path.dirname(filePath), { recursive: true })
104
- await fs.writeFile(filePath, contents)
105
- }
106
-
107
- function getManagedFilePath(relativePath) {
108
- const normalizedPath = normalizeProjectPath(relativePath)
109
- const absolutePath = path.resolve(root, ...normalizedPath.split('/'))
110
- const relativeToRoot = path.relative(root, absolutePath)
111
-
112
- if (
113
- !normalizedPath ||
114
- relativeToRoot.startsWith('..') ||
115
- path.isAbsolute(relativeToRoot)
116
- ) {
117
- throw new GeneratorDiagnosticError(
118
- 'UNSAFE_GENERATED_PATH',
119
- `Generated file cannot leave the project: ${relativePath}`,
120
- 'Use a project-relative source path inside src.'
121
- )
122
- }
123
-
124
- return absolutePath
125
- }
126
-
127
- async function readGeneratedManifest() {
128
- try {
129
- const manifest = JSON.parse(await fs.readFile(manifestPath, 'utf8'))
130
-
131
- return Array.isArray(manifest.files)
132
- ? manifest.files.map(normalizeProjectPath)
133
- : []
134
- } catch {
135
- return []
136
- }
137
- }
138
-
139
- async function writeGeneratedManifest(files) {
140
- await writeFile(
141
- manifestPath,
142
- `${JSON.stringify({ files: [...files].sort() }, null, 2)}\n`
143
- )
144
- }
145
-
146
- async function main() {
147
- const rawSchema = await fs.readFile(schemaPath, 'utf8')
148
- const schema = JSON.parse(rawSchema)
149
- const pages = schema.pages ?? []
150
- const appShell = schema.appShell ?? { header: null, footer: null }
151
-
152
- validatePages(pages)
153
-
154
- await fs.mkdir(outputRoot, { recursive: true })
155
- const previousManagedFiles = await readGeneratedManifest()
156
- const nextManagedFiles = new Set([
157
- 'src/App.tsx',
158
- ...pages.map(getPageSourcePath),
159
- ])
160
-
161
- if (appShell.header || appShell.footer) {
162
- nextManagedFiles.add('src/layouts/AppLayout.tsx')
163
- }
164
-
165
- for (const staleFile of previousManagedFiles) {
166
- if (!nextManagedFiles.has(staleFile)) {
167
- await fs.rm(getManagedFilePath(staleFile), { force: true })
168
- }
169
- }
170
-
171
- for (const page of pages) {
172
- await writeFile(
173
- getManagedFilePath(getPageSourcePath(page)),
174
- generatePageSource(page)
175
- )
176
- }
177
-
178
- if (appShell.header || appShell.footer) {
179
- await writeFile(
180
- path.join(outputRoot, 'layouts', 'AppLayout.tsx'),
181
- generateLayoutSource(appShell)
182
- )
183
- }
184
-
185
- await writeFile(
186
- path.join(outputRoot, 'App.tsx'),
187
- generateAppSource(pages, appShell)
188
- )
189
- await writeGeneratedManifest(nextManagedFiles)
190
-
191
- console.log(`Generated ${pages.length} page(s) into src`)
192
- }
193
-
194
- main().catch((error) => {
195
- const diagnostic = createGeneratorDiagnostic(error)
196
- console.error(`${diagnosticMarker}${JSON.stringify(diagnostic)}`)
197
- process.exit(1)
198
- })
199
-
200
- function createGeneratorDiagnostic(error) {
201
- const isInvalidJson = error instanceof SyntaxError
202
- const message =
203
- error instanceof Error ? error.message : 'React source generation failed.'
204
-
205
- return {
206
- severity: 'error',
207
- phase: 'generate',
208
- code: isInvalidJson
209
- ? 'INVALID_SCHEMA_JSON'
210
- : error instanceof GeneratorDiagnosticError
211
- ? error.code
212
- : 'GENERATION_FAILED',
213
- title: isInvalidJson
214
- ? 'Project schema is not valid JSON'
215
- : 'React source generation failed',
216
- message,
217
- path: 'visualbuild/pages.json',
218
- suggestion: isInvalidJson
219
- ? 'Fix the JSON syntax, then save again. The last generated React files were kept.'
220
- : error instanceof GeneratorDiagnosticError
221
- ? error.suggestion
222
- : 'Review the page schema and try again. The last generated React files were kept.',
223
- }
224
- }
1
+ import fs from 'node:fs/promises'
2
+ import path from 'node:path'
3
+ import {
4
+ generateAppSource,
5
+ generateLayoutSource,
6
+ generatePageSource,
7
+ createPageSourcePath,
8
+ getPageSourcePath,
9
+ normalizeProjectPath,
10
+ } from '../visual-app-builder/shared/generateReactSource.mjs'
11
+ import { loadVisualBuildConfig } from '../visual-app-builder/shared/visualbuildConfig.mjs'
12
+ import { syncCustomComponentRoot } from '../visual-app-builder/shared/syncCustomComponentSource.mjs'
13
+
14
+ const root = path.resolve(process.argv[2] ?? process.cwd())
15
+ const diagnosticMarker = 'VISUALBUILD_DIAGNOSTIC:'
16
+
17
+ class GeneratorDiagnosticError extends Error {
18
+ constructor(code, message, suggestion) {
19
+ super(message)
20
+ this.name = 'GeneratorDiagnosticError'
21
+ this.code = code
22
+ this.suggestion = suggestion
23
+ }
24
+ }
25
+
26
+ function isPathInsideDirectory(filePath, directoryPath) {
27
+ return (
28
+ filePath === directoryPath ||
29
+ filePath.startsWith(`${directoryPath.replace(/\/+$/, '')}/`)
30
+ )
31
+ }
32
+
33
+ function validatePages(pages, config) {
34
+ const seenRoutes = new Set()
35
+ const seenSourcePaths = new Set()
36
+
37
+ for (const page of pages) {
38
+ if (!page.name || typeof page.name !== 'string') {
39
+ throw new GeneratorDiagnosticError(
40
+ 'INVALID_PAGE_NAME',
41
+ 'Every page needs a name.',
42
+ 'Give each page a non-empty name in the page manager.'
43
+ )
44
+ }
45
+
46
+ if (!page.route || typeof page.route !== 'string') {
47
+ throw new GeneratorDiagnosticError(
48
+ 'MISSING_PAGE_ROUTE',
49
+ `Page "${page.name}" needs a route.`,
50
+ 'Add a route in the page manager before saving.'
51
+ )
52
+ }
53
+
54
+ if (!page.route.startsWith('/')) {
55
+ throw new GeneratorDiagnosticError(
56
+ 'INVALID_PAGE_ROUTE',
57
+ `Route "${page.route}" must start with "/".`,
58
+ `Change the route to "/${page.route.replace(/^\/+/, '')}".`
59
+ )
60
+ }
61
+
62
+ if (seenRoutes.has(page.route)) {
63
+ throw new GeneratorDiagnosticError(
64
+ 'DUPLICATE_PAGE_ROUTE',
65
+ `Duplicate route found: ${page.route}`,
66
+ 'Give every registered page a unique route.'
67
+ )
68
+ }
69
+
70
+ const sourcePath = getPageSourcePath(page)
71
+
72
+ if (
73
+ !isPathInsideDirectory(sourcePath, config.paths.sourceDir) ||
74
+ !/\.(jsx|tsx)$/i.test(sourcePath) ||
75
+ sourcePath.includes('/../') ||
76
+ sourcePath.endsWith('/..')
77
+ ) {
78
+ throw new GeneratorDiagnosticError(
79
+ 'INVALID_PAGE_SOURCE_PATH',
80
+ `Page "${page.name}" needs a .jsx or .tsx sourcePath inside ${config.paths.sourceDir}.`,
81
+ `Move the page source under ${config.paths.sourceDir} and use a .jsx or .tsx extension.`
82
+ )
83
+ }
84
+
85
+ const layoutPath = `${config.paths.layoutsDir}/AppLayout.tsx`
86
+ if (
87
+ sourcePath === config.paths.appFile ||
88
+ sourcePath === layoutPath
89
+ ) {
90
+ throw new GeneratorDiagnosticError(
91
+ 'RESERVED_PAGE_SOURCE_PATH',
92
+ `Page source path is reserved: ${sourcePath}`,
93
+ 'Choose a page file that does not replace App.tsx or AppLayout.tsx.'
94
+ )
95
+ }
96
+
97
+ if (seenSourcePaths.has(sourcePath)) {
98
+ throw new GeneratorDiagnosticError(
99
+ 'DUPLICATE_PAGE_SOURCE_PATH',
100
+ `Duplicate page source path found: ${sourcePath}`,
101
+ 'Give every registered page its own source file.'
102
+ )
103
+ }
104
+
105
+ seenRoutes.add(page.route)
106
+ seenSourcePaths.add(sourcePath)
107
+ }
108
+ }
109
+
110
+ async function writeFile(filePath, contents) {
111
+ await fs.mkdir(path.dirname(filePath), { recursive: true })
112
+ await fs.writeFile(filePath, contents)
113
+ }
114
+
115
+ function getManagedFilePath(relativePath) {
116
+ const normalizedPath = normalizeProjectPath(relativePath)
117
+ const absolutePath = path.resolve(root, ...normalizedPath.split('/'))
118
+ const relativeToRoot = path.relative(root, absolutePath)
119
+
120
+ if (
121
+ !normalizedPath ||
122
+ relativeToRoot.startsWith('..') ||
123
+ path.isAbsolute(relativeToRoot)
124
+ ) {
125
+ throw new GeneratorDiagnosticError(
126
+ 'UNSAFE_GENERATED_PATH',
127
+ `Generated file cannot leave the project: ${relativePath}`,
128
+ 'Use a project-relative path inside the configured source directory.'
129
+ )
130
+ }
131
+
132
+ return absolutePath
133
+ }
134
+
135
+ async function readGeneratedManifest(manifestPath) {
136
+ try {
137
+ const manifest = JSON.parse(await fs.readFile(manifestPath, 'utf8'))
138
+
139
+ return Array.isArray(manifest.files)
140
+ ? manifest.files.map(normalizeProjectPath)
141
+ : []
142
+ } catch {
143
+ return []
144
+ }
145
+ }
146
+
147
+ async function writeGeneratedManifest(manifestPath, files) {
148
+ await writeFile(
149
+ manifestPath,
150
+ `${JSON.stringify({ files: [...files].sort() }, null, 2)}\n`
151
+ )
152
+ }
153
+
154
+ async function main() {
155
+ const config = await loadVisualBuildConfig(root)
156
+ const schemaPath = getManagedFilePath(config.paths.schemaFile)
157
+ const outputRoot = getManagedFilePath(config.paths.sourceDir)
158
+ const manifestPath = getManagedFilePath(
159
+ config.paths.generatedManifestFile
160
+ )
161
+ const rawSchema = await fs.readFile(schemaPath, 'utf8')
162
+ const schema = JSON.parse(rawSchema)
163
+ const pages = (schema.pages ?? []).map((page) => ({
164
+ ...page,
165
+ sourcePath:
166
+ page.sourcePath ??
167
+ createPageSourcePath(page.name, config.paths.pagesDir),
168
+ }))
169
+ const appShell = schema.appShell ?? { header: null, footer: null }
170
+ const customComponents = config.components ?? []
171
+
172
+ validatePages(pages, config)
173
+ await syncRegisteredComponentSources(
174
+ pages,
175
+ appShell,
176
+ customComponents
177
+ )
178
+
179
+ await fs.mkdir(outputRoot, { recursive: true })
180
+ const previousManagedFiles = await readGeneratedManifest(manifestPath)
181
+ const nextManagedFiles = new Set(pages.map(getPageSourcePath))
182
+ const layoutPath = `${config.paths.layoutsDir}/AppLayout.tsx`
183
+
184
+ if (config.generator.emitApp) {
185
+ nextManagedFiles.add(config.paths.appFile)
186
+ }
187
+
188
+ if (
189
+ config.generator.emitLayout &&
190
+ (appShell.header || appShell.footer)
191
+ ) {
192
+ nextManagedFiles.add(layoutPath)
193
+ }
194
+
195
+ if (config.generator.cleanStaleFiles) {
196
+ const retainedDeveloperFiles = new Set([
197
+ ...(!config.generator.emitApp ? [config.paths.appFile] : []),
198
+ ...(!config.generator.emitLayout ? [layoutPath] : []),
199
+ ])
200
+
201
+ for (const staleFile of previousManagedFiles) {
202
+ if (
203
+ !nextManagedFiles.has(staleFile) &&
204
+ !retainedDeveloperFiles.has(staleFile)
205
+ ) {
206
+ await fs.rm(getManagedFilePath(staleFile), { force: true })
207
+ }
208
+ }
209
+ }
210
+
211
+ for (const page of pages) {
212
+ await writeFile(
213
+ getManagedFilePath(getPageSourcePath(page)),
214
+ generatePageSource(page, customComponents)
215
+ )
216
+ }
217
+
218
+ if (
219
+ config.generator.emitLayout &&
220
+ (appShell.header || appShell.footer)
221
+ ) {
222
+ await writeFile(
223
+ getManagedFilePath(layoutPath),
224
+ generateLayoutSource(appShell, customComponents, {
225
+ layoutFile: layoutPath,
226
+ })
227
+ )
228
+ }
229
+
230
+ if (config.generator.emitApp) {
231
+ await writeFile(
232
+ getManagedFilePath(config.paths.appFile),
233
+ generateAppSource(pages, appShell, {
234
+ appFile: config.paths.appFile,
235
+ layoutFile: layoutPath,
236
+ emitLayout: config.generator.emitLayout,
237
+ })
238
+ )
239
+ }
240
+ await writeGeneratedManifest(
241
+ manifestPath,
242
+ config.generator.cleanStaleFiles
243
+ ? nextManagedFiles
244
+ : new Set([...previousManagedFiles, ...nextManagedFiles])
245
+ )
246
+
247
+ console.log(
248
+ `Generated ${pages.length} page(s) into ${config.paths.sourceDir}`
249
+ )
250
+ }
251
+
252
+ async function syncRegisteredComponentSources(
253
+ pages,
254
+ appShell,
255
+ customComponents
256
+ ) {
257
+ const roots = [
258
+ ...(appShell.header ? [appShell.header] : []),
259
+ ...(appShell.footer ? [appShell.footer] : []),
260
+ ...pages.flatMap((page) => page.components ?? []),
261
+ ]
262
+
263
+ for (const definition of customComponents) {
264
+ const node = findNodeByType(roots, definition.name)
265
+ if (!node) continue
266
+
267
+ const sourcePath = getManagedFilePath(definition.importPath)
268
+ const source = await fs.readFile(sourcePath, 'utf8')
269
+ const nextSource = syncCustomComponentRoot(source, {
270
+ name: definition.name,
271
+ rootTag: node.props?.wrapperTag,
272
+ id: node.props?.id,
273
+ className: node.props?.className,
274
+ attributes: node.props?.attributes,
275
+ children: getCustomComponentTreeChildren(node),
276
+ })
277
+
278
+ if (nextSource !== source) {
279
+ await writeFile(sourcePath, nextSource)
280
+ }
281
+ }
282
+ }
283
+
284
+ function getCustomComponentTreeChildren(node) {
285
+ const tree = node.props?.componentTree
286
+ return tree &&
287
+ typeof tree === 'object' &&
288
+ Array.isArray(tree.children)
289
+ ? tree.children
290
+ : undefined
291
+ }
292
+
293
+ function findNodeByType(nodes, type) {
294
+ for (const node of nodes) {
295
+ if (node.type === type) return node
296
+ const child = findNodeByType(node.children ?? [], type)
297
+ if (child) return child
298
+ }
299
+ return null
300
+ }
301
+
302
+ main().catch((error) => {
303
+ const diagnostic = createGeneratorDiagnostic(error)
304
+ console.error(`${diagnosticMarker}${JSON.stringify(diagnostic)}`)
305
+ process.exit(1)
306
+ })
307
+
308
+ function createGeneratorDiagnostic(error) {
309
+ const isInvalidJson = error instanceof SyntaxError
310
+ const message =
311
+ error instanceof Error ? error.message : 'React source generation failed.'
312
+
313
+ return {
314
+ severity: 'error',
315
+ phase: 'generate',
316
+ code: isInvalidJson
317
+ ? 'INVALID_SCHEMA_JSON'
318
+ : error instanceof GeneratorDiagnosticError
319
+ ? error.code
320
+ : 'GENERATION_FAILED',
321
+ title: isInvalidJson
322
+ ? 'Project schema is not valid JSON'
323
+ : 'React source generation failed',
324
+ message,
325
+ path: 'visualbuild/pages.json',
326
+ suggestion: isInvalidJson
327
+ ? 'Fix the JSON syntax, then save again. The last generated React files were kept.'
328
+ : error instanceof GeneratorDiagnosticError
329
+ ? error.suggestion
330
+ : 'Review the page schema and try again. The last generated React files were kept.',
331
+ }
332
+ }
@@ -0,0 +1,6 @@
1
+ visualbuild/
2
+ visualbuild.config.*
3
+ visual-app-builder/
4
+ visual-builder/
5
+ node_modules/
6
+ dist/