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,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
+ [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
+ })
@@ -0,0 +1,224 @@
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
+ }
@@ -0,0 +1,21 @@
1
+ # __DISPLAY_NAME__
2
+
3
+ This is the deployable React application generated by VisualBuild. It contains
4
+ only the source and dependencies required to run the application.
5
+
6
+ ## Run
7
+
8
+ ```powershell
9
+ npm install
10
+ npm.cmd run dev
11
+ ```
12
+
13
+ ## Build
14
+
15
+ ```powershell
16
+ npm.cmd run build
17
+ ```
18
+
19
+ The visual schema lives in `visualbuild/pages.json`. The optional sibling
20
+ `visual-builder` project can edit this application visually and regenerate its
21
+ managed React files. This application remains runnable without that editor.
@@ -0,0 +1,25 @@
1
+ import js from '@eslint/js'
2
+ import globals from 'globals'
3
+ import reactHooks from 'eslint-plugin-react-hooks'
4
+ import reactRefresh from 'eslint-plugin-react-refresh'
5
+ import tseslint from 'typescript-eslint'
6
+ import { defineConfig, globalIgnores } from 'eslint/config'
7
+
8
+ export default defineConfig([
9
+ globalIgnores(['dist']),
10
+ {
11
+ files: ['**/*.{ts,tsx}'],
12
+ extends: [
13
+ js.configs.recommended,
14
+ tseslint.configs.recommended,
15
+ reactHooks.configs.flat.recommended,
16
+ reactRefresh.configs.vite,
17
+ ],
18
+ languageOptions: {
19
+ globals: globals.browser,
20
+ parserOptions: {
21
+ tsconfigRootDir: import.meta.dirname,
22
+ },
23
+ },
24
+ },
25
+ ])
@@ -0,0 +1,15 @@
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
+ <title>__DISPLAY_NAME__</title>
7
+ <link rel="preconnect" href="https://fonts.googleapis.com" />
8
+ <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
9
+ <link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet" />
10
+ </head>
11
+ <body>
12
+ <div id="root"></div>
13
+ <script type="module" src="/src/main.tsx"></script>
14
+ </body>
15
+ </html>
@@ -0,0 +1,23 @@
1
+ @import "tailwindcss";
2
+
3
+ *,
4
+ *::before,
5
+ *::after {
6
+ box-sizing: border-box;
7
+ }
8
+
9
+ body {
10
+ margin: 0;
11
+ min-width: 320px;
12
+ min-height: 100vh;
13
+ font-family:
14
+ Inter,
15
+ system-ui,
16
+ -apple-system,
17
+ BlinkMacSystemFont,
18
+ "Segoe UI",
19
+ sans-serif;
20
+ background: #f8fafc;
21
+ color: #111827;
22
+ -webkit-font-smoothing: antialiased;
23
+ }
@@ -0,0 +1,10 @@
1
+ import { StrictMode } from 'react'
2
+ import { createRoot } from 'react-dom/client'
3
+ import './index.css'
4
+ import App from './App.tsx'
5
+
6
+ createRoot(document.getElementById('root')!).render(
7
+ <StrictMode>
8
+ <App />
9
+ </StrictMode>,
10
+ )
@@ -0,0 +1,21 @@
1
+ {
2
+ "compilerOptions": {
3
+ "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
4
+ "target": "es2023",
5
+ "lib": ["ES2023", "DOM"],
6
+ "module": "esnext",
7
+ "types": ["vite/client"],
8
+ "skipLibCheck": true,
9
+ "moduleResolution": "bundler",
10
+ "allowImportingTsExtensions": true,
11
+ "verbatimModuleSyntax": true,
12
+ "moduleDetection": "force",
13
+ "noEmit": true,
14
+ "jsx": "react-jsx",
15
+ "noUnusedLocals": true,
16
+ "noUnusedParameters": true,
17
+ "erasableSyntaxOnly": true,
18
+ "noFallthroughCasesInSwitch": true
19
+ },
20
+ "include": ["src"]
21
+ }
@@ -0,0 +1,7 @@
1
+ {
2
+ "files": [],
3
+ "references": [
4
+ { "path": "./tsconfig.app.json" },
5
+ { "path": "./tsconfig.node.json" }
6
+ ]
7
+ }
@@ -0,0 +1,19 @@
1
+ {
2
+ "compilerOptions": {
3
+ "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
4
+ "target": "es2023",
5
+ "lib": ["ES2023"],
6
+ "types": ["node"],
7
+ "skipLibCheck": true,
8
+ "module": "nodenext",
9
+ "allowImportingTsExtensions": true,
10
+ "verbatimModuleSyntax": true,
11
+ "moduleDetection": "force",
12
+ "noEmit": true,
13
+ "noUnusedLocals": true,
14
+ "noUnusedParameters": true,
15
+ "erasableSyntaxOnly": true,
16
+ "noFallthroughCasesInSwitch": true
17
+ },
18
+ "include": ["vite.config.ts"]
19
+ }
@@ -0,0 +1,3 @@
1
+ {
2
+ "files": []
3
+ }
@@ -0,0 +1,12 @@
1
+ {
2
+ "meta": {
3
+ "projectName": "my-app",
4
+ "framework": "react",
5
+ "styling": "tailwind"
6
+ },
7
+ "appShell": {
8
+ "header": null,
9
+ "footer": null
10
+ },
11
+ "pages": []
12
+ }
@@ -0,0 +1,7 @@
1
+ import { defineConfig } from 'vite'
2
+ import react from '@vitejs/plugin-react'
3
+ import tailwindcss from '@tailwindcss/vite'
4
+
5
+ export default defineConfig({
6
+ plugins: [react(), tailwindcss()],
7
+ })
@@ -0,0 +1,21 @@
1
+ # VisualBuild Editor
2
+
3
+ This package contains the optional VisualBuild editor and generator for the
4
+ sibling `__PROJECT_NAME__` React application.
5
+
6
+ ## Run
7
+
8
+ ```powershell
9
+ npm install
10
+ npm.cmd run visual-builder
11
+ ```
12
+
13
+ ## Generate Without Opening The Editor
14
+
15
+ ```powershell
16
+ npm.cmd run vb:generate
17
+ ```
18
+
19
+ The editor reads and writes `../__PROJECT_NAME__/visualbuild/pages.json` and
20
+ manages generated files in `../__PROJECT_NAME__/src`. It is intentionally
21
+ separate from the deployable application and does not need to be deployed.
@@ -0,0 +1,25 @@
1
+ import js from '@eslint/js'
2
+ import globals from 'globals'
3
+ import reactHooks from 'eslint-plugin-react-hooks'
4
+ import reactRefresh from 'eslint-plugin-react-refresh'
5
+ import tseslint from 'typescript-eslint'
6
+ import { defineConfig, globalIgnores } from 'eslint/config'
7
+
8
+ export default defineConfig([
9
+ globalIgnores(['dist']),
10
+ {
11
+ files: ['**/*.{ts,tsx}'],
12
+ extends: [
13
+ js.configs.recommended,
14
+ tseslint.configs.recommended,
15
+ reactHooks.configs.flat.recommended,
16
+ reactRefresh.configs.vite,
17
+ ],
18
+ languageOptions: {
19
+ globals: globals.browser,
20
+ parserOptions: {
21
+ tsconfigRootDir: import.meta.dirname,
22
+ },
23
+ },
24
+ },
25
+ ])
@@ -0,0 +1,21 @@
1
+ {
2
+ "compilerOptions": {
3
+ "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
4
+ "target": "es2023",
5
+ "lib": ["ES2023", "DOM"],
6
+ "module": "esnext",
7
+ "types": ["vite/client"],
8
+ "skipLibCheck": true,
9
+ "moduleResolution": "bundler",
10
+ "allowImportingTsExtensions": true,
11
+ "verbatimModuleSyntax": true,
12
+ "moduleDetection": "force",
13
+ "noEmit": true,
14
+ "jsx": "react-jsx",
15
+ "noUnusedLocals": true,
16
+ "noUnusedParameters": true,
17
+ "erasableSyntaxOnly": true,
18
+ "noFallthroughCasesInSwitch": true
19
+ },
20
+ "include": ["visual-app-builder/src"]
21
+ }
@@ -0,0 +1,7 @@
1
+ {
2
+ "files": [],
3
+ "references": [
4
+ { "path": "./tsconfig.app.json" },
5
+ { "path": "./tsconfig.node.json" }
6
+ ]
7
+ }
@@ -0,0 +1,22 @@
1
+ {
2
+ "compilerOptions": {
3
+ "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
4
+ "target": "es2023",
5
+ "lib": ["ES2023"],
6
+ "types": ["node"],
7
+ "skipLibCheck": true,
8
+ "module": "nodenext",
9
+ "allowImportingTsExtensions": true,
10
+ "verbatimModuleSyntax": true,
11
+ "moduleDetection": "force",
12
+ "noEmit": true,
13
+ "noUnusedLocals": true,
14
+ "noUnusedParameters": true,
15
+ "erasableSyntaxOnly": true,
16
+ "noFallthroughCasesInSwitch": true
17
+ },
18
+ "include": [
19
+ "visual-app-builder/vite.config.ts",
20
+ "visual-app-builder/server/**/*.ts"
21
+ ]
22
+ }
@@ -0,0 +1,15 @@
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
+ <title>VisualBuild</title>
7
+ <link rel="preconnect" href="https://fonts.googleapis.com" />
8
+ <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
9
+ <link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet" />
10
+ </head>
11
+ <body>
12
+ <div id="root"></div>
13
+ <script type="module" src="/src/main.tsx"></script>
14
+ </body>
15
+ </html>