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.
- package/README.md +919 -58
- package/docs/user-guide.md +759 -0
- package/package.json +92 -85
- package/scripts/create-builder-app.mjs +513 -501
- package/scripts/vb-dev.mjs +41 -32
- package/scripts/vb-generate.mjs +332 -224
- package/templates/default/app/.vercelignore +6 -0
- package/templates/default/app/README.md +56 -21
- package/templates/default/app/visualbuild/components.json +3 -0
- package/templates/default/app/visualbuild/config.d.ts +48 -0
- package/templates/default/app/visualbuild.config.ts +38 -0
- package/templates/default/builder/README.md +37 -21
- package/visual-app-builder/server/parseReactPage.ts +776 -571
- package/visual-app-builder/shared/createReactComponentSource.d.mts +2 -0
- package/visual-app-builder/shared/createReactComponentSource.mjs +45 -0
- package/visual-app-builder/shared/generateReactSource.d.mts +57 -37
- package/visual-app-builder/shared/generateReactSource.mjs +608 -443
- package/visual-app-builder/shared/npmBuildCommand.d.mts +17 -0
- package/visual-app-builder/shared/npmBuildCommand.mjs +26 -0
- package/visual-app-builder/shared/syncCustomComponentSource.d.mts +13 -0
- package/visual-app-builder/shared/syncCustomComponentSource.mjs +345 -0
- package/visual-app-builder/shared/vercelDeployment.d.mts +31 -0
- package/visual-app-builder/shared/vercelDeployment.mjs +266 -0
- package/visual-app-builder/shared/visualbuildConfig.d.mts +144 -0
- package/visual-app-builder/shared/visualbuildConfig.mjs +578 -0
- package/visual-app-builder/src/App.tsx +1090 -874
- package/visual-app-builder/src/components/Canvas.tsx +1116 -1059
- package/visual-app-builder/src/components/CodePanel.tsx +1147 -812
- package/visual-app-builder/src/components/ComponentSidebar.tsx +365 -302
- package/visual-app-builder/src/components/DeploymentModal.tsx +295 -0
- package/visual-app-builder/src/components/PageSwitcher.tsx +133 -133
- package/visual-app-builder/src/components/ProjectExplorer.tsx +1054 -1054
- package/visual-app-builder/src/components/PropertiesPanel.tsx +792 -692
- package/visual-app-builder/src/components/Topbar.tsx +257 -128
- package/visual-app-builder/src/data/componentRegistry.tsx +613 -292
- package/visual-app-builder/src/data/tailwindClassCatalog.ts +95 -0
- package/visual-app-builder/src/index.css +383 -111
- package/visual-app-builder/src/stores/useAppStore.ts +2385 -1265
- package/visual-app-builder/src/theme.ts +71 -0
- package/visual-app-builder/src/types/index.ts +350 -261
- package/visual-app-builder/src/utils/codegen.ts +90 -66
- package/visual-app-builder/src/utils/deployment.ts +54 -0
- package/visual-app-builder/src/utils/projectPersistence.ts +218 -177
- package/visual-app-builder/vite.config.ts +1946 -1479
package/scripts/vb-dev.mjs
CHANGED
|
@@ -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
|
-
[
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
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
|
+
})
|
package/scripts/vb-generate.mjs
CHANGED
|
@@ -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
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
const
|
|
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
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
'
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
`
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
)
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
)
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
throw new GeneratorDiagnosticError(
|
|
91
|
-
'
|
|
92
|
-
`
|
|
93
|
-
'
|
|
94
|
-
)
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
)
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
const
|
|
156
|
-
const
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
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
|
+
}
|