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
|
@@ -1,571 +1,776 @@
|
|
|
1
|
-
import { randomUUID } from 'node:crypto'
|
|
2
|
-
import { parse } from '@babel/parser'
|
|
3
|
-
|
|
4
|
-
export interface ImportedVisualNode {
|
|
5
|
-
id: string
|
|
6
|
-
type: string
|
|
7
|
-
label?: string
|
|
8
|
-
props: Record<string, unknown>
|
|
9
|
-
children: ImportedVisualNode[]
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
export interface ImportedPageTree {
|
|
13
|
-
root: ImportedVisualNode
|
|
14
|
-
components: ImportedVisualNode[]
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
'
|
|
26
|
-
|
|
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
|
-
'
|
|
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
|
-
|
|
156
|
-
|
|
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
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
if (
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
node
|
|
285
|
-
|
|
286
|
-
)
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
}
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
}
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
)
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
const
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
}
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
}
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
) {
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
}
|
|
554
|
-
|
|
555
|
-
function
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
1
|
+
import { randomUUID } from 'node:crypto'
|
|
2
|
+
import { parse } from '@babel/parser'
|
|
3
|
+
|
|
4
|
+
export interface ImportedVisualNode {
|
|
5
|
+
id: string
|
|
6
|
+
type: string
|
|
7
|
+
label?: string
|
|
8
|
+
props: Record<string, unknown>
|
|
9
|
+
children: ImportedVisualNode[]
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export interface ImportedPageTree {
|
|
13
|
+
root: ImportedVisualNode
|
|
14
|
+
components: ImportedVisualNode[]
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export interface ImportedComponentPreview {
|
|
18
|
+
rootTag: string
|
|
19
|
+
rootProps: Record<string, unknown>
|
|
20
|
+
children: ImportedVisualNode[]
|
|
21
|
+
rendersChildren: boolean
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export const componentChildrenSlotType =
|
|
25
|
+
'__visualbuild_children_slot__'
|
|
26
|
+
|
|
27
|
+
type AstNode = {
|
|
28
|
+
type: string
|
|
29
|
+
[key: string]: unknown
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const supportedVisualTags = new Set([
|
|
33
|
+
'header',
|
|
34
|
+
'main',
|
|
35
|
+
'div',
|
|
36
|
+
'section',
|
|
37
|
+
'nav',
|
|
38
|
+
'article',
|
|
39
|
+
'aside',
|
|
40
|
+
'footer',
|
|
41
|
+
'address',
|
|
42
|
+
'hgroup',
|
|
43
|
+
'h1',
|
|
44
|
+
'h2',
|
|
45
|
+
'h3',
|
|
46
|
+
'h4',
|
|
47
|
+
'h5',
|
|
48
|
+
'h6',
|
|
49
|
+
'p',
|
|
50
|
+
'br',
|
|
51
|
+
'hr',
|
|
52
|
+
'pre',
|
|
53
|
+
'blockquote',
|
|
54
|
+
'span',
|
|
55
|
+
'strong',
|
|
56
|
+
'b',
|
|
57
|
+
'em',
|
|
58
|
+
'i',
|
|
59
|
+
'u',
|
|
60
|
+
's',
|
|
61
|
+
'small',
|
|
62
|
+
'mark',
|
|
63
|
+
'sub',
|
|
64
|
+
'sup',
|
|
65
|
+
'code',
|
|
66
|
+
'kbd',
|
|
67
|
+
'samp',
|
|
68
|
+
'var',
|
|
69
|
+
'abbr',
|
|
70
|
+
'cite',
|
|
71
|
+
'q',
|
|
72
|
+
'time',
|
|
73
|
+
'a',
|
|
74
|
+
'ul',
|
|
75
|
+
'ol',
|
|
76
|
+
'li',
|
|
77
|
+
'dl',
|
|
78
|
+
'dt',
|
|
79
|
+
'dd',
|
|
80
|
+
'menu',
|
|
81
|
+
'button',
|
|
82
|
+
'img',
|
|
83
|
+
'picture',
|
|
84
|
+
'source',
|
|
85
|
+
'figure',
|
|
86
|
+
'figcaption',
|
|
87
|
+
'video',
|
|
88
|
+
'audio',
|
|
89
|
+
'iframe',
|
|
90
|
+
'canvas',
|
|
91
|
+
'svg',
|
|
92
|
+
'form',
|
|
93
|
+
'label',
|
|
94
|
+
'input',
|
|
95
|
+
'textarea',
|
|
96
|
+
'select',
|
|
97
|
+
'option',
|
|
98
|
+
'optgroup',
|
|
99
|
+
'fieldset',
|
|
100
|
+
'legend',
|
|
101
|
+
'datalist',
|
|
102
|
+
'output',
|
|
103
|
+
'progress',
|
|
104
|
+
'meter',
|
|
105
|
+
'table',
|
|
106
|
+
'caption',
|
|
107
|
+
'thead',
|
|
108
|
+
'tbody',
|
|
109
|
+
'tfoot',
|
|
110
|
+
'tr',
|
|
111
|
+
'th',
|
|
112
|
+
'td',
|
|
113
|
+
'colgroup',
|
|
114
|
+
'col',
|
|
115
|
+
'details',
|
|
116
|
+
'summary',
|
|
117
|
+
'dialog',
|
|
118
|
+
])
|
|
119
|
+
|
|
120
|
+
export class StaticJsxImportError extends Error {
|
|
121
|
+
constructor(message: string) {
|
|
122
|
+
super(message)
|
|
123
|
+
this.name = 'StaticJsxImportError'
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
export function parseReactPageSource(
|
|
128
|
+
source: string,
|
|
129
|
+
filename = 'Page.tsx',
|
|
130
|
+
customComponentNames: Iterable<string> = []
|
|
131
|
+
): ImportedPageTree {
|
|
132
|
+
let program: AstNode
|
|
133
|
+
|
|
134
|
+
try {
|
|
135
|
+
program = parse(source, {
|
|
136
|
+
sourceType: 'module',
|
|
137
|
+
sourceFilename: filename,
|
|
138
|
+
plugins: ['jsx', 'typescript'],
|
|
139
|
+
}).program as unknown as AstNode
|
|
140
|
+
} catch (error) {
|
|
141
|
+
throw new StaticJsxImportError(formatParserError(error))
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
const returnedJsx = findReturnedJsx(program)
|
|
145
|
+
|
|
146
|
+
if (!returnedJsx) {
|
|
147
|
+
throw new StaticJsxImportError(
|
|
148
|
+
'The managed page must return one static JSX element or fragment.'
|
|
149
|
+
)
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
return convertReturnedJsx(returnedJsx, new Set(customComponentNames))
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
export function parseReactComponentPreview(
|
|
156
|
+
source: string,
|
|
157
|
+
filename = 'Component.tsx',
|
|
158
|
+
customComponentNames: Iterable<string> = []
|
|
159
|
+
): ImportedComponentPreview {
|
|
160
|
+
let program: AstNode
|
|
161
|
+
|
|
162
|
+
try {
|
|
163
|
+
program = parse(source, {
|
|
164
|
+
sourceType: 'module',
|
|
165
|
+
sourceFilename: filename,
|
|
166
|
+
plugins: ['jsx', 'typescript'],
|
|
167
|
+
}).program as unknown as AstNode
|
|
168
|
+
} catch (error) {
|
|
169
|
+
throw new StaticJsxImportError(formatParserError(error))
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
const returnedJsx = findReturnedJsx(program)
|
|
173
|
+
|
|
174
|
+
if (!returnedJsx) {
|
|
175
|
+
throw new StaticJsxImportError(
|
|
176
|
+
'The component must return one static JSX element or fragment.'
|
|
177
|
+
)
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
const tree = convertReturnedJsx(
|
|
181
|
+
returnedJsx,
|
|
182
|
+
new Set(customComponentNames),
|
|
183
|
+
true
|
|
184
|
+
)
|
|
185
|
+
|
|
186
|
+
return {
|
|
187
|
+
rootTag: tree.root.type,
|
|
188
|
+
rootProps: tree.root.props,
|
|
189
|
+
children: tree.components,
|
|
190
|
+
rendersChildren: containsNodeType(
|
|
191
|
+
tree.components,
|
|
192
|
+
componentChildrenSlotType
|
|
193
|
+
),
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
export function reconcileImportedTree(
|
|
198
|
+
imported: ImportedPageTree,
|
|
199
|
+
current: ImportedPageTree,
|
|
200
|
+
customComponentNames: Iterable<string> = []
|
|
201
|
+
): ImportedPageTree {
|
|
202
|
+
const customTypes = new Set(customComponentNames)
|
|
203
|
+
|
|
204
|
+
return {
|
|
205
|
+
root: reconcileNode(
|
|
206
|
+
imported.root,
|
|
207
|
+
current.root,
|
|
208
|
+
current.root.id,
|
|
209
|
+
customTypes
|
|
210
|
+
),
|
|
211
|
+
components: reconcileNodeList(
|
|
212
|
+
imported.components,
|
|
213
|
+
current.components,
|
|
214
|
+
customTypes
|
|
215
|
+
),
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
export function visualTreesEqual(
|
|
220
|
+
first: ImportedPageTree,
|
|
221
|
+
second: ImportedPageTree
|
|
222
|
+
) {
|
|
223
|
+
return JSON.stringify(canonicalize(first)) === JSON.stringify(canonicalize(second))
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
function findReturnedJsx(program: AstNode) {
|
|
227
|
+
const body = asNodeArray(program.body)
|
|
228
|
+
const defaultExport = body.find(
|
|
229
|
+
(node) => node.type === 'ExportDefaultDeclaration'
|
|
230
|
+
)
|
|
231
|
+
|
|
232
|
+
if (defaultExport) {
|
|
233
|
+
const declaration = asNode(defaultExport.declaration)
|
|
234
|
+
const resolved = resolveExportedDeclaration(declaration, body)
|
|
235
|
+
const result = findJsxInFunction(resolved)
|
|
236
|
+
|
|
237
|
+
if (result) return result
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
for (const statement of body) {
|
|
241
|
+
const result = findJsxInFunction(statement)
|
|
242
|
+
|
|
243
|
+
if (result) return result
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
return null
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
function resolveExportedDeclaration(
|
|
250
|
+
declaration: AstNode | null,
|
|
251
|
+
body: AstNode[]
|
|
252
|
+
) {
|
|
253
|
+
if (!declaration || declaration.type !== 'Identifier') {
|
|
254
|
+
return declaration
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
const name = String(declaration.name ?? '')
|
|
258
|
+
|
|
259
|
+
for (const statement of body) {
|
|
260
|
+
if (
|
|
261
|
+
statement.type === 'FunctionDeclaration' &&
|
|
262
|
+
asNode(statement.id)?.name === name
|
|
263
|
+
) {
|
|
264
|
+
return statement
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
if (statement.type === 'VariableDeclaration') {
|
|
268
|
+
for (const item of asNodeArray(statement.declarations)) {
|
|
269
|
+
if (asNode(item.id)?.name === name) {
|
|
270
|
+
return asNode(item.init)
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
return declaration
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
function findJsxInFunction(node: AstNode | null): AstNode | null {
|
|
280
|
+
if (!node) return null
|
|
281
|
+
|
|
282
|
+
if (
|
|
283
|
+
node.type === 'ArrowFunctionExpression' ||
|
|
284
|
+
node.type === 'FunctionExpression' ||
|
|
285
|
+
node.type === 'FunctionDeclaration'
|
|
286
|
+
) {
|
|
287
|
+
const body = unwrapExpression(asNode(node.body))
|
|
288
|
+
|
|
289
|
+
if (isJsx(body)) return body
|
|
290
|
+
return findReturnStatement(body)
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
if (node.type === 'ExportNamedDeclaration') {
|
|
294
|
+
return findJsxInFunction(asNode(node.declaration))
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
if (node.type === 'VariableDeclaration') {
|
|
298
|
+
for (const item of asNodeArray(node.declarations)) {
|
|
299
|
+
const result = findJsxInFunction(asNode(item.init))
|
|
300
|
+
if (result) return result
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
return null
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
function findReturnStatement(node: AstNode | null): AstNode | null {
|
|
308
|
+
if (!node) return null
|
|
309
|
+
|
|
310
|
+
if (node.type === 'ReturnStatement') {
|
|
311
|
+
const argument = unwrapExpression(asNode(node.argument))
|
|
312
|
+
return isJsx(argument) ? argument : null
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
for (const value of Object.values(node)) {
|
|
316
|
+
if (Array.isArray(value)) {
|
|
317
|
+
for (const child of value) {
|
|
318
|
+
const result = findReturnStatement(asNode(child))
|
|
319
|
+
if (result) return result
|
|
320
|
+
}
|
|
321
|
+
} else {
|
|
322
|
+
const child = asNode(value)
|
|
323
|
+
const result = findReturnStatement(child)
|
|
324
|
+
if (result) return result
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
return null
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
function unwrapExpression(node: AstNode | null): AstNode | null {
|
|
332
|
+
if (
|
|
333
|
+
node &&
|
|
334
|
+
[
|
|
335
|
+
'ParenthesizedExpression',
|
|
336
|
+
'TSAsExpression',
|
|
337
|
+
'TSTypeAssertion',
|
|
338
|
+
'TSSatisfiesExpression',
|
|
339
|
+
].includes(node.type)
|
|
340
|
+
) {
|
|
341
|
+
return unwrapExpression(asNode(node.expression))
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
return node
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
function isJsx(node: AstNode | null): node is AstNode {
|
|
348
|
+
return Boolean(
|
|
349
|
+
node &&
|
|
350
|
+
['JSXElement', 'JSXFragment'].includes(node.type)
|
|
351
|
+
)
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
function convertReturnedJsx(
|
|
355
|
+
jsx: AstNode,
|
|
356
|
+
customComponentNames: Set<string>,
|
|
357
|
+
allowChildrenSlot = false
|
|
358
|
+
): ImportedPageTree {
|
|
359
|
+
if (jsx.type === 'JSXFragment') {
|
|
360
|
+
return {
|
|
361
|
+
root: createImportedNode('main', { className: 'min-h-screen' }),
|
|
362
|
+
components: convertJsxChildren(
|
|
363
|
+
asNodeArray(jsx.children),
|
|
364
|
+
customComponentNames,
|
|
365
|
+
allowChildrenSlot
|
|
366
|
+
),
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
const root = convertJsxElement(
|
|
371
|
+
jsx,
|
|
372
|
+
customComponentNames,
|
|
373
|
+
allowChildrenSlot
|
|
374
|
+
)
|
|
375
|
+
const components = [...root.children]
|
|
376
|
+
const text = String(root.props.text ?? '').trim()
|
|
377
|
+
|
|
378
|
+
if (text) {
|
|
379
|
+
components.unshift(createImportedNode('span', { text, className: '' }))
|
|
380
|
+
delete root.props.text
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
root.children = []
|
|
384
|
+
return { root, components }
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
function convertJsxElement(
|
|
388
|
+
jsx: AstNode,
|
|
389
|
+
customComponentNames: Set<string>,
|
|
390
|
+
allowChildrenSlot = false
|
|
391
|
+
): ImportedVisualNode {
|
|
392
|
+
const opening = asNode(jsx.openingElement)
|
|
393
|
+
const nameNode = asNode(opening?.name)
|
|
394
|
+
|
|
395
|
+
if (!opening || !nameNode || nameNode.type !== 'JSXIdentifier') {
|
|
396
|
+
throw new StaticJsxImportError(
|
|
397
|
+
'Namespaced tags and member-expression components are not supported.'
|
|
398
|
+
)
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
const tagName = String(nameNode.name ?? '')
|
|
402
|
+
|
|
403
|
+
if (
|
|
404
|
+
!supportedVisualTags.has(tagName) &&
|
|
405
|
+
!customComponentNames.has(tagName)
|
|
406
|
+
) {
|
|
407
|
+
throw new StaticJsxImportError(
|
|
408
|
+
`Unsupported JSX element <${tagName}>. Register the component before using it in a managed page.`
|
|
409
|
+
)
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
const props = convertJsxAttributes(asNodeArray(opening.attributes))
|
|
413
|
+
const rawChildren = asNodeArray(jsx.children)
|
|
414
|
+
const childItems = convertChildItems(
|
|
415
|
+
rawChildren,
|
|
416
|
+
customComponentNames,
|
|
417
|
+
allowChildrenSlot
|
|
418
|
+
)
|
|
419
|
+
const hasElementChild = childItems.some(
|
|
420
|
+
(item) => typeof item !== 'string'
|
|
421
|
+
)
|
|
422
|
+
const children = childItems.flatMap((item) =>
|
|
423
|
+
typeof item === 'string'
|
|
424
|
+
? hasElementChild
|
|
425
|
+
? [createImportedNode('span', { text: item, className: '' })]
|
|
426
|
+
: []
|
|
427
|
+
: [item]
|
|
428
|
+
)
|
|
429
|
+
const directText = childItems
|
|
430
|
+
.filter((item): item is string => typeof item === 'string')
|
|
431
|
+
.join(' ')
|
|
432
|
+
.trim()
|
|
433
|
+
|
|
434
|
+
if (directText && !hasElementChild) {
|
|
435
|
+
props.text = directText
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
const wrappedComponentName = String(
|
|
439
|
+
props['data-visualbuild-component'] ?? ''
|
|
440
|
+
)
|
|
441
|
+
const wrappedComponent = children.find(
|
|
442
|
+
(child) =>
|
|
443
|
+
child.type === wrappedComponentName &&
|
|
444
|
+
customComponentNames.has(child.type)
|
|
445
|
+
)
|
|
446
|
+
|
|
447
|
+
if (wrappedComponent) {
|
|
448
|
+
const additionalAttributes = Object.fromEntries(
|
|
449
|
+
Object.entries(props).filter(
|
|
450
|
+
([name]) =>
|
|
451
|
+
![
|
|
452
|
+
'data-visualbuild-component',
|
|
453
|
+
'id',
|
|
454
|
+
'className',
|
|
455
|
+
'text',
|
|
456
|
+
].includes(name)
|
|
457
|
+
)
|
|
458
|
+
)
|
|
459
|
+
|
|
460
|
+
return createImportedNode(
|
|
461
|
+
wrappedComponent.type,
|
|
462
|
+
{
|
|
463
|
+
...wrappedComponent.props,
|
|
464
|
+
wrapperTag: tagName,
|
|
465
|
+
id: props.id ?? '',
|
|
466
|
+
className: props.className ?? '',
|
|
467
|
+
attributes: additionalAttributes,
|
|
468
|
+
},
|
|
469
|
+
wrappedComponent.children
|
|
470
|
+
)
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
return createImportedNode(tagName, props, children)
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
function convertJsxAttributes(attributes: AstNode[]) {
|
|
477
|
+
const props: Record<string, unknown> = {}
|
|
478
|
+
|
|
479
|
+
for (const attribute of attributes) {
|
|
480
|
+
if (attribute.type === 'JSXSpreadAttribute') {
|
|
481
|
+
throw new StaticJsxImportError(
|
|
482
|
+
'Spread attributes are not supported by reverse sync.'
|
|
483
|
+
)
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
if (attribute.type !== 'JSXAttribute') continue
|
|
487
|
+
const nameNode = asNode(attribute.name)
|
|
488
|
+
|
|
489
|
+
if (!nameNode || nameNode.type !== 'JSXIdentifier') {
|
|
490
|
+
throw new StaticJsxImportError('Only standard JSX attributes are supported.')
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
const rawName = String(nameNode.name ?? '')
|
|
494
|
+
const name = rawName === 'class' ? 'className' : rawName
|
|
495
|
+
const valueNode = asNode(attribute.value)
|
|
496
|
+
|
|
497
|
+
if (!valueNode) {
|
|
498
|
+
props[name] = true
|
|
499
|
+
continue
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
if (valueNode.type === 'StringLiteral') {
|
|
503
|
+
props[name] = valueNode.value
|
|
504
|
+
continue
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
if (valueNode.type === 'JSXExpressionContainer') {
|
|
508
|
+
const value = getStaticExpressionValue(asNode(valueNode.expression))
|
|
509
|
+
|
|
510
|
+
if (value === undefined) {
|
|
511
|
+
throw new StaticJsxImportError(
|
|
512
|
+
`Attribute "${name}" must use a static string, number, boolean, or null.`
|
|
513
|
+
)
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
props[name] = value
|
|
517
|
+
continue
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
throw new StaticJsxImportError(
|
|
521
|
+
`Attribute "${name}" uses unsupported JSX syntax.`
|
|
522
|
+
)
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
return props
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
function getStaticExpressionValue(node: AstNode | null): unknown {
|
|
529
|
+
const value = unwrapExpression(node)
|
|
530
|
+
|
|
531
|
+
if (!value) return undefined
|
|
532
|
+
if (value.type === 'StringLiteral' || value.type === 'NumericLiteral') {
|
|
533
|
+
return value.value
|
|
534
|
+
}
|
|
535
|
+
if (value.type === 'BooleanLiteral') return value.value
|
|
536
|
+
if (value.type === 'NullLiteral') return null
|
|
537
|
+
if (
|
|
538
|
+
value.type === 'TemplateLiteral' &&
|
|
539
|
+
asNodeArray(value.expressions).length === 0
|
|
540
|
+
) {
|
|
541
|
+
return asNodeArray(value.quasis)
|
|
542
|
+
.map((quasi) => String(asNode(quasi.value)?.cooked ?? ''))
|
|
543
|
+
.join('')
|
|
544
|
+
}
|
|
545
|
+
if (value.type === 'UnaryExpression' && value.operator === '-') {
|
|
546
|
+
const argument = asNode(value.argument)
|
|
547
|
+
if (argument?.type === 'NumericLiteral') {
|
|
548
|
+
return -Number(argument.value)
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
return undefined
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
function convertJsxChildren(
|
|
556
|
+
children: AstNode[],
|
|
557
|
+
customComponentNames: Set<string>,
|
|
558
|
+
allowChildrenSlot = false
|
|
559
|
+
) {
|
|
560
|
+
return convertChildItems(
|
|
561
|
+
children,
|
|
562
|
+
customComponentNames,
|
|
563
|
+
allowChildrenSlot
|
|
564
|
+
).flatMap((item) =>
|
|
565
|
+
typeof item === 'string'
|
|
566
|
+
? [createImportedNode('span', { text: item, className: '' })]
|
|
567
|
+
: [item]
|
|
568
|
+
)
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
function convertChildItems(
|
|
572
|
+
children: AstNode[],
|
|
573
|
+
customComponentNames: Set<string>,
|
|
574
|
+
allowChildrenSlot = false
|
|
575
|
+
) {
|
|
576
|
+
const result: Array<string | ImportedVisualNode> = []
|
|
577
|
+
|
|
578
|
+
for (const child of children) {
|
|
579
|
+
if (child.type === 'JSXElement') {
|
|
580
|
+
result.push(
|
|
581
|
+
convertJsxElement(
|
|
582
|
+
child,
|
|
583
|
+
customComponentNames,
|
|
584
|
+
allowChildrenSlot
|
|
585
|
+
)
|
|
586
|
+
)
|
|
587
|
+
continue
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
if (child.type === 'JSXFragment') {
|
|
591
|
+
result.push(
|
|
592
|
+
...convertChildItems(
|
|
593
|
+
asNodeArray(child.children),
|
|
594
|
+
customComponentNames,
|
|
595
|
+
allowChildrenSlot
|
|
596
|
+
)
|
|
597
|
+
)
|
|
598
|
+
continue
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
if (child.type === 'JSXText') {
|
|
602
|
+
const text = normalizeJsxText(String(child.value ?? ''))
|
|
603
|
+
if (text) result.push(text)
|
|
604
|
+
continue
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
if (child.type === 'JSXExpressionContainer') {
|
|
608
|
+
const expression = asNode(child.expression)
|
|
609
|
+
|
|
610
|
+
if (expression?.type === 'JSXEmptyExpression') continue
|
|
611
|
+
if (
|
|
612
|
+
allowChildrenSlot &&
|
|
613
|
+
expression?.type === 'Identifier' &&
|
|
614
|
+
expression.name === 'children'
|
|
615
|
+
) {
|
|
616
|
+
result.push(createImportedNode(componentChildrenSlotType, {}))
|
|
617
|
+
continue
|
|
618
|
+
}
|
|
619
|
+
const value = getStaticExpressionValue(expression)
|
|
620
|
+
|
|
621
|
+
if (typeof value === 'string' || typeof value === 'number') {
|
|
622
|
+
result.push(String(value))
|
|
623
|
+
continue
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
throw new StaticJsxImportError(
|
|
627
|
+
'Dynamic JSX expressions are not supported by reverse sync.'
|
|
628
|
+
)
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
if (child.type === 'JSXSpreadChild') {
|
|
632
|
+
throw new StaticJsxImportError(
|
|
633
|
+
'Spread children are not supported by reverse sync.'
|
|
634
|
+
)
|
|
635
|
+
}
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
return result
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
function normalizeJsxText(value: string) {
|
|
642
|
+
return value.replace(/\s+/g, ' ').trim()
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
function canonicalize(value: unknown): unknown {
|
|
646
|
+
if (Array.isArray(value)) {
|
|
647
|
+
return value.map(canonicalize)
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
if (value && typeof value === 'object') {
|
|
651
|
+
return Object.fromEntries(
|
|
652
|
+
Object.entries(value)
|
|
653
|
+
.filter(([, entryValue]) => entryValue !== undefined)
|
|
654
|
+
.sort(([firstKey], [secondKey]) => firstKey.localeCompare(secondKey))
|
|
655
|
+
.map(([key, entryValue]) => [key, canonicalize(entryValue)])
|
|
656
|
+
)
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
return value
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
function reconcileNodeList(
|
|
663
|
+
imported: ImportedVisualNode[],
|
|
664
|
+
current: ImportedVisualNode[],
|
|
665
|
+
customComponentNames: Set<string>
|
|
666
|
+
) {
|
|
667
|
+
const used = new Set<number>()
|
|
668
|
+
|
|
669
|
+
return imported.map((node, index) => {
|
|
670
|
+
let matchIndex =
|
|
671
|
+
current[index]?.type === node.type
|
|
672
|
+
? index
|
|
673
|
+
: current.findIndex(
|
|
674
|
+
(candidate, candidateIndex) =>
|
|
675
|
+
!used.has(candidateIndex) && candidate.type === node.type
|
|
676
|
+
)
|
|
677
|
+
|
|
678
|
+
if (matchIndex < 0 || used.has(matchIndex)) matchIndex = -1
|
|
679
|
+
if (matchIndex >= 0) used.add(matchIndex)
|
|
680
|
+
|
|
681
|
+
return reconcileNode(
|
|
682
|
+
node,
|
|
683
|
+
current[matchIndex] ?? null,
|
|
684
|
+
undefined,
|
|
685
|
+
customComponentNames
|
|
686
|
+
)
|
|
687
|
+
})
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
function reconcileNode(
|
|
691
|
+
imported: ImportedVisualNode,
|
|
692
|
+
current: ImportedVisualNode | null,
|
|
693
|
+
forcedId: string | undefined,
|
|
694
|
+
customComponentNames: Set<string>
|
|
695
|
+
): ImportedVisualNode {
|
|
696
|
+
const props =
|
|
697
|
+
current &&
|
|
698
|
+
customComponentNames.has(imported.type) &&
|
|
699
|
+
current.type === imported.type
|
|
700
|
+
? preserveCustomRootProps(imported.props, current.props)
|
|
701
|
+
: imported.props
|
|
702
|
+
|
|
703
|
+
return {
|
|
704
|
+
...imported,
|
|
705
|
+
id: forcedId ?? current?.id ?? imported.id,
|
|
706
|
+
label: current?.label ?? imported.label,
|
|
707
|
+
props,
|
|
708
|
+
children: reconcileNodeList(
|
|
709
|
+
imported.children,
|
|
710
|
+
current?.children ?? [],
|
|
711
|
+
customComponentNames
|
|
712
|
+
),
|
|
713
|
+
}
|
|
714
|
+
}
|
|
715
|
+
|
|
716
|
+
const customRootPropNames = [
|
|
717
|
+
'wrapperTag',
|
|
718
|
+
'id',
|
|
719
|
+
'className',
|
|
720
|
+
'attributes',
|
|
721
|
+
'componentTree',
|
|
722
|
+
] as const
|
|
723
|
+
|
|
724
|
+
function preserveCustomRootProps(
|
|
725
|
+
imported: Record<string, unknown>,
|
|
726
|
+
current: Record<string, unknown>
|
|
727
|
+
) {
|
|
728
|
+
const rootProps = Object.fromEntries(
|
|
729
|
+
customRootPropNames
|
|
730
|
+
.filter((name) => Object.hasOwn(current, name))
|
|
731
|
+
.map((name) => [name, current[name]])
|
|
732
|
+
)
|
|
733
|
+
|
|
734
|
+
return {
|
|
735
|
+
...imported,
|
|
736
|
+
...rootProps,
|
|
737
|
+
}
|
|
738
|
+
}
|
|
739
|
+
|
|
740
|
+
function createImportedNode(
|
|
741
|
+
type: string,
|
|
742
|
+
props: Record<string, unknown>,
|
|
743
|
+
children: ImportedVisualNode[] = []
|
|
744
|
+
): ImportedVisualNode {
|
|
745
|
+
return {
|
|
746
|
+
id: randomUUID(),
|
|
747
|
+
type,
|
|
748
|
+
props,
|
|
749
|
+
children,
|
|
750
|
+
}
|
|
751
|
+
}
|
|
752
|
+
|
|
753
|
+
function containsNodeType(nodes: ImportedVisualNode[], type: string): boolean {
|
|
754
|
+
return nodes.some(
|
|
755
|
+
(node) =>
|
|
756
|
+
node.type === type || containsNodeType(node.children, type)
|
|
757
|
+
)
|
|
758
|
+
}
|
|
759
|
+
|
|
760
|
+
function asNode(value: unknown): AstNode | null {
|
|
761
|
+
return value && typeof value === 'object' && 'type' in value
|
|
762
|
+
? (value as AstNode)
|
|
763
|
+
: null
|
|
764
|
+
}
|
|
765
|
+
|
|
766
|
+
function asNodeArray(value: unknown): AstNode[] {
|
|
767
|
+
return Array.isArray(value)
|
|
768
|
+
? value.map(asNode).filter((node): node is AstNode => Boolean(node))
|
|
769
|
+
: []
|
|
770
|
+
}
|
|
771
|
+
|
|
772
|
+
function formatParserError(error: unknown) {
|
|
773
|
+
if (!(error instanceof Error)) return 'The page contains invalid JSX.'
|
|
774
|
+
const location = error.message.match(/\(\d+:\d+\)/)?.[0]
|
|
775
|
+
return `Unable to parse the managed page${location ? ` at ${location}` : ''}: ${error.message}`
|
|
776
|
+
}
|